From b6c1acb547fa547dab5c8cec90396a1eb13dffb3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Feb 2026 10:49:26 +0000
Subject: [PATCH 1/3] Initial plan
--
2.52.0
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 2/3] 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
--
2.52.0
From 187c67e8f74327205169ee4ecddc35460c0a236f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Feb 2026 11:03:39 +0000
Subject: [PATCH 3/3] Fix invalid material names in prices.yml
(GLAZED_TERRACOTTA, HORSE_ARMOR, etc.)
Co-authored-by: henriquescrrrr <192057244+henriquescrrrr@users.noreply.github.com>
---
ServerShop/src/main/resources/prices.yml | 30 +++++++++++++++++++-----
1 file changed, 24 insertions(+), 6 deletions(-)
diff --git a/ServerShop/src/main/resources/prices.yml b/ServerShop/src/main/resources/prices.yml
index 5ca0a32..cb0a58a 100644
--- a/ServerShop/src/main/resources/prices.yml
+++ b/ServerShop/src/main/resources/prices.yml
@@ -99,7 +99,22 @@ categories:
GREEN_TERRACOTTA: 4.0
RED_TERRACOTTA: 4.0
BLACK_TERRACOTTA: 4.0
- GLAZED_TERRACOTTA: 6.0
+ WHITE_GLAZED_TERRACOTTA: 6.0
+ ORANGE_GLAZED_TERRACOTTA: 6.0
+ MAGENTA_GLAZED_TERRACOTTA: 6.0
+ LIGHT_BLUE_GLAZED_TERRACOTTA: 6.0
+ YELLOW_GLAZED_TERRACOTTA: 6.0
+ LIME_GLAZED_TERRACOTTA: 6.0
+ PINK_GLAZED_TERRACOTTA: 6.0
+ GRAY_GLAZED_TERRACOTTA: 6.0
+ LIGHT_GRAY_GLAZED_TERRACOTTA: 6.0
+ CYAN_GLAZED_TERRACOTTA: 6.0
+ PURPLE_GLAZED_TERRACOTTA: 6.0
+ BLUE_GLAZED_TERRACOTTA: 6.0
+ BROWN_GLAZED_TERRACOTTA: 6.0
+ GREEN_GLAZED_TERRACOTTA: 6.0
+ RED_GLAZED_TERRACOTTA: 6.0
+ BLACK_GLAZED_TERRACOTTA: 6.0
WHITE_CONCRETE: 4.0
ORANGE_CONCRETE: 4.0
MAGENTA_CONCRETE: 4.0
@@ -379,7 +394,7 @@ categories:
BOOKSHELF: 15.0
CHISELED_BOOKSHELF: 18.0
DECORATED_POT: 8.0
- BANNER_PATTERN: 15.0
+ FLOWER_BANNER_PATTERN: 15.0
WHITE_BANNER: 12.0
WHITE_BED: 15.0
WHITE_CARPET: 2.0
@@ -424,7 +439,7 @@ categories:
BIG_DRIPLEAF: 5.0
SMALL_DRIPLEAF: 3.0
HANGING_ROOTS: 2.0
- HEAD_PLAYER: 20.0
+ PLAYER_HEAD: 20.0
Tools:
icon: DIAMOND_PICKAXE
@@ -514,7 +529,10 @@ categories:
CHAINMAIL_BOOTS: 20.0
TURTLE_HELMET: 50.0
WOLF_ARMOR: 60.0
- HORSE_ARMOR: 40.0
+ LEATHER_HORSE_ARMOR: 30.0
+ IRON_HORSE_ARMOR: 40.0
+ GOLDEN_HORSE_ARMOR: 60.0
+ DIAMOND_HORSE_ARMOR: 100.0
Brewing:
icon: BREWING_STAND
@@ -523,7 +541,7 @@ categories:
BREWING_STAND: 25.0
BLAZE_POWDER: 12.0
GLASS_BOTTLE: 2.0
- WATER_BOTTLE: 3.0
+ GLASS_BOTTLE: 2.0
NETHER_WART: 4.0
SPIDER_EYE: 3.0
FERMENTED_SPIDER_EYE: 8.0
@@ -579,7 +597,7 @@ categories:
WET_SPONGE: 28.0
SCAFFOLDING: 3.0
LADDER: 3.0
- SIGN: 3.0
+ OAK_SIGN: 3.0
TRIAL_KEY: 30.0
OMINOUS_TRIAL_KEY: 80.0
WIND_CHARGE: 15.0
--
2.52.0