diff --git a/src/main/java/com/yourname/sellplugin/SellPlugin.java b/src/main/java/com/yourname/sellplugin/SellPlugin.java index c791f3b..94a7a49 100644 --- a/src/main/java/com/yourname/sellplugin/SellPlugin.java +++ b/src/main/java/com/yourname/sellplugin/SellPlugin.java @@ -12,7 +12,7 @@ import com.yourname.sellplugin.listener.WorthPacketListener; import com.yourname.sellplugin.listener.WorthRefreshListener; import com.yourname.sellplugin.manager.ConfigManager; -import com.yourname.sellplugin.manager.DailyBonusManager; +import com.yourname.sellplugin.manager.ConfigMigrator; import com.yourname.sellplugin.manager.MultiplierManager; import com.yourname.sellplugin.manager.PriceManager; import com.yourname.sellplugin.manager.SellManager; @@ -25,7 +25,6 @@ public class SellPlugin extends JavaPlugin { private ConfigManager configManager; private PriceManager priceManager; private MultiplierManager multiplierManager; - private DailyBonusManager dailyBonusManager; private SellManager sellManager; private WorthVisibilityManager worthVisibilityManager; private WorthPacketListener worthPacketListener; @@ -33,13 +32,13 @@ public class SellPlugin extends JavaPlugin { @Override public void onEnable() { saveDefaultConfig(); + new ConfigMigrator(this).migrate(); configManager = new ConfigManager(this); priceManager = new PriceManager(this); priceManager.loadPrices(); multiplierManager = new MultiplierManager(this); - dailyBonusManager = new DailyBonusManager(this); sellManager = new SellManager(this); worthVisibilityManager = new WorthVisibilityManager(this); @@ -86,7 +85,6 @@ public void onDisable() { public ConfigManager getConfigManager() { return configManager; } public PriceManager getPriceManager() { return priceManager; } public MultiplierManager getMultiplierManager() { return multiplierManager; } - public DailyBonusManager getDailyBonusManager() { return dailyBonusManager; } public SellManager getSellManager() { return sellManager; } public WorthVisibilityManager getWorthVisibilityManager() { return worthVisibilityManager; } } diff --git a/src/main/java/com/yourname/sellplugin/command/SellMultiCommand.java b/src/main/java/com/yourname/sellplugin/command/SellMultiCommand.java index a3bf6f5..7c43803 100644 --- a/src/main/java/com/yourname/sellplugin/command/SellMultiCommand.java +++ b/src/main/java/com/yourname/sellplugin/command/SellMultiCommand.java @@ -21,6 +21,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } + if (!plugin.getConfigManager().isProgressGuiEnabled()) { + player.sendMessage(plugin.getConfigManager().getText("feature-disabled", "&cThis feature is currently disabled.")); + return true; + } + if (!player.hasPermission("sellplugin.use")) { player.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; diff --git a/src/main/java/com/yourname/sellplugin/command/TopSellCommand.java b/src/main/java/com/yourname/sellplugin/command/TopSellCommand.java index b1ffd00..31e6772 100644 --- a/src/main/java/com/yourname/sellplugin/command/TopSellCommand.java +++ b/src/main/java/com/yourname/sellplugin/command/TopSellCommand.java @@ -22,6 +22,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } + if (!plugin.getConfigManager().isTopSellEnabled()) { + player.sendMessage(plugin.getConfigManager().getText("feature-disabled", "&cThis feature is currently disabled.")); + return true; + } + if (!player.hasPermission("sellplugin.topsell")) { player.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; diff --git a/src/main/java/com/yourname/sellplugin/gui/CategoryItemsGUI.java b/src/main/java/com/yourname/sellplugin/gui/CategoryItemsGUI.java index 61c9a63..b7f7103 100644 --- a/src/main/java/com/yourname/sellplugin/gui/CategoryItemsGUI.java +++ b/src/main/java/com/yourname/sellplugin/gui/CategoryItemsGUI.java @@ -166,8 +166,6 @@ private ItemStack buildItemDisplay(String itemKey) { PriceManager pm = plugin.getPriceManager(); double base = pm.getPrice(itemKey); String itemCategory = pm.getCategory(itemKey); - double earned = plugin.getMultiplierManager().getMultiplier(player, itemCategory); - double daily = plugin.getDailyBonusManager().getDailyBonus(itemCategory); double effectiveMultiplier = plugin.getMultiplierManager().getEffectiveMultiplier(player, itemCategory); double effective = base * effectiveMultiplier; @@ -178,14 +176,8 @@ private ItemStack buildItemDisplay(String itemKey) { lore.add(ChatColor.DARK_GRAY + "━━━━━━━━━━━━━━━━━━━━━"); lore.add(ChatColor.GRAY + " ▸ " + ChatColor.WHITE + "Base: " + ChatColor.GREEN + "$" + NumberFormatter.format(base)); - if (daily > 0) { - lore.add(ChatColor.GRAY + " ▸ " + ChatColor.WHITE + "Mult: " - + ChatColor.AQUA + String.format("%.2fx", earned) - + ChatColor.GOLD + " (+" + String.format("%.2fx", daily) + " today)"); - } else { - lore.add(ChatColor.GRAY + " ▸ " + ChatColor.WHITE + "Mult: " - + ChatColor.AQUA + String.format("%.2fx", effectiveMultiplier)); - } + lore.add(ChatColor.GRAY + " ▸ " + ChatColor.WHITE + "Mult: " + + ChatColor.AQUA + String.format("%.2fx", effectiveMultiplier)); lore.add(ChatColor.GRAY + " ▸ " + ChatColor.WHITE + "Price: " + ChatColor.GREEN + "$" + NumberFormatter.format(effective)); lore.add(ChatColor.DARK_GRAY + "━━━━━━━━━━━━━━━━━━━━━"); @@ -210,7 +202,7 @@ private ItemStack resolveItemStack(String itemKey) { if (itemKey.contains(":")) { String[] parts = itemKey.split(":", 2); Material mat = Material.matchMaterial(parts[0]); - if (mat == null) return new ItemStack(Material.BARRIER); + if (mat == null || mat.isAir() || !mat.isItem()) return new ItemStack(Material.BARRIER); ItemStack item = new ItemStack(mat); ItemMeta meta = item.getItemMeta(); @@ -226,7 +218,8 @@ private ItemStack resolveItemStack(String itemKey) { return item; } Material mat = Material.matchMaterial(itemKey); - return new ItemStack(mat != null ? mat : Material.BARRIER); + if (mat == null || mat.isAir() || !mat.isItem()) return new ItemStack(Material.BARRIER); + return new ItemStack(mat); } // ── Item clicked ───────────────────────────────────────────────────────── diff --git a/src/main/java/com/yourname/sellplugin/gui/CategoryProgressGUI.java b/src/main/java/com/yourname/sellplugin/gui/CategoryProgressGUI.java index 7738e62..31c1d6b 100644 --- a/src/main/java/com/yourname/sellplugin/gui/CategoryProgressGUI.java +++ b/src/main/java/com/yourname/sellplugin/gui/CategoryProgressGUI.java @@ -112,25 +112,8 @@ private void populate() { // ── Snake path ────────────────────────────────────────────────────── double mult = plugin.getMultiplierManager().getMultiplier(player, categoryId); double moneyEarned = plugin.getMultiplierManager().getMoneyEarned(player, categoryId); - double dailyBonus = plugin.getDailyBonusManager().getDailyBonus(categoryId); buildSnakePath(mult, moneyEarned); - // ── Daily bonus indicator (slot 4, top centre) ───────────────────── - if (dailyBonus > 0) { - String separator = cfg.getText("lore-separator", "&8━━━━━━━━━━━━━━━━━━━"); - List boostLore = new ArrayList<>(); - boostLore.add(separator); - boostLore.add(ChatColor.GRAY + " ▸ " + SmallCaps.convert(cfg.getText("category-progress.daily-boost-bonus-label", "bonus: ")) - + ChatColor.YELLOW + "+" + String.format("%.2f", dailyBonus) + "x"); - boostLore.add(ChatColor.GRAY + " ▸ " + SmallCaps.convert(cfg.getText("category-progress.daily-boost-effective-label", "effective: ")) - + ChatColor.GREEN + String.format("%.2fx", mult + dailyBonus)); - boostLore.add(separator); - boostLore.add(ChatColor.GRAY + SmallCaps.convert(cfg.getText("category-progress.daily-boost-resets", "resets at midnight."))); - inv.setItem(4, makeItem(Material.BLAZE_POWDER, - SmallCaps.convert(cfg.getText("category-progress.daily-boost-title", "&6&l\uD83D\uDD25 Daily Boost Active!")), - boostLore)); - } - // ── Back button (bottom-right) ────────────────────────────────────── List backLore = cfg.getIconLore("back", Collections.singletonList(ChatColor.GRAY + " ▸ " + SmallCaps.convert(cfg.getText("category-progress.back-lore", "return to the main menu.")))); diff --git a/src/main/java/com/yourname/sellplugin/gui/GUIListener.java b/src/main/java/com/yourname/sellplugin/gui/GUIListener.java index a9a1af7..53a5b39 100644 --- a/src/main/java/com/yourname/sellplugin/gui/GUIListener.java +++ b/src/main/java/com/yourname/sellplugin/gui/GUIListener.java @@ -35,13 +35,18 @@ public void onDrag(InventoryDragEvent e) { // ShopMainGUI: allow drags in the item-placement area (0-44), // cancel if any slot touches the protected bottom row (45-53). - if (holder instanceof ShopMainGUI) { + if (holder instanceof ShopMainGUI shopGUI) { for (int slot : e.getRawSlots()) { if (slot >= ShopMainGUI.BOTTOM_ROW_START && slot <= 53) { e.setCancelled(true); return; } } + // Allow the drag, then refresh the sell button so the value updates. + Player dragger = (e.getWhoClicked() instanceof Player p) ? p : null; + if (dragger != null) { + Scheduler.runEntityLater(plugin, dragger, shopGUI::refreshSellButton, 1L); + } return; // allow the drag } @@ -71,8 +76,12 @@ public void onClick(InventoryClickEvent e) { if (holder instanceof ShopMainGUI shopGUI) { Inventory clicked = e.getClickedInventory(); - // Click in player inventory (bottom) – allow freely + // Click in player inventory (bottom) – allow freely, but a + // shift-click / number-key / move-to-other-inventory action can push + // an item up into the shop area without the top inventory being the + // clicked one. Schedule a sell-button refresh so the value updates. if (clicked != null && clicked.equals(player.getInventory())) { + Scheduler.runEntityLater(plugin, player, shopGUI::refreshSellButton, 1L); return; } @@ -318,7 +327,7 @@ private void sellGuiItems(Player player, ShopMainGUI shopGUI) { String cat = pl.getPriceManager().getCategory(key); double mult = pl.getMultiplierManager().getEffectiveMultiplier(player, cat); int amount = item.getAmount(); - double earned = base * mult * amount; + double earned = pl.getSellManager().enchantedUnitPrice(item, base) * mult * amount; totalEarned += earned; totalItems += amount; categoryEarnings.merge(cat, earned, Double::sum); @@ -392,7 +401,7 @@ public void onClose(InventoryCloseEvent e) { String cat = pl.getPriceManager().getCategory(key); double mult = pl.getMultiplierManager().getEffectiveMultiplier(player, cat); int amount = item.getAmount(); - double earned = base * mult * amount; + double earned = pl.getSellManager().enchantedUnitPrice(item, base) * mult * amount; totalEarned += earned; totalItems += amount; categoryEarnings.merge(cat, earned, Double::sum); diff --git a/src/main/java/com/yourname/sellplugin/gui/SellAllGUI.java b/src/main/java/com/yourname/sellplugin/gui/SellAllGUI.java index 5e59a7c..01ed55c 100644 --- a/src/main/java/com/yourname/sellplugin/gui/SellAllGUI.java +++ b/src/main/java/com/yourname/sellplugin/gui/SellAllGUI.java @@ -58,12 +58,8 @@ private void populate(Player player) { if (raw.contains("{multipliers}")) { for (String cat : categories) { double m = plugin.getMultiplierManager().getEffectiveMultiplier(player, cat); - double daily = plugin.getDailyBonusManager().getDailyBonus(cat); - String suffix = daily > 0 - ? ChatColor.GOLD + " (\uD83D\uDD25 +" + String.format("%.2f", daily) + "x)" - : ""; lore.add(ChatColor.translateAlternateColorCodes('&', - "&e \u25b6 &f" + cat + ": &a" + NumberFormatter.format(m) + "x" + suffix)); + "&e \u25b6 &f" + cat + ": &a" + NumberFormatter.format(m) + "x")); } } else { lore.add(ChatColor.translateAlternateColorCodes('&', raw)); diff --git a/src/main/java/com/yourname/sellplugin/gui/SellMultiGUI.java b/src/main/java/com/yourname/sellplugin/gui/SellMultiGUI.java index b92d4f7..1acefb7 100644 --- a/src/main/java/com/yourname/sellplugin/gui/SellMultiGUI.java +++ b/src/main/java/com/yourname/sellplugin/gui/SellMultiGUI.java @@ -56,8 +56,7 @@ private ItemStack buildMultiplierIcon(String catId) { ConfigManager cfg = plugin.getConfigManager(); double multiplier = plugin.getMultiplierManager().getMultiplier(player, catId); - double dailyBonus = plugin.getDailyBonusManager().getDailyBonus(catId); - double effective = multiplier + dailyBonus; + double effective = multiplier; String separator = cfg.getText("lore-separator", "&8━━━━━━━━━━━━━━━━━━━"); @@ -65,10 +64,6 @@ private ItemStack buildMultiplierIcon(String catId) { lore.add(separator); lore.add(ChatColor.GRAY + " ▸ " + SmallCaps.convert(cfg.getText("sellmulti.earned-label", "earned: ")) + ChatColor.AQUA + String.format("%.2fx", multiplier)); - if (dailyBonus > 0) { - lore.add(ChatColor.GOLD + " ▸ \uD83D\uDD25 " + SmallCaps.convert(cfg.getText("sellmulti.daily-boost-label", "daily boost: ")) - + ChatColor.YELLOW + "+" + String.format("%.2f", dailyBonus) + "x"); - } lore.add(ChatColor.GRAY + " ▸ " + SmallCaps.convert(cfg.getText("sellmulti.effective-label", "effective: ")) + ChatColor.GREEN + String.format("%.2fx", effective)); lore.add(separator); diff --git a/src/main/java/com/yourname/sellplugin/gui/WorthGUI.java b/src/main/java/com/yourname/sellplugin/gui/WorthGUI.java index 3c4524a..4d7b3bf 100644 --- a/src/main/java/com/yourname/sellplugin/gui/WorthGUI.java +++ b/src/main/java/com/yourname/sellplugin/gui/WorthGUI.java @@ -210,7 +210,7 @@ private ItemStack resolveItemStack(String itemKey) { if (itemKey.contains(":")) { String[] parts = itemKey.split(":", 2); Material mat = Material.matchMaterial(parts[0]); - if (mat == null) return new ItemStack(Material.BARRIER); + if (mat == null || mat.isAir() || !mat.isItem()) return new ItemStack(Material.BARRIER); ItemStack item = new ItemStack(mat); ItemMeta meta = item.getItemMeta(); @@ -224,7 +224,8 @@ private ItemStack resolveItemStack(String itemKey) { return item; } Material mat = Material.matchMaterial(itemKey); - return new ItemStack(mat != null ? mat : Material.BARRIER); + if (mat == null || mat.isAir() || !mat.isItem()) return new ItemStack(Material.BARRIER); + return new ItemStack(mat); } /** Cycle to the next filter category. */ diff --git a/src/main/java/com/yourname/sellplugin/listener/WorthPacketListener.java b/src/main/java/com/yourname/sellplugin/listener/WorthPacketListener.java index 584c4af..7175014 100644 --- a/src/main/java/com/yourname/sellplugin/listener/WorthPacketListener.java +++ b/src/main/java/com/yourname/sellplugin/listener/WorthPacketListener.java @@ -16,6 +16,7 @@ import com.yourname.sellplugin.gui.ShopMainGUI; import com.yourname.sellplugin.gui.TopSellGUI; import com.yourname.sellplugin.util.NumberFormatter; +import org.bukkit.GameMode; import org.bukkit.block.DoubleChest; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; @@ -61,14 +62,44 @@ public void register() { public void onPacketSending(PacketEvent event) { Player viewer = event.getPlayer(); if (viewer == null) return; - if (!WorthPacketListener.this.plugin.getWorthVisibilityManager() - .isVisible(viewer.getUniqueId())) return; - if (!shouldDecorate(viewer)) return; + if (!worthAllowedFor(viewer)) return; + + Inventory top = viewer.getOpenInventory().getTopInventory(); + InventoryHolder holder = top.getHolder(); + boolean pluginGui = isPluginGui(holder); + int topSize = top.getSize(); + + // Window id: 0 = the player's own inventory, negative = cursor / + // direct player-slot set, positive = an open container window. + int windowId = 0; + boolean windowKnown = false; + try { + windowId = event.getPacket().getIntegers().read(0); + windowKnown = true; + } catch (Exception ignored) {} + boolean ownInventory = windowKnown && windowId <= 0; if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) { if (event.getPacket().getItemModifier().size() <= 0) return; + + boolean decorate; + if (ownInventory) { + // Player's own inventory / cursor item: always follow it, + // even while a custom GUI is open, so a held item keeps its + // worth line. + decorate = true; + } else if (pluginGui) { + // Only the mirrored player-inventory portion of a plugin + // GUI gets decorated; the GUI's own slots never do. + int slot = readSetSlotIndex(event); + decorate = slot >= topSize; + } else { + decorate = shouldDecorate(viewer); + } + if (!decorate) return; + ItemStack item = event.getPacket().getItemModifier().read(0); - ItemStack updated = addWorthLore(event.getPlayer(), item); + ItemStack updated = addWorthLore(viewer, item); if (updated != item) { event.getPacket().getItemModifier().write(0, updated); } @@ -79,12 +110,30 @@ public void onPacketSending(PacketEvent event) { List items = event.getPacket().getItemListModifier().read(0); if (items == null || items.isEmpty()) return; + // Index of the first slot in this packet that belongs to the + // player's own inventory (everything from here on is decorated). + int playerStart; + if (ownInventory) { + playerStart = 0; + } else if (pluginGui) { + playerStart = topSize; + } else if (shouldDecorate(viewer)) { + playerStart = 0; + } else { + return; + } + boolean changed = false; List updatedItems = new ArrayList<>(items.size()); - for (ItemStack item : items) { - ItemStack updated = addWorthLore(event.getPlayer(), item); - updatedItems.add(updated); - changed |= updated != item; + for (int i = 0; i < items.size(); i++) { + ItemStack item = items.get(i); + if (i >= playerStart) { + ItemStack updated = addWorthLore(viewer, item); + updatedItems.add(updated); + changed |= updated != item; + } else { + updatedItems.add(item); + } } if (changed) { @@ -225,6 +274,36 @@ private boolean isBlank(String line) { return true; } + /** + * Whether the worth line may be injected for this viewer at all. Respects the + * per-player visibility toggle and hides the line from creative-mode players + * (whose clients echo lore back to the server, baking it into real items) + * unless explicitly enabled via {@code worth.show-in-creative}. + */ + private boolean worthAllowedFor(Player viewer) { + if (!plugin.getConfigManager().isWorthEnabled()) return false; + if (!plugin.getWorthVisibilityManager().isVisible(viewer.getUniqueId())) return false; + if (viewer.getGameMode() == GameMode.CREATIVE + && !plugin.getConfigManager().isWorthShownInCreative()) return false; + return true; + } + + /** Reads the slot index from a SET_SLOT packet, or -1 if it can't be read. */ + private int readSetSlotIndex(PacketEvent event) { + try { + if (event.getPacket().getShorts().size() > 0) { + return event.getPacket().getShorts().read(0); + } + } catch (Exception ignored) {} + try { + // Some mappings expose the slot as the 3rd integer (windowId, stateId, slot). + if (event.getPacket().getIntegers().size() > 2) { + return event.getPacket().getIntegers().read(2); + } + } catch (Exception ignored) {} + return -1; + } + private boolean shouldDecorate(Player player) { Inventory topInventory = player.getOpenInventory().getTopInventory(); InventoryHolder holder = topInventory.getHolder(); diff --git a/src/main/java/com/yourname/sellplugin/listener/WorthRefreshListener.java b/src/main/java/com/yourname/sellplugin/listener/WorthRefreshListener.java index 6f00e47..74ced43 100644 --- a/src/main/java/com/yourname/sellplugin/listener/WorthRefreshListener.java +++ b/src/main/java/com/yourname/sellplugin/listener/WorthRefreshListener.java @@ -6,6 +6,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.player.PlayerJoinEvent; @@ -41,6 +42,19 @@ public void onClose(InventoryCloseEvent event) { } } + /** + * Picking an item onto the cursor and putting it back down are ordinary + * clicks; without a redraw the moved item (and the cursor item) come through + * bare. Re-sending the inventory the tick after any click keeps the worth + * line visible while items are held and replaced. + */ + @EventHandler + public void onClick(InventoryClickEvent event) { + if (event.getWhoClicked() instanceof Player player) { + refresh(player); + } + } + @EventHandler public void onJoin(PlayerJoinEvent event) { refresh(event.getPlayer()); diff --git a/src/main/java/com/yourname/sellplugin/manager/ConfigManager.java b/src/main/java/com/yourname/sellplugin/manager/ConfigManager.java index c78c9dc..b40a8f6 100644 --- a/src/main/java/com/yourname/sellplugin/manager/ConfigManager.java +++ b/src/main/java/com/yourname/sellplugin/manager/ConfigManager.java @@ -15,15 +15,40 @@ public ConfigManager(SellPlugin plugin) { this.plugin = plugin; } - // ---- Daily bonus --------------------------------------------------------- - /** Flat multiplier amount added to a boosted category's multiplier for the day. */ - public double getDailyBonusAmount() { - return plugin.getConfig().getDouble("daily-bonus.bonus-amount", 0.4); + // ---- Feature toggles ----------------------------------------------------- + /** + * Generic feature switch under the {@code features.} section. Every + * major piece of the plugin can be turned off here without touching code. + */ + public boolean isFeatureEnabled(String key, boolean def) { + return plugin.getConfig().getBoolean("features." + key, def); + } + + public boolean isMultipliersEnabled() { return isFeatureEnabled("multipliers", true); } + public boolean isEnchantmentPricingEnabled(){ return isFeatureEnabled("enchantment-pricing", true); } + public boolean isShulkerSellingEnabled() { return isFeatureEnabled("shulker-selling", true); } + public boolean isProgressGuiEnabled() { return isFeatureEnabled("progress-gui", true); } + public boolean isTopSellEnabled() { return isFeatureEnabled("top-sell", true); } + public boolean isActionBarEnabled() { return isFeatureEnabled("action-bar", true); } + + // ---- Enchantment pricing ------------------------------------------------- + /** Base value added per enchantment level for enchantments not explicitly listed. */ + public double getEnchantDefaultValuePerLevel() { + return plugin.getConfig().getDouble("enchantments.default-value-per-level", 50.0); } - /** Number of categories to boost per day. */ - public int getDailyBoostedCount() { - return plugin.getConfig().getInt("daily-bonus.boosted-count", 2); + /** Factor the price is multiplied by for each distinct enchantment on the item. */ + public double getEnchantMultiplierPerEnchantment() { + return plugin.getConfig().getDouble("enchantments.multiplier-per-enchantment", 1.1); + } + + /** + * Value added per level for a specific enchantment key (e.g. "sharpness"), + * falling back to {@link #getEnchantDefaultValuePerLevel()} when unlisted. + */ + public double getEnchantValue(String enchantKey) { + return plugin.getConfig().getDouble("enchantments.values." + enchantKey, + getEnchantDefaultValuePerLevel()); } // ---- Multiplier ------------------------------------------------------- @@ -89,10 +114,25 @@ public boolean isWorthEnabled() { return plugin.getConfig().getBoolean("worth.enabled", true); } + /** + * Whether to inject the worth line for players in creative mode. Creative + * clients echo whatever lore they are shown back to the server, which bakes + * the line into the real item and causes duplicates — so this defaults to + * false. Only turn it on if you understand that trade-off. + */ + public boolean isWorthShownInCreative() { + return plugin.getConfig().getBoolean("worth.show-in-creative", false); + } + public String getWorthFormat() { return color(plugin.getConfig().getString("worth.format", "&7Worth &a&l${worth}")); } + /** Config schema version, used by the auto-migrator. 0 = pre-versioning. */ + public int getConfigVersion() { + return plugin.getConfig().getInt("config-version", 0); + } + // ---- SellAll GUI (simple /sellall GUI) -------------------------------- public String getSellAllGuiTitle() { return color(plugin.getConfig().getString("sell-all-gui.title", "&8&lSell All Items")); diff --git a/src/main/java/com/yourname/sellplugin/manager/ConfigMigrator.java b/src/main/java/com/yourname/sellplugin/manager/ConfigMigrator.java new file mode 100644 index 0000000..6248502 --- /dev/null +++ b/src/main/java/com/yourname/sellplugin/manager/ConfigMigrator.java @@ -0,0 +1,103 @@ +package com.yourname.sellplugin.manager; + +import com.yourname.sellplugin.SellPlugin; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * Automatically brings an existing {@code config.yml} up to date whenever the + * bundled schema version is newer than the one on disk (or the file has no + * version at all, which is treated as "very old"). + * + *

Before changing anything it writes a timestamped backup next to the config + * so nothing is ever lost. Migration then: + *

    + *
  • adds any options the user is missing (copied from the bundled defaults),
  • + *
  • removes options that no longer exist, and
  • + *
  • stamps the current schema version.
  • + *
+ * + *

Note: rewriting the file through Bukkit strips hand-written comments. The + * pre-migration backup keeps the original (comments and all) intact. + */ +public class ConfigMigrator { + + /** Bump this whenever the bundled config.yml gains or drops options. */ + public static final int CURRENT_VERSION = 1; + + /** Keys that used to exist but have been removed from the plugin. */ + private static final List OBSOLETE_KEYS = List.of( + "daily-bonus" + ); + + private final SellPlugin plugin; + + public ConfigMigrator(SellPlugin plugin) { + this.plugin = plugin; + } + + /** Runs migration if needed. Safe to call every startup. */ + public void migrate() { + File configFile = new File(plugin.getDataFolder(), "config.yml"); + // saveDefaultConfig() runs before us, so a brand-new install already has + // the current file – nothing to migrate. + if (!configFile.exists()) return; + + FileConfiguration config = plugin.getConfig(); + int version = config.getInt("config-version", 0); + if (version >= CURRENT_VERSION) return; + + plugin.getLogger().info("Old config detected (version " + version + + "); migrating to version " + CURRENT_VERSION + "."); + + backup(configFile, version); + + // Merge any missing options from the bundled defaults. + try (InputStream defStream = plugin.getResource("config.yml")) { + if (defStream != null) { + YamlConfiguration defaults = YamlConfiguration.loadConfiguration( + new InputStreamReader(defStream, StandardCharsets.UTF_8)); + config.setDefaults(defaults); + config.options().copyDefaults(true); + } + } catch (IOException e) { + plugin.getLogger().warning("Could not read bundled config defaults: " + e.getMessage()); + } + + // Drop options that no longer exist. + for (String key : OBSOLETE_KEYS) { + config.set(key, null); + } + + // Stamp the new version and persist. + config.set("config-version", CURRENT_VERSION); + plugin.saveConfig(); + + plugin.getLogger().info("Config migration complete. A backup of your old " + + "config was saved in the plugin folder."); + } + + private void backup(File configFile, int version) { + try { + String stamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()); + File backup = new File(plugin.getDataFolder(), + "config-backup-v" + version + "-" + stamp + ".yml"); + Files.copy(configFile.toPath(), backup.toPath(), StandardCopyOption.REPLACE_EXISTING); + plugin.getLogger().info("Backed up existing config to " + backup.getName() + "."); + } catch (IOException e) { + plugin.getLogger().warning("Could not back up config.yml before migrating: " + + e.getMessage()); + } + } +} diff --git a/src/main/java/com/yourname/sellplugin/manager/DailyBonusManager.java b/src/main/java/com/yourname/sellplugin/manager/DailyBonusManager.java deleted file mode 100644 index e07a601..0000000 --- a/src/main/java/com/yourname/sellplugin/manager/DailyBonusManager.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.yourname.sellplugin.manager; - -import com.yourname.sellplugin.SellPlugin; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.time.LocalDate; -import java.util.*; - -/** - * Manages daily category bonuses. - *

- * Each day a configurable number of random categories receive a bonus multiplier - * that is added (not multiplied) to the player's current earned multiplier. - * Bonuses are persisted to {@code daily-bonus.yml} and automatically re-rolled - * when a new day is first detected. - */ -public class DailyBonusManager { - - private final SellPlugin plugin; - private final File bonusFile; - - /** The calendar date (ISO string) for the currently stored bonuses. */ - private String currentDate = ""; - - /** Categories that have the active daily bonus. */ - private Set boostedCategories = new HashSet<>(); - - public DailyBonusManager(SellPlugin plugin) { - this.plugin = plugin; - this.bonusFile = new File(plugin.getDataFolder(), "daily-bonus.yml"); - load(); - } - - // ── Load / save ────────────────────────────────────────────────────────── - - private void load() { - YamlConfiguration config = YamlConfiguration.loadConfiguration(bonusFile); - String storedDate = config.getString("date", ""); - String today = LocalDate.now().toString(); - - if (!today.equals(storedDate)) { - rollNewBonuses(today); - } else { - currentDate = storedDate; - boostedCategories = new HashSet<>(config.getStringList("boosted-categories")); - } - } - - private void save() { - YamlConfiguration config = new YamlConfiguration(); - config.set("date", currentDate); - config.set("boosted-categories", new ArrayList<>(boostedCategories)); - try { - config.save(bonusFile); - } catch (IOException e) { - plugin.getLogger().severe("Failed to save daily-bonus.yml: " + e.getMessage()); - } - } - - // ── Daily roll ─────────────────────────────────────────────────────────── - - /** - * Picks a new set of boosted categories for {@code date} and persists it. - */ - private void rollNewBonuses(String date) { - currentDate = date; - List allCategories = new ArrayList<>(plugin.getPriceManager().getCategories()); - Collections.shuffle(allCategories); - - int count = plugin.getConfigManager().getDailyBoostedCount(); - boostedCategories = new HashSet<>(); - for (int i = 0; i < Math.min(count, allCategories.size()); i++) { - boostedCategories.add(allCategories.get(i)); - } - save(); - - plugin.getLogger().info("Daily bonus re-rolled for " + date - + " → boosted: " + boostedCategories); - } - - // ── Public API ─────────────────────────────────────────────────────────── - - /** - * Returns the flat multiplier bonus that should be added to the - * player's earned multiplier for the given category today. - * Returns 0.0 if the category is not boosted. - */ - public double getDailyBonus(String category) { - checkAndRollIfNeeded(); - return boostedCategories.contains(category) - ? plugin.getConfigManager().getDailyBonusAmount() - : 0.0; - } - - /** - * Returns an unmodifiable view of the currently boosted category IDs. - * Triggers a lazy reset if necessary. - */ - public Set getBoostedCategories() { - checkAndRollIfNeeded(); - return Collections.unmodifiableSet(boostedCategories); - } - - /** Rolls new bonuses if the current stored date no longer matches today. */ - private void checkAndRollIfNeeded() { - String today = LocalDate.now().toString(); - if (!today.equals(currentDate)) { - rollNewBonuses(today); - } - } -} diff --git a/src/main/java/com/yourname/sellplugin/manager/MultiplierManager.java b/src/main/java/com/yourname/sellplugin/manager/MultiplierManager.java index eb2c6eb..45b8aed 100644 --- a/src/main/java/com/yourname/sellplugin/manager/MultiplierManager.java +++ b/src/main/java/com/yourname/sellplugin/manager/MultiplierManager.java @@ -87,6 +87,7 @@ public void saveAll() { // The returned value is stepped: 1.0, 1.1, 1.2, … up to maxMultiplier. // ----------------------------------------------------------------------- public double getMultiplier(Player p, String category) { + if (!plugin.getConfigManager().isMultipliersEnabled()) return 1.0; if (!cache.containsKey(p.getUniqueId())) loadPlayer(p.getUniqueId()); double moneyEarned = cache.get(p.getUniqueId()).getOrDefault(category, 0.0); @@ -111,15 +112,12 @@ public double getMultiplier(Player p, String category) { } /** - * Returns the player's total effective multiplier for a category, - * which is the earned multiplier plus today's daily bonus (if any). - * Use this for all sell calculations and display. + * Returns the player's effective multiplier for a category. Kept as a + * distinct method (rather than inlining {@link #getMultiplier}) so callers + * don't need to change; the daily-bonus component was removed. */ public double getEffectiveMultiplier(Player p, String category) { - double earned = getMultiplier(p, category); - DailyBonusManager dbm = plugin.getDailyBonusManager(); - if (dbm == null) return earned; - return earned + dbm.getDailyBonus(category); + return getMultiplier(p, category); } /** diff --git a/src/main/java/com/yourname/sellplugin/manager/SellManager.java b/src/main/java/com/yourname/sellplugin/manager/SellManager.java index cb528f4..9c18e92 100644 --- a/src/main/java/com/yourname/sellplugin/manager/SellManager.java +++ b/src/main/java/com/yourname/sellplugin/manager/SellManager.java @@ -5,9 +5,12 @@ import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.block.ShulkerBox; +import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BlockStateMeta; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; import java.util.Collections; import java.util.HashMap; @@ -36,7 +39,7 @@ public SellResult sellAll(Player player) { ItemStack item = player.getInventory().getItem(i); if (item == null || item.getType() == Material.AIR) continue; - if (isShulkerBox(item)) { + if (sellShulker(item)) { ShulkerSellData data = sellShulkerContents(player, item, null, null); totalEarned += data.earned; totalItems += data.items; @@ -53,7 +56,7 @@ public SellResult sellAll(Player player) { String cat = plugin.getPriceManager().getCategory(key); double mult = plugin.getMultiplierManager().getEffectiveMultiplier(player, cat); int amount = item.getAmount(); - double earned = base * mult * amount; + double earned = enchantedUnitPrice(item, base) * mult * amount; totalEarned += earned; totalItems += amount; categoryEarnings.merge(cat, earned, Double::sum); @@ -76,7 +79,7 @@ public SellResult sellCategory(Player player, String category) { ItemStack item = player.getInventory().getItem(i); if (item == null || item.getType() == Material.AIR) continue; - if (isShulkerBox(item)) { + if (sellShulker(item)) { ShulkerSellData data = sellShulkerContents(player, item, category, null); totalEarned += data.earned; totalItems += data.items; @@ -95,7 +98,7 @@ public SellResult sellCategory(Player player, String category) { double mult = plugin.getMultiplierManager().getEffectiveMultiplier(player, cat); int amount = item.getAmount(); - double earned = base * mult * amount; + double earned = enchantedUnitPrice(item, base) * mult * amount; totalEarned += earned; totalItems += amount; categoryEarnings.merge(cat, earned, Double::sum); @@ -124,7 +127,7 @@ public SellResult sellItemType(Player player, String itemKey) { ItemStack item = player.getInventory().getItem(i); if (item == null || item.getType() == Material.AIR) continue; - if (isShulkerBox(item)) { + if (sellShulker(item)) { ShulkerSellData data = sellShulkerContents(player, item, null, itemKey); totalEarned += data.earned; totalItems += data.items; @@ -136,7 +139,7 @@ public SellResult sellItemType(Player player, String itemKey) { if (!itemKey.equalsIgnoreCase(key)) continue; int amount = item.getAmount(); - double earned = base * mult * amount; + double earned = enchantedUnitPrice(item, base) * mult * amount; totalEarned += earned; totalItems += amount; categoryEarnings.merge(cat, earned, Double::sum); @@ -155,7 +158,7 @@ public SellPreview previewSellAll(Player player) { for (ItemStack item : player.getInventory().getStorageContents()) { if (item == null || item.getType() == Material.AIR) continue; - if (isShulkerBox(item)) { + if (sellShulker(item)) { ShulkerSellData data = peekShulkerContents(player, item, null, null); itemCount += data.items; value += data.earned; @@ -169,7 +172,7 @@ public SellPreview previewSellAll(Player player) { String cat = plugin.getPriceManager().getCategory(key); double mult = plugin.getMultiplierManager().getEffectiveMultiplier(player, cat); itemCount += item.getAmount(); - value += base * mult * item.getAmount(); + value += enchantedUnitPrice(item, base) * mult * item.getAmount(); } return new SellPreview(itemCount, value); } @@ -182,7 +185,7 @@ public int countCategoryItems(Player player, String category) { for (ItemStack item : player.getInventory().getStorageContents()) { if (item == null || item.getType() == Material.AIR) continue; - if (isShulkerBox(item)) { + if (sellShulker(item)) { total += peekShulkerContents(player, item, category, null).items; continue; } @@ -199,11 +202,14 @@ public int countCategoryItems(Player player, String category) { // Calculate value of sellable items in a category (with multiplier) // --------------------------------------------------------------- public double calculateCategoryValue(Player player, String category) { + // The multiplier depends only on the category, which is fixed here, so + // look it up once instead of once per matching item. + double mult = plugin.getMultiplierManager().getEffectiveMultiplier(player, category); double total = 0.0; for (ItemStack item : player.getInventory().getStorageContents()) { if (item == null || item.getType() == Material.AIR) continue; - if (isShulkerBox(item)) { + if (sellShulker(item)) { total += peekShulkerContents(player, item, category, null).earned; continue; } @@ -214,8 +220,7 @@ public double calculateCategoryValue(Player player, String category) { if (!category.equalsIgnoreCase(cat)) continue; double base = plugin.getPriceManager().getPrice(key); if (base <= 0) continue; - double mult = plugin.getMultiplierManager().getEffectiveMultiplier(player, cat); - total += base * mult * item.getAmount(); + total += enchantedUnitPrice(item, base) * mult * item.getAmount(); } return total; } @@ -235,7 +240,7 @@ public double calculateItemWorth(Player player, ItemStack item) { String category = plugin.getPriceManager().getCategory(key); double multiplier = plugin.getMultiplierManager().getEffectiveMultiplier(player, category); - return base * multiplier * item.getAmount(); + return enchantedUnitPrice(item, base) * multiplier * item.getAmount(); } // --------------------------------------------------------------- @@ -269,10 +274,12 @@ private SellResult finalizeSell(Player player, double totalEarned, int totalItem public void sendSellNotification(Player player, double amount, int itemCount) { String formatted = NumberFormatter.format(amount); - // Action bar: always shown – "+$amount" - String actionBarText = plugin.getConfigManager().getText("action-bar", "&a+${amount}") - .replace("{amount}", formatted); - player.sendActionBar(actionBarText); + // Action bar: "+$amount" (toggleable) + if (plugin.getConfigManager().isActionBarEnabled()) { + String actionBarText = plugin.getConfigManager().getText("action-bar", "&a+${amount}") + .replace("{amount}", formatted); + player.sendActionBar(actionBarText); + } // Title notification: only if enabled in config if (plugin.getConfigManager().isTitleNotificationEnabled()) { @@ -307,9 +314,58 @@ public void sendSellNotification(Player player, double amount, int itemCount) { // Shulker box helpers // --------------------------------------------------------------- - /** Returns true if the item is any colour of shulker box. */ + /** + * Returns true if the item is a shulker box of any colour, including the + * uncoloured {@code SHULKER_BOX} (whose name does not end with + * {@code _SHULKER_BOX}, which is why it used to be skipped). + */ public static boolean isShulkerBox(ItemStack item) { - return item != null && item.getType().name().endsWith("_SHULKER_BOX"); + if (item == null) return false; + Material type = item.getType(); + return type == Material.SHULKER_BOX || type.name().endsWith("_SHULKER_BOX"); + } + + /** True when the item should be dived into and its contents sold. */ + private boolean sellShulker(ItemStack item) { + return plugin.getConfigManager().isShulkerSellingEnabled() && isShulkerBox(item); + } + + // --------------------------------------------------------------- + // Enchantment-aware unit pricing + // --------------------------------------------------------------- + + /** + * Adjusts a single item's base price for its enchantments: each + * enchantment's configured value (× its level) is added to the base, then + * the total is multiplied by a factor (default 1.1) for every distinct + * enchantment on the item. Unenchanted items return the base unchanged. + */ + public double enchantedUnitPrice(ItemStack item, double base) { + if (item == null || !plugin.getConfigManager().isEnchantmentPricingEnabled()) return base; + + Map enchants = collectEnchantments(item); + if (enchants.isEmpty()) return base; + + double added = 0.0; + int count = 0; + for (Map.Entry e : enchants.entrySet()) { + String key = e.getKey().getKey().getKey(); // e.g. "sharpness" + added += plugin.getConfigManager().getEnchantValue(key) * e.getValue(); + count++; + } + + double factor = plugin.getConfigManager().getEnchantMultiplierPerEnchantment(); + return (base + added) * Math.pow(factor, count); + } + + /** Merges an item's applied enchantments with any stored (book) enchantments. */ + private Map collectEnchantments(ItemStack item) { + Map merged = new HashMap<>(item.getEnchantments()); + ItemMeta meta = item.getItemMeta(); + if (meta instanceof EnchantmentStorageMeta storage) { + storage.getStoredEnchants().forEach((ench, lvl) -> merged.merge(ench, lvl, Math::max)); + } + return merged; } /** @@ -343,7 +399,7 @@ public ShulkerSellData sellShulkerContents(Player player, ItemStack shulkerItem, double mult = plugin.getMultiplierManager().getEffectiveMultiplier(player, cat); int amount = inner.getAmount(); - double earned = base * mult * amount; + double earned = enchantedUnitPrice(inner, base) * mult * amount; totalEarned += earned; totalItems += amount; categoryEarnings.merge(cat, earned, Double::sum); @@ -386,9 +442,10 @@ private ShulkerSellData peekShulkerContents(Player player, ItemStack shulkerItem double mult = plugin.getMultiplierManager().getEffectiveMultiplier(player, cat); int amount = inner.getAmount(); - totalEarned += base * mult * amount; + double earned = enchantedUnitPrice(inner, base) * mult * amount; + totalEarned += earned; totalItems += amount; - categoryEarnings.merge(cat, base * mult * amount, Double::sum); + categoryEarnings.merge(cat, earned, Double::sum); } return new ShulkerSellData(totalEarned, totalItems, categoryEarnings); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 6beef97..a4c32b0 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -2,6 +2,54 @@ # SELL PLUGIN CONFIG # # ============================================================ # +# Internal schema version. DO NOT edit this by hand – the plugin uses it to +# auto-migrate old configs (adding any new options you're missing) and it makes +# a timestamped backup of your old file first. If this key is missing the plugin +# assumes an old config and migrates anyway. +config-version: 1 + +# ---- Feature toggles --------------------------------------- # +# Turn any major part of the plugin on or off. Everything defaults to on. +features: + # Category sell multipliers (earn more the more you sell in a category). + # When off, everything sells at a flat 1.0x. + multipliers: true + # Add extra value for enchantments (see the "enchantments" section below). + enchantment-pricing: true + # Allow selling the contents of shulker boxes. + shulker-selling: true + # The /sellmulti multiplier / progress-bar GUI. + progress-gui: true + # The /topsell leaderboard. + top-sell: true + # The "+$amount" action-bar pop-up shown after a sale. + action-bar: true + +# ---- Enchantment pricing ----------------------------------- # +# When enchantment-pricing is enabled, an item's price is calculated as: +# (base price + sum of each enchantment's value × its level) +# × (multiplier-per-enchantment ^ number of distinct enchantments) +# Example: a base $100 sword with Sharpness V and Unbreaking III, using the +# defaults below (value 50/level, factor 1.1): +# added = 50×5 + 50×3 = 400 → base+added = 500 +# factor = 1.1 ^ 2 distinct enchants = 1.21 +# price = 500 × 1.21 = $605 +enchantments: + # Value added per enchantment level for any enchantment not listed below. + default-value-per-level: 50.0 + # The price is multiplied by this for EACH distinct enchantment on the item. + multiplier-per-enchantment: 1.1 + # Per-enchantment overrides. Keys are the vanilla enchantment ids (lowercase). + # Anything not listed uses default-value-per-level above. + values: + sharpness: 60.0 + protection: 40.0 + mending: 300.0 + unbreaking: 30.0 + efficiency: 40.0 + fortune: 150.0 + silk_touch: 200.0 + # Money required to unlock the first multiplier level (1.1x). # Each subsequent level costs (previous cost × multiplier). # e.g. start-multiplier: 1000, multiplier: 1.6 @@ -12,18 +60,11 @@ multiplier: 1.6 # Maximum multiplier cap (used for progress bar display) max-multiplier: 3.0 -# ---- Daily Category Bonus ------------------------------------------ # -# Every day a random selection of categories receive a flat bonus added -# to the player's earned multiplier (not multiplied). -# e.g. bonus-amount: 0.4 means fishing at 1.2x earns 1.6x that day. -daily-bonus: - # How many categories get the daily boost (randomly chosen) - boosted-count: 2 - # Flat amount added to the multiplier for boosted categories - bonus-amount: 0.4 - -# ---- Progress bar (snake path) colours ------------------------------------ # -# Must be valid glass-pane Material names. +# ---- Progress bar (snake path) --------------------------------------------- # +# The /sellmulti → category progress menu draws a "snake" of milestone nodes. +# Each node is a block that changes appearance based on its state. Any valid +# Material works here (glass panes, wool, concrete, ores, …) – customise freely. +# The text on every node is fully customisable under messages.category-progress. progress-bar: completed-color: "LIME_STAINED_GLASS_PANE" inprogress-color: "YELLOW_STAINED_GLASS_PANE" @@ -63,6 +104,11 @@ filler-block: "BLACK_STAINED_GLASS_PANE" worth: enabled: true format: "&7Worth &a&l${worth}" + # Show the worth line to players in CREATIVE mode? + # Leave this false. Creative clients echo item lore back to the server, which + # bakes the worth line into the real item and causes duplicate lines. Only + # enable if you fully understand that trade-off. + show-in-creative: false # ---- Main Shop GUI (/sell) ---------------------------------- # # This opens the 9x6 sell GUI where players place items to sell. @@ -244,6 +290,8 @@ messages: sold-items: "&aYou sold &e{amount} &aitems for &e${price}&a!" nothing-to-sell: "&cYou have no sellable items in your inventory." economy-error: "&cAn economy error occurred. Please contact an admin." + # Shown when a player uses a command for a feature that's toggled off. + feature-disabled: "&cThis feature is currently disabled." # ── Commands / console messages ─────────────────────────────── player-only-command: "&cOnly players can use this command." @@ -276,7 +324,6 @@ messages: sellmulti: title: "&8&lMultipliers" earned-label: "earned: " - daily-boost-label: "daily boost: " effective-label: "effective: " click-to-view: "click to view progress" @@ -292,10 +339,6 @@ messages: # ── Category Progress GUI (multiplier snake path) ───────────── category-progress: - daily-boost-title: "&6&l\uD83D\uDD25 Daily Boost Active!" - daily-boost-bonus-label: "bonus: " - daily-boost-effective-label: "effective: " - daily-boost-resets: "resets at midnight." back-lore: "return to the main menu." node-multiplier-suffix: "multiplier" node-status-label: "status: " diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index fee4b92..7cdae6e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: SellPlugin -version: 2.2.0 +version: 2.3.0 main: com.yourname.sellplugin.SellPlugin api-version: 1.13 folia-supported: true