WIP: Add ServerShop plugin — server-run global market with configurable pricing #2

Draft
Copilot wants to merge 3 commits from copilot/add-server-shop-plugin into main
28 changed files with 4416 additions and 0 deletions
+17
View File
@@ -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
+166
View File
@@ -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.
+104
View File
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pt.henrique</groupId>
<artifactId>servershop</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>ServerShop</name>
<description>A server-run global market plugin for Paper/Purpur 1.21.1 with configurable pricing and GUI-driven UX</description>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<paper.version>1.21.1-R0.1-SNAPSHOT</paper.version>
</properties>
<repositories>
<!-- Paper Repository -->
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<!-- Vault Repository -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- Paper API -->
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${paper.version}</version>
<scope>provided</scope>
</dependency>
<!-- Vault API for Economy -->
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7.1</version>
<scope>provided</scope>
</dependency>
<!-- SQLite JDBC Driver -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.sqlite</pattern>
<shadedPattern>pt.henrique.servershop.libs.sqlite</shadedPattern>
</relocation>
</relocations>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
@@ -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.
* <p>
* 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.
* </p>
*
* @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; }
}
@@ -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<String, ShopCategory> categories = new LinkedHashMap<>();
private final Map<Material, String> 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<ShopCategory> 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<Material> getAllMaterials() {
return Collections.unmodifiableSet(materialToCategory.keySet());
}
}
@@ -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<Material> 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<Material> 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();
}
}
@@ -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<String> 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();
}
}
@@ -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);
}
}
@@ -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<String, String> 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<String, String> 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<String, String> 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<String> getList(String path) {
List<String> 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<String> getList(String path, Map<String, String> replacements) {
List<String> raw = getList(path);
List<String> 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<String, String> replacements) {
if (replacements == null || replacements.isEmpty()) return text;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
text = text.replace("{" + entry.getKey() + "}", entry.getValue());
}
return text;
}
}
@@ -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<Economy> 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);
}
}
}
@@ -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<Material> 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<Material> 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<String, String> 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<String> 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;
}
}
@@ -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<UUID> 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<ItemPrice> 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);
}
}
}
@@ -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<String, String> 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<String> 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<String, String> 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<String, String> 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;
}
}
@@ -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<ShopCategory> categories = plugin.getCategoryRegistry().getCategories();
int index = 0;
for (ShopCategory category : categories) {
if (index >= categorySlots.length) break;
Map<String, String> placeholders = Map.of("count", String.valueOf(category.getItemCount()));
List<String> 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<ShopCategory> 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;
}
}
@@ -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<ItemPrice> pagedItems;
private final List<ItemPrice> 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<String, String> placeholders = Map.of(
"buy_price", buyPriceStr,
"sell_price", sellPriceStr,
"spread", spreadStr
);
List<String> 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;
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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<Material, ItemPrice> 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<Material, OverrideData> 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<Material, ItemPrice> 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<ItemPrice> search(String query) {
String lower = query.toLowerCase();
List<ItemPrice> 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<Material, OverrideData> loadOverrides(ConfigurationSection section) {
Map<Material, OverrideData> 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) {}
}
@@ -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<String, String> placeholders) {
public static ShopResult success(String messageKey, Map<String, String> placeholders) {
return new ShopResult(true, messageKey, placeholders);
}
public static ShopResult failure(String messageKey, Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<Integer, ItemStack> 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;
}
}
}
}
@@ -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
}
}
@@ -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 &amp; 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<Component> lore) {
if (meta != null) {
meta.lore(lore);
}
return this;
}
/**
* Sets the lore from string lines with color codes.
*
* @param lines the lore lines with &amp; color codes
* @return this builder
*/
public ItemBuilder loreStr(List<String> lines) {
if (meta != null && lines != null) {
List<Component> 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 &amp; 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<String> 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<String> applyPlaceholders(List<String> template, Map<String, String> replacements) {
if (template == null) return new ArrayList<>();
List<String> result = new ArrayList<>();
for (String line : template) {
String processed = line;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
processed = processed.replace("{" + entry.getKey() + "}", entry.getValue());
}
result.add(processed);
}
return result;
}
}
@@ -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 &amp; 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 &amp; 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);
}
}
+97
View File
@@ -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
@@ -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"
@@ -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"
+39
View File
@@ -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
+645
View File
@@ -0,0 +1,645 @@
# ============================================================
# 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
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
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
FLOWER_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
PLAYER_HEAD: 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
LEATHER_HORSE_ARMOR: 30.0
IRON_HORSE_ARMOR: 40.0
GOLDEN_HORSE_ARMOR: 60.0
DIAMOND_HORSE_ARMOR: 100.0
Brewing:
icon: BREWING_STAND
sell-multiplier: 0.25
items:
BREWING_STAND: 25.0
BLAZE_POWDER: 12.0
GLASS_BOTTLE: 2.0
GLASS_BOTTLE: 2.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
OAK_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: <price> (override buy price)
# sell: <price> (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