From 06a4d1df484cf275990be3d5c0d773ca3f12d859 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 11:02:24 +0000 Subject: [PATCH] Add ServerShop plugin: complete Maven project with GUI, pricing, economy, i18n Co-authored-by: henriquescrrrr <192057244+henriquescrrrr@users.noreply.github.com> --- .gitignore | 17 + ServerShop/README.md | 166 +++++ ServerShop/pom.xml | 104 +++ .../pt/henrique/servershop/ServerShop.java | 127 ++++ .../servershop/category/CategoryRegistry.java | 138 ++++ .../servershop/category/ShopCategory.java | 64 ++ .../servershop/command/ShopCommand.java | 74 +++ .../servershop/config/ConfigManager.java | 145 ++++ .../servershop/config/MessageManager.java | 178 +++++ .../servershop/economy/EconomyManager.java | 139 ++++ .../henrique/servershop/gui/CategoryGui.java | 219 ++++++ .../henrique/servershop/gui/GuiManager.java | 226 +++++++ .../servershop/gui/ItemDetailGui.java | 279 ++++++++ .../henrique/servershop/gui/MainShopGui.java | 169 +++++ .../servershop/gui/SearchResultsGui.java | 193 ++++++ .../pt/henrique/servershop/gui/ShopGui.java | 39 ++ .../servershop/listener/GuiListener.java | 124 ++++ .../servershop/pricing/ItemPrice.java | 64 ++ .../servershop/pricing/PricingService.java | 195 ++++++ .../servershop/service/ShopService.java | 277 ++++++++ .../transaction/TransactionLogger.java | 117 ++++ .../henrique/servershop/util/ItemBuilder.java | 220 ++++++ .../pt/henrique/servershop/util/TextUtil.java | 85 +++ ServerShop/src/main/resources/config.yml | 97 +++ ServerShop/src/main/resources/lang/en_US.yml | 138 ++++ ServerShop/src/main/resources/lang/pt_PT.yml | 138 ++++ ServerShop/src/main/resources/plugin.yml | 39 ++ ServerShop/src/main/resources/prices.yml | 627 ++++++++++++++++++ 28 files changed, 4398 insertions(+) create mode 100644 .gitignore create mode 100644 ServerShop/README.md create mode 100644 ServerShop/pom.xml create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/ServerShop.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/category/CategoryRegistry.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/category/ShopCategory.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/command/ShopCommand.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/config/ConfigManager.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/config/MessageManager.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/economy/EconomyManager.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/gui/CategoryGui.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/gui/GuiManager.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/gui/ItemDetailGui.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/gui/MainShopGui.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/gui/SearchResultsGui.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/gui/ShopGui.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/listener/GuiListener.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/pricing/ItemPrice.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/pricing/PricingService.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/service/ShopService.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/transaction/TransactionLogger.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/util/ItemBuilder.java create mode 100644 ServerShop/src/main/java/pt/henrique/servershop/util/TextUtil.java create mode 100644 ServerShop/src/main/resources/config.yml create mode 100644 ServerShop/src/main/resources/lang/en_US.yml create mode 100644 ServerShop/src/main/resources/lang/pt_PT.yml create mode 100644 ServerShop/src/main/resources/plugin.yml create mode 100644 ServerShop/src/main/resources/prices.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..904d3e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Maven build artifacts +target/ +dependency-reduced-pom.xml + +# IDE files +.idea/ +*.iml +.DS_Store +*.class + +# OS files +Thumbs.db +.DS_Store + +# ServerShop build artifacts +ServerShop/target/ +ServerShop/dependency-reduced-pom.xml diff --git a/ServerShop/README.md b/ServerShop/README.md new file mode 100644 index 0000000..c3a22d7 --- /dev/null +++ b/ServerShop/README.md @@ -0,0 +1,166 @@ +# ServerShop + +A professional **server-run global market** plugin for **Paper/Purpur 1.21.1** servers. This plugin provides a GUI-driven shop where players can buy and sell all game items at configurable prices. + +## Overview + +ServerShop is designed to work alongside the [CommunityMarket](../README.md) player-to-player marketplace. It features a **large spread** between buy and sell prices, ensuring that player-to-player trading via CommunityMarket remains the preferred option for best deals. + +### How the Spread Works + +- **Buy Price**: What the server charges when selling to players (higher) +- **Sell Price**: What the server pays when buying from players (lower) +- **Default Spread**: 75% (`sellMultiplier = 0.25`) + +For example, a Diamond with a buy price of `$100`: +- Server sells to player: **$100** +- Server buys from player: **$25** (100 × 0.25) +- Players trading via CommunityMarket might sell for **$60-80**, making it better for both buyer and seller + +## Features + +- **GUI-Only Interface**: All interactions through intuitive inventory GUIs +- **Category System**: Items organized into 10 categories (Blocks, Ores, Farming, Food, Mob Drops, Redstone, Decoration, Tools, Combat, Brewing, Misc) +- **Search**: Find items by name via chat input +- **Pagination**: Browse large categories with page navigation +- **Quantity Selection**: Adjust amounts with -32/-16/-8/-1/+1/+8/+16/+32 and MIN/MAX buttons +- **Sell Hand / Sell Inventory**: Quick-sell shortcuts from the main menu +- **Vault Economy**: Full integration with any Vault-compatible economy plugin +- **Multi-Language**: English (en_US) and Portuguese (pt_PT) included +- **Transaction Logging**: SQLite database for tracking all transactions +- **Anti-Exploit**: Comprehensive protection against item duplication and GUI exploits +- **Configurable Pricing**: Per-category multipliers, per-item overrides, buy/sell toggles + +## Requirements + +- **Paper** or **Purpur** 1.21.1+ +- **Java 21** +- **Vault** + an economy provider (e.g., EssentialsX, CMI) + +## Installation + +1. Download the `ServerShop-1.0.0.jar` +2. Place it in your server's `plugins/` folder +3. Make sure **Vault** and an economy plugin are installed +4. Start/restart the server +5. Edit configuration files in `plugins/ServerShop/` + +## Commands + +| Command | Description | Permission | +|---------|-------------|------------| +| `/shop` | Opens the server shop GUI | `servershop.use` | +| `/servershop` | Alias for `/shop` | `servershop.use` | +| `/shop reload` | Reloads all configuration | `servershop.admin.reload` | + +## Permissions + +| Permission | Description | Default | +|------------|-------------|---------| +| `servershop.use` | Access the server shop | `true` | +| `servershop.buy` | Buy items from the shop | `true` | +| `servershop.sell` | Sell items to the shop | `true` | +| `servershop.sell.hand` | Use the "sell hand" feature | `true` | +| `servershop.sell.inventory` | Use the "sell inventory" feature | `true` | +| `servershop.admin` | Admin permissions | `op` | +| `servershop.admin.reload` | Reload configuration | `op` | + +## Configuration + +### config.yml +Main plugin settings including: +- Language selection +- Economy settings (symbol, decimal places, sell tax) +- Pricing defaults (global sell multiplier) +- Shop behavior (full inventory action, sell hand/inventory toggles) +- GUI settings (titles, filler items, sounds) +- Transaction logging + +### prices.yml +Item pricing configuration with: +- **Categories**: Define categories with icon, sell multiplier, and item lists +- **Items**: Each item has a buy price; sell price is auto-calculated +- **Overrides**: Per-item buy/sell price overrides and enable/disable toggles + +#### Adding/Editing Prices + +```yaml +categories: + MyCategory: + icon: DIAMOND # Category icon material + sell-multiplier: 0.30 # Override global multiplier + buy-enabled: true + sell-enabled: true + items: + DIAMOND: 100.0 # Buy price = $100, Sell = $100 * 0.30 = $30 + EMERALD: 50.0 # Buy price = $50, Sell = $50 * 0.30 = $15 +``` + +#### Per-Item Overrides + +```yaml +overrides: + ELYTRA: + sell: 500.0 # Override auto-calculated sell price + COMMAND_BLOCK: + buy-enabled: false # Cannot be purchased + sell-enabled: false # Cannot be sold +``` + +### Language Files +Located in `lang/en_US.yml` and `lang/pt_PT.yml`. All GUI labels and messages are fully customizable. + +## Building from Source + +```bash +cd ServerShop +mvn clean package +``` + +The compiled JAR will be in `ServerShop/target/`. + +## Architecture + +``` +pt.henrique.servershop +├── ServerShop.java # Main plugin class +├── command/ +│ └── ShopCommand.java # /shop command handler +├── config/ +│ ├── ConfigManager.java # Main config (config.yml) +│ └── MessageManager.java # Language/message system +├── economy/ +│ └── EconomyManager.java # Vault economy integration +├── pricing/ +│ ├── PricingService.java # Price calculation & lookup +│ └── ItemPrice.java # Price data model +├── category/ +│ ├── CategoryRegistry.java # Category management +│ └── ShopCategory.java # Category data model +├── gui/ +│ ├── ShopGui.java # GUI interface +│ ├── GuiManager.java # GUI orchestration +│ ├── MainShopGui.java # Main menu +│ ├── CategoryGui.java # Category browser +│ ├── ItemDetailGui.java # Buy/sell detail view +│ └── SearchResultsGui.java # Search results +├── service/ +│ └── ShopService.java # Buy/sell business logic +├── transaction/ +│ └── TransactionLogger.java # SQLite transaction logging +├── listener/ +│ └── GuiListener.java # Anti-exploit GUI listener +└── util/ + ├── ItemBuilder.java # Fluent ItemStack builder + └── TextUtil.java # Text formatting utilities +``` + +## Known Limitations + +- **Special Items**: Enchanted books, potions, and other items with special metadata are excluded by default (configurable via `pricing.include-special-items`) +- **Dynamic Pricing**: v1.0 uses static, config-driven pricing only. Dynamic supply/demand pricing may be added in a future version +- **New Items**: Items added in future Minecraft updates will automatically fall into the "Misc" category if not explicitly configured + +## License + +MIT License - See [LICENSE](../LICENSE) for details. diff --git a/ServerShop/pom.xml b/ServerShop/pom.xml new file mode 100644 index 0000000..5a60f71 --- /dev/null +++ b/ServerShop/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + pt.henrique + servershop + 1.0.0 + jar + + ServerShop + A server-run global market plugin for Paper/Purpur 1.21.1 with configurable pricing and GUI-driven UX + + + 21 + 21 + UTF-8 + 1.21.1-R0.1-SNAPSHOT + + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + jitpack.io + https://jitpack.io + + + + + + + io.papermc.paper + paper-api + ${paper.version} + provided + + + + + com.github.MilkBowl + VaultAPI + 1.7.1 + provided + + + + + org.xerial + sqlite-jdbc + 3.45.1.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + 21 + 21 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + org.sqlite + pt.henrique.servershop.libs.sqlite + + + true + + + + + + + + + src/main/resources + true + + + + + diff --git a/ServerShop/src/main/java/pt/henrique/servershop/ServerShop.java b/ServerShop/src/main/java/pt/henrique/servershop/ServerShop.java new file mode 100644 index 0000000..8ab023e --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/ServerShop.java @@ -0,0 +1,127 @@ +package pt.henrique.servershop; + +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; +import pt.henrique.servershop.category.CategoryRegistry; +import pt.henrique.servershop.command.ShopCommand; +import pt.henrique.servershop.config.ConfigManager; +import pt.henrique.servershop.config.MessageManager; +import pt.henrique.servershop.economy.EconomyManager; +import pt.henrique.servershop.gui.GuiManager; +import pt.henrique.servershop.listener.GuiListener; +import pt.henrique.servershop.pricing.PricingService; +import pt.henrique.servershop.service.ShopService; +import pt.henrique.servershop.transaction.TransactionLogger; + +/** + * ServerShop - A server-run global market plugin for Paper/Purpur 1.21.1. + *

+ * Provides a GUI-driven shop where players can buy and sell all game items + * at configurable prices. Designed with a large spread between buy and sell + * prices to support the CommunityMarket player-to-player economy. + *

+ * + * @author henrique + * @version 1.0.0 + */ +public class ServerShop extends JavaPlugin { + + private ConfigManager configManager; + private MessageManager messageManager; + private EconomyManager economyManager; + private CategoryRegistry categoryRegistry; + private PricingService pricingService; + private ShopService shopService; + private GuiManager guiManager; + private TransactionLogger transactionLogger; + + @Override + public void onEnable() { + // 1. Configuration + configManager = new ConfigManager(this); + configManager.reload(); + + // 2. Language / Messages + messageManager = new MessageManager(this); + messageManager.reload(); + + // 3. Economy (Vault) + economyManager = new EconomyManager(this); + if (!economyManager.setupEconomy()) { + getLogger().severe("=============================================="); + getLogger().severe(" ServerShop requires Vault + an economy plugin!"); + getLogger().severe(" Plugin will be disabled."); + getLogger().severe("=============================================="); + getServer().getPluginManager().disablePlugin(this); + return; + } + + // 4. Categories + categoryRegistry = new CategoryRegistry(this); + categoryRegistry.reload(); + + // 5. Pricing + pricingService = new PricingService(this); + pricingService.reload(); + + // 6. Services + shopService = new ShopService(this); + + // 7. Transaction Logging + transactionLogger = new TransactionLogger(this); + transactionLogger.initialize(); + + // 8. GUI Manager + guiManager = new GuiManager(this); + + // 9. Register listeners + getServer().getPluginManager().registerEvents(guiManager, this); + getServer().getPluginManager().registerEvents(new GuiListener(this), this); + + // 10. Register commands + ShopCommand shopCommand = new ShopCommand(this); + PluginCommand cmd = getCommand("shop"); + if (cmd != null) { + cmd.setExecutor(shopCommand); + cmd.setTabCompleter(shopCommand); + } + + getLogger().info("=============================================="); + getLogger().info(" ServerShop v" + getDescription().getVersion() + " enabled!"); + getLogger().info(" Categories: " + categoryRegistry.getCategories().size()); + getLogger().info(" Items: " + pricingService.getAllPrices().size()); + getLogger().info("=============================================="); + } + + @Override + public void onDisable() { + if (transactionLogger != null) { + transactionLogger.close(); + } + getLogger().info("ServerShop disabled."); + } + + /** + * Reloads all plugin configuration, prices, and language files. + */ + public void reloadPlugin() { + configManager.reload(); + messageManager.reload(); + categoryRegistry.reload(); + pricingService.reload(); + getLogger().info("ServerShop configuration reloaded."); + } + + // ================================================================ + // Service Accessors + // ================================================================ + + public ConfigManager getConfigManager() { return configManager; } + public MessageManager getMessageManager() { return messageManager; } + public EconomyManager getEconomyManager() { return economyManager; } + public CategoryRegistry getCategoryRegistry() { return categoryRegistry; } + public PricingService getPricingService() { return pricingService; } + public ShopService getShopService() { return shopService; } + public GuiManager getGuiManager() { return guiManager; } + public TransactionLogger getTransactionLogger() { return transactionLogger; } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/category/CategoryRegistry.java b/ServerShop/src/main/java/pt/henrique/servershop/category/CategoryRegistry.java new file mode 100644 index 0000000..54a942e --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/category/CategoryRegistry.java @@ -0,0 +1,138 @@ +package pt.henrique.servershop.category; + +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import pt.henrique.servershop.ServerShop; + +import java.io.File; +import java.util.*; +import java.util.logging.Level; + +/** + * Registry that manages all shop categories and their item assignments. + * Loads from prices.yml and provides lookup methods. + */ +public class CategoryRegistry { + + private final ServerShop plugin; + private final LinkedHashMap categories = new LinkedHashMap<>(); + private final Map materialToCategory = new HashMap<>(); + + public CategoryRegistry(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Loads categories and item assignments from prices.yml. + */ + public void reload() { + categories.clear(); + materialToCategory.clear(); + + File pricesFile = new File(plugin.getDataFolder(), "prices.yml"); + if (!pricesFile.exists()) { + plugin.saveResource("prices.yml", false); + } + + FileConfiguration pricesConfig = YamlConfiguration.loadConfiguration(pricesFile); + ConfigurationSection categoriesSection = pricesConfig.getConfigurationSection("categories"); + + if (categoriesSection == null) { + plugin.getLogger().severe("No 'categories' section found in prices.yml!"); + return; + } + + double globalMultiplier = plugin.getConfigManager().getDefaultSellMultiplier(); + + for (String categoryName : categoriesSection.getKeys(false)) { + ConfigurationSection catSection = categoriesSection.getConfigurationSection(categoryName); + if (catSection == null) continue; + + // Parse category icon + Material icon; + try { + icon = Material.valueOf(catSection.getString("icon", "CHEST")); + } catch (IllegalArgumentException e) { + icon = Material.CHEST; + plugin.getLogger().warning("Invalid icon material for category " + categoryName); + } + + double sellMultiplier = catSection.getDouble("sell-multiplier", globalMultiplier); + boolean buyEnabled = catSection.getBoolean("buy-enabled", true); + boolean sellEnabled = catSection.getBoolean("sell-enabled", true); + + ShopCategory category = new ShopCategory(categoryName, icon, sellMultiplier, buyEnabled, sellEnabled); + + // Parse items + ConfigurationSection itemsSection = catSection.getConfigurationSection("items"); + if (itemsSection != null) { + for (String materialName : itemsSection.getKeys(false)) { + try { + Material material = Material.valueOf(materialName); + if (material.isAir()) continue; + category.addItem(material); + materialToCategory.put(material, categoryName); + } catch (IllegalArgumentException e) { + plugin.getLogger().log(Level.WARNING, + "Invalid material in category " + categoryName + ": " + materialName); + } + } + } + + categories.put(categoryName, category); + } + + plugin.getLogger().info("Loaded " + categories.size() + " categories with " + + materialToCategory.size() + " items."); + } + + /** + * Gets all registered categories in order. + * + * @return unmodifiable collection of categories + */ + public Collection getCategories() { + return Collections.unmodifiableCollection(categories.values()); + } + + /** + * Gets a category by name. + * + * @param name the category name + * @return the category, or null if not found + */ + public ShopCategory getCategory(String name) { + return categories.get(name); + } + + /** + * Gets the category name for a material. + * + * @param material the material + * @return the category name, or "Misc" if not found + */ + public String getCategoryForMaterial(Material material) { + return materialToCategory.getOrDefault(material, "Misc"); + } + + /** + * Checks if a material belongs to any category. + * + * @param material the material + * @return true if the material is registered + */ + public boolean hasMaterial(Material material) { + return materialToCategory.containsKey(material); + } + + /** + * Gets all registered materials across all categories. + * + * @return set of registered materials + */ + public Set getAllMaterials() { + return Collections.unmodifiableSet(materialToCategory.keySet()); + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/category/ShopCategory.java b/ServerShop/src/main/java/pt/henrique/servershop/category/ShopCategory.java new file mode 100644 index 0000000..a4ae81f --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/category/ShopCategory.java @@ -0,0 +1,64 @@ +package pt.henrique.servershop.category; + +import org.bukkit.Material; + +import java.util.*; + +/** + * Represents a shop category containing a set of items. + */ +public class ShopCategory { + + private final String name; + private final Material icon; + private final double sellMultiplier; + private final boolean buyEnabled; + private final boolean sellEnabled; + private final List items; + + /** + * Constructs a ShopCategory. + * + * @param name the display name of the category + * @param icon the material for the category icon + * @param sellMultiplier the sell multiplier for this category + * @param buyEnabled whether buying is enabled for this category + * @param sellEnabled whether selling is enabled for this category + */ + public ShopCategory(String name, Material icon, double sellMultiplier, + boolean buyEnabled, boolean sellEnabled) { + this.name = name; + this.icon = icon; + this.sellMultiplier = sellMultiplier; + this.buyEnabled = buyEnabled; + this.sellEnabled = sellEnabled; + this.items = new ArrayList<>(); + } + + public String getName() { return name; } + public Material getIcon() { return icon; } + public double getSellMultiplier() { return sellMultiplier; } + public boolean isBuyEnabled() { return buyEnabled; } + public boolean isSellEnabled() { return sellEnabled; } + public List getItems() { return Collections.unmodifiableList(items); } + + /** + * Adds a material to this category. + * + * @param material the material to add + */ + public void addItem(Material material) { + if (!items.contains(material)) { + items.add(material); + } + } + + /** + * Gets the number of items in this category. + * + * @return the item count + */ + public int getItemCount() { + return items.size(); + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/command/ShopCommand.java b/ServerShop/src/main/java/pt/henrique/servershop/command/ShopCommand.java new file mode 100644 index 0000000..0d97da3 --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/command/ShopCommand.java @@ -0,0 +1,74 @@ +package pt.henrique.servershop.command; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.config.MessageManager; + +import java.util.Collections; +import java.util.List; + +/** + * Handles the /shop (and /servershop) command. + * Players: opens the shop GUI. + * Admins: /shop reload to reload configuration. + */ +public class ShopCommand implements CommandExecutor, TabCompleter { + + private final ServerShop plugin; + + public ShopCommand(ServerShop plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + MessageManager msg = plugin.getMessageManager(); + + // Admin reload subcommand + if (args.length > 0 && args[0].equalsIgnoreCase("reload")) { + if (!sender.hasPermission("servershop.admin.reload")) { + sender.sendMessage(msg.getPrefixed("no-permission")); + return true; + } + plugin.reloadPlugin(); + sender.sendMessage(msg.getPrefixed("reload-success")); + return true; + } + + // Player-only for GUI + if (!(sender instanceof Player player)) { + sender.sendMessage(msg.getPrefixed("player-only")); + return true; + } + + // Permission check + if (!player.hasPermission("servershop.use")) { + player.sendMessage(msg.getPrefixed("no-permission")); + return true; + } + + // Economy check + if (!plugin.getEconomyManager().isAvailable()) { + player.sendMessage(msg.getPrefixed("economy-unavailable")); + return true; + } + + // Open the main shop GUI + plugin.getGuiManager().openMainGui(player); + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 1 && sender.hasPermission("servershop.admin.reload")) { + if ("reload".startsWith(args[0].toLowerCase())) { + return List.of("reload"); + } + } + return Collections.emptyList(); + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/config/ConfigManager.java b/ServerShop/src/main/java/pt/henrique/servershop/config/ConfigManager.java new file mode 100644 index 0000000..49e0e75 --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/config/ConfigManager.java @@ -0,0 +1,145 @@ +package pt.henrique.servershop.config; + +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import pt.henrique.servershop.ServerShop; + +import java.io.File; +import java.util.logging.Level; + +/** + * Manages the main plugin configuration (config.yml). + * Provides typed getters for all configuration values with sensible defaults. + */ +public class ConfigManager { + + private final ServerShop plugin; + private FileConfiguration config; + + // Cached values + private String language; + private String currencySymbol; + private int decimalPlaces; + private double sellTax; + private double defaultSellMultiplier; + private boolean includeSpecialItems; + private String fullInventoryAction; + private boolean enableSellHand; + private boolean enableSellInventory; + private int maxTransactionQuantity; + private String mainTitle; + private String categoryTitle; + private String detailTitle; + private String searchTitle; + private Material fillerMaterial; + private String fillerName; + private int itemsPerPage; + private boolean soundsEnabled; + private Sound openSound; + private Sound clickSound; + private Sound buySound; + private Sound sellSound; + private Sound errorSound; + private boolean loggingEnabled; + private String databaseFile; + + public ConfigManager(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Loads or reloads the configuration from disk. + */ + public void reload() { + plugin.saveDefaultConfig(); + plugin.reloadConfig(); + config = plugin.getConfig(); + loadSettings(); + } + + private void loadSettings() { + language = config.getString("language", "en_US"); + currencySymbol = config.getString("economy.symbol", "$"); + decimalPlaces = config.getInt("economy.decimal-places", 2); + sellTax = config.getDouble("economy.sell-tax", 0.0); + defaultSellMultiplier = config.getDouble("pricing.default-sell-multiplier", 0.25); + includeSpecialItems = config.getBoolean("pricing.include-special-items", false); + fullInventoryAction = config.getString("shop.full-inventory-action", "DROP"); + enableSellHand = config.getBoolean("shop.enable-sell-hand", true); + enableSellInventory = config.getBoolean("shop.enable-sell-inventory", true); + maxTransactionQuantity = config.getInt("shop.max-transaction-quantity", 2304); + mainTitle = config.getString("gui.main-title", "&8Server Shop"); + categoryTitle = config.getString("gui.category-title", "&8{category}"); + detailTitle = config.getString("gui.detail-title", "&8Buy/Sell"); + searchTitle = config.getString("gui.search-title", "&8Search: {query}"); + fillerName = config.getString("gui.filler-name", " "); + itemsPerPage = config.getInt("gui.items-per-page", 28); + soundsEnabled = config.getBoolean("gui.sounds.enabled", true); + loggingEnabled = config.getBoolean("logging.enabled", true); + databaseFile = config.getString("logging.database-file", "transactions.db"); + + // Parse filler material safely + try { + fillerMaterial = Material.valueOf(config.getString("gui.filler-material", "GRAY_STAINED_GLASS_PANE")); + } catch (IllegalArgumentException e) { + fillerMaterial = Material.GRAY_STAINED_GLASS_PANE; + } + + // Parse sounds safely + openSound = parseSound(config.getString("gui.sounds.open", "BLOCK_CHEST_OPEN")); + clickSound = parseSound(config.getString("gui.sounds.click", "UI_BUTTON_CLICK")); + buySound = parseSound(config.getString("gui.sounds.buy", "ENTITY_EXPERIENCE_ORB_PICKUP")); + sellSound = parseSound(config.getString("gui.sounds.sell", "ENTITY_EXPERIENCE_ORB_PICKUP")); + errorSound = parseSound(config.getString("gui.sounds.error", "ENTITY_VILLAGER_NO")); + } + + private Sound parseSound(String name) { + try { + return Sound.valueOf(name); + } catch (IllegalArgumentException e) { + plugin.getLogger().log(Level.WARNING, "Invalid sound: " + name); + return null; + } + } + + // ================================================================ + // Getters + // ================================================================ + + public String getLanguage() { return language; } + public String getCurrencySymbol() { return currencySymbol; } + public int getDecimalPlaces() { return decimalPlaces; } + public double getSellTax() { return sellTax; } + public double getDefaultSellMultiplier() { return defaultSellMultiplier; } + public boolean isIncludeSpecialItems() { return includeSpecialItems; } + public String getFullInventoryAction() { return fullInventoryAction; } + public boolean isEnableSellHand() { return enableSellHand; } + public boolean isEnableSellInventory() { return enableSellInventory; } + public int getMaxTransactionQuantity() { return maxTransactionQuantity; } + public String getMainTitle() { return mainTitle; } + public String getCategoryTitle() { return categoryTitle; } + public String getDetailTitle() { return detailTitle; } + public String getSearchTitle() { return searchTitle; } + public Material getFillerMaterial() { return fillerMaterial; } + public String getFillerName() { return fillerName; } + public int getItemsPerPage() { return itemsPerPage; } + public boolean isSoundsEnabled() { return soundsEnabled; } + public Sound getOpenSound() { return openSound; } + public Sound getClickSound() { return clickSound; } + public Sound getBuySound() { return buySound; } + public Sound getSellSound() { return sellSound; } + public Sound getErrorSound() { return errorSound; } + public boolean isLoggingEnabled() { return loggingEnabled; } + public String getDatabaseFile() { return databaseFile; } + + /** + * Whether to drop items when inventory is full (vs cancel purchase). + * + * @return true if items should be dropped + */ + public boolean isDropOnFullInventory() { + return "DROP".equalsIgnoreCase(fullInventoryAction); + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/config/MessageManager.java b/ServerShop/src/main/java/pt/henrique/servershop/config/MessageManager.java new file mode 100644 index 0000000..31b5a0f --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/config/MessageManager.java @@ -0,0 +1,178 @@ +package pt.henrique.servershop.config; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.util.TextUtil; + +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.logging.Level; + +/** + * Manages localized messages from language files. + * Supports placeholders with {key} syntax and Adventure component output. + */ +public class MessageManager { + + private final ServerShop plugin; + private FileConfiguration langConfig; + private FileConfiguration fallbackConfig; + private final Map cache = new HashMap<>(); + + public MessageManager(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Loads or reloads the language file based on config. + */ + public void reload() { + cache.clear(); + String language = plugin.getConfigManager().getLanguage(); + + // Save language files from jar if they don't exist + saveLanguageFile("en_US"); + saveLanguageFile("pt_PT"); + + // Load selected language + File langFile = new File(plugin.getDataFolder(), "lang/" + language + ".yml"); + if (langFile.exists()) { + langConfig = YamlConfiguration.loadConfiguration(langFile); + } else { + plugin.getLogger().warning("Language file not found: " + language + ".yml, falling back to en_US"); + langFile = new File(plugin.getDataFolder(), "lang/en_US.yml"); + langConfig = YamlConfiguration.loadConfiguration(langFile); + } + + // Load fallback (en_US from jar) + InputStream fallbackStream = plugin.getResource("lang/en_US.yml"); + if (fallbackStream != null) { + fallbackConfig = YamlConfiguration.loadConfiguration( + new InputStreamReader(fallbackStream, StandardCharsets.UTF_8)); + } + } + + private void saveLanguageFile(String lang) { + File file = new File(plugin.getDataFolder(), "lang/" + lang + ".yml"); + if (!file.exists()) { + file.getParentFile().mkdirs(); + try { + plugin.saveResource("lang/" + lang + ".yml", false); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Could not save language file: " + lang + ".yml", e); + } + } + } + + /** + * Gets a raw message string from the language file. + * + * @param path the config path + * @return the raw message string + */ + public String getRaw(String path) { + // Check cache first + String cached = cache.get(path); + if (cached != null) return cached; + + String value = langConfig.getString(path); + if (value == null && fallbackConfig != null) { + value = fallbackConfig.getString(path); + } + if (value == null) { + value = "&cMissing message: " + path; + } + + cache.put(path, value); + return value; + } + + /** + * Gets a message with the plugin prefix. + * + * @param path the message path (under "messages.") + * @param replacements the placeholder replacements + * @return the formatted message string with prefix + */ + public String getPrefixed(String path, Map replacements) { + String prefix = getRaw("prefix"); + String message = getRaw("messages." + path); + return TextUtil.color(applyReplacements(prefix + message, replacements)); + } + + /** + * Gets a message with the plugin prefix (no placeholders). + * + * @param path the message path (under "messages.") + * @return the formatted message string with prefix + */ + public String getPrefixed(String path) { + return getPrefixed(path, Collections.emptyMap()); + } + + /** + * Gets a raw message with replacements applied. + * + * @param path the full config path + * @param replacements the placeholder replacements + * @return the formatted message string + */ + public String get(String path, Map replacements) { + return TextUtil.color(applyReplacements(getRaw(path), replacements)); + } + + /** + * Gets a raw message. + * + * @param path the full config path + * @return the formatted message string + */ + public String get(String path) { + return TextUtil.color(getRaw(path)); + } + + /** + * Gets a list of strings from the language file. + * + * @param path the config path + * @return the list of strings + */ + public List getList(String path) { + List list = langConfig.getStringList(path); + if (list.isEmpty() && fallbackConfig != null) { + list = fallbackConfig.getStringList(path); + } + return list; + } + + /** + * Gets a list of strings with replacements applied. + * + * @param path the config path + * @param replacements the placeholder replacements + * @return the processed list + */ + public List getList(String path, Map replacements) { + List raw = getList(path); + List result = new ArrayList<>(); + for (String line : raw) { + result.add(TextUtil.color(applyReplacements(line, replacements))); + } + return result; + } + + /** + * Applies placeholder replacements to a string. + */ + private String applyReplacements(String text, Map replacements) { + if (replacements == null || replacements.isEmpty()) return text; + for (Map.Entry entry : replacements.entrySet()) { + text = text.replace("{" + entry.getKey() + "}", entry.getValue()); + } + return text; + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/economy/EconomyManager.java b/ServerShop/src/main/java/pt/henrique/servershop/economy/EconomyManager.java new file mode 100644 index 0000000..13486a4 --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/economy/EconomyManager.java @@ -0,0 +1,139 @@ +package pt.henrique.servershop.economy; + +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.RegisteredServiceProvider; +import pt.henrique.servershop.ServerShop; + +import java.util.logging.Level; + +/** + * Manages Vault economy integration. + * Provides safe methods for balance checks, withdrawals, and deposits. + */ +public class EconomyManager { + + private final ServerShop plugin; + private Economy economy; + + public EconomyManager(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Attempts to set up the Vault economy provider. + * + * @return true if economy was successfully hooked + */ + public boolean setupEconomy() { + if (Bukkit.getPluginManager().getPlugin("Vault") == null) { + plugin.getLogger().severe("Vault not found! ServerShop requires Vault for economy."); + return false; + } + + RegisteredServiceProvider rsp = Bukkit.getServicesManager().getRegistration(Economy.class); + if (rsp == null) { + plugin.getLogger().severe("No economy provider found! Install an economy plugin (e.g., EssentialsX, CMI)."); + return false; + } + + economy = rsp.getProvider(); + plugin.getLogger().info("Hooked into economy: " + economy.getName()); + return true; + } + + /** + * Checks if the economy system is available. + * + * @return true if economy is ready + */ + public boolean isAvailable() { + return economy != null; + } + + /** + * Gets a player's current balance. + * + * @param player the player + * @return the balance, or 0 if economy unavailable + */ + public double getBalance(Player player) { + if (economy == null) return 0; + try { + return economy.getBalance(player); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Error getting balance for " + player.getName(), e); + return 0; + } + } + + /** + * Withdraws money from a player's account. + * + * @param player the player + * @param amount the amount to withdraw + * @return true if the withdrawal was successful + */ + public boolean withdraw(Player player, double amount) { + if (economy == null || amount <= 0) return false; + try { + EconomyResponse response = economy.withdrawPlayer(player, amount); + return response.transactionSuccess(); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Error withdrawing from " + player.getName(), e); + return false; + } + } + + /** + * Deposits money into a player's account. + * + * @param player the player + * @param amount the amount to deposit + * @return true if the deposit was successful + */ + public boolean deposit(Player player, double amount) { + if (economy == null || amount <= 0) return false; + try { + EconomyResponse response = economy.depositPlayer(player, amount); + return response.transactionSuccess(); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Error depositing to " + player.getName(), e); + return false; + } + } + + /** + * Checks if a player has enough money. + * + * @param player the player + * @param amount the amount to check + * @return true if the player has enough + */ + public boolean has(Player player, double amount) { + if (economy == null) return false; + try { + return economy.has(player, amount); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Error checking balance for " + player.getName(), e); + return false; + } + } + + /** + * Formats a monetary amount using the economy provider's format. + * + * @param amount the amount + * @return the formatted string + */ + public String format(double amount) { + if (economy == null) return String.format("$%.2f", amount); + try { + return economy.format(amount); + } catch (Exception e) { + return String.format("$%.2f", amount); + } + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/gui/CategoryGui.java b/ServerShop/src/main/java/pt/henrique/servershop/gui/CategoryGui.java new file mode 100644 index 0000000..314f61b --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/gui/CategoryGui.java @@ -0,0 +1,219 @@ +package pt.henrique.servershop.gui; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.category.ShopCategory; +import pt.henrique.servershop.config.ConfigManager; +import pt.henrique.servershop.config.MessageManager; +import pt.henrique.servershop.pricing.ItemPrice; +import pt.henrique.servershop.util.ItemBuilder; +import pt.henrique.servershop.util.TextUtil; + +import java.util.*; + +/** + * GUI showing items within a specific category with pagination. + */ +public class CategoryGui implements ShopGui { + + private final ServerShop plugin; + private final Player player; + private final ShopCategory category; + private final int page; + private final Inventory inventory; + private final List pagedItems; + + // Layout: 4 rows of 7 items (slots 10-16, 19-25, 28-34, 37-43) = 28 items per page + private static final int[] ITEM_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43 + }; + + private static final int BACK_SLOT = 45; + private static final int PREV_SLOT = 48; + private static final int INFO_SLOT = 49; + private static final int NEXT_SLOT = 50; + private static final int CLOSE_SLOT = 53; + + public CategoryGui(ServerShop plugin, Player player, ShopCategory category, int page) { + this.plugin = plugin; + this.player = player; + this.category = category; + this.page = page; + + // Calculate page items + List allItems = category.getItems(); + int perPage = ITEM_SLOTS.length; + int start = page * perPage; + int end = Math.min(start + perPage, allItems.size()); + this.pagedItems = (start < allItems.size()) ? allItems.subList(start, end) : List.of(); + + ConfigManager config = plugin.getConfigManager(); + MessageManager msg = plugin.getMessageManager(); + + String title = msg.get("gui.titles.category", Map.of("category", category.getName())); + this.inventory = Bukkit.createInventory(this, 54, TextUtil.colorize(title)); + + buildGui(); + } + + private void buildGui() { + ConfigManager config = plugin.getConfigManager(); + MessageManager msg = plugin.getMessageManager(); + + // Fill with filler + ItemStack filler = new ItemBuilder(config.getFillerMaterial()) + .name(config.getFillerName()).hideFlags().build(); + for (int i = 0; i < 54; i++) { + inventory.setItem(i, filler); + } + + // Place items + for (int i = 0; i < pagedItems.size() && i < ITEM_SLOTS.length; i++) { + Material material = pagedItems.get(i); + ItemPrice price = plugin.getPricingService().getPrice(material); + if (price == null) continue; + + String buyPriceStr = price.isBuyEnabled() ? + plugin.getEconomyManager().format(price.getBuyPrice()) : "N/A"; + String sellPriceStr = price.isSellEnabled() ? + plugin.getEconomyManager().format(price.getSellPrice()) : "N/A"; + String spreadStr = String.format("%.0f", price.getSpread()); + + Map placeholders = Map.of( + "buy_price", buyPriceStr, + "sell_price", sellPriceStr, + "spread", spreadStr + ); + + String lorePath; + if (!price.isBuyEnabled() && !price.isSellEnabled()) { + lorePath = "gui.lore.both-disabled-lore"; + } else if (!price.isBuyEnabled()) { + lorePath = "gui.lore.buy-disabled-lore"; + } else if (!price.isSellEnabled()) { + lorePath = "gui.lore.sell-disabled-lore"; + } else { + lorePath = "gui.lore.item-entry"; + } + + List lore = msg.getList(lorePath, placeholders); + String displayName = "&f" + TextUtil.formatMaterialName(material.name()); + + ItemStack item = new ItemBuilder(material) + .name(displayName) + .loreStr(lore) + .hideFlags() + .build(); + + inventory.setItem(ITEM_SLOTS[i], item); + } + + // Back button + ItemStack backButton = ItemBuilder.button(Material.ARROW, + msg.get("gui.buttons.back"), + msg.getList("gui.buttons.back-lore")); + inventory.setItem(BACK_SLOT, backButton); + + // Previous page + int totalPages = getTotalPages(); + if (page > 0) { + ItemStack prevButton = ItemBuilder.button(Material.PAPER, + msg.get("gui.buttons.previous-page"), + msg.getList("gui.buttons.previous-page-lore", + Map.of("page", String.valueOf(page)))); + inventory.setItem(PREV_SLOT, prevButton); + } + + // Info (page indicator) + ItemStack infoItem = new ItemBuilder(Material.BOOK) + .name("&bPage " + (page + 1) + " / " + Math.max(1, totalPages)) + .loreStr("&7" + category.getItemCount() + " items") + .build(); + inventory.setItem(INFO_SLOT, infoItem); + + // Next page + if (page < totalPages - 1) { + ItemStack nextButton = ItemBuilder.button(Material.PAPER, + msg.get("gui.buttons.next-page"), + msg.getList("gui.buttons.next-page-lore", + Map.of("page", String.valueOf(page + 2)))); + inventory.setItem(NEXT_SLOT, nextButton); + } + + // Close button + ItemStack closeButton = ItemBuilder.button(Material.BARRIER, + msg.get("gui.buttons.close"), + msg.getList("gui.buttons.close-lore")); + inventory.setItem(CLOSE_SLOT, closeButton); + } + + @Override + public void handleClick(InventoryClickEvent event, Player player) { + event.setCancelled(true); + int slot = event.getRawSlot(); + if (slot < 0 || slot >= 54) return; + + ConfigManager config = plugin.getConfigManager(); + GuiManager guiManager = plugin.getGuiManager(); + + // Check if clicked an item slot + for (int i = 0; i < ITEM_SLOTS.length && i < pagedItems.size(); i++) { + if (slot == ITEM_SLOTS[i]) { + Material material = pagedItems.get(i); + playSound(player, config.getClickSound()); + guiManager.openItemDetailGui(player, material, category.getName()); + return; + } + } + + // Back to main menu + if (slot == BACK_SLOT) { + playSound(player, config.getClickSound()); + guiManager.openMainGui(player); + return; + } + + // Previous page + if (slot == PREV_SLOT && page > 0) { + playSound(player, config.getClickSound()); + guiManager.openCategoryGui(player, category, page - 1); + return; + } + + // Next page + if (slot == NEXT_SLOT && page < getTotalPages() - 1) { + playSound(player, config.getClickSound()); + guiManager.openCategoryGui(player, category, page + 1); + return; + } + + // Close + if (slot == CLOSE_SLOT) { + player.closeInventory(); + } + } + + private int getTotalPages() { + return Math.max(1, (int) Math.ceil((double) category.getItemCount() / ITEM_SLOTS.length)); + } + + private void playSound(Player player, Sound sound) { + if (plugin.getConfigManager().isSoundsEnabled() && sound != null) { + player.playSound(player.getLocation(), sound, 0.5f, 1.0f); + } + } + + @Override + public Inventory getInventory() { + return inventory; + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/gui/GuiManager.java b/ServerShop/src/main/java/pt/henrique/servershop/gui/GuiManager.java new file mode 100644 index 0000000..767e0a4 --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/gui/GuiManager.java @@ -0,0 +1,226 @@ +package pt.henrique.servershop.gui; + +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.inventory.ItemStack; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.category.ShopCategory; +import pt.henrique.servershop.config.MessageManager; +import pt.henrique.servershop.economy.EconomyManager; +import pt.henrique.servershop.pricing.ItemPrice; +import pt.henrique.servershop.service.ShopService; + +import java.util.*; + +/** + * Manages all GUI interactions and navigation. + * Acts as a facade for opening and coordinating shop screens. + */ +public class GuiManager implements Listener { + + private final ServerShop plugin; + /** Players waiting to type a search query in chat. */ + private final Set searchPending = new HashSet<>(); + + public GuiManager(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Opens the main shop GUI for a player. + * + * @param player the player + */ + public void openMainGui(Player player) { + MainShopGui gui = new MainShopGui(plugin, player); + player.openInventory(gui.getInventory()); + playOpenSound(player); + } + + /** + * Opens a category page for a player. + * + * @param player the player + * @param category the category to display + * @param page the page number + */ + public void openCategoryGui(Player player, ShopCategory category, int page) { + CategoryGui gui = new CategoryGui(plugin, player, category, page); + player.openInventory(gui.getInventory()); + } + + /** + * Opens the item detail GUI for a specific material. + * + * @param player the player + * @param material the material to view + * @param fromCategory the category to return to + */ + public void openItemDetailGui(Player player, Material material, String fromCategory) { + ItemDetailGui gui = new ItemDetailGui(plugin, player, material, fromCategory); + player.openInventory(gui.getInventory()); + } + + /** + * Opens search results for a query. + * + * @param player the player + * @param query the search query + * @param page the page number + */ + public void openSearchResults(Player player, String query, int page) { + List results = plugin.getPricingService().search(query); + if (results.isEmpty()) { + MessageManager msg = plugin.getMessageManager(); + player.sendMessage(msg.getPrefixed("search-no-results", Map.of("query", query))); + openMainGui(player); + return; + } + SearchResultsGui gui = new SearchResultsGui(plugin, player, query, page); + player.openInventory(gui.getInventory()); + } + + /** + * Initiates the search flow: closes inventory and asks player to type in chat. + * + * @param player the player + */ + public void openSearchInput(Player player) { + player.closeInventory(); + searchPending.add(player.getUniqueId()); + MessageManager msg = plugin.getMessageManager(); + player.sendMessage(msg.getPrefixed("search-prompt")); + } + + /** + * Handles chat messages for search input. + */ + @EventHandler(priority = EventPriority.LOW) + public void onChat(AsyncChatEvent event) { + Player player = event.getPlayer(); + if (!searchPending.remove(player.getUniqueId())) return; + + event.setCancelled(true); + String query = PlainTextComponentSerializer.plainText().serialize(event.message()).trim(); + + // Open search results on the main thread + Bukkit.getScheduler().runTask(plugin, () -> openSearchResults(player, query, 0)); + } + + /** + * Handles the "sell hand" action. + */ + public void handleSellHand(Player player) { + MessageManager msg = plugin.getMessageManager(); + + if (!player.hasPermission("servershop.sell.hand")) { + player.sendMessage(msg.getPrefixed("no-permission")); + return; + } + + ItemStack held = player.getInventory().getItemInMainHand(); + if (held.getType().isAir()) { + player.sendMessage(msg.getPrefixed("sell-hand-empty")); + return; + } + + Material material = held.getType(); + ItemPrice price = plugin.getPricingService().getPrice(material); + if (price == null || !price.isSellEnabled() || price.getSellPrice() <= 0) { + player.sendMessage(msg.getPrefixed("sell-hand-not-sellable")); + return; + } + + int amount = held.getAmount(); + ShopService.ShopResult result = plugin.getShopService().sell(player, material, amount); + player.sendMessage(msg.getPrefixed(result.messageKey(), result.placeholders())); + + if (result.success()) { + playSound(player, plugin.getConfigManager().getSellSound()); + } else { + playSound(player, plugin.getConfigManager().getErrorSound()); + } + + // Reopen main gui + Bukkit.getScheduler().runTaskLater(plugin, () -> openMainGui(player), 1L); + } + + /** + * Handles the "sell inventory" action. + */ + public void handleSellInventory(Player player) { + MessageManager msg = plugin.getMessageManager(); + EconomyManager economy = plugin.getEconomyManager(); + + if (!player.hasPermission("servershop.sell.inventory")) { + player.sendMessage(msg.getPrefixed("no-permission")); + return; + } + + double totalEarnings = 0; + boolean soldAnything = false; + + // Iterate through all inventory items + for (ItemStack item : player.getInventory().getContents()) { + if (item == null || item.getType().isAir()) continue; + + Material material = item.getType(); + ItemPrice price = plugin.getPricingService().getPrice(material); + if (price == null || !price.isSellEnabled() || price.getSellPrice() <= 0) continue; + + int amount = item.getAmount(); + double earnings = price.getSellPrice() * amount; + double tax = earnings * plugin.getConfigManager().getSellTax(); + double net = earnings - tax; + + // Remove the items + item.setAmount(0); + totalEarnings += net; + soldAnything = true; + + // Log each material sale + plugin.getTransactionLogger().log( + player.getUniqueId(), player.getName(), material, amount, net, + pt.henrique.servershop.transaction.TransactionLogger.TransactionType.SELL + ); + } + + if (!soldAnything) { + player.sendMessage(msg.getPrefixed("sell-inventory-nothing")); + Bukkit.getScheduler().runTaskLater(plugin, () -> openMainGui(player), 1L); + return; + } + + // Deposit total + economy.deposit(player, totalEarnings); + player.sendMessage(msg.getPrefixed("sell-inventory-success", + Map.of("price", economy.format(totalEarnings)))); + playSound(player, plugin.getConfigManager().getSellSound()); + + // Reopen main gui + Bukkit.getScheduler().runTaskLater(plugin, () -> openMainGui(player), 1L); + } + + /** + * Removes a player from the search pending set (e.g., on disconnect). + */ + public void cancelSearch(UUID uuid) { + searchPending.remove(uuid); + } + + private void playOpenSound(Player player) { + playSound(player, plugin.getConfigManager().getOpenSound()); + } + + private void playSound(Player player, org.bukkit.Sound sound) { + if (plugin.getConfigManager().isSoundsEnabled() && sound != null) { + player.playSound(player.getLocation(), sound, 0.5f, 1.0f); + } + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/gui/ItemDetailGui.java b/ServerShop/src/main/java/pt/henrique/servershop/gui/ItemDetailGui.java new file mode 100644 index 0000000..d3bf029 --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/gui/ItemDetailGui.java @@ -0,0 +1,279 @@ +package pt.henrique.servershop.gui; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.config.ConfigManager; +import pt.henrique.servershop.config.MessageManager; +import pt.henrique.servershop.economy.EconomyManager; +import pt.henrique.servershop.pricing.ItemPrice; +import pt.henrique.servershop.service.ShopService; +import pt.henrique.servershop.util.ItemBuilder; +import pt.henrique.servershop.util.TextUtil; + +import java.util.*; + +/** + * GUI for viewing item details with quantity selection and buy/sell buttons. + * Layout (54 slots): + * Row 1 (0-8): filler + * Row 2 (9-17): -32 -16 -8 -1 [ITEM+INFO] +1 +8 +16 +32 + * Row 3 (18-26): filler with MIN/MAX + * Row 4 (27-35): filler + * Row 5 (36-44): back [BUY] filler [SELL] close + * Row 6 (45-53): filler + */ +public class ItemDetailGui implements ShopGui { + + private final ServerShop plugin; + private final Player player; + private final Material material; + private final String fromCategory; + private final Inventory inventory; + + private int quantity = 1; + + // Slot constants + private static final int ITEM_INFO_SLOT = 13; + private static final int DEC_32_SLOT = 9; + private static final int DEC_16_SLOT = 10; + private static final int DEC_8_SLOT = 11; + private static final int DEC_1_SLOT = 12; + private static final int INC_1_SLOT = 14; + private static final int INC_8_SLOT = 15; + private static final int INC_16_SLOT = 16; + private static final int INC_32_SLOT = 17; + private static final int MIN_SLOT = 21; + private static final int MAX_SLOT = 23; + private static final int BACK_SLOT = 36; + private static final int BUY_SLOT = 39; + private static final int SELL_SLOT = 41; + private static final int CLOSE_SLOT = 44; + + public ItemDetailGui(ServerShop plugin, Player player, Material material, String fromCategory) { + this.plugin = plugin; + this.player = player; + this.material = material; + this.fromCategory = fromCategory; + + MessageManager msg = plugin.getMessageManager(); + String title = msg.get("gui.titles.item-detail"); + this.inventory = Bukkit.createInventory(this, 54, TextUtil.colorize(title)); + + updateGui(); + } + + private void updateGui() { + ConfigManager config = plugin.getConfigManager(); + MessageManager msg = plugin.getMessageManager(); + EconomyManager economy = plugin.getEconomyManager(); + ItemPrice price = plugin.getPricingService().getPrice(material); + + // Fill with filler + ItemStack filler = new ItemBuilder(config.getFillerMaterial()) + .name(config.getFillerName()).hideFlags().build(); + for (int i = 0; i < 54; i++) { + inventory.setItem(i, filler); + } + + if (price == null) return; + + String buyPriceStr = price.isBuyEnabled() ? economy.format(price.getBuyPrice()) : "N/A"; + String sellPriceStr = price.isSellEnabled() ? economy.format(price.getSellPrice()) : "N/A"; + String spreadStr = String.format("%.0f", price.getSpread()); + String balanceStr = economy.format(economy.getBalance(player)); + String buyTotalStr = price.isBuyEnabled() ? + economy.format(price.getBuyPrice() * quantity) : "N/A"; + String sellTotalStr = price.isSellEnabled() ? + economy.format(price.getSellPrice() * quantity) : "N/A"; + + Map placeholders = new HashMap<>(); + placeholders.put("buy_price", buyPriceStr); + placeholders.put("sell_price", sellPriceStr); + placeholders.put("spread", spreadStr); + placeholders.put("balance", balanceStr); + placeholders.put("amount", String.valueOf(quantity)); + placeholders.put("buy_total", buyTotalStr); + placeholders.put("sell_total", sellTotalStr); + + // Item info display + List infoLore = msg.getList("gui.lore.item-detail-info", placeholders); + String itemName = "&f" + TextUtil.formatMaterialName(material.name()); + ItemStack infoItem = new ItemBuilder(material) + .name(itemName) + .loreStr(infoLore) + .amount(Math.min(quantity, 64)) + .hideFlags() + .build(); + inventory.setItem(ITEM_INFO_SLOT, infoItem); + + // Quantity adjustment buttons + inventory.setItem(DEC_32_SLOT, createQuantityButton(Material.RED_STAINED_GLASS_PANE, + msg.get("gui.buttons.decrease-32"))); + inventory.setItem(DEC_16_SLOT, createQuantityButton(Material.RED_STAINED_GLASS_PANE, + msg.get("gui.buttons.decrease-16"))); + inventory.setItem(DEC_8_SLOT, createQuantityButton(Material.RED_STAINED_GLASS_PANE, + msg.get("gui.buttons.decrease-8"))); + inventory.setItem(DEC_1_SLOT, createQuantityButton(Material.RED_STAINED_GLASS_PANE, + msg.get("gui.buttons.decrease-1"))); + inventory.setItem(INC_1_SLOT, createQuantityButton(Material.LIME_STAINED_GLASS_PANE, + msg.get("gui.buttons.increase-1"))); + inventory.setItem(INC_8_SLOT, createQuantityButton(Material.LIME_STAINED_GLASS_PANE, + msg.get("gui.buttons.increase-8"))); + inventory.setItem(INC_16_SLOT, createQuantityButton(Material.LIME_STAINED_GLASS_PANE, + msg.get("gui.buttons.increase-16"))); + inventory.setItem(INC_32_SLOT, createQuantityButton(Material.LIME_STAINED_GLASS_PANE, + msg.get("gui.buttons.increase-32"))); + + // MIN / MAX buttons + inventory.setItem(MIN_SLOT, createQuantityButton(Material.YELLOW_STAINED_GLASS_PANE, + msg.get("gui.buttons.min-quantity"))); + inventory.setItem(MAX_SLOT, createQuantityButton(Material.YELLOW_STAINED_GLASS_PANE, + msg.get("gui.buttons.max-quantity"))); + + // Buy button + if (price.isBuyEnabled() && price.getBuyPrice() > 0) { + Map buyPlaceholders = Map.of( + "amount", String.valueOf(quantity), + "price", buyTotalStr + ); + ItemStack buyButton = ItemBuilder.button(Material.EMERALD, + msg.get("gui.buttons.buy"), + msg.getList("gui.buttons.buy-lore", buyPlaceholders)); + inventory.setItem(BUY_SLOT, buyButton); + } + + // Sell button + if (price.isSellEnabled() && price.getSellPrice() > 0) { + Map sellPlaceholders = Map.of( + "amount", String.valueOf(quantity), + "price", sellTotalStr + ); + ItemStack sellButton = ItemBuilder.button(Material.GOLD_INGOT, + msg.get("gui.buttons.sell"), + msg.getList("gui.buttons.sell-lore", sellPlaceholders)); + inventory.setItem(SELL_SLOT, sellButton); + } + + // Back button + ItemStack backButton = ItemBuilder.button(Material.ARROW, + msg.get("gui.buttons.back"), + msg.getList("gui.buttons.back-lore")); + inventory.setItem(BACK_SLOT, backButton); + + // Close button + ItemStack closeButton = ItemBuilder.button(Material.BARRIER, + msg.get("gui.buttons.close"), + msg.getList("gui.buttons.close-lore")); + inventory.setItem(CLOSE_SLOT, closeButton); + } + + private ItemStack createQuantityButton(Material mat, String name) { + return new ItemBuilder(mat).name(name).hideFlags().build(); + } + + @Override + public void handleClick(InventoryClickEvent event, Player player) { + event.setCancelled(true); + int slot = event.getRawSlot(); + if (slot < 0 || slot >= 54) return; + + ConfigManager config = plugin.getConfigManager(); + int maxQty = config.getMaxTransactionQuantity(); + + switch (slot) { + case DEC_32_SLOT -> adjustQuantity(-32, maxQty); + case DEC_16_SLOT -> adjustQuantity(-16, maxQty); + case DEC_8_SLOT -> adjustQuantity(-8, maxQty); + case DEC_1_SLOT -> adjustQuantity(-1, maxQty); + case INC_1_SLOT -> adjustQuantity(1, maxQty); + case INC_8_SLOT -> adjustQuantity(8, maxQty); + case INC_16_SLOT -> adjustQuantity(16, maxQty); + case INC_32_SLOT -> adjustQuantity(32, maxQty); + case MIN_SLOT -> { quantity = 1; updateGui(); } + case MAX_SLOT -> { quantity = maxQty; updateGui(); } + case BUY_SLOT -> handleBuy(player); + case SELL_SLOT -> handleSell(player); + case BACK_SLOT -> { + playSound(player, config.getClickSound()); + if (fromCategory != null) { + var cat = plugin.getCategoryRegistry().getCategory(fromCategory); + if (cat != null) { + plugin.getGuiManager().openCategoryGui(player, cat, 0); + return; + } + } + plugin.getGuiManager().openMainGui(player); + } + case CLOSE_SLOT -> player.closeInventory(); + default -> { + // Ignore other slots + } + } + } + + private void adjustQuantity(int delta, int max) { + quantity = Math.max(1, Math.min(quantity + delta, max)); + updateGui(); + } + + private void handleBuy(Player player) { + ItemPrice price = plugin.getPricingService().getPrice(material); + if (price == null || !price.isBuyEnabled()) { + playSound(player, plugin.getConfigManager().getErrorSound()); + return; + } + + ShopService.ShopResult result = plugin.getShopService().buy(player, material, quantity); + MessageManager msg = plugin.getMessageManager(); + + player.sendMessage(msg.getPrefixed(result.messageKey(), result.placeholders())); + + if (result.success()) { + playSound(player, plugin.getConfigManager().getBuySound()); + } else { + playSound(player, plugin.getConfigManager().getErrorSound()); + } + + // Refresh the GUI to show updated balance + updateGui(); + } + + private void handleSell(Player player) { + ItemPrice price = plugin.getPricingService().getPrice(material); + if (price == null || !price.isSellEnabled()) { + playSound(player, plugin.getConfigManager().getErrorSound()); + return; + } + + ShopService.ShopResult result = plugin.getShopService().sell(player, material, quantity); + MessageManager msg = plugin.getMessageManager(); + + player.sendMessage(msg.getPrefixed(result.messageKey(), result.placeholders())); + + if (result.success()) { + playSound(player, plugin.getConfigManager().getSellSound()); + } else { + playSound(player, plugin.getConfigManager().getErrorSound()); + } + + // Refresh the GUI to show updated balance/inventory + updateGui(); + } + + private void playSound(Player player, Sound sound) { + if (plugin.getConfigManager().isSoundsEnabled() && sound != null) { + player.playSound(player.getLocation(), sound, 0.5f, 1.0f); + } + } + + @Override + public Inventory getInventory() { + return inventory; + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/gui/MainShopGui.java b/ServerShop/src/main/java/pt/henrique/servershop/gui/MainShopGui.java new file mode 100644 index 0000000..3f922e0 --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/gui/MainShopGui.java @@ -0,0 +1,169 @@ +package pt.henrique.servershop.gui; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.category.ShopCategory; +import pt.henrique.servershop.config.ConfigManager; +import pt.henrique.servershop.config.MessageManager; +import pt.henrique.servershop.util.ItemBuilder; +import pt.henrique.servershop.util.TextUtil; + +import java.util.*; + +/** + * The main shop GUI showing category buttons, search, and sell options. + */ +public class MainShopGui implements ShopGui { + + private final ServerShop plugin; + private final Player player; + private final Inventory inventory; + + // Slot assignments + private static final int SEARCH_SLOT = 49; + private static final int SELL_HAND_SLOT = 46; + private static final int SELL_INVENTORY_SLOT = 47; + private static final int CLOSE_SLOT = 53; + + public MainShopGui(ServerShop plugin, Player player) { + this.plugin = plugin; + this.player = player; + + ConfigManager config = plugin.getConfigManager(); + MessageManager msg = plugin.getMessageManager(); + + String title = msg.get("gui.titles.main-menu"); + this.inventory = Bukkit.createInventory(this, 54, TextUtil.colorize(title)); + + buildGui(); + } + + private void buildGui() { + ConfigManager config = plugin.getConfigManager(); + MessageManager msg = plugin.getMessageManager(); + + // Fill border with filler + ItemStack filler = new ItemBuilder(config.getFillerMaterial()) + .name(config.getFillerName()).hideFlags().build(); + for (int i = 0; i < 54; i++) { + inventory.setItem(i, filler); + } + + // Place category buttons in the center area (slots 10-16, 19-25, 28-34) + int[] categorySlots = {10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34}; + Collection categories = plugin.getCategoryRegistry().getCategories(); + int index = 0; + + for (ShopCategory category : categories) { + if (index >= categorySlots.length) break; + + Map placeholders = Map.of("count", String.valueOf(category.getItemCount())); + List lore = msg.getList("gui.category.lore", placeholders); + + ItemStack icon = new ItemBuilder(category.getIcon()) + .name("&a" + category.getName()) + .loreStr(lore) + .hideFlags() + .build(); + + inventory.setItem(categorySlots[index], icon); + index++; + } + + // Search button + ItemStack searchButton = ItemBuilder.button(Material.COMPASS, + msg.get("gui.buttons.search"), + msg.getList("gui.buttons.search-lore")); + inventory.setItem(SEARCH_SLOT, searchButton); + + // Sell hand button + if (config.isEnableSellHand()) { + ItemStack sellHand = ItemBuilder.button(Material.GOLDEN_SWORD, + msg.get("gui.buttons.sell-hand"), + msg.getList("gui.buttons.sell-hand-lore")); + inventory.setItem(SELL_HAND_SLOT, sellHand); + } + + // Sell inventory button + if (config.isEnableSellInventory()) { + ItemStack sellInv = ItemBuilder.button(Material.CHEST, + msg.get("gui.buttons.sell-inventory"), + msg.getList("gui.buttons.sell-inventory-lore")); + inventory.setItem(SELL_INVENTORY_SLOT, sellInv); + } + + // Close button + ItemStack closeButton = ItemBuilder.button(Material.BARRIER, + msg.get("gui.buttons.close"), + msg.getList("gui.buttons.close-lore")); + inventory.setItem(CLOSE_SLOT, closeButton); + } + + @Override + public void handleClick(InventoryClickEvent event, Player player) { + event.setCancelled(true); + int slot = event.getRawSlot(); + + // Only handle clicks in the top inventory + if (slot < 0 || slot >= 54) return; + + ConfigManager config = plugin.getConfigManager(); + GuiManager guiManager = plugin.getGuiManager(); + + // Category slots + int[] categorySlots = {10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34}; + List categories = new ArrayList<>(plugin.getCategoryRegistry().getCategories()); + + for (int i = 0; i < categorySlots.length && i < categories.size(); i++) { + if (slot == categorySlots[i]) { + playSound(player, config.getClickSound()); + guiManager.openCategoryGui(player, categories.get(i), 0); + return; + } + } + + // Search button + if (slot == SEARCH_SLOT) { + playSound(player, config.getClickSound()); + guiManager.openSearchInput(player); + return; + } + + // Sell hand + if (slot == SELL_HAND_SLOT && config.isEnableSellHand()) { + playSound(player, config.getClickSound()); + guiManager.handleSellHand(player); + return; + } + + // Sell inventory + if (slot == SELL_INVENTORY_SLOT && config.isEnableSellInventory()) { + playSound(player, config.getClickSound()); + guiManager.handleSellInventory(player); + return; + } + + // Close + if (slot == CLOSE_SLOT) { + player.closeInventory(); + return; + } + } + + private void playSound(Player player, Sound sound) { + if (plugin.getConfigManager().isSoundsEnabled() && sound != null) { + player.playSound(player.getLocation(), sound, 0.5f, 1.0f); + } + } + + @Override + public Inventory getInventory() { + return inventory; + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/gui/SearchResultsGui.java b/ServerShop/src/main/java/pt/henrique/servershop/gui/SearchResultsGui.java new file mode 100644 index 0000000..e74485f --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/gui/SearchResultsGui.java @@ -0,0 +1,193 @@ +package pt.henrique.servershop.gui; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.config.ConfigManager; +import pt.henrique.servershop.config.MessageManager; +import pt.henrique.servershop.pricing.ItemPrice; +import pt.henrique.servershop.util.ItemBuilder; +import pt.henrique.servershop.util.TextUtil; + +import java.util.*; + +/** + * GUI showing search results with pagination. + */ +public class SearchResultsGui implements ShopGui { + + private final ServerShop plugin; + private final Player player; + private final String query; + private final int page; + private final Inventory inventory; + private final List pagedItems; + private final List allResults; + + private static final int[] ITEM_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43 + }; + + private static final int BACK_SLOT = 45; + private static final int PREV_SLOT = 48; + private static final int INFO_SLOT = 49; + private static final int NEXT_SLOT = 50; + private static final int CLOSE_SLOT = 53; + + public SearchResultsGui(ServerShop plugin, Player player, String query, int page) { + this.plugin = plugin; + this.player = player; + this.query = query; + this.page = page; + + this.allResults = plugin.getPricingService().search(query); + int perPage = ITEM_SLOTS.length; + int start = page * perPage; + int end = Math.min(start + perPage, allResults.size()); + this.pagedItems = (start < allResults.size()) ? allResults.subList(start, end) : List.of(); + + MessageManager msg = plugin.getMessageManager(); + String title = msg.get("gui.titles.search-results", Map.of("query", query)); + this.inventory = Bukkit.createInventory(this, 54, TextUtil.colorize(title)); + + buildGui(); + } + + private void buildGui() { + ConfigManager config = plugin.getConfigManager(); + MessageManager msg = plugin.getMessageManager(); + + // Fill with filler + ItemStack filler = new ItemBuilder(config.getFillerMaterial()) + .name(config.getFillerName()).hideFlags().build(); + for (int i = 0; i < 54; i++) { + inventory.setItem(i, filler); + } + + // Place items + for (int i = 0; i < pagedItems.size() && i < ITEM_SLOTS.length; i++) { + ItemPrice price = pagedItems.get(i); + String buyPriceStr = price.isBuyEnabled() ? + plugin.getEconomyManager().format(price.getBuyPrice()) : "N/A"; + String sellPriceStr = price.isSellEnabled() ? + plugin.getEconomyManager().format(price.getSellPrice()) : "N/A"; + String spreadStr = String.format("%.0f", price.getSpread()); + + Map placeholders = Map.of( + "buy_price", buyPriceStr, + "sell_price", sellPriceStr, + "spread", spreadStr + ); + + List lore = msg.getList("gui.lore.item-entry", placeholders); + String displayName = "&f" + TextUtil.formatMaterialName(price.getMaterial().name()); + + ItemStack item = new ItemBuilder(price.getMaterial()) + .name(displayName) + .loreStr(lore) + .hideFlags() + .build(); + + inventory.setItem(ITEM_SLOTS[i], item); + } + + // Navigation buttons + ItemStack backButton = ItemBuilder.button(Material.ARROW, + msg.get("gui.buttons.back"), + msg.getList("gui.buttons.back-lore")); + inventory.setItem(BACK_SLOT, backButton); + + int totalPages = getTotalPages(); + if (page > 0) { + ItemStack prevButton = ItemBuilder.button(Material.PAPER, + msg.get("gui.buttons.previous-page"), + msg.getList("gui.buttons.previous-page-lore", + Map.of("page", String.valueOf(page)))); + inventory.setItem(PREV_SLOT, prevButton); + } + + ItemStack infoItem = new ItemBuilder(Material.BOOK) + .name("&bPage " + (page + 1) + " / " + Math.max(1, totalPages)) + .loreStr("&7" + allResults.size() + " results for '" + query + "'") + .build(); + inventory.setItem(INFO_SLOT, infoItem); + + if (page < totalPages - 1) { + ItemStack nextButton = ItemBuilder.button(Material.PAPER, + msg.get("gui.buttons.next-page"), + msg.getList("gui.buttons.next-page-lore", + Map.of("page", String.valueOf(page + 2)))); + inventory.setItem(NEXT_SLOT, nextButton); + } + + ItemStack closeButton = ItemBuilder.button(Material.BARRIER, + msg.get("gui.buttons.close"), + msg.getList("gui.buttons.close-lore")); + inventory.setItem(CLOSE_SLOT, closeButton); + } + + @Override + public void handleClick(InventoryClickEvent event, Player player) { + event.setCancelled(true); + int slot = event.getRawSlot(); + if (slot < 0 || slot >= 54) return; + + ConfigManager config = plugin.getConfigManager(); + GuiManager guiManager = plugin.getGuiManager(); + + // Check item slots + for (int i = 0; i < ITEM_SLOTS.length && i < pagedItems.size(); i++) { + if (slot == ITEM_SLOTS[i]) { + ItemPrice price = pagedItems.get(i); + playSound(player, config.getClickSound()); + guiManager.openItemDetailGui(player, price.getMaterial(), price.getCategory()); + return; + } + } + + if (slot == BACK_SLOT) { + playSound(player, config.getClickSound()); + guiManager.openMainGui(player); + return; + } + + if (slot == PREV_SLOT && page > 0) { + playSound(player, config.getClickSound()); + guiManager.openSearchResults(player, query, page - 1); + return; + } + + if (slot == NEXT_SLOT && page < getTotalPages() - 1) { + playSound(player, config.getClickSound()); + guiManager.openSearchResults(player, query, page + 1); + return; + } + + if (slot == CLOSE_SLOT) { + player.closeInventory(); + } + } + + private int getTotalPages() { + return Math.max(1, (int) Math.ceil((double) allResults.size() / ITEM_SLOTS.length)); + } + + private void playSound(Player player, Sound sound) { + if (plugin.getConfigManager().isSoundsEnabled() && sound != null) { + player.playSound(player.getLocation(), sound, 0.5f, 1.0f); + } + } + + @Override + public Inventory getInventory() { + return inventory; + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/gui/ShopGui.java b/ServerShop/src/main/java/pt/henrique/servershop/gui/ShopGui.java new file mode 100644 index 0000000..41e9a9c --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/gui/ShopGui.java @@ -0,0 +1,39 @@ +package pt.henrique.servershop.gui; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +/** + * Interface for all shop GUI screens. + * Implementing classes serve as InventoryHolder for click event delegation. + */ +public interface ShopGui extends InventoryHolder { + + /** + * Handles a click event in this GUI. + * + * @param event the click event + * @param player the player who clicked + */ + void handleClick(InventoryClickEvent event, Player player); + + /** + * Called when the GUI is closed. + * + * @param player the player who closed the GUI + */ + default void handleClose(Player player) { + // Default: no action + } + + /** + * Whether this GUI allows item movement (shift-click from player inventory, etc.). + * + * @return true if item movement is allowed + */ + default boolean allowsItemMovement() { + return false; + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/listener/GuiListener.java b/ServerShop/src/main/java/pt/henrique/servershop/listener/GuiListener.java new file mode 100644 index 0000000..d4fbf5c --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/listener/GuiListener.java @@ -0,0 +1,124 @@ +package pt.henrique.servershop.listener; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.*; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.gui.ShopGui; + +/** + * Listens for inventory events and delegates to ShopGui implementations. + * Provides comprehensive anti-exploit protections against item duplication. + */ +public class GuiListener implements Listener { + + private final ServerShop plugin; + + public GuiListener(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Handles all click events within shop GUIs. + * Cancels illegal interactions and delegates valid clicks to the GUI handler. + */ + @EventHandler(priority = EventPriority.HIGH) + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) return; + + Inventory topInventory = event.getView().getTopInventory(); + if (!(topInventory.getHolder() instanceof ShopGui gui)) return; + + // Block all item movement in our GUIs unless explicitly allowed + if (!gui.allowsItemMovement()) { + // Block shift-click from bottom inventory + if (event.getClick().isShiftClick() && event.getRawSlot() >= topInventory.getSize()) { + event.setCancelled(true); + return; + } + + // Block number key swaps + if (event.getClick() == ClickType.NUMBER_KEY) { + event.setCancelled(true); + return; + } + + // Block swap/offhand + if (event.getClick() == ClickType.SWAP_OFFHAND) { + event.setCancelled(true); + return; + } + + // Block double-click (collects items from all inventories) + if (event.getClick() == ClickType.DOUBLE_CLICK) { + event.setCancelled(true); + return; + } + } + + // Delegate to the GUI handler for clicks in the top inventory + if (event.getRawSlot() < topInventory.getSize()) { + gui.handleClick(event, player); + } else { + // Clicks in the player's own inventory are cancelled for safety + event.setCancelled(true); + } + } + + /** + * Blocks all drag events in shop GUIs. + */ + @EventHandler(priority = EventPriority.HIGH) + public void onInventoryDrag(InventoryDragEvent event) { + if (!(event.getWhoClicked() instanceof Player)) return; + + Inventory topInventory = event.getView().getTopInventory(); + if (!(topInventory.getHolder() instanceof ShopGui gui)) return; + + if (!gui.allowsItemMovement()) { + // Cancel if any dragged slot is in the top inventory + for (int slot : event.getRawSlots()) { + if (slot < topInventory.getSize()) { + event.setCancelled(true); + return; + } + } + } + } + + /** + * Blocks item movement between inventories via hoppers etc. + */ + @EventHandler(priority = EventPriority.HIGH) + public void onInventoryMoveItem(InventoryMoveItemEvent event) { + if (event.getDestination().getHolder() instanceof ShopGui || + event.getSource().getHolder() instanceof ShopGui) { + event.setCancelled(true); + } + } + + /** + * Handles GUI close events. + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onInventoryClose(InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player player)) return; + + Inventory topInventory = event.getView().getTopInventory(); + if (topInventory.getHolder() instanceof ShopGui gui) { + gui.handleClose(player); + } + } + + /** + * Cleans up when a player disconnects. + */ + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + plugin.getGuiManager().cancelSearch(event.getPlayer().getUniqueId()); + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/pricing/ItemPrice.java b/ServerShop/src/main/java/pt/henrique/servershop/pricing/ItemPrice.java new file mode 100644 index 0000000..c145eba --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/pricing/ItemPrice.java @@ -0,0 +1,64 @@ +package pt.henrique.servershop.pricing; + +import org.bukkit.Material; + +/** + * Represents the price configuration for a single item. + * Contains buy price, sell price, and enable/disable flags. + */ +public class ItemPrice { + + private final Material material; + private final double buyPrice; + private final double sellPrice; + private final boolean buyEnabled; + private final boolean sellEnabled; + private final String category; + + /** + * Constructs an ItemPrice. + * + * @param material the material + * @param buyPrice the price to buy from the server + * @param sellPrice the price to sell to the server + * @param buyEnabled whether buying is enabled + * @param sellEnabled whether selling is enabled + * @param category the category this item belongs to + */ + public ItemPrice(Material material, double buyPrice, double sellPrice, + boolean buyEnabled, boolean sellEnabled, String category) { + this.material = material; + this.buyPrice = buyPrice; + this.sellPrice = sellPrice; + this.buyEnabled = buyEnabled; + this.sellEnabled = sellEnabled; + this.category = category; + } + + public Material getMaterial() { return material; } + public double getBuyPrice() { return buyPrice; } + public double getSellPrice() { return sellPrice; } + public boolean isBuyEnabled() { return buyEnabled; } + public boolean isSellEnabled() { return sellEnabled; } + public String getCategory() { return category; } + + /** + * Calculates the spread percentage between buy and sell prices. + * Spread = ((buyPrice - sellPrice) / buyPrice) * 100 + * + * @return the spread percentage, or 0 if buy price is 0 + */ + public double getSpread() { + if (buyPrice <= 0) return 0; + return ((buyPrice - sellPrice) / buyPrice) * 100.0; + } + + /** + * Whether this item has any valid transaction available (buy or sell). + * + * @return true if at least one transaction type is enabled with a positive price + */ + public boolean isAvailable() { + return (buyEnabled && buyPrice > 0) || (sellEnabled && sellPrice > 0); + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/pricing/PricingService.java b/ServerShop/src/main/java/pt/henrique/servershop/pricing/PricingService.java new file mode 100644 index 0000000..d35f99a --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/pricing/PricingService.java @@ -0,0 +1,195 @@ +package pt.henrique.servershop.pricing; + +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.category.CategoryRegistry; +import pt.henrique.servershop.category.ShopCategory; + +import java.io.File; +import java.util.*; +import java.util.logging.Level; + +/** + * Service responsible for calculating and providing item prices. + * Loads base prices from prices.yml and applies category multipliers and per-item overrides. + */ +public class PricingService { + + private final ServerShop plugin; + private final Map prices = new HashMap<>(); + + public PricingService(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Loads all prices from prices.yml, applying multipliers and overrides. + */ + public void reload() { + prices.clear(); + + File pricesFile = new File(plugin.getDataFolder(), "prices.yml"); + if (!pricesFile.exists()) { + plugin.saveResource("prices.yml", false); + } + + FileConfiguration pricesConfig = YamlConfiguration.loadConfiguration(pricesFile); + double globalMultiplier = plugin.getConfigManager().getDefaultSellMultiplier(); + + // Load overrides section + ConfigurationSection overridesSection = pricesConfig.getConfigurationSection("overrides"); + Map overrides = loadOverrides(overridesSection); + + // Load prices from categories + CategoryRegistry registry = plugin.getCategoryRegistry(); + ConfigurationSection categoriesSection = pricesConfig.getConfigurationSection("categories"); + + if (categoriesSection == null) return; + + for (String categoryName : categoriesSection.getKeys(false)) { + ConfigurationSection catSection = categoriesSection.getConfigurationSection(categoryName); + if (catSection == null) continue; + + ShopCategory category = registry.getCategory(categoryName); + if (category == null) continue; + + double catMultiplier = catSection.getDouble("sell-multiplier", globalMultiplier); + boolean catBuyEnabled = catSection.getBoolean("buy-enabled", true); + boolean catSellEnabled = catSection.getBoolean("sell-enabled", true); + + ConfigurationSection itemsSection = catSection.getConfigurationSection("items"); + if (itemsSection == null) continue; + + for (String materialName : itemsSection.getKeys(false)) { + try { + Material material = Material.valueOf(materialName); + if (material.isAir()) continue; + + double buyPrice = itemsSection.getDouble(materialName); + double sellPrice = buyPrice * catMultiplier; + boolean buyEnabled = catBuyEnabled; + boolean sellEnabled = catSellEnabled; + + // Apply per-item overrides + OverrideData override = overrides.get(material); + if (override != null) { + if (override.buyPrice >= 0) buyPrice = override.buyPrice; + if (override.sellPrice >= 0) sellPrice = override.sellPrice; + if (override.buyEnabled != null) buyEnabled = override.buyEnabled; + if (override.sellEnabled != null) sellEnabled = override.sellEnabled; + } + + // Round prices to configured decimal places + int dp = plugin.getConfigManager().getDecimalPlaces(); + double factor = Math.pow(10, dp); + buyPrice = Math.round(buyPrice * factor) / factor; + sellPrice = Math.round(sellPrice * factor) / factor; + + prices.put(material, new ItemPrice(material, buyPrice, sellPrice, + buyEnabled, sellEnabled, categoryName)); + + } catch (IllegalArgumentException e) { + plugin.getLogger().log(Level.WARNING, + "Invalid material in pricing: " + materialName); + } + } + } + + plugin.getLogger().info("Loaded pricing for " + prices.size() + " items."); + } + + /** + * Gets the price data for a material. + * + * @param material the material + * @return the item price, or null if not priced + */ + public ItemPrice getPrice(Material material) { + return prices.get(material); + } + + /** + * Gets all priced items. + * + * @return unmodifiable map of all prices + */ + public Map getAllPrices() { + return Collections.unmodifiableMap(prices); + } + + /** + * Checks if a material has pricing configured. + * + * @param material the material + * @return true if pricing exists + */ + public boolean hasPrice(Material material) { + return prices.containsKey(material); + } + + /** + * Searches for items matching a query string. + * + * @param query the search query + * @return list of matching item prices + */ + public List search(String query) { + String lower = query.toLowerCase(); + List results = new ArrayList<>(); + for (ItemPrice price : prices.values()) { + String name = price.getMaterial().name().toLowerCase().replace("_", " "); + if (name.contains(lower)) { + results.add(price); + } + } + // Sort by relevance (exact match first, then alphabetical) + results.sort((a, b) -> { + String nameA = a.getMaterial().name().toLowerCase(); + String nameB = b.getMaterial().name().toLowerCase(); + boolean exactA = nameA.replace("_", " ").equals(lower); + boolean exactB = nameB.replace("_", " ").equals(lower); + if (exactA && !exactB) return -1; + if (!exactA && exactB) return 1; + return nameA.compareTo(nameB); + }); + return results; + } + + // ================================================================ + // Override loading helper + // ================================================================ + + private Map loadOverrides(ConfigurationSection section) { + Map overrides = new HashMap<>(); + if (section == null) return overrides; + + for (String materialName : section.getKeys(false)) { + try { + Material material = Material.valueOf(materialName); + ConfigurationSection itemSection = section.getConfigurationSection(materialName); + if (itemSection == null) continue; + + double buyPrice = itemSection.contains("buy") ? itemSection.getDouble("buy") : -1; + double sellPrice = itemSection.contains("sell") ? itemSection.getDouble("sell") : -1; + Boolean buyEnabled = itemSection.contains("buy-enabled") ? + itemSection.getBoolean("buy-enabled") : null; + Boolean sellEnabled = itemSection.contains("sell-enabled") ? + itemSection.getBoolean("sell-enabled") : null; + + overrides.put(material, new OverrideData(buyPrice, sellPrice, buyEnabled, sellEnabled)); + } catch (IllegalArgumentException e) { + plugin.getLogger().log(Level.WARNING, "Invalid material in overrides: " + materialName); + } + } + return overrides; + } + + /** + * Internal data class for per-item overrides. + */ + private record OverrideData(double buyPrice, double sellPrice, + Boolean buyEnabled, Boolean sellEnabled) {} +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/service/ShopService.java b/ServerShop/src/main/java/pt/henrique/servershop/service/ShopService.java new file mode 100644 index 0000000..ca570fb --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/service/ShopService.java @@ -0,0 +1,277 @@ +package pt.henrique.servershop.service; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import pt.henrique.servershop.ServerShop; +import pt.henrique.servershop.config.ConfigManager; +import pt.henrique.servershop.economy.EconomyManager; +import pt.henrique.servershop.pricing.ItemPrice; +import pt.henrique.servershop.pricing.PricingService; +import pt.henrique.servershop.transaction.TransactionLogger; +import pt.henrique.servershop.util.TextUtil; + +import java.util.HashMap; +import java.util.Map; + +/** + * Core service handling buy and sell operations. + * All validation and economy interactions go through this service. + */ +public class ShopService { + + private final ServerShop plugin; + + public ShopService(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Result of a shop operation (buy/sell). + */ + public record ShopResult(boolean success, String messageKey, Map placeholders) { + public static ShopResult success(String messageKey, Map placeholders) { + return new ShopResult(true, messageKey, placeholders); + } + + public static ShopResult failure(String messageKey, Map placeholders) { + return new ShopResult(false, messageKey, placeholders); + } + + public static ShopResult failure(String messageKey) { + return new ShopResult(false, messageKey, Map.of()); + } + } + + /** + * Attempts to process a buy transaction for a player. + * + * @param player the buyer + * @param material the material to buy + * @param amount the quantity to buy + * @return the result of the operation + */ + public ShopResult buy(Player player, Material material, int amount) { + PricingService pricing = plugin.getPricingService(); + EconomyManager economy = plugin.getEconomyManager(); + ConfigManager config = plugin.getConfigManager(); + + // Validate economy + if (!economy.isAvailable()) { + return ShopResult.failure("economy-unavailable"); + } + + // Validate item price + ItemPrice itemPrice = pricing.getPrice(material); + if (itemPrice == null || !itemPrice.isBuyEnabled()) { + return ShopResult.failure("buy-disabled"); + } + + if (itemPrice.getBuyPrice() <= 0) { + return ShopResult.failure("buy-zero-price"); + } + + // Validate amount + amount = Math.max(1, Math.min(amount, config.getMaxTransactionQuantity())); + + double totalCost = itemPrice.getBuyPrice() * amount; + String itemName = TextUtil.formatMaterialName(material.name()); + + // Re-check balance at transaction time (anti-exploit) + if (!economy.has(player, totalCost)) { + Map placeholders = Map.of( + "price", economy.format(totalCost), + "balance", economy.format(economy.getBalance(player)) + ); + return ShopResult.failure("buy-insufficient-funds", placeholders); + } + + // Check inventory space + int freeSpace = calculateFreeSpace(player, material); + if (freeSpace <= 0 && !config.isDropOnFullInventory()) { + return ShopResult.failure("buy-inventory-full-cancel"); + } + + // Withdraw money + if (!economy.withdraw(player, totalCost)) { + Map placeholders = Map.of( + "price", economy.format(totalCost), + "balance", economy.format(economy.getBalance(player)) + ); + return ShopResult.failure("buy-insufficient-funds", placeholders); + } + + // Give items + boolean dropped = giveItems(player, material, amount); + + // Log transaction + plugin.getTransactionLogger().log( + player.getUniqueId(), player.getName(), material, amount, totalCost, + TransactionLogger.TransactionType.BUY + ); + + Map placeholders = new HashMap<>(); + placeholders.put("amount", String.valueOf(amount)); + placeholders.put("item", itemName); + placeholders.put("price", economy.format(totalCost)); + + if (dropped) { + // Send additional message about dropped items + player.sendMessage(plugin.getMessageManager().getPrefixed("buy-inventory-full-drop")); + } + + return ShopResult.success("buy-success", placeholders); + } + + /** + * Attempts to process a sell transaction for a player. + * + * @param player the seller + * @param material the material to sell + * @param amount the quantity to sell + * @return the result of the operation + */ + public ShopResult sell(Player player, Material material, int amount) { + PricingService pricing = plugin.getPricingService(); + EconomyManager economy = plugin.getEconomyManager(); + ConfigManager config = plugin.getConfigManager(); + + // Validate economy + if (!economy.isAvailable()) { + return ShopResult.failure("economy-unavailable"); + } + + // Validate item price + ItemPrice itemPrice = pricing.getPrice(material); + if (itemPrice == null || !itemPrice.isSellEnabled()) { + return ShopResult.failure("sell-disabled"); + } + + if (itemPrice.getSellPrice() <= 0) { + return ShopResult.failure("sell-zero-price"); + } + + // Validate amount + amount = Math.max(1, Math.min(amount, config.getMaxTransactionQuantity())); + + // Re-check inventory at transaction time (anti-exploit) + int playerHas = countMaterial(player, material); + if (playerHas < amount) { + String itemName = TextUtil.formatMaterialName(material.name()); + Map placeholders = Map.of( + "amount", String.valueOf(amount), + "item", itemName + ); + return ShopResult.failure("sell-insufficient-items", placeholders); + } + + double totalEarnings = itemPrice.getSellPrice() * amount; + double tax = totalEarnings * config.getSellTax(); + double netEarnings = totalEarnings - tax; + String itemName = TextUtil.formatMaterialName(material.name()); + + // Remove items from inventory + removeItems(player, material, amount); + + // Deposit money + if (!economy.deposit(player, netEarnings)) { + // If deposit fails, give items back + giveItems(player, material, amount); + return ShopResult.failure("economy-unavailable"); + } + + // Log transaction + plugin.getTransactionLogger().log( + player.getUniqueId(), player.getName(), material, amount, netEarnings, + TransactionLogger.TransactionType.SELL + ); + + Map placeholders = new HashMap<>(); + placeholders.put("amount", String.valueOf(amount)); + placeholders.put("item", itemName); + placeholders.put("price", economy.format(netEarnings)); + placeholders.put("tax", economy.format(tax)); + + String messageKey = tax > 0 ? "sell-success-tax" : "sell-success"; + return ShopResult.success(messageKey, placeholders); + } + + /** + * Counts how many of a specific material a player has in their inventory. + * + * @param player the player + * @param material the material to count + * @return the total count + */ + public int countMaterial(Player player, Material material) { + int count = 0; + for (ItemStack item : player.getInventory().getContents()) { + if (item != null && item.getType() == material) { + count += item.getAmount(); + } + } + return count; + } + + /** + * Calculates how many of a material can fit in a player's inventory. + */ + private int calculateFreeSpace(Player player, Material material) { + int maxStack = material.getMaxStackSize(); + int free = 0; + for (ItemStack item : player.getInventory().getStorageContents()) { + if (item == null || item.getType().isAir()) { + free += maxStack; + } else if (item.getType() == material) { + free += maxStack - item.getAmount(); + } + } + return free; + } + + /** + * Gives items to a player, dropping overflow at their feet. + * + * @return true if some items had to be dropped + */ + private boolean giveItems(Player player, Material material, int amount) { + boolean dropped = false; + int maxStack = material.getMaxStackSize(); + + while (amount > 0) { + int stackSize = Math.min(amount, maxStack); + ItemStack stack = new ItemStack(material, stackSize); + HashMap overflow = player.getInventory().addItem(stack); + + if (!overflow.isEmpty()) { + if (plugin.getConfigManager().isDropOnFullInventory()) { + for (ItemStack leftover : overflow.values()) { + player.getWorld().dropItemNaturally(player.getLocation(), leftover); + } + dropped = true; + } + } + amount -= stackSize; + } + return dropped; + } + + /** + * Removes a specific amount of a material from a player's inventory. + */ + private void removeItems(Player player, Material material, int amount) { + ItemStack[] contents = player.getInventory().getContents(); + for (int i = 0; i < contents.length && amount > 0; i++) { + ItemStack item = contents[i]; + if (item != null && item.getType() == material) { + int take = Math.min(item.getAmount(), amount); + if (take >= item.getAmount()) { + player.getInventory().setItem(i, null); + } else { + item.setAmount(item.getAmount() - take); + } + amount -= take; + } + } + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/transaction/TransactionLogger.java b/ServerShop/src/main/java/pt/henrique/servershop/transaction/TransactionLogger.java new file mode 100644 index 0000000..9af08cb --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/transaction/TransactionLogger.java @@ -0,0 +1,117 @@ +package pt.henrique.servershop.transaction; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import pt.henrique.servershop.ServerShop; + +import java.io.File; +import java.sql.*; +import java.util.UUID; +import java.util.logging.Level; + +/** + * Logs buy/sell transactions to a SQLite database. + * All database operations run asynchronously to avoid blocking the main thread. + */ +public class TransactionLogger { + + private final ServerShop plugin; + private Connection connection; + private boolean enabled; + + public TransactionLogger(ServerShop plugin) { + this.plugin = plugin; + } + + /** + * Initializes the database connection and creates tables if needed. + */ + public void initialize() { + enabled = plugin.getConfigManager().isLoggingEnabled(); + if (!enabled) { + plugin.getLogger().info("Transaction logging is disabled."); + return; + } + + try { + String dbFile = plugin.getConfigManager().getDatabaseFile(); + File dbPath = new File(plugin.getDataFolder(), dbFile); + dbPath.getParentFile().mkdirs(); + + String url = "jdbc:sqlite:" + dbPath.getAbsolutePath(); + connection = DriverManager.getConnection(url); + + // Create transactions table + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS transactions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "uuid TEXT NOT NULL, " + + "player_name TEXT NOT NULL, " + + "material TEXT NOT NULL, " + + "amount INTEGER NOT NULL, " + + "total REAL NOT NULL, " + + "type TEXT NOT NULL, " + + "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP" + + ")" + ); + } + + plugin.getLogger().info("Transaction logging initialized (SQLite)."); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to initialize transaction database!", e); + enabled = false; + } + } + + /** + * Logs a transaction asynchronously. + * + * @param playerUuid the player's UUID + * @param playerName the player's name + * @param material the material traded + * @param amount the quantity traded + * @param total the total price + * @param type the transaction type (BUY or SELL) + */ + public void log(UUID playerUuid, String playerName, Material material, + int amount, double total, TransactionType type) { + if (!enabled || connection == null) return; + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO transactions (uuid, player_name, material, amount, total, type) " + + "VALUES (?, ?, ?, ?, ?, ?)")) { + ps.setString(1, playerUuid.toString()); + ps.setString(2, playerName); + ps.setString(3, material.name()); + ps.setInt(4, amount); + ps.setDouble(5, total); + ps.setString(6, type.name()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "Failed to log transaction", e); + } + }); + } + + /** + * Closes the database connection. + */ + public void close() { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "Error closing transaction database", e); + } + } + } + + /** + * Transaction type enum. + */ + public enum TransactionType { + BUY, SELL + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/util/ItemBuilder.java b/ServerShop/src/main/java/pt/henrique/servershop/util/ItemBuilder.java new file mode 100644 index 0000000..c5a63ef --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/util/ItemBuilder.java @@ -0,0 +1,220 @@ +package pt.henrique.servershop.util; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Fluent builder for creating ItemStacks with custom display properties. + */ +public class ItemBuilder { + + private final ItemStack item; + private final ItemMeta meta; + + /** + * Creates a new ItemBuilder for the given material. + * + * @param material the material type + */ + public ItemBuilder(Material material) { + this(material, 1); + } + + /** + * Creates a new ItemBuilder for the given material and amount. + * + * @param material the material type + * @param amount the stack amount + */ + public ItemBuilder(Material material, int amount) { + this.item = new ItemStack(material, amount); + this.meta = item.getItemMeta(); + } + + /** + * Creates a new ItemBuilder from an existing ItemStack (cloned). + * + * @param item the item to clone + */ + public ItemBuilder(ItemStack item) { + this.item = item.clone(); + this.meta = this.item.getItemMeta(); + } + + /** + * Sets the display name using Adventure Component. + * + * @param name the display name component + * @return this builder + */ + public ItemBuilder name(Component name) { + if (meta != null) { + meta.displayName(name); + } + return this; + } + + /** + * Sets the display name from a string with color codes. + * + * @param name the display name with & color codes + * @return this builder + */ + public ItemBuilder name(String name) { + return name(TextUtil.colorize(name)); + } + + /** + * Sets the lore from a list of Adventure Components. + * + * @param lore the lore lines + * @return this builder + */ + public ItemBuilder lore(List lore) { + if (meta != null) { + meta.lore(lore); + } + return this; + } + + /** + * Sets the lore from string lines with color codes. + * + * @param lines the lore lines with & color codes + * @return this builder + */ + public ItemBuilder loreStr(List lines) { + if (meta != null && lines != null) { + List components = new ArrayList<>(); + for (String line : lines) { + components.add(TextUtil.colorize(line)); + } + meta.lore(components); + } + return this; + } + + /** + * Sets the lore from string lines with color codes. + * + * @param lines the lore lines with & color codes + * @return this builder + */ + public ItemBuilder loreStr(String... lines) { + return loreStr(Arrays.asList(lines)); + } + + /** + * Adds a glowing enchantment effect (hidden). + * + * @return this builder + */ + public ItemBuilder glow() { + if (meta != null) { + meta.addEnchant(Enchantment.UNBREAKING, 1, true); + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + } + return this; + } + + /** + * Sets the item amount. + * + * @param amount the stack size + * @return this builder + */ + public ItemBuilder amount(int amount) { + item.setAmount(Math.max(1, Math.min(amount, 64))); + return this; + } + + /** + * Hides all item flags. + * + * @return this builder + */ + public ItemBuilder hideFlags() { + if (meta != null) { + meta.addItemFlags(ItemFlag.values()); + } + return this; + } + + /** + * Sets a custom model data value. + * + * @param data the custom model data + * @return this builder + */ + public ItemBuilder customModelData(int data) { + if (meta != null) { + meta.setCustomModelData(data); + } + return this; + } + + /** + * Builds and returns the final ItemStack. + * + * @return the constructed ItemStack + */ + public ItemStack build() { + if (meta != null) { + item.setItemMeta(meta); + } + return item; + } + + /** + * Creates a simple named item with lore, useful for GUI buttons. + * + * @param material the material + * @param name the display name + * @param lore the lore lines + * @return the constructed ItemStack + */ + public static ItemStack button(Material material, String name, String... lore) { + return new ItemBuilder(material).name(name).loreStr(lore).hideFlags().build(); + } + + /** + * Creates a simple named item with lore from a list. + * + * @param material the material + * @param name the display name + * @param lore the lore lines + * @return the constructed ItemStack + */ + public static ItemStack button(Material material, String name, List lore) { + return new ItemBuilder(material).name(name).loreStr(lore).hideFlags().build(); + } + + /** + * Applies placeholder replacements to a list of lore strings. + * + * @param template the lore template lines + * @param replacements the placeholder key-value pairs + * @return the processed lore lines + */ + public static List applyPlaceholders(List template, Map replacements) { + if (template == null) return new ArrayList<>(); + List result = new ArrayList<>(); + for (String line : template) { + String processed = line; + for (Map.Entry entry : replacements.entrySet()) { + processed = processed.replace("{" + entry.getKey() + "}", entry.getValue()); + } + result.add(processed); + } + return result; + } +} diff --git a/ServerShop/src/main/java/pt/henrique/servershop/util/TextUtil.java b/ServerShop/src/main/java/pt/henrique/servershop/util/TextUtil.java new file mode 100644 index 0000000..add596b --- /dev/null +++ b/ServerShop/src/main/java/pt/henrique/servershop/util/TextUtil.java @@ -0,0 +1,85 @@ +package pt.henrique.servershop.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +/** + * Utility class for text formatting and color code processing. + */ +public final class TextUtil { + + private static final LegacyComponentSerializer LEGACY_SERIALIZER = + LegacyComponentSerializer.legacyAmpersand(); + + private TextUtil() { + // Utility class + } + + /** + * Converts a string with ampersand color codes into an Adventure Component. + * + * @param text the raw text with & color codes + * @return the formatted Component + */ + public static Component colorize(String text) { + if (text == null || text.isEmpty()) { + return Component.empty(); + } + return LEGACY_SERIALIZER.deserialize(text); + } + + /** + * Converts a string with ampersand color codes into a legacy string with section sign codes. + * + * @param text the raw text with & color codes + * @return the formatted string with § color codes + */ + public static String color(String text) { + if (text == null) return ""; + return text.replace('&', '\u00A7'); + } + + /** + * Strips all color codes from a string. + * + * @param text the text to strip + * @return the text without color codes + */ + public static String stripColor(String text) { + if (text == null) return ""; + return text.replaceAll("[&§][0-9a-fk-orA-FK-OR]", ""); + } + + /** + * Formats a material name for display. + * Converts DIAMOND_SWORD to Diamond Sword. + * + * @param materialName the raw material name + * @return the formatted display name + */ + public static String formatMaterialName(String materialName) { + if (materialName == null || materialName.isEmpty()) return ""; + String[] parts = materialName.toLowerCase().split("_"); + StringBuilder sb = new StringBuilder(); + for (String part : parts) { + if (!part.isEmpty()) { + if (!sb.isEmpty()) sb.append(' '); + sb.append(Character.toUpperCase(part.charAt(0))); + sb.append(part.substring(1)); + } + } + return sb.toString(); + } + + /** + * Formats a currency value. + * + * @param amount the amount + * @param symbol the currency symbol + * @param decimalPlaces the number of decimal places + * @return the formatted currency string + */ + public static String formatCurrency(double amount, String symbol, int decimalPlaces) { + return symbol + String.format("%,." + decimalPlaces + "f", amount); + } +} diff --git a/ServerShop/src/main/resources/config.yml b/ServerShop/src/main/resources/config.yml new file mode 100644 index 0000000..b4fbbda --- /dev/null +++ b/ServerShop/src/main/resources/config.yml @@ -0,0 +1,97 @@ +# ============================================================ +# ServerShop - Configuration +# A server-run global market for Paper/Purpur 1.21.1 +# ============================================================ + +# Language: en_US or pt_PT +language: en_US + +# ============================================================ +# Economy Settings +# ============================================================ +economy: + # Currency symbol for display + symbol: "$" + # Decimal places for price display + decimal-places: 2 + # Tax rate on sell transactions (0.0 = no tax, 0.05 = 5%) + sell-tax: 0.0 + +# ============================================================ +# Pricing Settings +# ============================================================ +pricing: + # Global sell multiplier: sellPrice = buyPrice * sell-multiplier + # Low value (0.25) creates a large spread to keep CommunityMarket valuable + default-sell-multiplier: 0.25 + + # Whether to include items with special meta (enchanted books, potions, etc.) + # If false, only base items are shown in the shop + include-special-items: false + +# ============================================================ +# Shop Behavior +# ============================================================ +shop: + # What to do when inventory is full during purchase: + # DROP - drop items at player feet + # CANCEL - cancel the purchase + full-inventory-action: DROP + + # Enable "sell hand" button in main menu + enable-sell-hand: true + # Enable "sell inventory" button in main menu + enable-sell-inventory: true + + # Maximum quantity for a single transaction + max-transaction-quantity: 2304 + +# ============================================================ +# GUI Settings +# ============================================================ +gui: + # Main menu title (supports color codes with &) + main-title: "&8Server Shop" + # Category page title + category-title: "&8{category}" + # Item detail title + detail-title: "&8Buy/Sell" + # Search results title + search-title: "&8Search: {query}" + + # Filler item for empty slots (Material name) + filler-material: GRAY_STAINED_GLASS_PANE + # Filler item display name (empty = invisible name) + filler-name: " " + + # Items per page in category view + items-per-page: 28 + + # Play sounds on actions + sounds: + enabled: true + open: BLOCK_CHEST_OPEN + click: UI_BUTTON_CLICK + buy: ENTITY_EXPERIENCE_ORB_PICKUP + sell: ENTITY_EXPERIENCE_ORB_PICKUP + error: ENTITY_VILLAGER_NO + +# ============================================================ +# Transaction Logging (SQLite) +# ============================================================ +logging: + # Enable transaction logging to SQLite + enabled: true + # Database file (relative to plugin data folder) + database-file: transactions.db + +# ============================================================ +# Permissions (reference only - actual nodes in plugin.yml) +# ============================================================ +# servershop.use - Access the shop +# servershop.buy - Buy items +# servershop.sell - Sell items +# servershop.sell.hand - Use sell-hand feature +# servershop.sell.inventory - Use sell-inventory feature +# servershop.admin - Admin commands +# servershop.admin.reload - Reload configuration diff --git a/ServerShop/src/main/resources/lang/en_US.yml b/ServerShop/src/main/resources/lang/en_US.yml new file mode 100644 index 0000000..fbe20bf --- /dev/null +++ b/ServerShop/src/main/resources/lang/en_US.yml @@ -0,0 +1,138 @@ +# ============================================================ +# ServerShop - English (en_US) Language File +# ============================================================ + +prefix: "&7[&6ServerShop&7] " + +messages: + # General + no-permission: "&cYou don't have permission to do that." + player-only: "&cThis command can only be used by players." + reload-success: "&aConfiguration reloaded successfully." + economy-unavailable: "&cEconomy system is not available. Please contact an admin." + + # Buying + buy-success: "&aYou purchased &f{amount}x {item} &afor &6{price}&a." + buy-insufficient-funds: "&cYou don't have enough money. You need &6{price}&c but only have &6{balance}&c." + buy-inventory-full-drop: "&eYour inventory was full. Items were dropped at your feet." + buy-inventory-full-cancel: "&cYour inventory is full. Purchase cancelled." + buy-disabled: "&cThis item is not available for purchase." + buy-zero-price: "&cThis item cannot be purchased from the server." + + # Selling + sell-success: "&aYou sold &f{amount}x {item} &afor &6{price}&a." + sell-success-tax: "&aYou sold &f{amount}x {item} &afor &6{price} &7(tax: &6{tax}&7)&a." + sell-insufficient-items: "&cYou don't have enough items. You need &f{amount}x {item}&c." + sell-disabled: "&cThis item cannot be sold to the server." + sell-zero-price: "&cThis item has no sell value." + sell-hand-empty: "&cYou're not holding any items." + sell-hand-not-sellable: "&cThe item you're holding cannot be sold." + sell-inventory-nothing: "&cYou don't have any sellable items in your inventory." + sell-inventory-success: "&aSold all sellable items for &6{price}&a." + + # Confirmation + confirm-sell-hand: "&eClick to confirm selling &f{amount}x {item} &efor &6{price}&e." + confirm-sell-inventory: "&eClick to confirm selling all inventory items for &6{price}&e." + + # Search + search-prompt: "&eType the item name to search for:" + search-no-results: "&cNo items found matching '&f{query}&c'." + + # Admin + admin-usage: "&eUsage: /shop reload" + +gui: + titles: + main-menu: "&8Server Shop" + category: "&8{category}" + item-detail: "&8Buy/Sell" + search-results: "&8Search: {query}" + confirm-sell-hand: "&8Confirm Sale" + confirm-sell-inventory: "&8Confirm Sale" + + buttons: + search: "&eSearch" + search-lore: + - "&7Click to search for items" + sell-hand: "&6Sell Hand" + sell-hand-lore: + - "&7Click to sell the item" + - "&7you're holding" + sell-inventory: "&6Sell Inventory" + sell-inventory-lore: + - "&7Click to sell all sellable" + - "&7items in your inventory" + back: "&cBack" + back-lore: + - "&7Return to previous menu" + previous-page: "&ePrevious Page" + previous-page-lore: + - "&7Page {page}" + next-page: "&eNext Page" + next-page-lore: + - "&7Page {page}" + close: "&cClose" + close-lore: + - "&7Close the shop" + buy: "&a&lBUY" + buy-lore: + - "&7Click to buy &f{amount}x" + - "&7Cost: &6{price}" + sell: "&c&lSELL" + sell-lore: + - "&7Click to sell &f{amount}x" + - "&7Earn: &6{price}" + confirm: "&a&lCONFIRM" + confirm-lore: + - "&7Click to confirm" + cancel: "&c&lCANCEL" + cancel-lore: + - "&7Click to cancel" + decrease-1: "&c-1" + decrease-8: "&c-8" + decrease-16: "&c-16" + decrease-32: "&c-32" + increase-1: "&a+1" + increase-8: "&a+8" + increase-16: "&a+16" + increase-32: "&a+32" + min-quantity: "&eMin" + max-quantity: "&eMax" + info: "&bInfo" + + lore: + item-entry: + - "&7Buy: &a{buy_price}" + - "&7Sell: &c{sell_price}" + - "&7Spread: &e{spread}%" + - "" + - "&eClick to view details" + item-detail-info: + - "" + - "&7Buy Price: &a{buy_price}" + - "&7Sell Price: &c{sell_price}" + - "&7Server Spread: &e{spread}%" + - "" + - "&7Your Balance: &6{balance}" + - "&7Quantity: &f{amount}" + - "" + - "&7Buy Total: &a{buy_total}" + - "&7Sell Total: &c{sell_total}" + buy-disabled-lore: + - "&7Buy: &8Not available" + - "&7Sell: &c{sell_price}" + - "" + - "&eClick to view details" + sell-disabled-lore: + - "&7Buy: &a{buy_price}" + - "&7Sell: &8Not available" + - "" + - "&eClick to view details" + both-disabled-lore: + - "&7Buy: &8Not available" + - "&7Sell: &8Not available" + + category: + lore: + - "&7Click to browse" + - "&7{count} items available" diff --git a/ServerShop/src/main/resources/lang/pt_PT.yml b/ServerShop/src/main/resources/lang/pt_PT.yml new file mode 100644 index 0000000..e6c3aaa --- /dev/null +++ b/ServerShop/src/main/resources/lang/pt_PT.yml @@ -0,0 +1,138 @@ +# ============================================================ +# ServerShop - Português (pt_PT) Ficheiro de Idioma +# ============================================================ + +prefix: "&7[&6LojaServidor&7] " + +messages: + # Geral + no-permission: "&cNão tens permissão para fazer isso." + player-only: "&cEste comando só pode ser usado por jogadores." + reload-success: "&aConfiguração recarregada com sucesso." + economy-unavailable: "&cO sistema de economia não está disponível. Contacta um admin." + + # Compras + buy-success: "&aCompraste &f{amount}x {item} &apor &6{price}&a." + buy-insufficient-funds: "&cNão tens dinheiro suficiente. Precisas de &6{price}&c mas só tens &6{balance}&c." + buy-inventory-full-drop: "&eO teu inventário estava cheio. Os itens foram largados aos teus pés." + buy-inventory-full-cancel: "&cO teu inventário está cheio. Compra cancelada." + buy-disabled: "&cEste item não está disponível para compra." + buy-zero-price: "&cEste item não pode ser comprado ao servidor." + + # Vendas + sell-success: "&aVendeste &f{amount}x {item} &apor &6{price}&a." + sell-success-tax: "&aVendeste &f{amount}x {item} &apor &6{price} &7(taxa: &6{tax}&7)&a." + sell-insufficient-items: "&cNão tens itens suficientes. Precisas de &f{amount}x {item}&c." + sell-disabled: "&cEste item não pode ser vendido ao servidor." + sell-zero-price: "&cEste item não tem valor de venda." + sell-hand-empty: "&cNão estás a segurar nenhum item." + sell-hand-not-sellable: "&cO item que estás a segurar não pode ser vendido." + sell-inventory-nothing: "&cNão tens itens vendáveis no teu inventário." + sell-inventory-success: "&aVendeste todos os itens vendáveis por &6{price}&a." + + # Confirmação + confirm-sell-hand: "&eClica para confirmar a venda de &f{amount}x {item} &epor &6{price}&e." + confirm-sell-inventory: "&eClica para confirmar a venda de todos os itens do inventário por &6{price}&e." + + # Pesquisa + search-prompt: "&eEscreve o nome do item para pesquisar:" + search-no-results: "&cNenhum item encontrado com '&f{query}&c'." + + # Admin + admin-usage: "&eUso: /shop reload" + +gui: + titles: + main-menu: "&8Loja do Servidor" + category: "&8{category}" + item-detail: "&8Comprar/Vender" + search-results: "&8Pesquisa: {query}" + confirm-sell-hand: "&8Confirmar Venda" + confirm-sell-inventory: "&8Confirmar Venda" + + buttons: + search: "&ePesquisar" + search-lore: + - "&7Clica para pesquisar itens" + sell-hand: "&6Vender Mão" + sell-hand-lore: + - "&7Clica para vender o item" + - "&7que estás a segurar" + sell-inventory: "&6Vender Inventário" + sell-inventory-lore: + - "&7Clica para vender todos" + - "&7os itens vendáveis" + back: "&cVoltar" + back-lore: + - "&7Voltar ao menu anterior" + previous-page: "&ePágina Anterior" + previous-page-lore: + - "&7Página {page}" + next-page: "&ePróxima Página" + next-page-lore: + - "&7Página {page}" + close: "&cFechar" + close-lore: + - "&7Fechar a loja" + buy: "&a&lCOMPRAR" + buy-lore: + - "&7Clica para comprar &f{amount}x" + - "&7Custo: &6{price}" + sell: "&c&lVENDER" + sell-lore: + - "&7Clica para vender &f{amount}x" + - "&7Ganho: &6{price}" + confirm: "&a&lCONFIRMAR" + confirm-lore: + - "&7Clica para confirmar" + cancel: "&c&lCANCELAR" + cancel-lore: + - "&7Clica para cancelar" + decrease-1: "&c-1" + decrease-8: "&c-8" + decrease-16: "&c-16" + decrease-32: "&c-32" + increase-1: "&a+1" + increase-8: "&a+8" + increase-16: "&a+16" + increase-32: "&a+32" + min-quantity: "&eMín" + max-quantity: "&eMáx" + info: "&bInfo" + + lore: + item-entry: + - "&7Compra: &a{buy_price}" + - "&7Venda: &c{sell_price}" + - "&7Margem: &e{spread}%" + - "" + - "&eClica para ver detalhes" + item-detail-info: + - "" + - "&7Preço de Compra: &a{buy_price}" + - "&7Preço de Venda: &c{sell_price}" + - "&7Margem do Servidor: &e{spread}%" + - "" + - "&7O Teu Saldo: &6{balance}" + - "&7Quantidade: &f{amount}" + - "" + - "&7Total Compra: &a{buy_total}" + - "&7Total Venda: &c{sell_total}" + buy-disabled-lore: + - "&7Compra: &8Não disponível" + - "&7Venda: &c{sell_price}" + - "" + - "&eClica para ver detalhes" + sell-disabled-lore: + - "&7Compra: &a{buy_price}" + - "&7Venda: &8Não disponível" + - "" + - "&eClica para ver detalhes" + both-disabled-lore: + - "&7Compra: &8Não disponível" + - "&7Venda: &8Não disponível" + + category: + lore: + - "&7Clica para explorar" + - "&7{count} itens disponíveis" diff --git a/ServerShop/src/main/resources/plugin.yml b/ServerShop/src/main/resources/plugin.yml new file mode 100644 index 0000000..fb22629 --- /dev/null +++ b/ServerShop/src/main/resources/plugin.yml @@ -0,0 +1,39 @@ +name: ServerShop +version: ${project.version} +main: pt.henrique.servershop.ServerShop +api-version: '1.21' +description: Server-run global market with configurable pricing, GUI-driven interface, and Vault economy integration +author: henrique +website: https://github.com/henriquescrrrr/CommunityMarket +depend: [Vault] +load: POSTWORLD + +commands: + shop: + description: Opens the server shop GUI + usage: /shop + aliases: [servershop] + permission: servershop.use + +permissions: + servershop.use: + description: Allows access to the server shop + default: true + servershop.buy: + description: Allows buying items from the server shop + default: true + servershop.sell: + description: Allows selling items to the server shop + default: true + servershop.sell.hand: + description: Allows selling items held in hand + default: true + servershop.sell.inventory: + description: Allows selling all items from inventory + default: true + servershop.admin: + description: Admin permission for server shop management + default: op + servershop.admin.reload: + description: Allows reloading the server shop configuration + default: op diff --git a/ServerShop/src/main/resources/prices.yml b/ServerShop/src/main/resources/prices.yml new file mode 100644 index 0000000..5ca0a32 --- /dev/null +++ b/ServerShop/src/main/resources/prices.yml @@ -0,0 +1,627 @@ +# ============================================================ +# ServerShop - Prices Configuration +# All prices are BUY prices (server sells to player). +# SELL prices are auto-calculated: buyPrice * sell-multiplier +# Override sell prices per-item with explicit 'sell' key. +# ============================================================ + +# ============================================================ +# Category Definitions +# Each category has: +# icon - Material for the category icon +# sell-multiplier - Override global multiplier (optional) +# buy-enabled - Allow buying from this category (default: true) +# sell-enabled - Allow selling in this category (default: true) +# items - Map of Material -> price config +# ============================================================ +categories: + Blocks: + icon: GRASS_BLOCK + sell-multiplier: 0.25 + buy-enabled: true + sell-enabled: true + items: + STONE: 2.0 + GRANITE: 2.0 + POLISHED_GRANITE: 3.0 + DIORITE: 2.0 + POLISHED_DIORITE: 3.0 + ANDESITE: 2.0 + POLISHED_ANDESITE: 3.0 + DEEPSLATE: 3.0 + COBBLED_DEEPSLATE: 2.5 + POLISHED_DEEPSLATE: 4.0 + CALCITE: 4.0 + TUFF: 2.5 + DRIPSTONE_BLOCK: 4.0 + GRASS_BLOCK: 3.0 + DIRT: 1.0 + COARSE_DIRT: 1.5 + PODZOL: 4.0 + ROOTED_DIRT: 2.0 + MUD: 2.0 + COBBLESTONE: 1.5 + OAK_PLANKS: 2.0 + SPRUCE_PLANKS: 2.0 + BIRCH_PLANKS: 2.0 + JUNGLE_PLANKS: 2.5 + ACACIA_PLANKS: 2.5 + DARK_OAK_PLANKS: 2.5 + MANGROVE_PLANKS: 3.0 + CHERRY_PLANKS: 3.0 + BAMBOO_PLANKS: 3.0 + CRIMSON_PLANKS: 5.0 + WARPED_PLANKS: 5.0 + SAND: 1.5 + RED_SAND: 2.0 + GRAVEL: 1.5 + CLAY: 3.0 + BRICKS: 5.0 + STONE_BRICKS: 4.0 + MOSSY_STONE_BRICKS: 5.0 + CRACKED_STONE_BRICKS: 5.0 + CHISELED_STONE_BRICKS: 6.0 + PACKED_MUD: 3.0 + MUD_BRICKS: 4.0 + SANDSTONE: 4.0 + RED_SANDSTONE: 5.0 + SMOOTH_STONE: 3.5 + PRISMARINE: 8.0 + PRISMARINE_BRICKS: 10.0 + DARK_PRISMARINE: 12.0 + NETHERRACK: 1.0 + BASALT: 3.0 + POLISHED_BASALT: 4.0 + SMOOTH_BASALT: 4.0 + BLACKSTONE: 3.0 + POLISHED_BLACKSTONE: 4.0 + POLISHED_BLACKSTONE_BRICKS: 5.0 + END_STONE: 5.0 + END_STONE_BRICKS: 6.0 + PURPUR_BLOCK: 8.0 + PURPUR_PILLAR: 10.0 + OBSIDIAN: 15.0 + CRYING_OBSIDIAN: 20.0 + TERRACOTTA: 3.0 + WHITE_TERRACOTTA: 4.0 + ORANGE_TERRACOTTA: 4.0 + MAGENTA_TERRACOTTA: 4.0 + LIGHT_BLUE_TERRACOTTA: 4.0 + YELLOW_TERRACOTTA: 4.0 + LIME_TERRACOTTA: 4.0 + PINK_TERRACOTTA: 4.0 + GRAY_TERRACOTTA: 4.0 + LIGHT_GRAY_TERRACOTTA: 4.0 + CYAN_TERRACOTTA: 4.0 + PURPLE_TERRACOTTA: 4.0 + BLUE_TERRACOTTA: 4.0 + BROWN_TERRACOTTA: 4.0 + GREEN_TERRACOTTA: 4.0 + RED_TERRACOTTA: 4.0 + BLACK_TERRACOTTA: 4.0 + GLAZED_TERRACOTTA: 6.0 + WHITE_CONCRETE: 4.0 + ORANGE_CONCRETE: 4.0 + MAGENTA_CONCRETE: 4.0 + LIGHT_BLUE_CONCRETE: 4.0 + YELLOW_CONCRETE: 4.0 + LIME_CONCRETE: 4.0 + PINK_CONCRETE: 4.0 + GRAY_CONCRETE: 4.0 + LIGHT_GRAY_CONCRETE: 4.0 + CYAN_CONCRETE: 4.0 + PURPLE_CONCRETE: 4.0 + BLUE_CONCRETE: 4.0 + BROWN_CONCRETE: 4.0 + GREEN_CONCRETE: 4.0 + RED_CONCRETE: 4.0 + BLACK_CONCRETE: 4.0 + AMETHYST_BLOCK: 12.0 + COPPER_BLOCK: 40.0 + EXPOSED_COPPER: 35.0 + WEATHERED_COPPER: 30.0 + OXIDIZED_COPPER: 25.0 + CUT_COPPER: 45.0 + ICE: 3.0 + PACKED_ICE: 6.0 + BLUE_ICE: 12.0 + SNOW_BLOCK: 2.0 + MOSS_BLOCK: 4.0 + SCULK: 8.0 + + Ores: + icon: DIAMOND_ORE + sell-multiplier: 0.30 + items: + COAL: 3.0 + CHARCOAL: 2.5 + RAW_IRON: 8.0 + RAW_COPPER: 5.0 + RAW_GOLD: 15.0 + IRON_INGOT: 12.0 + COPPER_INGOT: 8.0 + GOLD_INGOT: 25.0 + DIAMOND: 100.0 + EMERALD: 50.0 + LAPIS_LAZULI: 10.0 + REDSTONE: 6.0 + QUARTZ: 8.0 + AMETHYST_SHARD: 8.0 + NETHERITE_SCRAP: 250.0 + NETHERITE_INGOT: 1000.0 + COAL_ORE: 5.0 + IRON_ORE: 10.0 + COPPER_ORE: 7.0 + GOLD_ORE: 20.0 + DIAMOND_ORE: 120.0 + EMERALD_ORE: 60.0 + LAPIS_ORE: 15.0 + REDSTONE_ORE: 10.0 + NETHER_QUARTZ_ORE: 12.0 + NETHER_GOLD_ORE: 18.0 + ANCIENT_DEBRIS: 300.0 + DEEPSLATE_COAL_ORE: 6.0 + DEEPSLATE_IRON_ORE: 12.0 + DEEPSLATE_COPPER_ORE: 9.0 + DEEPSLATE_GOLD_ORE: 25.0 + DEEPSLATE_DIAMOND_ORE: 140.0 + DEEPSLATE_EMERALD_ORE: 70.0 + DEEPSLATE_LAPIS_ORE: 18.0 + DEEPSLATE_REDSTONE_ORE: 12.0 + IRON_BLOCK: 108.0 + GOLD_BLOCK: 225.0 + DIAMOND_BLOCK: 900.0 + EMERALD_BLOCK: 450.0 + LAPIS_BLOCK: 90.0 + REDSTONE_BLOCK: 54.0 + COAL_BLOCK: 27.0 + COPPER_BLOCK: 72.0 + NETHERITE_BLOCK: 9000.0 + + Farming: + icon: WHEAT + sell-multiplier: 0.20 + items: + WHEAT_SEEDS: 0.5 + WHEAT: 2.0 + HAY_BLOCK: 18.0 + BEETROOT_SEEDS: 0.5 + BEETROOT: 2.0 + CARROT: 2.0 + POTATO: 2.0 + POISONOUS_POTATO: 1.0 + MELON_SLICE: 1.5 + MELON: 12.0 + PUMPKIN: 5.0 + SUGAR_CANE: 2.0 + SUGAR: 2.5 + COCOA_BEANS: 3.0 + BAMBOO: 1.0 + CACTUS: 2.0 + CHORUS_FRUIT: 5.0 + CHORUS_FLOWER: 8.0 + SWEET_BERRIES: 2.0 + GLOW_BERRIES: 4.0 + KELP: 1.5 + DRIED_KELP: 2.0 + DRIED_KELP_BLOCK: 18.0 + NETHER_WART: 4.0 + NETHER_WART_BLOCK: 36.0 + BONE_MEAL: 2.0 + EGG: 2.0 + HONEYCOMB: 5.0 + HONEY_BOTTLE: 8.0 + HONEYCOMB_BLOCK: 20.0 + OAK_SAPLING: 3.0 + SPRUCE_SAPLING: 3.0 + BIRCH_SAPLING: 3.0 + JUNGLE_SAPLING: 4.0 + ACACIA_SAPLING: 4.0 + DARK_OAK_SAPLING: 4.0 + CHERRY_SAPLING: 5.0 + MANGROVE_PROPAGULE: 4.0 + OAK_LOG: 3.0 + SPRUCE_LOG: 3.0 + BIRCH_LOG: 3.0 + JUNGLE_LOG: 3.5 + ACACIA_LOG: 3.5 + DARK_OAK_LOG: 3.5 + MANGROVE_LOG: 4.0 + CHERRY_LOG: 4.0 + CRIMSON_STEM: 5.0 + WARPED_STEM: 5.0 + VINE: 3.0 + LILY_PAD: 3.0 + SEA_PICKLE: 4.0 + + Food: + icon: COOKED_BEEF + sell-multiplier: 0.20 + items: + APPLE: 3.0 + GOLDEN_APPLE: 100.0 + ENCHANTED_GOLDEN_APPLE: 500.0 + BREAD: 4.0 + COOKED_BEEF: 8.0 + COOKED_PORKCHOP: 8.0 + COOKED_CHICKEN: 6.0 + COOKED_MUTTON: 7.0 + COOKED_RABBIT: 6.0 + COOKED_COD: 5.0 + COOKED_SALMON: 6.0 + BEEF: 5.0 + PORKCHOP: 5.0 + CHICKEN: 3.0 + MUTTON: 4.0 + RABBIT: 4.0 + COD: 3.0 + SALMON: 4.0 + TROPICAL_FISH: 6.0 + PUFFERFISH: 5.0 + BAKED_POTATO: 5.0 + COOKIE: 3.0 + CAKE: 20.0 + PUMPKIN_PIE: 10.0 + MUSHROOM_STEW: 8.0 + RABBIT_STEW: 15.0 + BEETROOT_SOUP: 6.0 + SUSPICIOUS_STEW: 10.0 + DRIED_KELP: 2.0 + MELON_SLICE: 1.5 + GLOW_BERRIES: 4.0 + SWEET_BERRIES: 2.0 + + Mob_Drops: + icon: ROTTEN_FLESH + sell-multiplier: 0.25 + items: + ROTTEN_FLESH: 1.0 + BONE: 2.0 + STRING: 2.5 + SPIDER_EYE: 3.0 + GUNPOWDER: 5.0 + ENDER_PEARL: 15.0 + BLAZE_ROD: 20.0 + GHAST_TEAR: 25.0 + MAGMA_CREAM: 12.0 + SLIME_BALL: 8.0 + PRISMARINE_SHARD: 5.0 + PRISMARINE_CRYSTALS: 6.0 + PHANTOM_MEMBRANE: 15.0 + RABBIT_HIDE: 3.0 + RABBIT_FOOT: 10.0 + LEATHER: 5.0 + FEATHER: 2.0 + INK_SAC: 3.0 + GLOW_INK_SAC: 8.0 + ARROW: 2.0 + WITHER_SKELETON_SKULL: 100.0 + NETHER_STAR: 500.0 + SHULKER_SHELL: 50.0 + DRAGON_BREATH: 40.0 + EXPERIENCE_BOTTLE: 20.0 + TOTEM_OF_UNDYING: 200.0 + TRIDENT: 300.0 + NAUTILUS_SHELL: 25.0 + HEART_OF_THE_SEA: 100.0 + ECHO_SHARD: 30.0 + DISC_FRAGMENT_5: 20.0 + GOAT_HORN: 15.0 + ARMADILLO_SCUTE: 10.0 + BREEZE_ROD: 25.0 + + Redstone: + icon: REDSTONE + sell-multiplier: 0.25 + items: + REDSTONE: 6.0 + REDSTONE_BLOCK: 54.0 + REDSTONE_TORCH: 4.0 + REPEATER: 10.0 + COMPARATOR: 15.0 + PISTON: 15.0 + STICKY_PISTON: 20.0 + SLIME_BLOCK: 72.0 + HONEY_BLOCK: 32.0 + OBSERVER: 18.0 + HOPPER: 30.0 + DROPPER: 10.0 + DISPENSER: 12.0 + LEVER: 2.0 + STONE_BUTTON: 2.0 + OAK_BUTTON: 2.0 + STONE_PRESSURE_PLATE: 3.0 + OAK_PRESSURE_PLATE: 3.0 + TRIPWIRE_HOOK: 5.0 + TRAPPED_CHEST: 12.0 + DAYLIGHT_DETECTOR: 15.0 + NOTE_BLOCK: 10.0 + TARGET: 12.0 + TNT: 25.0 + RAIL: 5.0 + POWERED_RAIL: 20.0 + DETECTOR_RAIL: 15.0 + ACTIVATOR_RAIL: 15.0 + MINECART: 25.0 + CHEST_MINECART: 30.0 + HOPPER_MINECART: 45.0 + TNT_MINECART: 40.0 + FURNACE_MINECART: 30.0 + SCULK_SENSOR: 15.0 + CALIBRATED_SCULK_SENSOR: 25.0 + LIGHTNING_ROD: 15.0 + CRAFTER: 20.0 + + Decoration: + icon: PAINTING + sell-multiplier: 0.20 + items: + TORCH: 1.5 + SOUL_TORCH: 2.5 + LANTERN: 8.0 + SOUL_LANTERN: 10.0 + CANDLE: 4.0 + SEA_LANTERN: 15.0 + GLOWSTONE: 12.0 + SHROOMLIGHT: 10.0 + PAINTING: 5.0 + ITEM_FRAME: 5.0 + GLOW_ITEM_FRAME: 12.0 + ARMOR_STAND: 10.0 + FLOWER_POT: 4.0 + GLASS: 3.0 + GLASS_PANE: 2.0 + WHITE_STAINED_GLASS: 4.0 + WHITE_STAINED_GLASS_PANE: 3.0 + CHAIN: 6.0 + IRON_BARS: 5.0 + BELL: 50.0 + BOOKSHELF: 15.0 + CHISELED_BOOKSHELF: 18.0 + DECORATED_POT: 8.0 + BANNER_PATTERN: 15.0 + WHITE_BANNER: 12.0 + WHITE_BED: 15.0 + WHITE_CARPET: 2.0 + WHITE_WOOL: 4.0 + ORANGE_WOOL: 4.0 + MAGENTA_WOOL: 4.0 + LIGHT_BLUE_WOOL: 4.0 + YELLOW_WOOL: 4.0 + LIME_WOOL: 4.0 + PINK_WOOL: 4.0 + GRAY_WOOL: 4.0 + LIGHT_GRAY_WOOL: 4.0 + CYAN_WOOL: 4.0 + PURPLE_WOOL: 4.0 + BLUE_WOOL: 4.0 + BROWN_WOOL: 4.0 + GREEN_WOOL: 4.0 + RED_WOOL: 4.0 + BLACK_WOOL: 4.0 + DANDELION: 2.0 + POPPY: 2.0 + BLUE_ORCHID: 3.0 + ALLIUM: 3.0 + AZURE_BLUET: 3.0 + RED_TULIP: 3.0 + ORANGE_TULIP: 3.0 + WHITE_TULIP: 3.0 + PINK_TULIP: 3.0 + OXEYE_DAISY: 3.0 + CORNFLOWER: 3.0 + LILY_OF_THE_VALLEY: 3.0 + SUNFLOWER: 4.0 + LILAC: 4.0 + ROSE_BUSH: 4.0 + PEONY: 4.0 + WITHER_ROSE: 25.0 + TORCHFLOWER: 8.0 + PITCHER_PLANT: 10.0 + SPORE_BLOSSOM: 15.0 + MOSS_CARPET: 3.0 + PINK_PETALS: 3.0 + BIG_DRIPLEAF: 5.0 + SMALL_DRIPLEAF: 3.0 + HANGING_ROOTS: 2.0 + HEAD_PLAYER: 20.0 + + Tools: + icon: DIAMOND_PICKAXE + sell-multiplier: 0.20 + items: + WOODEN_PICKAXE: 3.0 + STONE_PICKAXE: 5.0 + IRON_PICKAXE: 30.0 + GOLDEN_PICKAXE: 55.0 + DIAMOND_PICKAXE: 210.0 + NETHERITE_PICKAXE: 1210.0 + WOODEN_AXE: 3.0 + STONE_AXE: 5.0 + IRON_AXE: 30.0 + GOLDEN_AXE: 55.0 + DIAMOND_AXE: 210.0 + NETHERITE_AXE: 1210.0 + WOODEN_SHOVEL: 2.0 + STONE_SHOVEL: 4.0 + IRON_SHOVEL: 15.0 + GOLDEN_SHOVEL: 30.0 + DIAMOND_SHOVEL: 110.0 + NETHERITE_SHOVEL: 1110.0 + WOODEN_HOE: 2.0 + STONE_HOE: 4.0 + IRON_HOE: 15.0 + GOLDEN_HOE: 30.0 + DIAMOND_HOE: 110.0 + NETHERITE_HOE: 1110.0 + SHEARS: 10.0 + FISHING_ROD: 10.0 + FLINT_AND_STEEL: 10.0 + COMPASS: 20.0 + RECOVERY_COMPASS: 50.0 + CLOCK: 20.0 + SPYGLASS: 15.0 + BRUSH: 15.0 + LEAD: 8.0 + NAME_TAG: 25.0 + BUCKET: 15.0 + WATER_BUCKET: 18.0 + LAVA_BUCKET: 25.0 + MILK_BUCKET: 20.0 + SADDLE: 30.0 + ELYTRA: 2000.0 + MACE: 1500.0 + + Combat: + icon: DIAMOND_SWORD + sell-multiplier: 0.20 + items: + WOODEN_SWORD: 2.0 + STONE_SWORD: 4.0 + IRON_SWORD: 18.0 + GOLDEN_SWORD: 30.0 + DIAMOND_SWORD: 210.0 + NETHERITE_SWORD: 1210.0 + BOW: 10.0 + CROSSBOW: 25.0 + ARROW: 2.0 + SPECTRAL_ARROW: 5.0 + TIPPED_ARROW: 8.0 + SHIELD: 15.0 + LEATHER_HELMET: 10.0 + LEATHER_CHESTPLATE: 16.0 + LEATHER_LEGGINGS: 14.0 + LEATHER_BOOTS: 8.0 + IRON_HELMET: 30.0 + IRON_CHESTPLATE: 48.0 + IRON_LEGGINGS: 42.0 + IRON_BOOTS: 24.0 + GOLDEN_HELMET: 60.0 + GOLDEN_CHESTPLATE: 96.0 + GOLDEN_LEGGINGS: 84.0 + GOLDEN_BOOTS: 48.0 + DIAMOND_HELMET: 260.0 + DIAMOND_CHESTPLATE: 416.0 + DIAMOND_LEGGINGS: 364.0 + DIAMOND_BOOTS: 208.0 + NETHERITE_HELMET: 1260.0 + NETHERITE_CHESTPLATE: 1416.0 + NETHERITE_LEGGINGS: 1364.0 + NETHERITE_BOOTS: 1208.0 + CHAINMAIL_HELMET: 25.0 + CHAINMAIL_CHESTPLATE: 40.0 + CHAINMAIL_LEGGINGS: 35.0 + CHAINMAIL_BOOTS: 20.0 + TURTLE_HELMET: 50.0 + WOLF_ARMOR: 60.0 + HORSE_ARMOR: 40.0 + + Brewing: + icon: BREWING_STAND + sell-multiplier: 0.25 + items: + BREWING_STAND: 25.0 + BLAZE_POWDER: 12.0 + GLASS_BOTTLE: 2.0 + WATER_BOTTLE: 3.0 + NETHER_WART: 4.0 + SPIDER_EYE: 3.0 + FERMENTED_SPIDER_EYE: 8.0 + GLISTERING_MELON_SLICE: 15.0 + GOLDEN_CARROT: 15.0 + RABBIT_FOOT: 10.0 + MAGMA_CREAM: 12.0 + GHAST_TEAR: 25.0 + PHANTOM_MEMBRANE: 15.0 + DRAGON_BREATH: 40.0 + SUGAR: 2.5 + REDSTONE: 6.0 + GLOWSTONE_DUST: 8.0 + GUNPOWDER: 5.0 + CAULDRON: 20.0 + + Misc: + icon: CHEST + sell-multiplier: 0.20 + items: + CHEST: 10.0 + ENDER_CHEST: 100.0 + BARREL: 12.0 + FURNACE: 10.0 + BLAST_FURNACE: 30.0 + SMOKER: 15.0 + CRAFTING_TABLE: 5.0 + ENCHANTING_TABLE: 200.0 + ANVIL: 90.0 + GRINDSTONE: 15.0 + SMITHING_TABLE: 20.0 + STONECUTTER: 12.0 + LOOM: 8.0 + CARTOGRAPHY_TABLE: 12.0 + FLETCHING_TABLE: 12.0 + COMPOSTER: 10.0 + LECTERN: 20.0 + BEACON: 2000.0 + CONDUIT: 300.0 + LODESTONE: 150.0 + RESPAWN_ANCHOR: 200.0 + JUKEBOX: 100.0 + END_CRYSTAL: 100.0 + FIRE_CHARGE: 5.0 + FIREWORK_ROCKET: 8.0 + FIREWORK_STAR: 10.0 + MAP: 5.0 + PAPER: 2.0 + BOOK: 5.0 + WRITABLE_BOOK: 8.0 + ENDER_EYE: 25.0 + SPONGE: 30.0 + WET_SPONGE: 28.0 + SCAFFOLDING: 3.0 + LADDER: 3.0 + SIGN: 3.0 + TRIAL_KEY: 30.0 + OMINOUS_TRIAL_KEY: 80.0 + WIND_CHARGE: 15.0 + +# ============================================================ +# Per-Item Overrides +# Override specific items with explicit buy/sell prices, +# or disable buying/selling for specific items. +# Format: +# MATERIAL_NAME: +# buy: (override buy price) +# sell: (override sell price) +# buy-enabled: false (disable buying) +# sell-enabled: false (disable selling) +# ============================================================ +overrides: + BEDROCK: + buy: 10000.0 + sell-enabled: false + COMMAND_BLOCK: + buy-enabled: false + sell-enabled: false + BARRIER: + buy-enabled: false + sell-enabled: false + SPAWNER: + buy: 5000.0 + sell-enabled: false + DRAGON_EGG: + buy-enabled: false + sell: 5000.0 + ENCHANTED_GOLDEN_APPLE: + sell: 200.0 + NETHER_STAR: + sell: 200.0 + ELYTRA: + sell: 500.0 + BEACON: + sell: 600.0 + NETHERITE_INGOT: + sell: 350.0 + NETHERITE_BLOCK: + sell: 3000.0 + MACE: + sell: 400.0