Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f228180350 | |||
| 072e7e294c | |||
| 3a708c054c |
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Maven build outputs
|
||||||
|
target/
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Eclipse
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Local dev
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>pt.henrique</groupId>
|
||||||
|
<artifactId>servershop</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>ServerShop</name>
|
||||||
|
<description>A server-run global market plugin for Paper/Purpur 1.21 with large buy/sell spread to complement CommunityMarket</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<paper.version>1.21.1-R0.1-SNAPSHOT</paper.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<!-- Paper Repository -->
|
||||||
|
<repository>
|
||||||
|
<id>papermc</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
<!-- Vault Repository -->
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Paper API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>${paper.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Vault API for Economy -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.MilkBowl</groupId>
|
||||||
|
<artifactId>VaultAPI</artifactId>
|
||||||
|
<version>1.7.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SQLite JDBC Driver -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.45.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.12.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.sqlite</pattern>
|
||||||
|
<shadedPattern>pt.henrique.servershop.libs.sqlite</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
<minimizeJar>false</minimizeJar>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 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."
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 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."
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 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