Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 187c67e8f7 | |||
| 06a4d1df48 | |||
| b6c1acb547 |
+8
-18
@@ -1,27 +1,17 @@
|
||||
# Maven build outputs
|
||||
# Maven build artifacts
|
||||
target/
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
# IntelliJ IDEA
|
||||
# IDE files
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Eclipse
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
*.class
|
||||
|
||||
# Windows
|
||||
# OS files
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
|
||||
# Local dev
|
||||
*.log
|
||||
# ServerShop build artifacts
|
||||
ServerShop/target/
|
||||
ServerShop/dependency-reduced-pom.xml
|
||||
|
||||
@@ -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.
|
||||
@@ -10,7 +10,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>ServerShop</name>
|
||||
<description>A server-run global market plugin for Paper/Purpur 1.21 with large buy/sell spread to complement CommunityMarket</description>
|
||||
<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>
|
||||
@@ -80,14 +80,13 @@
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>org.sqlite</pattern>
|
||||
<shadedPattern>pt.henrique.servershop.libs.sqlite</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
<minimizeJar>false</minimizeJar>
|
||||
<minimizeJar>true</minimizeJar>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
@@ -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 & 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 & 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 & 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 & color codes
|
||||
* @return the formatted Component
|
||||
*/
|
||||
public static Component colorize(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return Component.empty();
|
||||
}
|
||||
return LEGACY_SERIALIZER.deserialize(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string with ampersand color codes into a legacy string with section sign codes.
|
||||
*
|
||||
* @param text the raw text with & color codes
|
||||
* @return the formatted string with § color codes
|
||||
*/
|
||||
public static String color(String text) {
|
||||
if (text == null) return "";
|
||||
return text.replace('&', '\u00A7');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips all color codes from a string.
|
||||
*
|
||||
* @param text the text to strip
|
||||
* @return the text without color codes
|
||||
*/
|
||||
public static String stripColor(String text) {
|
||||
if (text == null) return "";
|
||||
return text.replaceAll("[&§][0-9a-fk-orA-FK-OR]", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a material name for display.
|
||||
* Converts DIAMOND_SWORD to Diamond Sword.
|
||||
*
|
||||
* @param materialName the raw material name
|
||||
* @return the formatted display name
|
||||
*/
|
||||
public static String formatMaterialName(String materialName) {
|
||||
if (materialName == null || materialName.isEmpty()) return "";
|
||||
String[] parts = materialName.toLowerCase().split("_");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String part : parts) {
|
||||
if (!part.isEmpty()) {
|
||||
if (!sb.isEmpty()) sb.append(' ');
|
||||
sb.append(Character.toUpperCase(part.charAt(0)));
|
||||
sb.append(part.substring(1));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a currency value.
|
||||
*
|
||||
* @param amount the amount
|
||||
* @param symbol the currency symbol
|
||||
* @param decimalPlaces the number of decimal places
|
||||
* @return the formatted currency string
|
||||
*/
|
||||
public static String formatCurrency(double amount, String symbol, int decimalPlaces) {
|
||||
return symbol + String.format("%,." + decimalPlaces + "f", amount);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,234 +0,0 @@
|
||||
# ServerShop
|
||||
|
||||
A professional **server-run global market** plugin for **Paper/Purpur 1.21** using **Java 21** and **Maven**.
|
||||
|
||||
ServerShop is designed to complement the [CommunityMarket](../README.md) player-to-player marketplace plugin. By maintaining a **large spread** between buy and sell prices (default: server buys at only 25% of the buy price), the server shop provides an economic safety net while keeping player-to-player trading attractive.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **GUI-only** — all interactions through clean, paginated GUIs
|
||||
- **Category-based shop** — Blocks, Ores, Farming, Food, Mob Drops, Redstone, Decoration, Tools, Combat, Brewing, Misc
|
||||
- **All Minecraft items** supported via `Material` enumeration; unknown/new items fall back to Misc
|
||||
- **Quantity selector** — adjust buy/sell quantity in the item detail GUI
|
||||
- **Vault economy integration** (required)
|
||||
- **Configurable pricing** — per-item and per-category overrides in `prices.yml`
|
||||
- **Large spread** — server buys at 25% of sell price by default (fully configurable)
|
||||
- **i18n** — English (`en_US`) and Portuguese (`pt_PT`) included; easy to add more
|
||||
- **Optional SQLite transaction logging**
|
||||
- **Anti-exploit** — shift-click, drag, number-swap events are all cancelled
|
||||
- **Modrinth-ready** documentation
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Version |
|
||||
|-------------|---------|
|
||||
| Paper / Purpur | 1.21+ |
|
||||
| Java | 21+ |
|
||||
| Vault | Any compatible version |
|
||||
| Economy plugin | e.g. EssentialsX, CMI |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download `ServerShop-<version>.jar` from Modrinth / GitHub Releases.
|
||||
2. Place the JAR in your server's `plugins/` folder.
|
||||
3. Make sure [Vault](https://www.spigotmc.org/resources/vault.34315/) and a compatible economy plugin are installed.
|
||||
4. Start the server — default `config.yml`, `prices.yml`, and language files will be generated.
|
||||
5. Edit `plugins/ServerShop/config.yml` and `plugins/ServerShop/prices.yml` as needed.
|
||||
6. Run `/shop reload` (requires `servershop.admin.reload`) to apply changes without restarting.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description | Permission |
|
||||
|---------|-------------|------------|
|
||||
| `/shop` | Opens the main shop GUI | `servershop.use` |
|
||||
| `/shop reload` | Reloads all configuration | `servershop.admin.reload` |
|
||||
|
||||
Aliases: `/servershop`, `/sshop`
|
||||
|
||||
---
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Default | Description |
|
||||
|------------|---------|-------------|
|
||||
| `servershop.*` | op | All permissions |
|
||||
| `servershop.use` | true | Open the shop |
|
||||
| `servershop.buy` | true | Buy items |
|
||||
| `servershop.sell` | true | Sell items |
|
||||
| `servershop.admin` | op | Admin commands |
|
||||
| `servershop.admin.reload` | op | Reload configuration |
|
||||
|
||||
---
|
||||
|
||||
## How Pricing Works (Spread Explanation)
|
||||
|
||||
The shop deliberately maintains a **large spread** between buy and sell prices to keep player-to-player trading in CommunityMarket more economically attractive.
|
||||
|
||||
**Example with default settings:**
|
||||
|
||||
| Item | Buy Price (player buys) | Sell Price (player sells) | Spread |
|
||||
|------|------------------------|--------------------------|--------|
|
||||
| Diamond | $50.00 | $12.50 | 75% |
|
||||
| Iron Ingot | $5.00 | $1.25 | 75% |
|
||||
| Wheat | $1.00 | $0.25 | 75% |
|
||||
|
||||
If a player wants to sell diamonds, they get **$12.50** from the server shop. On the CommunityMarket, another player might pay **$35–$45** — much more attractive.
|
||||
|
||||
### Configuring the spread
|
||||
|
||||
In `config.yml`:
|
||||
```yaml
|
||||
pricing:
|
||||
global-sell-multiplier: 0.25 # Server pays 25% of buy price (75% spread)
|
||||
```
|
||||
|
||||
Per-category overrides:
|
||||
```yaml
|
||||
pricing:
|
||||
category-sell-multipliers:
|
||||
BLOCKS: 0.20 # 80% spread on blocks
|
||||
ORES: 0.20 # 80% spread on ores
|
||||
FOOD: 0.30 # 70% spread on food
|
||||
```
|
||||
|
||||
Per-item explicit prices in `prices.yml`:
|
||||
```yaml
|
||||
categories:
|
||||
ORES:
|
||||
items:
|
||||
DIAMOND:
|
||||
buy-price: 50.00
|
||||
sell-price: 12.50 # explicit override
|
||||
NETHERITE_INGOT:
|
||||
buy-price: 500.00
|
||||
sell-enabled: false # cannot sell netherite back to the server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Edit Categories & Prices
|
||||
|
||||
### `prices.yml` structure
|
||||
|
||||
```yaml
|
||||
categories:
|
||||
CATEGORY_NAME:
|
||||
display-name: "&7Human Readable Name"
|
||||
icon: MATERIAL_NAME # icon shown in the category GUI
|
||||
buy-enabled: true # can players buy from this category?
|
||||
sell-enabled: true # can players sell to this category?
|
||||
items:
|
||||
MATERIAL_NAME:
|
||||
buy-price: 10.00 # price to buy 1x from server
|
||||
sell-price: 2.50 # price server pays for 1x (optional — uses multiplier if omitted)
|
||||
buy-enabled: true # per-item override (optional)
|
||||
sell-enabled: true # per-item override (optional)
|
||||
```
|
||||
|
||||
- Set `buy-price: -1` to disable buying a specific item.
|
||||
- Set `sell-price: -1` (or `sell-enabled: false`) to disable selling a specific item.
|
||||
- Items not listed in any category automatically appear in **Misc** with a default price of $10.00 buy / $2.50 sell.
|
||||
|
||||
### Special-meta items
|
||||
|
||||
By default, items with special metadata (enchanted books, potions, tipped arrows) are **excluded** because they can't be meaningfully sold without meta matching. You can change this:
|
||||
|
||||
```yaml
|
||||
features:
|
||||
include-special-meta-items: false # default
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### `config.yml`
|
||||
|
||||
```yaml
|
||||
language: en_US # Language (en_US, pt_PT)
|
||||
|
||||
economy:
|
||||
currency-format: "$#,##0.00" # Java DecimalFormat pattern
|
||||
currency-symbol: "$"
|
||||
|
||||
pricing:
|
||||
global-sell-multiplier: 0.25 # Global buy→sell multiplier
|
||||
sell-tax-percent: 0.0 # Optional tax on sell proceeds
|
||||
category-sell-multipliers: # Per-category overrides
|
||||
BLOCKS: 0.20
|
||||
|
||||
full-inventory-behavior: DROP # DROP or CANCEL
|
||||
|
||||
gui:
|
||||
main-title: "&6&lServer Shop"
|
||||
filler-material: GRAY_STAINED_GLASS_PANE
|
||||
|
||||
features:
|
||||
enable-buying: true
|
||||
enable-selling: true
|
||||
sell-hand-button: true
|
||||
sell-inventory-button: true
|
||||
include-special-meta-items: false
|
||||
|
||||
logging:
|
||||
enabled: true
|
||||
file: transactions.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transaction Logging
|
||||
|
||||
When `logging.enabled: true` (default), every buy and sell is recorded in an SQLite database at `plugins/ServerShop/transactions.db`.
|
||||
|
||||
Schema:
|
||||
```sql
|
||||
CREATE TABLE transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT,
|
||||
player TEXT,
|
||||
type TEXT, -- 'BUY' or 'SELL'
|
||||
material TEXT,
|
||||
amount INTEGER,
|
||||
unit_price REAL,
|
||||
total REAL,
|
||||
timestamp INTEGER -- Unix epoch seconds
|
||||
);
|
||||
```
|
||||
|
||||
You can query this with any SQLite client or DB browser to generate sales reports.
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- No admin GUI for viewing transaction stats (planned for v2).
|
||||
- Search functionality is not implemented in v1 (planned for v2).
|
||||
- Items with special meta (potions, enchanted books) are excluded by default; when enabled, only the base type is priced (no meta matching).
|
||||
- The `sell-inventory` button sells **all** sellable items in the inventory at once — use with caution.
|
||||
- Quantities are capped to 64 × inventory size; extremely large transactions may be slow.
|
||||
|
||||
---
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
cd servershop
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
The shaded JAR will be in `servershop/target/servershop-1.0.0.jar`.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT — see the root [LICENSE](../LICENSE) file.
|
||||
@@ -1,192 +0,0 @@
|
||||
package pt.henrique.servershop;
|
||||
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import pt.henrique.servershop.category.CategoryRegistry;
|
||||
import pt.henrique.servershop.command.ShopCommand;
|
||||
import pt.henrique.servershop.economy.EconomyManager;
|
||||
import pt.henrique.servershop.gui.*;
|
||||
import pt.henrique.servershop.i18n.LangManager;
|
||||
import pt.henrique.servershop.pricing.PricingService;
|
||||
import pt.henrique.servershop.storage.TransactionLogger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Main entry point for the <strong>ServerShop</strong> plugin.
|
||||
*
|
||||
* <p>ServerShop is a server-run global market designed to coexist with the
|
||||
* CommunityMarket player-to-player plugin. A deliberately large spread
|
||||
* between buy and sell prices keeps player-to-player trading more attractive.
|
||||
*
|
||||
* <p>Startup order:
|
||||
* <ol>
|
||||
* <li>Load {@code config.yml} and {@code prices.yml}</li>
|
||||
* <li>Initialise localisation (LangManager)</li>
|
||||
* <li>Hook into Vault economy (required)</li>
|
||||
* <li>Load categories (CategoryRegistry)</li>
|
||||
* <li>Load prices (PricingService)</li>
|
||||
* <li>Initialise optional transaction logger (SQLite)</li>
|
||||
* <li>Register commands and GUI event listener</li>
|
||||
* </ol>
|
||||
*/
|
||||
public final class ServerShop extends JavaPlugin {
|
||||
|
||||
private static ServerShop instance;
|
||||
|
||||
private FileConfiguration pricesConfig;
|
||||
|
||||
private LangManager langManager;
|
||||
private EconomyManager economyManager;
|
||||
private CategoryRegistry categoryRegistry;
|
||||
private PricingService pricingService;
|
||||
private TransactionLogger transactionLogger;
|
||||
|
||||
// GUI components
|
||||
private GuiController guiController;
|
||||
private MainCategoryGui mainCategoryGui;
|
||||
private CategoryGui categoryGui;
|
||||
private ItemDetailGui itemDetailGui;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
|
||||
// Ensure default config files are present on disk
|
||||
saveDefaultConfig();
|
||||
savePricesConfig();
|
||||
|
||||
// Reload config from disk (picks up any player edits)
|
||||
reloadConfig();
|
||||
|
||||
// Language
|
||||
langManager = new LangManager(this);
|
||||
langManager.load();
|
||||
|
||||
// Vault economy (required)
|
||||
economyManager = new EconomyManager(this);
|
||||
if (!economyManager.setup()) {
|
||||
getLogger().severe("Vault economy provider not found!");
|
||||
getLogger().severe("ServerShop requires Vault and a compatible economy plugin.");
|
||||
getLogger().severe("Disabling ServerShop.");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prices configuration
|
||||
pricesConfig = loadPricesConfig();
|
||||
|
||||
// Category registry and pricing
|
||||
categoryRegistry = new CategoryRegistry(this);
|
||||
categoryRegistry.load();
|
||||
|
||||
pricingService = new PricingService(this);
|
||||
pricingService.load();
|
||||
|
||||
// Optional transaction logger
|
||||
transactionLogger = new TransactionLogger(this);
|
||||
transactionLogger.initialize();
|
||||
|
||||
// GUI subsystem
|
||||
guiController = new GuiController();
|
||||
mainCategoryGui = new MainCategoryGui(this);
|
||||
categoryGui = new CategoryGui(this);
|
||||
itemDetailGui = new ItemDetailGui(this);
|
||||
|
||||
// Register GUI listener
|
||||
getServer().getPluginManager().registerEvents(new ShopGuiListener(this), this);
|
||||
|
||||
// Register /shop command
|
||||
ShopCommand shopCommand = new ShopCommand(this);
|
||||
PluginCommand cmd = getCommand("shop");
|
||||
if (cmd != null) {
|
||||
cmd.setExecutor(shopCommand);
|
||||
cmd.setTabCompleter(shopCommand);
|
||||
}
|
||||
|
||||
getLogger().info("ServerShop v" + getDescription().getVersion() + " enabled successfully.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (transactionLogger != null) {
|
||||
transactionLogger.shutdown();
|
||||
}
|
||||
getLogger().info("ServerShop disabled.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all configuration, language, categories, and prices.
|
||||
* Called by {@code /shop reload}.
|
||||
*
|
||||
* <p>The transaction logger is intentionally NOT reloaded here because
|
||||
* it holds an open database connection. The logging configuration is only
|
||||
* read at startup; a full server restart is required to change it.
|
||||
*/
|
||||
public void reload() {
|
||||
reloadConfig();
|
||||
pricesConfig = loadPricesConfig();
|
||||
langManager.load();
|
||||
categoryRegistry.load();
|
||||
pricingService.load();
|
||||
getLogger().info("ServerShop configuration reloaded.");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public static ServerShop getInstance() { return instance; }
|
||||
|
||||
/** @return the loaded {@code prices.yml} configuration */
|
||||
public FileConfiguration getPricesConfig() { return pricesConfig; }
|
||||
|
||||
public LangManager getLangManager() { return langManager; }
|
||||
public EconomyManager getEconomyManager() { return economyManager; }
|
||||
public CategoryRegistry getCategoryRegistry() { return categoryRegistry; }
|
||||
public PricingService getPricingService() { return pricingService; }
|
||||
public TransactionLogger getTransactionLogger() { return transactionLogger; }
|
||||
public GuiController getGuiController() { return guiController; }
|
||||
public MainCategoryGui getMainCategoryGui() { return mainCategoryGui; }
|
||||
public CategoryGui getCategoryGui() { return categoryGui; }
|
||||
public ItemDetailGui getItemDetailGui() { return itemDetailGui; }
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Saves the bundled {@code prices.yml} to the data folder if it does not
|
||||
* already exist.
|
||||
*/
|
||||
private void savePricesConfig() {
|
||||
File pricesFile = new File(getDataFolder(), "prices.yml");
|
||||
if (!pricesFile.exists()) {
|
||||
saveResource("prices.yml", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads {@code prices.yml} from the data folder, merging bundled defaults.
|
||||
*
|
||||
* @return loaded configuration
|
||||
*/
|
||||
private FileConfiguration loadPricesConfig() {
|
||||
File pricesFile = new File(getDataFolder(), "prices.yml");
|
||||
YamlConfiguration cfg = YamlConfiguration.loadConfiguration(pricesFile);
|
||||
|
||||
// Merge bundled defaults
|
||||
InputStream stream = getResource("prices.yml");
|
||||
if (stream != null) {
|
||||
YamlConfiguration defaults = YamlConfiguration.loadConfiguration(
|
||||
new InputStreamReader(stream, StandardCharsets.UTF_8));
|
||||
cfg.setDefaults(defaults);
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package pt.henrique.servershop.category;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
/**
|
||||
* Represents a shop category with its display configuration
|
||||
* and a mapping of which {@link Material} values belong to it.
|
||||
*/
|
||||
public final class Category {
|
||||
|
||||
private final String id;
|
||||
private final String displayName;
|
||||
private final Material icon;
|
||||
private final boolean buyEnabled;
|
||||
private final boolean sellEnabled;
|
||||
|
||||
/**
|
||||
* Constructs a new category.
|
||||
*
|
||||
* @param id internal identifier (e.g. "BLOCKS")
|
||||
* @param displayName colour-formatted display name shown in GUIs
|
||||
* @param icon material used as the category icon button
|
||||
* @param buyEnabled whether players can buy items in this category by default
|
||||
* @param sellEnabled whether players can sell items in this category by default
|
||||
*/
|
||||
public Category(String id, String displayName, Material icon,
|
||||
boolean buyEnabled, boolean sellEnabled) {
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
this.icon = icon;
|
||||
this.buyEnabled = buyEnabled;
|
||||
this.sellEnabled = sellEnabled;
|
||||
}
|
||||
|
||||
/** @return the unique category identifier */
|
||||
public String getId() { return id; }
|
||||
|
||||
/** @return colour-formatted display name */
|
||||
public String getDisplayName() { return displayName; }
|
||||
|
||||
/** @return icon material */
|
||||
public Material getIcon() { return icon; }
|
||||
|
||||
/** @return whether buying is enabled for this category */
|
||||
public boolean isBuyEnabled() { return buyEnabled; }
|
||||
|
||||
/** @return whether selling is enabled for this category */
|
||||
public boolean isSellEnabled() { return sellEnabled; }
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package pt.henrique.servershop.category;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Loads and caches all categories from {@code prices.yml}.
|
||||
* Provides a fallback "MISC" category for any item that is not explicitly
|
||||
* assigned to a category.
|
||||
*/
|
||||
public final class CategoryRegistry {
|
||||
|
||||
/** Fallback category used when a material has no explicit assignment. */
|
||||
public static final String MISC_ID = "MISC";
|
||||
|
||||
private final ServerShop plugin;
|
||||
|
||||
/** Ordered list of all categories (insertion order from config). */
|
||||
private final List<Category> categories = new ArrayList<>();
|
||||
|
||||
/** Maps a Material to the category it belongs to. */
|
||||
private final Map<Material, Category> materialCategoryMap = new EnumMap<>(Material.class);
|
||||
|
||||
public CategoryRegistry(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all categories from {@code prices.yml}.
|
||||
* Must be called (or re-called on reload) from the main thread before the
|
||||
* shop is opened by any player.
|
||||
*/
|
||||
public void load() {
|
||||
categories.clear();
|
||||
materialCategoryMap.clear();
|
||||
|
||||
ConfigurationSection root = plugin.getPricesConfig()
|
||||
.getConfigurationSection("categories");
|
||||
|
||||
if (root == null) {
|
||||
plugin.getLogger().warning("prices.yml has no 'categories' section!");
|
||||
ensureMiscCategory();
|
||||
return;
|
||||
}
|
||||
|
||||
for (String catId : root.getKeys(false)) {
|
||||
ConfigurationSection catSection = root.getConfigurationSection(catId);
|
||||
if (catSection == null) continue;
|
||||
|
||||
String displayName = catSection.getString("display-name", catId);
|
||||
String iconName = catSection.getString("icon", "CHEST");
|
||||
Material icon = parseMaterial(iconName, Material.CHEST);
|
||||
boolean buyEnabled = catSection.getBoolean("buy-enabled", true);
|
||||
boolean sellEnabled = catSection.getBoolean("sell-enabled", true);
|
||||
|
||||
Category category = new Category(catId, displayName, icon, buyEnabled, sellEnabled);
|
||||
categories.add(category);
|
||||
|
||||
// Register all items in this category so we can do reverse lookups
|
||||
ConfigurationSection itemsSection = catSection.getConfigurationSection("items");
|
||||
if (itemsSection != null) {
|
||||
for (String materialName : itemsSection.getKeys(false)) {
|
||||
Material mat = parseMaterial(materialName, null);
|
||||
if (mat != null) {
|
||||
materialCategoryMap.put(mat, category);
|
||||
} else {
|
||||
plugin.getLogger().warning(
|
||||
"prices.yml: unknown material '" + materialName
|
||||
+ "' in category '" + catId + "' — skipping.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureMiscCategory();
|
||||
plugin.getLogger().info("Loaded " + categories.size() + " categories with "
|
||||
+ materialCategoryMap.size() + " item mappings.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a MISC category exists for items without an explicit assignment.
|
||||
* If MISC is already in the list it is used as-is; otherwise a default one
|
||||
* is appended.
|
||||
*/
|
||||
private void ensureMiscCategory() {
|
||||
boolean hasMisc = categories.stream()
|
||||
.anyMatch(c -> c.getId().equals(MISC_ID));
|
||||
if (!hasMisc) {
|
||||
categories.add(new Category(MISC_ID, "&8Miscellaneous",
|
||||
Material.CHEST, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the category for a given material.
|
||||
* Falls back to the MISC category if not explicitly assigned.
|
||||
*
|
||||
* @param material the material to look up
|
||||
* @return the category; never {@code null}
|
||||
*/
|
||||
public Category getCategory(Material material) {
|
||||
Category cat = materialCategoryMap.get(material);
|
||||
if (cat != null) return cat;
|
||||
// Fallback: return the MISC category
|
||||
return categories.stream()
|
||||
.filter(c -> c.getId().equals(MISC_ID))
|
||||
.findFirst()
|
||||
.orElse(categories.isEmpty() ? null : categories.get(categories.size() - 1));
|
||||
}
|
||||
|
||||
/** @return an unmodifiable ordered list of all loaded categories */
|
||||
public List<Category> getCategories() {
|
||||
return Collections.unmodifiableList(categories);
|
||||
}
|
||||
|
||||
/** @return all materials assigned to a given category */
|
||||
public List<Material> getMaterialsInCategory(Category category) {
|
||||
List<Material> result = new ArrayList<>();
|
||||
for (Map.Entry<Material, Category> entry : materialCategoryMap.entrySet()) {
|
||||
if (entry.getValue().getId().equals(category.getId())) {
|
||||
result.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparing(Material::name));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private Material parseMaterial(String name, Material fallback) {
|
||||
if (name == null) return fallback;
|
||||
try {
|
||||
return Material.valueOf(name.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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 java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles the {@code /shop} (alias {@code /servershop}) command.
|
||||
*
|
||||
* <p>Usage:
|
||||
* <ul>
|
||||
* <li>{@code /shop} — opens the main category GUI for the player</li>
|
||||
* <li>{@code /shop reload} — reloads all configuration (requires {@code servershop.admin.reload})</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final 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) {
|
||||
|
||||
// Sub-command: reload
|
||||
if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
|
||||
if (!sender.hasPermission("servershop.admin.reload")) {
|
||||
sender.sendMessage(plugin.getLangManager().get("no-permission"));
|
||||
return true;
|
||||
}
|
||||
plugin.reload();
|
||||
sender.sendMessage(plugin.getLangManager().get("reload-success"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Player-only: open GUI
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(plugin.getLangManager().get("player-only"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!player.hasPermission("servershop.use")) {
|
||||
player.sendMessage(plugin.getLangManager().get("no-permission"));
|
||||
return true;
|
||||
}
|
||||
|
||||
plugin.getMainCategoryGui().open(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command,
|
||||
String alias, String[] args) {
|
||||
if (args.length == 1 && sender.hasPermission("servershop.admin.reload")) {
|
||||
return List.of("reload");
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package pt.henrique.servershop.economy;
|
||||
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
import net.milkbowl.vault.economy.EconomyResponse;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Wraps the Vault {@link Economy} service, providing balance checks,
|
||||
* deposits, withdrawals, and formatted currency strings.
|
||||
*/
|
||||
public final class EconomyManager {
|
||||
|
||||
private final ServerShop plugin;
|
||||
private Economy economy;
|
||||
private DecimalFormat format;
|
||||
|
||||
public EconomyManager(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into the Vault economy service.
|
||||
*
|
||||
* @return {@code true} if an economy provider was found, {@code false} otherwise
|
||||
*/
|
||||
public boolean setup() {
|
||||
if (plugin.getServer().getPluginManager().getPlugin("Vault") == null) {
|
||||
return false;
|
||||
}
|
||||
RegisteredServiceProvider<Economy> rsp =
|
||||
plugin.getServer().getServicesManager().getRegistration(Economy.class);
|
||||
if (rsp == null) return false;
|
||||
economy = rsp.getProvider();
|
||||
|
||||
String pattern = plugin.getConfig().getString("economy.currency-format", "$#,##0.00");
|
||||
try {
|
||||
format = new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(Locale.US));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().warning("Invalid currency-format in config.yml; using default.");
|
||||
format = new DecimalFormat("$#,##0.00", DecimalFormatSymbols.getInstance(Locale.US));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the raw Vault {@link Economy} instance
|
||||
*/
|
||||
public Economy getEconomy() { return economy; }
|
||||
|
||||
/**
|
||||
* @return {@code true} if the economy provider has been successfully hooked
|
||||
*/
|
||||
public boolean isAvailable() { return economy != null; }
|
||||
|
||||
/**
|
||||
* Gets a player's current balance.
|
||||
*
|
||||
* @param player the player
|
||||
* @return balance
|
||||
*/
|
||||
public double getBalance(Player player) {
|
||||
return economy.getBalance(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraws {@code amount} from the player's account.
|
||||
*
|
||||
* @param player the player
|
||||
* @param amount amount to withdraw (must be positive)
|
||||
* @return {@code true} on success
|
||||
*/
|
||||
public boolean withdraw(Player player, double amount) {
|
||||
if (amount <= 0) return false;
|
||||
EconomyResponse response = economy.withdrawPlayer(player, amount);
|
||||
return response.transactionSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposits {@code amount} into the player's account.
|
||||
*
|
||||
* @param player the player
|
||||
* @param amount amount to deposit (must be positive)
|
||||
* @return {@code true} on success
|
||||
*/
|
||||
public boolean deposit(Player player, double amount) {
|
||||
if (amount <= 0) return false;
|
||||
EconomyResponse response = economy.depositPlayer(player, amount);
|
||||
return response.transactionSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an amount as a currency string using the configured pattern.
|
||||
*
|
||||
* @param amount the monetary amount
|
||||
* @return formatted string, e.g. "$1,234.56"
|
||||
*/
|
||||
public String format(double amount) {
|
||||
return format.format(amount);
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package pt.henrique.servershop.gui;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
import pt.henrique.servershop.category.Category;
|
||||
import pt.henrique.servershop.i18n.LangManager;
|
||||
import pt.henrique.servershop.pricing.ItemPrice;
|
||||
import pt.henrique.servershop.pricing.PricingService;
|
||||
import pt.henrique.servershop.util.TextUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Paginated grid of all items within a single category.
|
||||
*
|
||||
* <p>Layout (54 slots):
|
||||
* <pre>
|
||||
* Row 0-4 (slots 0-44): item grid (9 per row × 5 rows = 45 slots)
|
||||
* Row 5 (slots 45-53): [BACK][fill][fill][PREV][fill][fill][NEXT][fill][fill]
|
||||
* </pre>
|
||||
*/
|
||||
public final class CategoryGui {
|
||||
|
||||
/** Number of item display slots per page. */
|
||||
static final int ITEMS_PER_PAGE = 45;
|
||||
|
||||
static final int SLOT_BACK = 45;
|
||||
static final int SLOT_PREV = 48;
|
||||
static final int SLOT_NEXT = 50;
|
||||
|
||||
private final ServerShop plugin;
|
||||
|
||||
public CategoryGui(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the category GUI for a player.
|
||||
*
|
||||
* @param player the player
|
||||
* @param category the category to display
|
||||
* @param page zero-based page index
|
||||
*/
|
||||
public void open(Player player, Category category, int page) {
|
||||
LangManager lang = plugin.getLangManager();
|
||||
PricingService pricing = plugin.getPricingService();
|
||||
|
||||
List<Material> allItems = plugin.getCategoryRegistry().getMaterialsInCategory(category);
|
||||
int totalPages = Math.max(1, (int) Math.ceil((double) allItems.size() / ITEMS_PER_PAGE));
|
||||
page = Math.max(0, Math.min(page, totalPages - 1));
|
||||
|
||||
String title = lang.get("gui.category-title", "category",
|
||||
TextUtil.stripColour(category.getDisplayName()));
|
||||
Inventory inv = plugin.getServer().createInventory(null, 54, title);
|
||||
|
||||
// Fill bottom row
|
||||
ItemStack filler = createFiller();
|
||||
for (int i = 45; i < 54; i++) inv.setItem(i, filler);
|
||||
// Fill remaining item slots with filler initially
|
||||
for (int i = 0; i < 45; i++) inv.setItem(i, filler);
|
||||
|
||||
// Populate items for this page
|
||||
int start = page * ITEMS_PER_PAGE;
|
||||
int end = Math.min(start + ITEMS_PER_PAGE, allItems.size());
|
||||
for (int i = start; i < end; i++) {
|
||||
Material mat = allItems.get(i);
|
||||
ItemPrice price = pricing.getPrice(mat, category);
|
||||
ItemStack button = createItemButton(mat, price, lang);
|
||||
inv.setItem(i - start, button);
|
||||
}
|
||||
|
||||
// Navigation buttons
|
||||
inv.setItem(SLOT_BACK, createBackButton(lang));
|
||||
|
||||
if (page > 0) {
|
||||
inv.setItem(SLOT_PREV, createPrevButton(lang, page, totalPages));
|
||||
}
|
||||
if (page < totalPages - 1) {
|
||||
inv.setItem(SLOT_NEXT, createNextButton(lang, page, totalPages));
|
||||
}
|
||||
|
||||
player.openInventory(inv);
|
||||
plugin.getGuiController().setGui(player, GuiType.CATEGORY, category, null, page);
|
||||
}
|
||||
|
||||
// ---- item builders ----
|
||||
|
||||
private ItemStack createFiller() {
|
||||
String matName = plugin.getConfig().getString("gui.filler-material", "GRAY_STAINED_GLASS_PANE");
|
||||
Material mat;
|
||||
try { mat = Material.valueOf(matName); }
|
||||
catch (IllegalArgumentException e) { mat = Material.GRAY_STAINED_GLASS_PANE; }
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) { meta.setDisplayName(" "); item.setItemMeta(meta); }
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createItemButton(Material material, ItemPrice price, LangManager lang) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
|
||||
meta.setDisplayName(TextUtil.colour("&f" + TextUtil.formatMaterialName(material)));
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
if (price.isBuyEnabled()) {
|
||||
lore.add(lang.get("gui.item-buy-price", "price",
|
||||
plugin.getEconomyManager().format(price.getBuyPrice())));
|
||||
} else {
|
||||
lore.add(lang.get("gui.item-buy-disabled"));
|
||||
}
|
||||
if (price.isSellEnabled()) {
|
||||
lore.add(lang.get("gui.item-sell-price", "price",
|
||||
plugin.getEconomyManager().format(price.getSellPrice())));
|
||||
} else {
|
||||
lore.add(lang.get("gui.item-sell-disabled"));
|
||||
}
|
||||
if (price.isBuyEnabled() && price.isSellEnabled()) {
|
||||
lore.add(lang.get("gui.item-spread", "spread",
|
||||
String.format("%.0f", price.getSpreadPercent())));
|
||||
}
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createBackButton(LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.BARRIER);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.back-button"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createPrevButton(LangManager lang, int page, int totalPages) {
|
||||
ItemStack item = new ItemStack(Material.ARROW);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.prev-page"));
|
||||
meta.setLore(List.of(lang.get("gui.page-info",
|
||||
"page", String.valueOf(page), // page is 0-indexed; prev page is (page-1)+1 = page
|
||||
"total", String.valueOf(totalPages))));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createNextButton(LangManager lang, int page, int totalPages) {
|
||||
ItemStack item = new ItemStack(Material.ARROW);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.next-page"));
|
||||
meta.setLore(List.of(lang.get("gui.page-info",
|
||||
"page", String.valueOf(page + 2),
|
||||
"total", String.valueOf(totalPages))));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package pt.henrique.servershop.gui;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import pt.henrique.servershop.category.Category;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Tracks which GUI screen each player currently has open and stores
|
||||
* contextual state (active category, selected material, page, quantity).
|
||||
*
|
||||
* <p>This is used by {@link ShopGuiListener} to route inventory-click events
|
||||
* to the correct handler logic without needing to inspect inventory titles.
|
||||
*/
|
||||
public final class GuiController {
|
||||
|
||||
/** Immutable state record for a single player's current GUI session. */
|
||||
record GuiState(GuiType type, Category category, Material material, int page) {}
|
||||
|
||||
/** Tracks open GUIs keyed by player UUID. */
|
||||
private final Map<UUID, GuiState> openGuis = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Records that {@code player} has opened a specific shop GUI screen.
|
||||
*
|
||||
* @param player the player
|
||||
* @param type which screen is open
|
||||
* @param category the active category (may be {@code null} for MAIN_CATEGORY)
|
||||
* @param material the selected material (only meaningful for ITEM_DETAIL)
|
||||
* @param page current page index (CATEGORY) or current quantity (ITEM_DETAIL)
|
||||
*/
|
||||
public void setGui(Player player, GuiType type, Category category,
|
||||
Material material, int page) {
|
||||
openGuis.put(player.getUniqueId(), new GuiState(type, category, material, page));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the GUI state when the player closes the inventory.
|
||||
*
|
||||
* @param player the player
|
||||
*/
|
||||
public void closeGui(Player player) {
|
||||
openGuis.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current GUI state for a player.
|
||||
*
|
||||
* @param player the player
|
||||
* @return state, or a {@link GuiType#NONE} state if none recorded
|
||||
*/
|
||||
public GuiState getState(Player player) {
|
||||
return openGuis.getOrDefault(player.getUniqueId(),
|
||||
new GuiState(GuiType.NONE, null, null, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player the player
|
||||
* @return {@code true} if the player has a shop GUI open
|
||||
*/
|
||||
public boolean hasShopGui(Player player) {
|
||||
return openGuis.containsKey(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package pt.henrique.servershop.gui;
|
||||
|
||||
/**
|
||||
* Identifies which GUI screen a player currently has open in the server shop.
|
||||
*/
|
||||
public enum GuiType {
|
||||
/** The main category-selection screen. */
|
||||
MAIN_CATEGORY,
|
||||
/** A paginated category item grid. */
|
||||
CATEGORY,
|
||||
/** The item detail / quantity-selector screen. */
|
||||
ITEM_DETAIL,
|
||||
/** Not a shop GUI (player has a different inventory open). */
|
||||
NONE
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package pt.henrique.servershop.gui;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
import pt.henrique.servershop.category.Category;
|
||||
import pt.henrique.servershop.i18n.LangManager;
|
||||
import pt.henrique.servershop.pricing.ItemPrice;
|
||||
import pt.henrique.servershop.pricing.PricingService;
|
||||
import pt.henrique.servershop.util.ItemUtil;
|
||||
import pt.henrique.servershop.util.TextUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The item detail GUI — shows buy/sell price for a specific material and
|
||||
* lets the player choose a quantity before confirming the transaction.
|
||||
*
|
||||
* <p>Slot layout (54 slots):
|
||||
* <pre>
|
||||
* Row 0: [fll][fll][fll][fll][ITEM][fll][fll][fll][fll]
|
||||
* Row 1: [fll][fll][fll][fll][fll][fll][fll][fll][fll]
|
||||
* Row 2: [fll][fll][fll][fll][fll][fll][fll][fll][fll]
|
||||
* Row 3: [-32][-16][-8 ][-1 ][QTY][+1 ][+8 ][+16][+32]
|
||||
* Row 4: [MIN][fll][BUY][fll][BAL][fll][SEL][fll][MAX]
|
||||
* Row 5: [BCK][fll][fll][fll][fll][fll][fll][fll][fll]
|
||||
* </pre>
|
||||
*/
|
||||
public final class ItemDetailGui {
|
||||
|
||||
// Slots (0-indexed)
|
||||
static final int SLOT_ITEM_DISPLAY = 4;
|
||||
|
||||
static final int SLOT_DEC32 = 27;
|
||||
static final int SLOT_DEC16 = 28;
|
||||
static final int SLOT_DEC8 = 29;
|
||||
static final int SLOT_DEC1 = 30;
|
||||
static final int SLOT_QTY = 31;
|
||||
static final int SLOT_INC1 = 32;
|
||||
static final int SLOT_INC8 = 33;
|
||||
static final int SLOT_INC16 = 34;
|
||||
static final int SLOT_INC32 = 35;
|
||||
|
||||
static final int SLOT_MIN = 36;
|
||||
static final int SLOT_BUY = 38;
|
||||
static final int SLOT_BALANCE = 40;
|
||||
static final int SLOT_SELL = 42;
|
||||
static final int SLOT_MAX = 44;
|
||||
|
||||
static final int SLOT_BACK = 45;
|
||||
|
||||
private final ServerShop plugin;
|
||||
|
||||
public ItemDetailGui(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the item detail GUI for a player.
|
||||
*
|
||||
* @param player the player
|
||||
* @param material the material being inspected
|
||||
* @param category the category this item belongs to (for buy/sell flags)
|
||||
* @param quantity initial quantity displayed
|
||||
*/
|
||||
public void open(Player player, Material material, Category category, int quantity) {
|
||||
LangManager lang = plugin.getLangManager();
|
||||
PricingService pricing = plugin.getPricingService();
|
||||
ItemPrice price = pricing.getPrice(material, category);
|
||||
|
||||
quantity = Math.max(1, quantity);
|
||||
|
||||
String title = lang.get("gui.detail-title", "item", TextUtil.formatMaterialName(material));
|
||||
Inventory inv = plugin.getServer().createInventory(null, 54, title);
|
||||
|
||||
// Fill everything with filler
|
||||
ItemStack filler = createFiller();
|
||||
for (int i = 0; i < 54; i++) inv.setItem(i, filler);
|
||||
|
||||
// Item display
|
||||
inv.setItem(SLOT_ITEM_DISPLAY, createItemDisplay(material, price, lang));
|
||||
|
||||
// Quantity adjustment buttons
|
||||
inv.setItem(SLOT_DEC32, createAdjustButton(Material.RED_STAINED_GLASS_PANE, "-32", -32));
|
||||
inv.setItem(SLOT_DEC16, createAdjustButton(Material.RED_STAINED_GLASS_PANE, "-16", -16));
|
||||
inv.setItem(SLOT_DEC8, createAdjustButton(Material.RED_STAINED_GLASS_PANE, "-8", -8));
|
||||
inv.setItem(SLOT_DEC1, createAdjustButton(Material.RED_STAINED_GLASS_PANE, "-1", -1));
|
||||
inv.setItem(SLOT_QTY, createQuantityDisplay(quantity, lang));
|
||||
inv.setItem(SLOT_INC1, createAdjustButton(Material.LIME_STAINED_GLASS_PANE, "+1", 1));
|
||||
inv.setItem(SLOT_INC8, createAdjustButton(Material.LIME_STAINED_GLASS_PANE, "+8", 8));
|
||||
inv.setItem(SLOT_INC16, createAdjustButton(Material.LIME_STAINED_GLASS_PANE, "+16", 16));
|
||||
inv.setItem(SLOT_INC32, createAdjustButton(Material.LIME_STAINED_GLASS_PANE, "+32", 32));
|
||||
|
||||
// MIN / MAX buttons
|
||||
inv.setItem(SLOT_MIN, createMinButton(lang));
|
||||
inv.setItem(SLOT_MAX, createMaxButton(player, price, lang));
|
||||
|
||||
// Wallet display
|
||||
inv.setItem(SLOT_BALANCE, createWalletDisplay(player, lang));
|
||||
|
||||
// Buy / Sell buttons
|
||||
double buyTotal = price.isBuyEnabled() ? price.getBuyPrice() * quantity : 0;
|
||||
double sellTotal = price.isSellEnabled() ? price.getSellPrice() * quantity : 0;
|
||||
int inInventory = ItemUtil.countInInventory(player, material);
|
||||
|
||||
if (price.isBuyEnabled()) {
|
||||
inv.setItem(SLOT_BUY, createBuyButton(quantity, buyTotal, player, lang));
|
||||
}
|
||||
if (price.isSellEnabled()) {
|
||||
inv.setItem(SLOT_SELL, createSellButton(quantity, sellTotal, inInventory, player, lang));
|
||||
}
|
||||
|
||||
// Back button
|
||||
inv.setItem(SLOT_BACK, createBackButton(lang));
|
||||
|
||||
player.openInventory(inv);
|
||||
plugin.getGuiController().setGui(player, GuiType.ITEM_DETAIL, category, material, quantity);
|
||||
}
|
||||
|
||||
// ---- item builders ----
|
||||
|
||||
private ItemStack createFiller() {
|
||||
String matName = plugin.getConfig().getString("gui.filler-material", "GRAY_STAINED_GLASS_PANE");
|
||||
Material mat;
|
||||
try { mat = Material.valueOf(matName); }
|
||||
catch (IllegalArgumentException e) { mat = Material.GRAY_STAINED_GLASS_PANE; }
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) { meta.setDisplayName(" "); item.setItemMeta(meta); }
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createItemDisplay(Material material, ItemPrice price, LangManager lang) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
|
||||
meta.setDisplayName(TextUtil.colour("&f&l" + TextUtil.formatMaterialName(material)));
|
||||
List<String> lore = new ArrayList<>();
|
||||
if (price.isBuyEnabled()) {
|
||||
lore.add(lang.get("gui.item-buy-price", "price",
|
||||
plugin.getEconomyManager().format(price.getBuyPrice())));
|
||||
} else {
|
||||
lore.add(lang.get("gui.item-buy-disabled"));
|
||||
}
|
||||
if (price.isSellEnabled()) {
|
||||
lore.add(lang.get("gui.item-sell-price", "price",
|
||||
plugin.getEconomyManager().format(price.getSellPrice())));
|
||||
} else {
|
||||
lore.add(lang.get("gui.item-sell-disabled"));
|
||||
}
|
||||
if (price.isBuyEnabled() && price.isSellEnabled()) {
|
||||
lore.add(lang.get("gui.item-spread", "spread",
|
||||
String.format("%.0f", price.getSpreadPercent())));
|
||||
}
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createAdjustButton(Material mat, String label, int delta) {
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
// Use the delta sign for colour cue
|
||||
String colour = delta < 0 ? "&c" : "&a";
|
||||
meta.setDisplayName(TextUtil.colour(colour + label));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createQuantityDisplay(int qty, LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.PAPER);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.quantity-display", "qty", String.valueOf(qty)));
|
||||
meta.setLore(lang.getList("gui.quantity-lore"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createMinButton(LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.WHITE_STAINED_GLASS_PANE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.min-button"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createMaxButton(Player player, ItemPrice price, LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.WHITE_STAINED_GLASS_PANE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
// Show both MAX buy (by balance) and MAX sell (by inventory)
|
||||
String label = price.isBuyEnabled()
|
||||
? lang.get("gui.max-button-buy")
|
||||
: lang.get("gui.max-button-sell");
|
||||
meta.setDisplayName(label);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createWalletDisplay(Player player, LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.EMERALD);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
double bal = plugin.getEconomyManager().getBalance(player);
|
||||
meta.setDisplayName(lang.get("gui.wallet-display", "balance",
|
||||
plugin.getEconomyManager().format(bal)));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createBuyButton(int qty, double total, Player player, LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.LIME_WOOL);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.buy-button", "qty", String.valueOf(qty)));
|
||||
double bal = plugin.getEconomyManager().getBalance(player);
|
||||
meta.setLore(lang.getList("gui.buy-button-lore",
|
||||
"total", plugin.getEconomyManager().format(total),
|
||||
"balance", plugin.getEconomyManager().format(bal)));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createSellButton(int qty, double total, int have, Player player, LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.RED_WOOL);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.sell-button", "qty", String.valueOf(qty)));
|
||||
double bal = plugin.getEconomyManager().getBalance(player);
|
||||
meta.setLore(lang.getList("gui.sell-button-lore",
|
||||
"total", plugin.getEconomyManager().format(total),
|
||||
"balance", plugin.getEconomyManager().format(bal),
|
||||
"have", String.valueOf(have)));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createBackButton(LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.BARRIER);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(lang.get("gui.back-button"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package pt.henrique.servershop.gui;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
import pt.henrique.servershop.category.Category;
|
||||
import pt.henrique.servershop.category.CategoryRegistry;
|
||||
import pt.henrique.servershop.i18n.LangManager;
|
||||
import pt.henrique.servershop.util.TextUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The main category-selection GUI opened by {@code /shop}.
|
||||
*
|
||||
* <p>Layout (54 slots):
|
||||
* <pre>
|
||||
* [CAT][CAT][CAT][CAT][CAT][CAT][CAT][fll][fll]
|
||||
* [CAT][CAT][CAT][CAT][CAT][CAT][CAT][fll][fll]
|
||||
* [CAT][CAT][CAT][CAT][CAT][CAT][CAT][fll][fll]
|
||||
* [CAT][CAT][CAT][CAT][CAT][CAT][CAT][fll][fll]
|
||||
* [CAT][CAT][CAT][CAT][CAT][CAT][CAT][fll][fll]
|
||||
* [fll][fll][fll][SELL_HAND][fll][SELL_INV][fll][fll][fll]
|
||||
* </pre>
|
||||
*/
|
||||
public final class MainCategoryGui {
|
||||
|
||||
/** Slot constants for special buttons on the bottom row. */
|
||||
static final int SLOT_SELL_HAND = 48;
|
||||
static final int SLOT_SELL_INV = 50;
|
||||
|
||||
private final ServerShop plugin;
|
||||
|
||||
public MainCategoryGui(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the main category GUI for a player.
|
||||
*
|
||||
* @param player the player to open the GUI for
|
||||
*/
|
||||
public void open(Player player) {
|
||||
LangManager lang = plugin.getLangManager();
|
||||
CategoryRegistry registry = plugin.getCategoryRegistry();
|
||||
|
||||
String title = lang.get("gui.main-title");
|
||||
Inventory inv = plugin.getServer().createInventory(null, 54, title);
|
||||
|
||||
// Fill background
|
||||
ItemStack filler = createFiller();
|
||||
for (int i = 0; i < 54; i++) inv.setItem(i, filler);
|
||||
|
||||
// Place category buttons in the first 45 slots
|
||||
List<Category> categories = registry.getCategories();
|
||||
int slot = 0;
|
||||
for (Category category : categories) {
|
||||
if (slot >= 45) break; // Max 45 category buttons
|
||||
|
||||
List<Material> items = registry.getMaterialsInCategory(category);
|
||||
int itemCount = items.size();
|
||||
|
||||
ItemStack button = createCategoryButton(category, itemCount, lang);
|
||||
inv.setItem(slot, button);
|
||||
slot++;
|
||||
}
|
||||
|
||||
// Bottom row special buttons
|
||||
boolean sellHandEnabled = plugin.getConfig().getBoolean("features.sell-hand-button", true);
|
||||
boolean sellInvEnabled = plugin.getConfig().getBoolean("features.sell-inventory-button", true);
|
||||
|
||||
if (sellHandEnabled) {
|
||||
inv.setItem(SLOT_SELL_HAND, createSellHandButton(lang));
|
||||
}
|
||||
if (sellInvEnabled) {
|
||||
inv.setItem(SLOT_SELL_INV, createSellInvButton(lang));
|
||||
}
|
||||
|
||||
player.openInventory(inv);
|
||||
plugin.getGuiController().setGui(player, GuiType.MAIN_CATEGORY, null, null, 0);
|
||||
}
|
||||
|
||||
// ---- item builders ----
|
||||
|
||||
private ItemStack createFiller() {
|
||||
String matName = plugin.getConfig().getString("gui.filler-material", "GRAY_STAINED_GLASS_PANE");
|
||||
Material mat;
|
||||
try {
|
||||
mat = Material.valueOf(matName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mat = Material.GRAY_STAINED_GLASS_PANE;
|
||||
}
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(" ");
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createCategoryButton(Category category, int itemCount, LangManager lang) {
|
||||
ItemStack item = new ItemStack(category.getIcon());
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
|
||||
meta.setDisplayName(TextUtil.colour(category.getDisplayName()));
|
||||
|
||||
List<String> lore = new ArrayList<>(
|
||||
lang.getList("gui.category-lore", "count", String.valueOf(itemCount)));
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createSellHandButton(LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.GOLD_INGOT);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
meta.setDisplayName(lang.get("gui.sell-hand-button"));
|
||||
meta.setLore(lang.getList("gui.sell-hand-lore"));
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack createSellInvButton(LangManager lang) {
|
||||
ItemStack item = new ItemStack(Material.CHEST);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return item;
|
||||
meta.setDisplayName(lang.get("gui.sell-inventory-button"));
|
||||
meta.setLore(lang.getList("gui.sell-inventory-lore"));
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
package pt.henrique.servershop.gui;
|
||||
|
||||
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.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
import pt.henrique.servershop.category.Category;
|
||||
import pt.henrique.servershop.category.CategoryRegistry;
|
||||
import pt.henrique.servershop.gui.GuiController.GuiState;
|
||||
import pt.henrique.servershop.i18n.LangManager;
|
||||
import pt.henrique.servershop.pricing.ItemPrice;
|
||||
import pt.henrique.servershop.pricing.PricingService;
|
||||
import pt.henrique.servershop.util.ItemUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles all inventory events for the ServerShop GUI screens.
|
||||
*
|
||||
* <p>All illegal interactions (shift-click, number-key swap, drag) are cancelled
|
||||
* when the top inventory belongs to a shop GUI to prevent item duplication.
|
||||
*/
|
||||
public final class ShopGuiListener implements Listener {
|
||||
|
||||
private final ServerShop plugin;
|
||||
|
||||
public ShopGuiListener(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Click events
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||
|
||||
GuiController controller = plugin.getGuiController();
|
||||
if (!controller.hasShopGui(player)) return;
|
||||
|
||||
// Always cancel the click to prevent item movement inside shop GUIs
|
||||
event.setCancelled(true);
|
||||
|
||||
// Only process clicks on the top inventory (the shop GUI itself)
|
||||
if (event.getClickedInventory() == null
|
||||
|| event.getClickedInventory() != event.getView().getTopInventory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GuiState state = controller.getState(player);
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
switch (state.type()) {
|
||||
case MAIN_CATEGORY -> handleMainCategoryClick(player, slot, state);
|
||||
case CATEGORY -> handleCategoryClick(player, slot, state);
|
||||
case ITEM_DETAIL -> handleItemDetailClick(player, slot, state);
|
||||
default -> { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
/** Drag events inside shop GUIs are always cancelled. */
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||
if (!plugin.getGuiController().hasShopGui(player)) return;
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
/** Remove GUI state when the player closes the inventory. */
|
||||
@EventHandler
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (!(event.getPlayer() instanceof Player player)) return;
|
||||
plugin.getGuiController().closeGui(player);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main category screen
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void handleMainCategoryClick(Player player, int slot, GuiState state) {
|
||||
// Special bottom-row buttons
|
||||
if (slot == MainCategoryGui.SLOT_SELL_HAND) {
|
||||
handleSellHand(player);
|
||||
return;
|
||||
}
|
||||
if (slot == MainCategoryGui.SLOT_SELL_INV) {
|
||||
handleSellInventory(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Category buttons occupy slots 0-44
|
||||
if (slot < 0 || slot >= 45) return;
|
||||
|
||||
List<Category> categories = plugin.getCategoryRegistry().getCategories();
|
||||
if (slot >= categories.size()) return;
|
||||
|
||||
Category chosen = categories.get(slot);
|
||||
plugin.getCategoryGui().open(player, chosen, 0);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Category item grid
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void handleCategoryClick(Player player, int slot, GuiState state) {
|
||||
Category category = state.category();
|
||||
int page = state.page();
|
||||
|
||||
if (slot == CategoryGui.SLOT_BACK) {
|
||||
plugin.getMainCategoryGui().open(player);
|
||||
return;
|
||||
}
|
||||
if (slot == CategoryGui.SLOT_PREV) {
|
||||
plugin.getCategoryGui().open(player, category, page - 1);
|
||||
return;
|
||||
}
|
||||
if (slot == CategoryGui.SLOT_NEXT) {
|
||||
plugin.getCategoryGui().open(player, category, page + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Item grid (slots 0-44)
|
||||
if (slot < 0 || slot >= 45) return;
|
||||
|
||||
List<Material> materials = plugin.getCategoryRegistry().getMaterialsInCategory(category);
|
||||
int index = page * CategoryGui.ITEMS_PER_PAGE + slot;
|
||||
if (index >= materials.size()) return;
|
||||
|
||||
Material chosen = materials.get(index);
|
||||
plugin.getItemDetailGui().open(player, chosen, category, 1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Item detail / quantity selector
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void handleItemDetailClick(Player player, int slot, GuiState state) {
|
||||
Category category = state.category();
|
||||
Material material = state.material();
|
||||
int qty = state.page(); // "page" field re-used for quantity in detail GUI
|
||||
|
||||
if (slot == ItemDetailGui.SLOT_BACK) {
|
||||
plugin.getCategoryGui().open(player, category, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Quantity adjustment
|
||||
int delta = switch (slot) {
|
||||
case ItemDetailGui.SLOT_DEC32 -> -32;
|
||||
case ItemDetailGui.SLOT_DEC16 -> -16;
|
||||
case ItemDetailGui.SLOT_DEC8 -> -8;
|
||||
case ItemDetailGui.SLOT_DEC1 -> -1;
|
||||
case ItemDetailGui.SLOT_INC1 -> 1;
|
||||
case ItemDetailGui.SLOT_INC8 -> 8;
|
||||
case ItemDetailGui.SLOT_INC16 -> 16;
|
||||
case ItemDetailGui.SLOT_INC32 -> 32;
|
||||
default -> 0;
|
||||
};
|
||||
|
||||
if (delta != 0) {
|
||||
int newQty = Math.max(1, qty + delta);
|
||||
plugin.getItemDetailGui().open(player, material, category, newQty);
|
||||
return;
|
||||
}
|
||||
|
||||
// MIN button → quantity 1
|
||||
if (slot == ItemDetailGui.SLOT_MIN) {
|
||||
plugin.getItemDetailGui().open(player, material, category, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// MAX button — different behaviour for buy vs sell
|
||||
if (slot == ItemDetailGui.SLOT_MAX) {
|
||||
handleMaxButton(player, material, category);
|
||||
return;
|
||||
}
|
||||
|
||||
// BUY confirm
|
||||
if (slot == ItemDetailGui.SLOT_BUY) {
|
||||
handleBuy(player, material, category, qty);
|
||||
return;
|
||||
}
|
||||
|
||||
// SELL confirm
|
||||
if (slot == ItemDetailGui.SLOT_SELL) {
|
||||
handleSell(player, material, category, qty);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMaxButton(Player player, Material material, Category category) {
|
||||
PricingService pricing = plugin.getPricingService();
|
||||
ItemPrice price = pricing.getPrice(material, category);
|
||||
|
||||
int maxQty = 1;
|
||||
|
||||
// Compute max by balance (for buying) or by inventory (for selling)
|
||||
if (price.isBuyEnabled()) {
|
||||
double balance = plugin.getEconomyManager().getBalance(player);
|
||||
int maxBuy = (int) (balance / price.getBuyPrice());
|
||||
maxQty = Math.max(1, maxBuy);
|
||||
} else if (price.isSellEnabled()) {
|
||||
int inInv = ItemUtil.countInInventory(player, material);
|
||||
maxQty = Math.max(1, inInv);
|
||||
}
|
||||
|
||||
plugin.getItemDetailGui().open(player, material, category, maxQty);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Buy logic
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void handleBuy(Player player, Material material, Category category, int qty) {
|
||||
if (!plugin.getConfig().getBoolean("features.enable-buying", true)) return;
|
||||
if (!player.hasPermission("servershop.buy")) return;
|
||||
|
||||
LangManager lang = plugin.getLangManager();
|
||||
PricingService pricing = plugin.getPricingService();
|
||||
ItemPrice price = pricing.getPrice(material, category);
|
||||
|
||||
if (!price.isBuyEnabled()) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("buy-fail-disabled"));
|
||||
return;
|
||||
}
|
||||
if (qty < 1) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("buy-fail-invalid-qty"));
|
||||
return;
|
||||
}
|
||||
|
||||
double total = price.getBuyPrice() * qty;
|
||||
double balance = plugin.getEconomyManager().getBalance(player);
|
||||
|
||||
// Re-validate balance at click time (anti-exploit)
|
||||
if (balance < total) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("buy-fail-no-money",
|
||||
"total", plugin.getEconomyManager().format(total),
|
||||
"balance", plugin.getEconomyManager().format(balance)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check inventory space and give items
|
||||
boolean dropOnFull = plugin.getConfig()
|
||||
.getString("full-inventory-behavior", "DROP").equalsIgnoreCase("DROP");
|
||||
if (!dropOnFull) {
|
||||
// Verify sufficient space BEFORE charging (anti-exploit)
|
||||
if (!ItemUtil.canFit(player, material, qty)) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("buy-fail-inventory-full-cancelled"));
|
||||
return;
|
||||
}
|
||||
// Charge the player, then give items
|
||||
plugin.getEconomyManager().withdraw(player, total);
|
||||
ItemUtil.addItems(player, material, qty, false);
|
||||
} else {
|
||||
// Charge first, then give items (dropping overflow)
|
||||
plugin.getEconomyManager().withdraw(player, total);
|
||||
int leftover = ItemUtil.addItems(player, material, qty, true);
|
||||
if (leftover > 0) {
|
||||
player.sendMessage(lang.get("buy-drop-notice"));
|
||||
}
|
||||
}
|
||||
|
||||
// Log the transaction
|
||||
plugin.getTransactionLogger().log(player, "BUY", material.name(),
|
||||
qty, price.getBuyPrice(), total);
|
||||
|
||||
// Notify and refresh the GUI
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("buy-success",
|
||||
"qty", String.valueOf(qty),
|
||||
"item", material.name(),
|
||||
"total", plugin.getEconomyManager().format(total)));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Sell logic
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void handleSell(Player player, Material material, Category category, int qty) {
|
||||
if (!plugin.getConfig().getBoolean("features.enable-selling", true)) return;
|
||||
if (!player.hasPermission("servershop.sell")) return;
|
||||
|
||||
LangManager lang = plugin.getLangManager();
|
||||
PricingService pricing = plugin.getPricingService();
|
||||
ItemPrice price = pricing.getPrice(material, category);
|
||||
|
||||
if (!price.isSellEnabled()) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("sell-fail-disabled"));
|
||||
return;
|
||||
}
|
||||
if (qty < 1) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("sell-fail-invalid-qty"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-validate inventory at click time (anti-exploit)
|
||||
int inInventory = ItemUtil.countInInventory(player, material);
|
||||
if (inInventory < qty) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("sell-fail-no-items", "item", material.name()));
|
||||
return;
|
||||
}
|
||||
|
||||
double unitSellPrice = price.getSellPrice();
|
||||
|
||||
// Apply sell tax if configured
|
||||
double taxPercent = plugin.getConfig().getDouble("pricing.sell-tax-percent", 0.0);
|
||||
if (taxPercent > 0) {
|
||||
unitSellPrice = unitSellPrice * (1.0 - taxPercent / 100.0);
|
||||
}
|
||||
|
||||
double total = unitSellPrice * qty;
|
||||
|
||||
// Remove items from inventory then deposit money
|
||||
ItemUtil.removeItems(player, material, qty);
|
||||
plugin.getEconomyManager().deposit(player, total);
|
||||
|
||||
// Log the transaction
|
||||
plugin.getTransactionLogger().log(player, "SELL", material.name(),
|
||||
qty, price.getSellPrice(), total);
|
||||
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("sell-success",
|
||||
"qty", String.valueOf(qty),
|
||||
"item", material.name(),
|
||||
"total", plugin.getEconomyManager().format(total)));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Sell hand / sell inventory (from main menu)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void handleSellHand(Player player) {
|
||||
if (!plugin.getConfig().getBoolean("features.enable-selling", true)) return;
|
||||
if (!player.hasPermission("servershop.sell")) return;
|
||||
|
||||
LangManager lang = plugin.getLangManager();
|
||||
var held = player.getInventory().getItemInMainHand();
|
||||
if (held.getType() == Material.AIR) {
|
||||
player.sendMessage(plugin.getLangManager().get("sell-all-nothing",
|
||||
"item", "air"));
|
||||
return;
|
||||
}
|
||||
|
||||
Material material = held.getType();
|
||||
Category category = plugin.getCategoryRegistry().getCategory(material);
|
||||
PricingService pricing = plugin.getPricingService();
|
||||
ItemPrice price = pricing.getPrice(material, category);
|
||||
|
||||
if (!price.isSellEnabled()) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("sell-fail-disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
int qty = ItemUtil.countInInventory(player, material);
|
||||
if (qty == 0) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("sell-all-nothing", "item", material.name()));
|
||||
return;
|
||||
}
|
||||
|
||||
double total = price.getSellPrice() * qty;
|
||||
ItemUtil.removeItems(player, material, qty);
|
||||
plugin.getEconomyManager().deposit(player, total);
|
||||
plugin.getTransactionLogger().log(player, "SELL", material.name(),
|
||||
qty, price.getSellPrice(), total);
|
||||
|
||||
player.closeInventory();
|
||||
player.sendMessage(lang.get("sell-all-success",
|
||||
"qty", String.valueOf(qty),
|
||||
"item", material.name(),
|
||||
"total", plugin.getEconomyManager().format(total)));
|
||||
}
|
||||
|
||||
private void handleSellInventory(Player player) {
|
||||
if (!plugin.getConfig().getBoolean("features.enable-selling", true)) return;
|
||||
if (!player.hasPermission("servershop.sell")) return;
|
||||
|
||||
// Show a sub-GUI for the item in hand, or a confirmation screen
|
||||
// For simplicity: sell ALL shoppable items in the player's inventory
|
||||
// that have a sell price configured.
|
||||
LangManager lang = plugin.getLangManager();
|
||||
double grandTotal = 0;
|
||||
int totalSold = 0;
|
||||
|
||||
for (Material mat : Material.values()) {
|
||||
if (!ItemUtil.isShoppable(mat)) continue;
|
||||
int count = ItemUtil.countInInventory(player, mat);
|
||||
if (count == 0) continue;
|
||||
|
||||
Category category = plugin.getCategoryRegistry().getCategory(mat);
|
||||
ItemPrice price = plugin.getPricingService().getPrice(mat, category);
|
||||
if (!price.isSellEnabled()) continue;
|
||||
|
||||
double itemTotal = price.getSellPrice() * count;
|
||||
ItemUtil.removeItems(player, mat, count);
|
||||
plugin.getEconomyManager().deposit(player, itemTotal);
|
||||
plugin.getTransactionLogger().log(player, "SELL", mat.name(),
|
||||
count, price.getSellPrice(), itemTotal);
|
||||
grandTotal += itemTotal;
|
||||
totalSold += count;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
if (totalSold == 0) {
|
||||
player.sendMessage(lang.get("sell-all-nothing", "item", "any items"));
|
||||
} else {
|
||||
player.sendMessage(lang.get("sell-all-success",
|
||||
"qty", String.valueOf(totalSold),
|
||||
"item", "items",
|
||||
"total", plugin.getEconomyManager().format(grandTotal)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package pt.henrique.servershop.i18n;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Loads and serves localised strings from the active language file
|
||||
* ({@code lang/<language>.yml}).
|
||||
*
|
||||
* <p>If a key is missing from the loaded file the default bundled language
|
||||
* file ({@code en_US}) is consulted as fallback.
|
||||
*/
|
||||
public final class LangManager {
|
||||
|
||||
private final ServerShop plugin;
|
||||
private FileConfiguration lang;
|
||||
private FileConfiguration fallback;
|
||||
|
||||
public LangManager(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the language file configured in {@code config.yml}.
|
||||
* Call this method on startup and on reload.
|
||||
*/
|
||||
public void load() {
|
||||
String language = plugin.getConfig().getString("language", "en_US");
|
||||
lang = loadLangFile(language);
|
||||
fallback = loadLangFile("en_US");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a translated string, substituting {@code %key% → value} placeholders.
|
||||
*
|
||||
* @param key the YAML path within the language file
|
||||
* @param replacements alternating key/value pairs, e.g. {@code "qty", "5"}
|
||||
* @return the translated, colour-decoded string
|
||||
*/
|
||||
public String get(String key, String... replacements) {
|
||||
String raw = lang.getString(key);
|
||||
if (raw == null) raw = fallback.getString(key);
|
||||
if (raw == null) return key; // last resort
|
||||
|
||||
raw = colour(raw);
|
||||
for (int i = 0; i + 1 < replacements.length; i += 2) {
|
||||
raw = raw.replace("%" + replacements[i] + "%", replacements[i + 1]);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a translated string list (for lore entries etc.).
|
||||
*
|
||||
* @param key the YAML path
|
||||
* @param replacements alternating key/value pairs
|
||||
* @return colour-decoded list; empty list if key not found
|
||||
*/
|
||||
public List<String> getList(String key, String... replacements) {
|
||||
List<String> raw = lang.getStringList(key);
|
||||
if (raw.isEmpty()) raw = fallback.getStringList(key);
|
||||
return raw.stream()
|
||||
.map(s -> {
|
||||
s = colour(s);
|
||||
for (int i = 0; i + 1 < replacements.length; i += 2) {
|
||||
s = s.replace("%" + replacements[i] + "%", replacements[i + 1]);
|
||||
}
|
||||
return s;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
/** Translates '&' colour codes into chat colour codes. */
|
||||
private String colour(String s) {
|
||||
return org.bukkit.ChatColor.translateAlternateColorCodes('&', s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a language file, first from the data folder then from the bundled
|
||||
* resources jar if it does not exist on disk.
|
||||
*/
|
||||
private FileConfiguration loadLangFile(String language) {
|
||||
File file = new File(plugin.getDataFolder(), "lang/" + language + ".yml");
|
||||
|
||||
// Save default resource if absent
|
||||
if (!file.exists()) {
|
||||
InputStream stream = plugin.getResource("lang/" + language + ".yml");
|
||||
if (stream != null) {
|
||||
plugin.saveResource("lang/" + language + ".yml", false);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.exists()) {
|
||||
YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file);
|
||||
// Merge defaults from bundled resource
|
||||
InputStream stream = plugin.getResource("lang/" + language + ".yml");
|
||||
if (stream != null) {
|
||||
YamlConfiguration defaults = YamlConfiguration.loadConfiguration(
|
||||
new InputStreamReader(stream, StandardCharsets.UTF_8));
|
||||
cfg.setDefaults(defaults);
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Nothing on disk and nothing in jar — return empty config
|
||||
plugin.getLogger().warning("Language file 'lang/" + language + ".yml' not found.");
|
||||
return new YamlConfiguration();
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package pt.henrique.servershop.pricing;
|
||||
|
||||
/**
|
||||
* Holds the buy price and sell price for a single item in the server shop.
|
||||
* A price of {@code -1} means that operation (buy or sell) is disabled.
|
||||
*/
|
||||
public final class ItemPrice {
|
||||
|
||||
/** Sentinel value indicating that buying or selling is disabled for this item. */
|
||||
public static final double DISABLED = -1.0;
|
||||
|
||||
private final double buyPrice;
|
||||
private final double sellPrice;
|
||||
|
||||
/**
|
||||
* @param buyPrice price server charges the player to buy 1 unit; use {@link #DISABLED} to disallow
|
||||
* @param sellPrice price server pays the player for 1 unit; use {@link #DISABLED} to disallow
|
||||
*/
|
||||
public ItemPrice(double buyPrice, double sellPrice) {
|
||||
this.buyPrice = buyPrice;
|
||||
this.sellPrice = sellPrice;
|
||||
}
|
||||
|
||||
/** @return price per unit when the player buys, or {@link #DISABLED} */
|
||||
public double getBuyPrice() { return buyPrice; }
|
||||
|
||||
/** @return price per unit when the player sells, or {@link #DISABLED} */
|
||||
public double getSellPrice() { return sellPrice; }
|
||||
|
||||
/** @return {@code true} if players can buy this item */
|
||||
public boolean isBuyEnabled() { return buyPrice > 0; }
|
||||
|
||||
/** @return {@code true} if players can sell this item */
|
||||
public boolean isSellEnabled() { return sellPrice > 0; }
|
||||
|
||||
/**
|
||||
* Calculates the spread percentage between buy and sell prices.
|
||||
* Spread = (buyPrice - sellPrice) / buyPrice * 100
|
||||
*
|
||||
* @return spread as a percentage, or 0 if buy is disabled
|
||||
*/
|
||||
public double getSpreadPercent() {
|
||||
if (!isBuyEnabled() || !isSellEnabled()) return 0;
|
||||
return ((buyPrice - sellPrice) / buyPrice) * 100.0;
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package pt.henrique.servershop.pricing;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
import pt.henrique.servershop.category.Category;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Resolves buy/sell prices for every material.
|
||||
*
|
||||
* <p>Resolution order (first match wins):
|
||||
* <ol>
|
||||
* <li>Per-item override in {@code prices.yml}</li>
|
||||
* <li>Category-level sell multiplier from {@code config.yml}</li>
|
||||
* <li>Global sell multiplier from {@code config.yml}</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>A sell price is derived from the buy price using
|
||||
* {@code sellPrice = buyPrice * sellMultiplier} unless an explicit
|
||||
* {@code sell-price} is specified in {@code prices.yml}.
|
||||
*/
|
||||
public final class PricingService {
|
||||
|
||||
private final ServerShop plugin;
|
||||
|
||||
/** Cache of resolved prices per material. */
|
||||
private final Map<Material, ItemPrice> priceCache = new EnumMap<>(Material.class);
|
||||
|
||||
public PricingService(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all prices from {@code prices.yml} into the cache.
|
||||
* Must be called (or re-called on reload) after {@link pt.henrique.servershop.category.CategoryRegistry#load()}.
|
||||
*/
|
||||
public void load() {
|
||||
priceCache.clear();
|
||||
|
||||
double globalSellMultiplier = plugin.getConfig()
|
||||
.getDouble("pricing.global-sell-multiplier", 0.25);
|
||||
|
||||
ConfigurationSection catSection = plugin.getPricesConfig()
|
||||
.getConfigurationSection("categories");
|
||||
if (catSection == null) return;
|
||||
|
||||
for (String catId : catSection.getKeys(false)) {
|
||||
ConfigurationSection cat = catSection.getConfigurationSection(catId);
|
||||
if (cat == null) continue;
|
||||
|
||||
boolean catBuyEnabled = cat.getBoolean("buy-enabled", true);
|
||||
boolean catSellEnabled = cat.getBoolean("sell-enabled", true);
|
||||
|
||||
// Per-category sell multiplier (falls back to global)
|
||||
double catSellMultiplier = plugin.getConfig()
|
||||
.getDouble("pricing.category-sell-multipliers." + catId, globalSellMultiplier);
|
||||
|
||||
ConfigurationSection items = cat.getConfigurationSection("items");
|
||||
if (items == null) continue;
|
||||
|
||||
for (String materialName : items.getKeys(false)) {
|
||||
Material mat = parseMaterial(materialName);
|
||||
if (mat == null) continue;
|
||||
|
||||
// Item may be a scalar (just buy-price) or a section
|
||||
double buyPrice;
|
||||
double sellPrice;
|
||||
boolean itemBuyEnabled = catBuyEnabled;
|
||||
boolean itemSellEnabled = catSellEnabled;
|
||||
|
||||
Object raw = items.get(materialName);
|
||||
if (raw instanceof ConfigurationSection itemSection) {
|
||||
buyPrice = itemSection.getDouble("buy-price", ItemPrice.DISABLED);
|
||||
// sell-price: explicit override or derived from multiplier
|
||||
if (itemSection.contains("sell-price")) {
|
||||
sellPrice = itemSection.getDouble("sell-price");
|
||||
} else if (buyPrice > 0) {
|
||||
sellPrice = buyPrice * catSellMultiplier;
|
||||
} else {
|
||||
sellPrice = ItemPrice.DISABLED;
|
||||
}
|
||||
// Per-item buy/sell enabled flags
|
||||
if (itemSection.contains("buy-enabled")) {
|
||||
itemBuyEnabled = itemSection.getBoolean("buy-enabled");
|
||||
}
|
||||
if (itemSection.contains("sell-enabled")) {
|
||||
itemSellEnabled = itemSection.getBoolean("sell-enabled");
|
||||
}
|
||||
} else {
|
||||
// scalar value — treat as buy-price
|
||||
buyPrice = items.getDouble(materialName, ItemPrice.DISABLED);
|
||||
sellPrice = buyPrice > 0 ? buyPrice * catSellMultiplier : ItemPrice.DISABLED;
|
||||
}
|
||||
|
||||
// Apply enabled flags: if disabled, force price to DISABLED
|
||||
if (!itemBuyEnabled) buyPrice = ItemPrice.DISABLED;
|
||||
if (!itemSellEnabled) sellPrice = ItemPrice.DISABLED;
|
||||
|
||||
priceCache.put(mat, new ItemPrice(buyPrice, sellPrice));
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Loaded prices for " + priceCache.size() + " materials.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ItemPrice} for a material.
|
||||
* If the material has no configured price a default is synthesised using the
|
||||
* global multiplier with a nominal base buy price of {@code 10.0}, so that
|
||||
* newly-added items never cause a NullPointerException.
|
||||
*
|
||||
* @param material the material to price
|
||||
* @return a non-null {@link ItemPrice}
|
||||
*/
|
||||
public ItemPrice getPrice(Material material) {
|
||||
ItemPrice cached = priceCache.get(material);
|
||||
if (cached != null) return cached;
|
||||
|
||||
// Unknown / unconfigured material: use nominal default
|
||||
double base = 10.0;
|
||||
double multiplier = plugin.getConfig()
|
||||
.getDouble("pricing.global-sell-multiplier", 0.25);
|
||||
return new ItemPrice(base, base * multiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the price for a material within a specific category context,
|
||||
* respecting the category-level buy/sell enabled flags.
|
||||
*
|
||||
* @param material the material
|
||||
* @param category the category (used for enabled checks)
|
||||
* @return resolved price
|
||||
*/
|
||||
public ItemPrice getPrice(Material material, Category category) {
|
||||
ItemPrice base = getPrice(material);
|
||||
|
||||
// If the category disables buying or selling, override those fields
|
||||
double buy = category.isBuyEnabled() ? base.getBuyPrice() : ItemPrice.DISABLED;
|
||||
double sell = category.isSellEnabled() ? base.getSellPrice() : ItemPrice.DISABLED;
|
||||
|
||||
if (buy == base.getBuyPrice() && sell == base.getSellPrice()) return base;
|
||||
return new ItemPrice(buy, sell);
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private Material parseMaterial(String name) {
|
||||
try {
|
||||
return Material.valueOf(name.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package pt.henrique.servershop.storage;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import pt.henrique.servershop.ServerShop;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Optional SQLite logger for buy/sell transactions.
|
||||
* All database writes are performed on the Bukkit async scheduler thread.
|
||||
*
|
||||
* <p>Schema:
|
||||
* <pre>
|
||||
* transactions (
|
||||
* id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
* uuid TEXT NOT NULL,
|
||||
* player TEXT NOT NULL,
|
||||
* type TEXT NOT NULL, -- 'BUY' or 'SELL'
|
||||
* material TEXT NOT NULL,
|
||||
* amount INTEGER NOT NULL,
|
||||
* unit_price REAL NOT NULL,
|
||||
* total REAL NOT NULL,
|
||||
* timestamp INTEGER NOT NULL -- Unix epoch seconds
|
||||
* )
|
||||
* </pre>
|
||||
*/
|
||||
public final class TransactionLogger {
|
||||
|
||||
private final ServerShop plugin;
|
||||
private Connection connection;
|
||||
private volatile boolean enabled;
|
||||
|
||||
public TransactionLogger(ServerShop plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens (or creates) the SQLite database and sets up the schema.
|
||||
* Call this on startup; it is safe to call from the main thread as
|
||||
* it only runs at plugin enable time.
|
||||
*
|
||||
* @return {@code true} if the logger was successfully initialised
|
||||
*/
|
||||
public boolean initialize() {
|
||||
enabled = plugin.getConfig().getBoolean("logging.enabled", true);
|
||||
if (!enabled) {
|
||||
plugin.getLogger().info("Transaction logging is disabled.");
|
||||
return true;
|
||||
}
|
||||
|
||||
String fileName = plugin.getConfig().getString("logging.file", "transactions.db");
|
||||
File dbFile = new File(plugin.getDataFolder(), fileName);
|
||||
plugin.getDataFolder().mkdirs();
|
||||
|
||||
try {
|
||||
// Use the bundled (shaded) sqlite-jdbc driver
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
createTable();
|
||||
plugin.getLogger().info("Transaction database initialised.");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to initialise transaction database", e);
|
||||
enabled = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a transaction asynchronously.
|
||||
*
|
||||
* @param player the player involved
|
||||
* @param type "BUY" or "SELL"
|
||||
* @param material the material name
|
||||
* @param amount the quantity
|
||||
* @param unitPrice the per-unit price
|
||||
* @param total the total transaction value
|
||||
*/
|
||||
public void log(Player player, String type, String material, int amount,
|
||||
double unitPrice, double total) {
|
||||
if (!enabled || connection == null) return;
|
||||
|
||||
final String uuid = player.getUniqueId().toString();
|
||||
final String name = player.getName();
|
||||
final long ts = System.currentTimeMillis() / 1000L;
|
||||
|
||||
// Fire and forget on async thread
|
||||
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
String sql = "INSERT INTO transactions (uuid, player, type, material, amount, "
|
||||
+ "unit_price, total, timestamp) VALUES (?,?,?,?,?,?,?,?)";
|
||||
try (PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||
ps.setString(1, uuid);
|
||||
ps.setString(2, name);
|
||||
ps.setString(3, type);
|
||||
ps.setString(4, material);
|
||||
ps.setInt(5, amount);
|
||||
ps.setDouble(6, unitPrice);
|
||||
ps.setDouble(7, total);
|
||||
ps.setLong(8, ts);
|
||||
ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to log transaction", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Closes the database connection gracefully. */
|
||||
public void shutdown() {
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Error closing transaction database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- private helpers ----
|
||||
|
||||
private void createTable() throws SQLException {
|
||||
String sql = """
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT NOT NULL,
|
||||
player TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
material TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
unit_price REAL NOT NULL,
|
||||
total REAL NOT NULL,
|
||||
timestamp INTEGER NOT NULL
|
||||
)
|
||||
""";
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
stmt.execute(sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package pt.henrique.servershop.util;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Inventory and item helper utilities.
|
||||
*/
|
||||
public final class ItemUtil {
|
||||
|
||||
private ItemUtil() {}
|
||||
|
||||
/**
|
||||
* Counts how many of a given material the player has in their inventory
|
||||
* (including off-hand and hotbar).
|
||||
*
|
||||
* @param player the player
|
||||
* @param material the material to count
|
||||
* @return total count across all inventory slots
|
||||
*/
|
||||
public static int countInInventory(Player player, Material material) {
|
||||
int count = 0;
|
||||
for (ItemStack item : player.getInventory().getContents()) {
|
||||
if (item != null && item.getType() == material) {
|
||||
count += item.getAmount();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes exactly {@code amount} of the given material from the player's inventory.
|
||||
* Does NOT check whether the player actually has enough — call {@link #countInInventory}
|
||||
* first to validate.
|
||||
*
|
||||
* @param player the player
|
||||
* @param material the material to remove
|
||||
* @param amount amount to remove
|
||||
*/
|
||||
public static void removeItems(Player player, Material material, int amount) {
|
||||
ItemStack[] contents = player.getInventory().getContents();
|
||||
int remaining = amount;
|
||||
for (int i = 0; i < contents.length && remaining > 0; i++) {
|
||||
ItemStack stack = contents[i];
|
||||
if (stack == null || stack.getType() != material) continue;
|
||||
|
||||
if (stack.getAmount() <= remaining) {
|
||||
remaining -= stack.getAmount();
|
||||
contents[i] = null;
|
||||
} else {
|
||||
stack.setAmount(stack.getAmount() - remaining);
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
player.getInventory().setContents(contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@code amount} of a material to the player's inventory,
|
||||
* dropping overflow at the player's feet.
|
||||
*
|
||||
* @param player the player
|
||||
* @param material the material to give
|
||||
* @param amount total amount to give
|
||||
* @param drop if {@code true} overflow items are dropped; if {@code false} the method
|
||||
* returns the number that could NOT be added (caller decides what to do)
|
||||
* @return number of items that could not fit in the inventory
|
||||
* (always 0 when {@code drop} is {@code true})
|
||||
*/
|
||||
public static int addItems(Player player, Material material, int amount, boolean drop) {
|
||||
int maxStack = new ItemStack(material).getMaxStackSize();
|
||||
int remaining = amount;
|
||||
|
||||
while (remaining > 0) {
|
||||
int batch = Math.min(remaining, maxStack);
|
||||
ItemStack stack = new ItemStack(material, batch);
|
||||
Map<Integer, ItemStack> leftover = player.getInventory().addItem(stack);
|
||||
|
||||
int leftoverTotal = leftover.values().stream()
|
||||
.mapToInt(ItemStack::getAmount).sum();
|
||||
|
||||
if (leftoverTotal > 0) {
|
||||
if (drop) {
|
||||
// Drop at player's feet
|
||||
player.getWorld().dropItem(player.getLocation(), new ItemStack(material, leftoverTotal));
|
||||
remaining = 0;
|
||||
} else {
|
||||
return remaining - (batch - leftoverTotal);
|
||||
}
|
||||
} else {
|
||||
remaining -= batch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the player's inventory can fit {@code amount} additional
|
||||
* units of {@code material} without any overflow.
|
||||
*
|
||||
* @param player the player
|
||||
* @param material the material to check
|
||||
* @param amount amount to fit
|
||||
* @return {@code true} if all {@code amount} units would fit
|
||||
*/
|
||||
public static boolean canFit(Player player, Material material, int amount) {
|
||||
int maxStack = new ItemStack(material).getMaxStackSize();
|
||||
int space = 0;
|
||||
|
||||
for (ItemStack slot : player.getInventory().getStorageContents()) {
|
||||
if (slot == null || slot.getType() == Material.AIR) {
|
||||
space += maxStack;
|
||||
} else if (slot.getType() == material && slot.getAmount() < maxStack) {
|
||||
space += maxStack - slot.getAmount();
|
||||
}
|
||||
if (space >= amount) return true;
|
||||
}
|
||||
return space >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a material is purchasable by the server shop
|
||||
* (not air, not legacy, not a technical/non-obtainable block).
|
||||
*
|
||||
* @param material the material to test
|
||||
* @return {@code true} if the material can appear in the shop
|
||||
*/
|
||||
public static boolean isShoppable(Material material) {
|
||||
if (material == Material.AIR) return false;
|
||||
if (!material.isItem()) return false;
|
||||
// Exclude cave air / void air variants
|
||||
if (material.name().contains("AIR")) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package pt.henrique.servershop.util;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Text / formatting helpers used throughout the plugin.
|
||||
*/
|
||||
public final class TextUtil {
|
||||
|
||||
private TextUtil() {}
|
||||
|
||||
/**
|
||||
* Converts a {@link Material} name into a human-readable title-case string.
|
||||
* E.g. {@code DIAMOND_PICKAXE} → {@code "Diamond Pickaxe"}.
|
||||
*
|
||||
* @param material the material
|
||||
* @return formatted display name
|
||||
*/
|
||||
public static String formatMaterialName(Material material) {
|
||||
String[] parts = material.name().split("_");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String part : parts) {
|
||||
if (!sb.isEmpty()) sb.append(' ');
|
||||
sb.append(part.charAt(0)).append(part.substring(1).toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates '&' colour codes and strips trailing whitespace.
|
||||
*
|
||||
* @param s the string to colour
|
||||
* @return colour-translated string
|
||||
*/
|
||||
public static String colour(String s) {
|
||||
return ChatColor.translateAlternateColorCodes('&', s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips all colour codes from a string.
|
||||
*
|
||||
* @param s the string
|
||||
* @return plain text
|
||||
*/
|
||||
public static String stripColour(String s) {
|
||||
return ChatColor.stripColor(colour(s));
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
# ============================================================
|
||||
# ServerShop Configuration
|
||||
# Server-run global market — designed to work alongside
|
||||
# CommunityMarket with a large buy/sell spread.
|
||||
# ============================================================
|
||||
|
||||
# Language (available: en_US, pt_PT)
|
||||
language: en_US
|
||||
|
||||
# Economy Settings
|
||||
economy:
|
||||
# Java DecimalFormat pattern for currency display
|
||||
currency-format: "$#,##0.00"
|
||||
currency-symbol: "$"
|
||||
|
||||
# Pricing Settings
|
||||
pricing:
|
||||
# Global multiplier applied to base buy price to derive sell price.
|
||||
# 0.25 means the server pays players only 25% of the buy price (large spread).
|
||||
# This keeps CommunityMarket attractive for player-to-player trades.
|
||||
global-sell-multiplier: 0.25
|
||||
|
||||
# Optional tax deducted from sell proceeds (0.0 = disabled)
|
||||
sell-tax-percent: 0.0
|
||||
|
||||
# Per-category overrides for sell multiplier (overrides global-sell-multiplier)
|
||||
category-sell-multipliers:
|
||||
BLOCKS: 0.20
|
||||
ORES: 0.20
|
||||
FARMING: 0.25
|
||||
FOOD: 0.25
|
||||
MOB_DROPS: 0.20
|
||||
REDSTONE: 0.25
|
||||
DECORATION: 0.20
|
||||
TOOLS: 0.15
|
||||
COMBAT: 0.15
|
||||
BREWING: 0.25
|
||||
MISC: 0.20
|
||||
|
||||
# Inventory behavior when player's inventory is full
|
||||
# Options: DROP (drop items at player feet) | CANCEL (refuse the purchase)
|
||||
full-inventory-behavior: DROP
|
||||
|
||||
# GUI Settings
|
||||
gui:
|
||||
# Title of the main category selection GUI
|
||||
main-title: "&6&lServer Shop"
|
||||
# Title pattern for category GUIs (%category% is replaced)
|
||||
category-title: "&6&lShop &8» &e%category%"
|
||||
# Title for item detail GUI (%item% is replaced)
|
||||
detail-title: "&6&lShop &8» &e%item%"
|
||||
# Material used for filler/border panes
|
||||
filler-material: GRAY_STAINED_GLASS_PANE
|
||||
# Material for navigation buttons
|
||||
nav-material: ARROW
|
||||
|
||||
# Feature toggles
|
||||
features:
|
||||
# Allow players to buy items
|
||||
enable-buying: true
|
||||
# Allow players to sell items
|
||||
enable-selling: true
|
||||
# Show "Sell Hand" button in the main GUI
|
||||
sell-hand-button: true
|
||||
# Show "Sell Inventory" button in the main GUI (sells all matching items)
|
||||
sell-inventory-button: true
|
||||
# Include special-meta items (potions, enchanted books) at base price
|
||||
include-special-meta-items: false
|
||||
|
||||
# Transaction Logging (SQLite)
|
||||
logging:
|
||||
enabled: true
|
||||
# Database file name (placed in plugin data folder)
|
||||
file: transactions.db
|
||||
@@ -1,97 +0,0 @@
|
||||
# ============================================================
|
||||
# ServerShop — English (en_US) Language File
|
||||
# ============================================================
|
||||
|
||||
# General
|
||||
prefix: "&8[&6ServerShop&8] &r"
|
||||
no-permission: "&cYou don't have permission to do that."
|
||||
player-only: "&cThis command can only be used by players."
|
||||
reload-success: "&aServerShop configuration reloaded."
|
||||
|
||||
# Economy errors
|
||||
no-vault: "&cVault economy is not available. ServerShop is disabled."
|
||||
|
||||
# Shop GUI
|
||||
gui:
|
||||
main-title: "&6&lServer Shop"
|
||||
category-title: "&6&lShop &8» &e%category%"
|
||||
detail-title: "&6&lShop &8» &e%item%"
|
||||
|
||||
# Category selection screen
|
||||
search-button: "&e&lSearch"
|
||||
search-lore:
|
||||
- "&7Click to search for an item"
|
||||
sell-hand-button: "&c&lSell Hand"
|
||||
sell-hand-lore:
|
||||
- "&7Sell the item you are holding"
|
||||
- "&7to the server shop"
|
||||
sell-inventory-button: "&c&lSell Inventory"
|
||||
sell-inventory-lore:
|
||||
- "&7Sell ALL matching items in"
|
||||
- "&7your inventory to the server shop"
|
||||
|
||||
# Category item lore
|
||||
category-lore:
|
||||
- "&7Click to browse %count% items"
|
||||
|
||||
# Item display in category grid
|
||||
item-buy-price: "&7Buy: &a%price%"
|
||||
item-sell-price: "&7Sell: &c%price%"
|
||||
item-spread: "&7Spread: &e%spread%%"
|
||||
item-buy-disabled: "&7Buy: &cNot available"
|
||||
item-sell-disabled: "&7Sell: &cNot available"
|
||||
|
||||
# Item detail GUI
|
||||
quantity-display: "&fQuantity: &e%qty%"
|
||||
quantity-lore:
|
||||
- "&7Left-click buttons to change quantity"
|
||||
buy-button: "&a&lBUY x%qty%"
|
||||
buy-button-lore:
|
||||
- "&7Total: &a%total%"
|
||||
- "&7Your balance: &e%balance%"
|
||||
sell-button: "&c&lSELL x%qty%"
|
||||
sell-button-lore:
|
||||
- "&7Total: &c%total%"
|
||||
- "&7Your balance: &e%balance%"
|
||||
- "&7In inventory: &f%have%"
|
||||
min-button: "&f&lMIN &7(x1)"
|
||||
max-button-buy: "&f&lMAX &7(by balance)"
|
||||
max-button-sell: "&f&lMAX &7(by inventory)"
|
||||
back-button: "&7« Back"
|
||||
close-button: "&c✖ Close"
|
||||
wallet-display: "&eBalance: &a%balance%"
|
||||
|
||||
# Pagination
|
||||
prev-page: "&7« Previous Page"
|
||||
next-page: "&7Next Page »"
|
||||
page-info: "&7Page %page% of %total%"
|
||||
no-items: "&cNo items in this category."
|
||||
|
||||
# Search
|
||||
search-title: "Search items..."
|
||||
search-prompt: "&7Type the item name in the anvil above"
|
||||
no-results: "&cNo results for \"%query%\""
|
||||
|
||||
# Confirmation for sell-inventory
|
||||
sell-inventory-confirm-title: "&4Confirm — Sell All?"
|
||||
sell-inventory-confirm-lore:
|
||||
- "&7Sell ALL %count%x %item%?"
|
||||
- "&7You will receive: &a%total%"
|
||||
- ""
|
||||
- "&aLeft-click &7to confirm"
|
||||
- "&cRight-click &7to cancel"
|
||||
|
||||
# Transaction messages
|
||||
buy-success: "&aYou bought &e%qty%x %item% &afor &e%total%&a."
|
||||
buy-fail-no-money: "&cYou don't have enough money. Need: &e%total%&c, have: &e%balance%&c."
|
||||
buy-fail-disabled: "&cBuying this item is currently disabled."
|
||||
buy-fail-invalid-qty: "&cInvalid quantity."
|
||||
buy-fail-inventory-full-cancelled: "&cYour inventory is full! Purchase cancelled."
|
||||
buy-drop-notice: "&eYour inventory was full — some items were dropped at your feet."
|
||||
|
||||
sell-success: "&aYou sold &e%qty%x %item% &afor &e%total%&a."
|
||||
sell-fail-no-items: "&cYou don't have enough &e%item%&c in your inventory."
|
||||
sell-fail-disabled: "&cSelling this item is currently disabled."
|
||||
sell-fail-invalid-qty: "&cInvalid quantity."
|
||||
sell-all-success: "&aYou sold &e%qty%x %item% &afor &e%total%&a."
|
||||
sell-all-nothing: "&cYou don't have any &e%item% &cto sell."
|
||||
@@ -1,90 +0,0 @@
|
||||
# ============================================================
|
||||
# ServerShop — Português (pt_PT) Language File
|
||||
# ============================================================
|
||||
|
||||
# Geral
|
||||
prefix: "&8[&6ServerShop&8] &r"
|
||||
no-permission: "&cNão tens permissão para fazer isso."
|
||||
player-only: "&cEste comando só pode ser usado por jogadores."
|
||||
reload-success: "&aConfiguração do ServerShop recarregada."
|
||||
|
||||
# Erros de economia
|
||||
no-vault: "&cVault economy não está disponível. ServerShop está desativado."
|
||||
|
||||
# GUI da loja
|
||||
gui:
|
||||
main-title: "&6&lLoja do Servidor"
|
||||
category-title: "&6&lLoja &8» &e%category%"
|
||||
detail-title: "&6&lLoja &8» &e%item%"
|
||||
|
||||
search-button: "&e&lPesquisar"
|
||||
search-lore:
|
||||
- "&7Clica para procurar um item"
|
||||
sell-hand-button: "&c&lVender na Mão"
|
||||
sell-hand-lore:
|
||||
- "&7Vende o item que tens na mão"
|
||||
- "&7à loja do servidor"
|
||||
sell-inventory-button: "&c&lVender Inventário"
|
||||
sell-inventory-lore:
|
||||
- "&7Vende TODOS os itens iguais"
|
||||
- "&7no teu inventário à loja"
|
||||
|
||||
category-lore:
|
||||
- "&7Clica para ver %count% itens"
|
||||
|
||||
item-buy-price: "&7Comprar: &a%price%"
|
||||
item-sell-price: "&7Vender: &c%price%"
|
||||
item-spread: "&7Spread: &e%spread%%"
|
||||
item-buy-disabled: "&7Comprar: &cNão disponível"
|
||||
item-sell-disabled: "&7Vender: &cNão disponível"
|
||||
|
||||
quantity-display: "&fQuantidade: &e%qty%"
|
||||
quantity-lore:
|
||||
- "&7Usa os botões para alterar a quantidade"
|
||||
buy-button: "&a&lCOMPRAR x%qty%"
|
||||
buy-button-lore:
|
||||
- "&7Total: &a%total%"
|
||||
- "&7Saldo: &e%balance%"
|
||||
sell-button: "&c&lVENDER x%qty%"
|
||||
sell-button-lore:
|
||||
- "&7Total: &c%total%"
|
||||
- "&7Saldo: &e%balance%"
|
||||
- "&7No inventário: &f%have%"
|
||||
min-button: "&f&lMÍN &7(x1)"
|
||||
max-button-buy: "&f&lMÁX &7(pelo saldo)"
|
||||
max-button-sell: "&f&lMÁX &7(pelo inventário)"
|
||||
back-button: "&7« Voltar"
|
||||
close-button: "&c✖ Fechar"
|
||||
wallet-display: "&eSaldo: &a%balance%"
|
||||
|
||||
prev-page: "&7« Página Anterior"
|
||||
next-page: "&7Próxima Página »"
|
||||
page-info: "&7Página %page% de %total%"
|
||||
no-items: "&cNenhum item nesta categoria."
|
||||
|
||||
search-title: "Pesquisar itens..."
|
||||
search-prompt: "&7Escreve o nome do item na bigorna"
|
||||
no-results: "&cSem resultados para \"%query%\""
|
||||
|
||||
sell-inventory-confirm-title: "&4Confirmar — Vender Tudo?"
|
||||
sell-inventory-confirm-lore:
|
||||
- "&7Vender TODOS %count%x %item%?"
|
||||
- "&7Receberás: &a%total%"
|
||||
- ""
|
||||
- "&aClique esquerdo &7para confirmar"
|
||||
- "&cClique direito &7para cancelar"
|
||||
|
||||
# Mensagens de transação
|
||||
buy-success: "&aCompraste &e%qty%x %item% &apor &e%total%&a."
|
||||
buy-fail-no-money: "&cNão tens dinheiro suficiente. Precisas: &e%total%&c, tens: &e%balance%&c."
|
||||
buy-fail-disabled: "&cA compra deste item está desativada."
|
||||
buy-fail-invalid-qty: "&cQuantidade inválida."
|
||||
buy-fail-inventory-full-cancelled: "&cO teu inventário está cheio! Compra cancelada."
|
||||
buy-drop-notice: "&eO teu inventário estava cheio — alguns itens foram largados aos teus pés."
|
||||
|
||||
sell-success: "&aVendeste &e%qty%x %item% &apor &e%total%&a."
|
||||
sell-fail-no-items: "&cNão tens &e%item% &csuficiente no inventário."
|
||||
sell-fail-disabled: "&cA venda deste item está desativada."
|
||||
sell-fail-invalid-qty: "&cQuantidade inválida."
|
||||
sell-all-success: "&aVendeste &e%qty%x %item% &apor &e%total%&a."
|
||||
sell-all-nothing: "&cNão tens &e%item% &cpara vender."
|
||||
@@ -1,51 +0,0 @@
|
||||
name: ServerShop
|
||||
version: '${project.version}'
|
||||
main: pt.henrique.servershop.ServerShop
|
||||
api-version: '1.21'
|
||||
description: A server-run global market with configurable pricing spread — complements CommunityMarket
|
||||
author: Henrique
|
||||
website: https://github.com/henrique/CommunityMarket
|
||||
|
||||
depend:
|
||||
- Vault
|
||||
|
||||
load: POSTWORLD
|
||||
|
||||
commands:
|
||||
shop:
|
||||
description: Opens the Server Shop main GUI
|
||||
usage: /<command>
|
||||
aliases: [servershop, sshop]
|
||||
permission: servershop.use
|
||||
|
||||
permissions:
|
||||
servershop.*:
|
||||
description: Grants all ServerShop permissions
|
||||
default: op
|
||||
children:
|
||||
servershop.use: true
|
||||
servershop.buy: true
|
||||
servershop.sell: true
|
||||
servershop.admin: true
|
||||
|
||||
servershop.use:
|
||||
description: Allows opening 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.admin:
|
||||
description: Allows admin commands (reload)
|
||||
default: op
|
||||
children:
|
||||
servershop.admin.reload: true
|
||||
|
||||
servershop.admin.reload:
|
||||
description: Allows reloading the Server Shop configuration
|
||||
default: op
|
||||
@@ -1,499 +0,0 @@
|
||||
# ============================================================
|
||||
# ServerShop — Prices & Categories
|
||||
# ============================================================
|
||||
# Structure:
|
||||
# categories:
|
||||
# <CATEGORY_NAME>:
|
||||
# display-name: "Human readable name"
|
||||
# icon: MATERIAL_NAME
|
||||
# buy-enabled: true/false # can players buy from this category?
|
||||
# sell-enabled: true/false # can players sell to this category?
|
||||
# items:
|
||||
# MATERIAL_NAME:
|
||||
# buy-price: 10.00 # price server charges player to buy 1x
|
||||
# sell-price: 2.50 # price server pays player for 1x
|
||||
# # (if omitted, sell-price = buy-price * category/global multiplier)
|
||||
# buy-enabled: true # override per item
|
||||
# sell-enabled: true # override per item
|
||||
#
|
||||
# Items not listed under any category fall into MISC automatically.
|
||||
# Set buy-price to -1 to disable buying for that item.
|
||||
# Set sell-price to -1 to disable selling for that item.
|
||||
# ============================================================
|
||||
|
||||
categories:
|
||||
|
||||
BLOCKS:
|
||||
display-name: "&7Blocks"
|
||||
icon: STONE
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
STONE: { buy-price: 1.00 }
|
||||
COBBLESTONE: { buy-price: 0.50 }
|
||||
DIRT: { buy-price: 0.20 }
|
||||
GRASS_BLOCK: { buy-price: 0.30 }
|
||||
SAND: { buy-price: 0.50 }
|
||||
GRAVEL: { buy-price: 0.50 }
|
||||
CLAY: { buy-price: 1.00 }
|
||||
NETHERRACK: { buy-price: 0.20 }
|
||||
SOUL_SAND: { buy-price: 1.50 }
|
||||
SOUL_SOIL: { buy-price: 1.50 }
|
||||
BASALT: { buy-price: 0.50 }
|
||||
BLACKSTONE: { buy-price: 0.50 }
|
||||
OBSIDIAN: { buy-price: 8.00 }
|
||||
CRYING_OBSIDIAN: { buy-price: 10.00 }
|
||||
OAK_LOG: { buy-price: 1.50 }
|
||||
SPRUCE_LOG: { buy-price: 1.50 }
|
||||
BIRCH_LOG: { buy-price: 1.50 }
|
||||
JUNGLE_LOG: { buy-price: 1.50 }
|
||||
ACACIA_LOG: { buy-price: 1.50 }
|
||||
DARK_OAK_LOG: { buy-price: 1.50 }
|
||||
MANGROVE_LOG: { buy-price: 1.50 }
|
||||
CHERRY_LOG: { buy-price: 1.50 }
|
||||
BAMBOO_BLOCK: { buy-price: 0.50 }
|
||||
OAK_PLANKS: { buy-price: 0.75 }
|
||||
SPRUCE_PLANKS: { buy-price: 0.75 }
|
||||
BIRCH_PLANKS: { buy-price: 0.75 }
|
||||
JUNGLE_PLANKS: { buy-price: 0.75 }
|
||||
ACACIA_PLANKS: { buy-price: 0.75 }
|
||||
DARK_OAK_PLANKS: { buy-price: 0.75 }
|
||||
MANGROVE_PLANKS: { buy-price: 0.75 }
|
||||
CHERRY_PLANKS: { buy-price: 0.75 }
|
||||
GRANITE: { buy-price: 0.80 }
|
||||
DIORITE: { buy-price: 0.80 }
|
||||
ANDESITE: { buy-price: 0.80 }
|
||||
CALCITE: { buy-price: 0.80 }
|
||||
TUFF: { buy-price: 0.80 }
|
||||
DEEPSLATE: { buy-price: 1.00 }
|
||||
MUD: { buy-price: 0.30 }
|
||||
PACKED_MUD: { buy-price: 0.60 }
|
||||
MUDDY_MANGROVE_ROOTS: { buy-price: 0.50 }
|
||||
ICE: { buy-price: 1.00 }
|
||||
PACKED_ICE: { buy-price: 2.00 }
|
||||
BLUE_ICE: { buy-price: 4.00 }
|
||||
SNOW_BLOCK: { buy-price: 0.50 }
|
||||
SANDSTONE: { buy-price: 1.00 }
|
||||
RED_SANDSTONE: { buy-price: 1.00 }
|
||||
PRISMARINE: { buy-price: 3.00 }
|
||||
PRISMARINE_BRICKS: { buy-price: 4.00 }
|
||||
DARK_PRISMARINE: { buy-price: 4.00 }
|
||||
SEA_LANTERN: { buy-price: 6.00 }
|
||||
PURPUR_BLOCK: { buy-price: 5.00 }
|
||||
END_STONE: { buy-price: 3.00 }
|
||||
END_STONE_BRICKS: { buy-price: 4.00 }
|
||||
|
||||
ORES:
|
||||
display-name: "&bOres & Minerals"
|
||||
icon: DIAMOND_ORE
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
COAL: { buy-price: 2.00 }
|
||||
CHARCOAL: { buy-price: 1.50 }
|
||||
IRON_INGOT: { buy-price: 5.00 }
|
||||
IRON_NUGGET: { buy-price: 0.60 }
|
||||
GOLD_INGOT: { buy-price: 8.00 }
|
||||
GOLD_NUGGET: { buy-price: 1.00 }
|
||||
COPPER_INGOT: { buy-price: 3.00 }
|
||||
RAW_IRON: { buy-price: 4.00 }
|
||||
RAW_GOLD: { buy-price: 7.00 }
|
||||
RAW_COPPER: { buy-price: 2.50 }
|
||||
DIAMOND: { buy-price: 50.00 }
|
||||
EMERALD: { buy-price: 40.00 }
|
||||
LAPIS_LAZULI: { buy-price: 5.00 }
|
||||
REDSTONE: { buy-price: 2.00 }
|
||||
QUARTZ: { buy-price: 2.00 }
|
||||
NETHERITE_INGOT: { buy-price: 500.00 }
|
||||
NETHERITE_SCRAP: { buy-price: 100.00 }
|
||||
AMETHYST_SHARD: { buy-price: 4.00 }
|
||||
ECHO_SHARD: { buy-price: 30.00 }
|
||||
COAL_ORE: { buy-price: 3.00 }
|
||||
DEEPSLATE_COAL_ORE: { buy-price: 4.00 }
|
||||
IRON_ORE: { buy-price: 6.00 }
|
||||
DEEPSLATE_IRON_ORE: { buy-price: 7.00 }
|
||||
GOLD_ORE: { buy-price: 10.00 }
|
||||
DEEPSLATE_GOLD_ORE: { buy-price: 11.00 }
|
||||
COPPER_ORE: { buy-price: 4.00 }
|
||||
DEEPSLATE_COPPER_ORE: { buy-price: 5.00 }
|
||||
DIAMOND_ORE: { buy-price: 60.00 }
|
||||
DEEPSLATE_DIAMOND_ORE: { buy-price: 70.00 }
|
||||
EMERALD_ORE: { buy-price: 50.00 }
|
||||
DEEPSLATE_EMERALD_ORE: { buy-price: 60.00 }
|
||||
LAPIS_ORE: { buy-price: 8.00 }
|
||||
DEEPSLATE_LAPIS_ORE: { buy-price: 10.00 }
|
||||
REDSTONE_ORE: { buy-price: 5.00 }
|
||||
DEEPSLATE_REDSTONE_ORE: { buy-price: 6.00 }
|
||||
NETHER_QUARTZ_ORE: { buy-price: 3.00 }
|
||||
NETHER_GOLD_ORE: { buy-price: 9.00 }
|
||||
ANCIENT_DEBRIS: { buy-price: 200.00 }
|
||||
|
||||
FARMING:
|
||||
display-name: "&aFarming"
|
||||
icon: WHEAT
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
WHEAT: { buy-price: 1.00 }
|
||||
WHEAT_SEEDS: { buy-price: 0.30 }
|
||||
CARROT: { buy-price: 0.80 }
|
||||
POTATO: { buy-price: 0.80 }
|
||||
BEETROOT: { buy-price: 0.80 }
|
||||
BEETROOT_SEEDS: { buy-price: 0.30 }
|
||||
PUMPKIN: { buy-price: 2.00 }
|
||||
PUMPKIN_SEEDS: { buy-price: 0.50 }
|
||||
MELON: { buy-price: 0.50 }
|
||||
MELON_SEEDS: { buy-price: 0.50 }
|
||||
SUGAR_CANE: { buy-price: 0.50 }
|
||||
CACTUS: { buy-price: 0.50 }
|
||||
BAMBOO: { buy-price: 0.20 }
|
||||
COCOA_BEANS: { buy-price: 1.00 }
|
||||
NETHER_WART: { buy-price: 2.00 }
|
||||
CHORUS_FRUIT: { buy-price: 3.00 }
|
||||
CHORUS_FLOWER: { buy-price: 5.00 }
|
||||
KELP: { buy-price: 0.30 }
|
||||
DRIED_KELP_BLOCK: { buy-price: 1.00 }
|
||||
SEA_PICKLE: { buy-price: 1.00 }
|
||||
LILY_PAD: { buy-price: 0.50 }
|
||||
VINE: { buy-price: 0.30 }
|
||||
GLOW_BERRIES: { buy-price: 1.00 }
|
||||
SWEET_BERRIES: { buy-price: 0.80 }
|
||||
TORCHFLOWER_SEEDS: { buy-price: 5.00 }
|
||||
PITCHER_POD: { buy-price: 5.00 }
|
||||
SNIFFER_EGG: { buy-price: 20.00 }
|
||||
|
||||
FOOD:
|
||||
display-name: "&eFood"
|
||||
icon: COOKED_BEEF
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
BREAD: { buy-price: 2.00 }
|
||||
APPLE: { buy-price: 1.50 }
|
||||
GOLDEN_APPLE: { buy-price: 20.00 }
|
||||
ENCHANTED_GOLDEN_APPLE: { buy-price: 200.00, sell-enabled: false }
|
||||
COOKED_BEEF: { buy-price: 3.00 }
|
||||
BEEF: { buy-price: 1.50 }
|
||||
COOKED_PORKCHOP: { buy-price: 3.00 }
|
||||
PORKCHOP: { buy-price: 1.50 }
|
||||
COOKED_CHICKEN: { buy-price: 2.50 }
|
||||
CHICKEN: { buy-price: 1.00 }
|
||||
COOKED_MUTTON: { buy-price: 3.00 }
|
||||
MUTTON: { buy-price: 1.50 }
|
||||
COOKED_RABBIT: { buy-price: 3.00 }
|
||||
RABBIT: { buy-price: 1.50 }
|
||||
COOKED_COD: { buy-price: 2.50 }
|
||||
COD: { buy-price: 1.00 }
|
||||
COOKED_SALMON: { buy-price: 2.50 }
|
||||
SALMON: { buy-price: 1.00 }
|
||||
TROPICAL_FISH: { buy-price: 0.50 }
|
||||
PUFFERFISH: { buy-price: 1.50 }
|
||||
CAKE: { buy-price: 5.00 }
|
||||
COOKIE: { buy-price: 1.00 }
|
||||
PUMPKIN_PIE: { buy-price: 2.00 }
|
||||
MUSHROOM_STEW: { buy-price: 3.00 }
|
||||
RABBIT_STEW: { buy-price: 5.00 }
|
||||
BEETROOT_SOUP: { buy-price: 3.00 }
|
||||
SUSPICIOUS_STEW: { buy-price: 4.00 }
|
||||
HONEY_BOTTLE: { buy-price: 5.00 }
|
||||
|
||||
MOB_DROPS:
|
||||
display-name: "&cMob Drops"
|
||||
icon: BONE
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
BONE: { buy-price: 1.50 }
|
||||
BONE_MEAL: { buy-price: 0.50 }
|
||||
STRING: { buy-price: 1.00 }
|
||||
SPIDER_EYE: { buy-price: 2.00 }
|
||||
ROTTEN_FLESH: { buy-price: 0.20 }
|
||||
ENDER_PEARL: { buy-price: 8.00 }
|
||||
BLAZE_ROD: { buy-price: 6.00 }
|
||||
BLAZE_POWDER: { buy-price: 3.00 }
|
||||
GHAST_TEAR: { buy-price: 10.00 }
|
||||
MAGMA_CREAM: { buy-price: 4.00 }
|
||||
SLIME_BALL: { buy-price: 4.00 }
|
||||
GUNPOWDER: { buy-price: 3.00 }
|
||||
FEATHER: { buy-price: 1.00 }
|
||||
LEATHER: { buy-price: 3.00 }
|
||||
RABBIT_HIDE: { buy-price: 1.50 }
|
||||
RABBIT_FOOT: { buy-price: 6.00 }
|
||||
INK_SAC: { buy-price: 1.50 }
|
||||
GLOW_INK_SAC: { buy-price: 5.00 }
|
||||
PRISMARINE_SHARD: { buy-price: 2.50 }
|
||||
PRISMARINE_CRYSTALS: { buy-price: 2.50 }
|
||||
SHULKER_SHELL: { buy-price: 20.00 }
|
||||
TOTEM_OF_UNDYING: { buy-price: 100.00, sell-price: 30.00 }
|
||||
NETHER_STAR: { buy-price: 200.00, sell-price: 50.00 }
|
||||
DRAGON_BREATH: { buy-price: 20.00 }
|
||||
ELYTRA: { buy-price: 300.00, sell-enabled: false }
|
||||
TURTLE_SCUTE: { buy-price: 8.00 }
|
||||
ARMADILLO_SCUTE: { buy-price: 5.00 }
|
||||
BREEZE_ROD: { buy-price: 15.00 }
|
||||
WIND_CHARGE: { buy-price: 8.00 }
|
||||
OMINOUS_TRIAL_KEY: { buy-price: 30.00 }
|
||||
TRIAL_KEY: { buy-price: 15.00 }
|
||||
|
||||
REDSTONE:
|
||||
display-name: "&cRedstone"
|
||||
icon: REDSTONE
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
REDSTONE: { buy-price: 2.00 }
|
||||
REDSTONE_BLOCK: { buy-price: 18.00 }
|
||||
REDSTONE_TORCH: { buy-price: 1.00 }
|
||||
REPEATER: { buy-price: 3.00 }
|
||||
COMPARATOR: { buy-price: 5.00 }
|
||||
OBSERVER: { buy-price: 4.00 }
|
||||
PISTON: { buy-price: 4.00 }
|
||||
STICKY_PISTON: { buy-price: 6.00 }
|
||||
DROPPER: { buy-price: 3.00 }
|
||||
DISPENSER: { buy-price: 5.00 }
|
||||
HOPPER: { buy-price: 8.00 }
|
||||
LEVER: { buy-price: 0.50 }
|
||||
STONE_BUTTON: { buy-price: 0.50 }
|
||||
OAK_BUTTON: { buy-price: 0.50 }
|
||||
STONE_PRESSURE_PLATE: { buy-price: 1.00 }
|
||||
OAK_PRESSURE_PLATE: { buy-price: 1.00 }
|
||||
TRIPWIRE_HOOK: { buy-price: 2.00 }
|
||||
TNT: { buy-price: 5.00 }
|
||||
DAYLIGHT_DETECTOR: { buy-price: 6.00 }
|
||||
TRAPPED_CHEST: { buy-price: 4.00 }
|
||||
TARGET: { buy-price: 3.00 }
|
||||
SCULK_SENSOR: { buy-price: 10.00 }
|
||||
CALIBRATED_SCULK_SENSOR: { buy-price: 20.00 }
|
||||
COPPER_BULB: { buy-price: 5.00 }
|
||||
|
||||
DECORATION:
|
||||
display-name: "&dDecoration"
|
||||
icon: FLOWER_POT
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
TORCH: { buy-price: 0.10 }
|
||||
LANTERN: { buy-price: 2.00 }
|
||||
SOUL_LANTERN: { buy-price: 2.50 }
|
||||
CANDLE: { buy-price: 1.50 }
|
||||
FLOWER_POT: { buy-price: 0.50 }
|
||||
ITEM_FRAME: { buy-price: 1.00 }
|
||||
GLOW_ITEM_FRAME: { buy-price: 5.00 }
|
||||
PAINTING: { buy-price: 1.50 }
|
||||
ARMOR_STAND: { buy-price: 3.00 }
|
||||
BANNER: { buy-price: 3.00 }
|
||||
OAK_SIGN: { buy-price: 1.00 }
|
||||
HANGING_SIGN: { buy-price: 2.00 }
|
||||
CHAIN: { buy-price: 1.50 }
|
||||
BELL: { buy-price: 15.00 }
|
||||
CONDUIT: { buy-price: 40.00 }
|
||||
BEACON: { buy-price: 250.00, sell-enabled: false }
|
||||
BOOKSHELF: { buy-price: 4.00 }
|
||||
CHISELED_BOOKSHELF: { buy-price: 5.00 }
|
||||
LECTERN: { buy-price: 6.00 }
|
||||
JUKEBOX: { buy-price: 10.00 }
|
||||
NOTE_BLOCK: { buy-price: 3.00 }
|
||||
GLASS: { buy-price: 1.50 }
|
||||
GLASS_PANE: { buy-price: 0.50 }
|
||||
STAINED_GLASS: { buy-price: 2.00 }
|
||||
STAINED_GLASS_PANE: { buy-price: 0.80 }
|
||||
TERRACOTTA: { buy-price: 1.50 }
|
||||
GLAZED_TERRACOTTA: { buy-price: 2.50 }
|
||||
CONCRETE: { buy-price: 2.00 }
|
||||
CONCRETE_POWDER: { buy-price: 1.50 }
|
||||
WOOL: { buy-price: 2.00 }
|
||||
CARPET: { buy-price: 1.50 }
|
||||
DANDELION: { buy-price: 0.30 }
|
||||
POPPY: { buy-price: 0.30 }
|
||||
BLUE_ORCHID: { buy-price: 0.50 }
|
||||
ALLIUM: { buy-price: 0.50 }
|
||||
SUNFLOWER: { buy-price: 0.50 }
|
||||
ROSE_BUSH: { buy-price: 0.50 }
|
||||
PEONY: { buy-price: 0.50 }
|
||||
WITHER_ROSE: { buy-price: 5.00 }
|
||||
CHERRY_LEAVES: { buy-price: 0.20 }
|
||||
PINK_PETALS: { buy-price: 0.50 }
|
||||
TORCHFLOWER: { buy-price: 4.00 }
|
||||
PITCHER_PLANT: { buy-price: 4.00 }
|
||||
|
||||
TOOLS:
|
||||
display-name: "&7Tools"
|
||||
icon: IRON_PICKAXE
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
WOODEN_PICKAXE: { buy-price: 5.00, sell-price: 1.00 }
|
||||
STONE_PICKAXE: { buy-price: 10.00, sell-price: 2.00 }
|
||||
IRON_PICKAXE: { buy-price: 20.00, sell-price: 4.00 }
|
||||
GOLDEN_PICKAXE: { buy-price: 12.00, sell-price: 2.00 }
|
||||
DIAMOND_PICKAXE: { buy-price: 80.00, sell-price: 15.00 }
|
||||
NETHERITE_PICKAXE: { buy-price: 600.00, sell-price: 100.00 }
|
||||
WOODEN_AXE: { buy-price: 5.00, sell-price: 1.00 }
|
||||
STONE_AXE: { buy-price: 10.00, sell-price: 2.00 }
|
||||
IRON_AXE: { buy-price: 20.00, sell-price: 4.00 }
|
||||
DIAMOND_AXE: { buy-price: 80.00, sell-price: 15.00 }
|
||||
NETHERITE_AXE: { buy-price: 600.00, sell-price: 100.00 }
|
||||
WOODEN_SHOVEL: { buy-price: 4.00, sell-price: 0.80 }
|
||||
STONE_SHOVEL: { buy-price: 8.00, sell-price: 1.50 }
|
||||
IRON_SHOVEL: { buy-price: 15.00, sell-price: 3.00 }
|
||||
DIAMOND_SHOVEL: { buy-price: 60.00, sell-price: 10.00 }
|
||||
NETHERITE_SHOVEL: { buy-price: 550.00, sell-price: 90.00 }
|
||||
WOODEN_HOE: { buy-price: 4.00, sell-price: 0.80 }
|
||||
STONE_HOE: { buy-price: 8.00, sell-price: 1.50 }
|
||||
IRON_HOE: { buy-price: 15.00, sell-price: 3.00 }
|
||||
DIAMOND_HOE: { buy-price: 60.00, sell-price: 10.00 }
|
||||
NETHERITE_HOE: { buy-price: 550.00, sell-price: 90.00 }
|
||||
SHEARS: { buy-price: 8.00, sell-price: 1.50 }
|
||||
FLINT_AND_STEEL: { buy-price: 6.00, sell-price: 1.00 }
|
||||
FISHING_ROD: { buy-price: 5.00, sell-price: 1.00 }
|
||||
COMPASS: { buy-price: 5.00 }
|
||||
CLOCK: { buy-price: 8.00 }
|
||||
SPYGLASS: { buy-price: 12.00 }
|
||||
BRUSH: { buy-price: 5.00 }
|
||||
BUCKET: { buy-price: 8.00 }
|
||||
WATER_BUCKET: { buy-price: 9.00 }
|
||||
LAVA_BUCKET: { buy-price: 12.00 }
|
||||
MILK_BUCKET: { buy-price: 5.00 }
|
||||
POWDER_SNOW_BUCKET: { buy-price: 5.00 }
|
||||
|
||||
COMBAT:
|
||||
display-name: "&4Combat"
|
||||
icon: IRON_SWORD
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
WOODEN_SWORD: { buy-price: 5.00, sell-price: 1.00 }
|
||||
STONE_SWORD: { buy-price: 10.00, sell-price: 2.00 }
|
||||
IRON_SWORD: { buy-price: 20.00, sell-price: 4.00 }
|
||||
GOLDEN_SWORD: { buy-price: 12.00, sell-price: 2.00 }
|
||||
DIAMOND_SWORD: { buy-price: 80.00, sell-price: 15.00 }
|
||||
NETHERITE_SWORD: { buy-price: 600.00, sell-price: 100.00 }
|
||||
BOW: { buy-price: 10.00, sell-price: 2.00 }
|
||||
CROSSBOW: { buy-price: 15.00, sell-price: 3.00 }
|
||||
TRIDENT: { buy-price: 80.00, sell-price: 15.00 }
|
||||
ARROW: { buy-price: 0.50 }
|
||||
SPECTRAL_ARROW: { buy-price: 1.00 }
|
||||
TIPPED_ARROW: { buy-price: 2.00, sell-enabled: false }
|
||||
SHIELD: { buy-price: 15.00, sell-price: 3.00 }
|
||||
IRON_HELMET: { buy-price: 25.00, sell-price: 5.00 }
|
||||
IRON_CHESTPLATE: { buy-price: 40.00, sell-price: 8.00 }
|
||||
IRON_LEGGINGS: { buy-price: 35.00, sell-price: 7.00 }
|
||||
IRON_BOOTS: { buy-price: 20.00, sell-price: 4.00 }
|
||||
DIAMOND_HELMET: { buy-price: 100.00, sell-price: 20.00 }
|
||||
DIAMOND_CHESTPLATE: { buy-price: 160.00, sell-price: 30.00 }
|
||||
DIAMOND_LEGGINGS: { buy-price: 140.00, sell-price: 25.00 }
|
||||
DIAMOND_BOOTS: { buy-price: 80.00, sell-price: 15.00 }
|
||||
NETHERITE_HELMET: { buy-price: 700.00, sell-price: 120.00 }
|
||||
NETHERITE_CHESTPLATE: { buy-price: 900.00, sell-price: 150.00 }
|
||||
NETHERITE_LEGGINGS: { buy-price: 850.00, sell-price: 140.00 }
|
||||
NETHERITE_BOOTS: { buy-price: 750.00, sell-price: 130.00 }
|
||||
GOLDEN_HELMET: { buy-price: 20.00, sell-price: 3.00 }
|
||||
GOLDEN_CHESTPLATE: { buy-price: 35.00, sell-price: 5.00 }
|
||||
GOLDEN_LEGGINGS: { buy-price: 30.00, sell-price: 4.50 }
|
||||
GOLDEN_BOOTS: { buy-price: 20.00, sell-price: 3.00 }
|
||||
|
||||
BREWING:
|
||||
display-name: "&5Brewing"
|
||||
icon: BREWING_STAND
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
BREWING_STAND: { buy-price: 8.00 }
|
||||
CAULDRON: { buy-price: 10.00 }
|
||||
GLASS_BOTTLE: { buy-price: 0.50 }
|
||||
WATER_BOTTLE: { buy-price: 1.00 }
|
||||
FERMENTED_SPIDER_EYE: { buy-price: 5.00 }
|
||||
SUGAR: { buy-price: 0.50 }
|
||||
GLISTERING_MELON_SLICE: { buy-price: 10.00 }
|
||||
GOLDEN_CARROT: { buy-price: 8.00 }
|
||||
PUFFERFISH: { buy-price: 4.00 }
|
||||
MAGMA_CREAM: { buy-price: 4.00 }
|
||||
PHANTOM_MEMBRANE: { buy-price: 8.00 }
|
||||
RABBIT_FOOT: { buy-price: 6.00 }
|
||||
TURTLE_HELMET: { buy-price: 50.00, sell-price: 8.00 }
|
||||
DRAGON_BREATH: { buy-price: 20.00 }
|
||||
|
||||
MISC:
|
||||
display-name: "&8Miscellaneous"
|
||||
icon: CHEST
|
||||
buy-enabled: true
|
||||
sell-enabled: true
|
||||
items:
|
||||
CHEST: { buy-price: 3.00 }
|
||||
TRAPPED_CHEST: { buy-price: 4.00 }
|
||||
BARREL: { buy-price: 4.00 }
|
||||
SHULKER_BOX: { buy-price: 40.00 }
|
||||
ENDER_CHEST: { buy-price: 30.00 }
|
||||
CRAFTING_TABLE: { buy-price: 2.00 }
|
||||
FURNACE: { buy-price: 3.00 }
|
||||
BLAST_FURNACE: { buy-price: 8.00 }
|
||||
SMOKER: { buy-price: 6.00 }
|
||||
ANVIL: { buy-price: 15.00 }
|
||||
GRINDSTONE: { buy-price: 8.00 }
|
||||
STONECUTTER: { buy-price: 6.00 }
|
||||
SMITHING_TABLE: { buy-price: 8.00 }
|
||||
LOOM: { buy-price: 5.00 }
|
||||
CARTOGRAPHY_TABLE: { buy-price: 5.00 }
|
||||
FLETCHING_TABLE: { buy-price: 5.00 }
|
||||
ENCHANTING_TABLE: { buy-price: 50.00 }
|
||||
EXPERIENCE_BOTTLE: { buy-price: 5.00 }
|
||||
NAME_TAG: { buy-price: 20.00 }
|
||||
SADDLE: { buy-price: 15.00, sell-price: 3.00 }
|
||||
LEAD: { buy-price: 5.00 }
|
||||
BOOK: { buy-price: 2.00 }
|
||||
BOOKSHELF: { buy-price: 4.00 }
|
||||
PAPER: { buy-price: 0.30 }
|
||||
MAP: { buy-price: 2.00 }
|
||||
CLOCK: { buy-price: 8.00 }
|
||||
COMPASS: { buy-price: 5.00 }
|
||||
TORCH: { buy-price: 0.10 }
|
||||
FLINT: { buy-price: 0.50 }
|
||||
STICK: { buy-price: 0.10 }
|
||||
BOWL: { buy-price: 0.20 }
|
||||
BRICK: { buy-price: 1.00 }
|
||||
NETHER_BRICK: { buy-price: 1.00 }
|
||||
CLAY_BALL: { buy-price: 0.30 }
|
||||
SNOWBALL: { buy-price: 0.10 }
|
||||
EGG: { buy-price: 0.20 }
|
||||
TURTLE_EGG: { buy-price: 3.00 }
|
||||
DRAGON_EGG: { buy-price: 1000.00, sell-enabled: false }
|
||||
HEART_OF_THE_SEA: { buy-price: 80.00 }
|
||||
NAUTILUS_SHELL: { buy-price: 10.00 }
|
||||
TRIDENT: { buy-price: 80.00, sell-price: 15.00 }
|
||||
MUSIC_DISC_13: { buy-price: 15.00 }
|
||||
MUSIC_DISC_CAT: { buy-price: 15.00 }
|
||||
MUSIC_DISC_BLOCKS: { buy-price: 15.00 }
|
||||
MUSIC_DISC_CHIRP: { buy-price: 15.00 }
|
||||
MUSIC_DISC_FAR: { buy-price: 15.00 }
|
||||
MUSIC_DISC_MALL: { buy-price: 15.00 }
|
||||
MUSIC_DISC_MELLOHI: { buy-price: 15.00 }
|
||||
MUSIC_DISC_STAL: { buy-price: 15.00 }
|
||||
MUSIC_DISC_STRAD: { buy-price: 15.00 }
|
||||
MUSIC_DISC_WARD: { buy-price: 15.00 }
|
||||
MUSIC_DISC_11: { buy-price: 15.00 }
|
||||
MUSIC_DISC_WAIT: { buy-price: 15.00 }
|
||||
MUSIC_DISC_OTHERSIDE: { buy-price: 20.00 }
|
||||
MUSIC_DISC_5: { buy-price: 20.00 }
|
||||
MUSIC_DISC_PIGSTEP: { buy-price: 20.00 }
|
||||
MUSIC_DISC_RELIC: { buy-price: 20.00 }
|
||||
DISC_FRAGMENT_5: { buy-price: 3.00 }
|
||||
POTTERY_SHARD_ARMS_UP: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_BLADE: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_BREWER: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_BURN: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_DANGER: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_EXPLORER: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_FRIEND: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_HEART: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_HEARTBREAK: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_HOWL: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_MINER: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_MOURNER: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_PLENTY: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_PRIZE: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_SHEAF: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_SHELTER: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_SKULL: { buy-price: 5.00 }
|
||||
POTTERY_SHARD_SNORT: { buy-price: 5.00 }
|
||||
DECORATED_POT: { buy-price: 8.00 }
|
||||
BUNDLE: { buy-price: 5.00 }
|
||||
Reference in New Issue
Block a user