Initial Realease
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
Generated
+18
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<annotationProcessing>
|
||||||
|
<profile name="Maven default annotation processors profile" enabled="true">
|
||||||
|
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||||
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
|
<outputRelativeToContentRoot value="true" />
|
||||||
|
<module name="communitymarket" />
|
||||||
|
</profile>
|
||||||
|
</annotationProcessing>
|
||||||
|
</component>
|
||||||
|
<component name="JavacSettings">
|
||||||
|
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||||
|
<module name="communitymarket" options="--enable-preview" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+35
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="essentials-releases" />
|
||||||
|
<option name="name" value="essentials-releases" />
|
||||||
|
<option name="url" value="https://repo.essentialsx.net/releases/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Central Repository" />
|
||||||
|
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jitpack.io" />
|
||||||
|
<option name="name" value="jitpack.io" />
|
||||||
|
<option name="url" value="https://jitpack.io" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="papermc-repo" />
|
||||||
|
<option name="name" value="papermc-repo" />
|
||||||
|
<option name="url" value="https://repo.papermc.io/repository/maven-public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Maven Central repository" />
|
||||||
|
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jboss.community" />
|
||||||
|
<option name="name" value="JBoss Community repository" />
|
||||||
|
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/communitymarket.iml" filepath="$PROJECT_DIR$/communitymarket.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# CommunityMarket
|
||||||
|
|
||||||
|
A professional, production-ready GUI-only marketplace plugin for Minecraft Paper 1.21.11.
|
||||||
|
|
||||||
|
Players can create fixed-price listings and auctions, browse, buy, bid, claim items, and withdraw earnings — all through intuitive GUIs. No complex commands to learn!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🛒 Fixed-Price Market
|
||||||
|
- Create listings with custom prices and durations
|
||||||
|
- Browse paginated listings with categories and sorting
|
||||||
|
- Safe atomic purchases to prevent double-buying
|
||||||
|
- Configurable taxes on sales
|
||||||
|
|
||||||
|
### 🔨 Auction System
|
||||||
|
- Start auctions with minimum bid and optional buyout
|
||||||
|
- Anti-snipe protection extends auction when bids arrive near the end
|
||||||
|
- Bid history and automatic outbid notifications
|
||||||
|
- Safe handling of auction endings and payouts
|
||||||
|
|
||||||
|
### 📦 Claim Storage
|
||||||
|
- Items from expired listings go to claim storage
|
||||||
|
- Won auction items are safely delivered
|
||||||
|
- Handles full inventories gracefully
|
||||||
|
|
||||||
|
### 💰 Earnings Management
|
||||||
|
- Pending earnings from sales
|
||||||
|
- Withdraw all at once
|
||||||
|
- Complete transaction history
|
||||||
|
|
||||||
|
### 🔐 Admin Features (GUI-based)
|
||||||
|
- View all listings and auctions
|
||||||
|
- Remove any listing
|
||||||
|
- Cancel/force-end auctions
|
||||||
|
- Reload configuration
|
||||||
|
|
||||||
|
### 🎮 Intuitive GUI Flow
|
||||||
|
The creation flow for listings and auctions:
|
||||||
|
1. **Main Menu** - Central hub for all actions
|
||||||
|
2. **Select Item** - Click an item from your inventory to select it
|
||||||
|
3. **Select Quantity** - Choose how many to sell (skipped for unstackable items)
|
||||||
|
4. **Settings** - Set price and duration with merged, clickable elements
|
||||||
|
5. **Confirm** - Review and confirm your listing/auction
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Server**: Paper 1.21.11 (or compatible)
|
||||||
|
- **Java**: Java 21
|
||||||
|
- **Economy**: One of the following:
|
||||||
|
- Vault + any Vault-compatible economy (Essentials, CMI, etc.)
|
||||||
|
- EssentialsX (fallback if Vault is not present)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download `CommunityMarket-1.0.0.jar`
|
||||||
|
2. Place it in your server's `plugins/` folder
|
||||||
|
3. Ensure you have an economy plugin installed (Vault recommended)
|
||||||
|
4. Start/restart your server
|
||||||
|
5. Edit `plugins/CommunityMarket/config.yml` as needed
|
||||||
|
6. Use `/market` to open the marketplace!
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Alias | Description | Permission |
|
||||||
|
|---------|-------|-------------|------------|
|
||||||
|
| `/market` | `/cmarket` | Opens the main market GUI | `communitymarket.use` |
|
||||||
|
|
||||||
|
**That's it!** Everything else is done through GUIs.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
| Permission | Description | Default |
|
||||||
|
|------------|-------------|---------|
|
||||||
|
| `communitymarket.*` | All permissions | op |
|
||||||
|
| `communitymarket.use` | Access the market GUI | true |
|
||||||
|
| `communitymarket.sell` | Create fixed-price listings | true |
|
||||||
|
| `communitymarket.auction` | Create auctions | true |
|
||||||
|
| `communitymarket.buy` | Purchase from the market | true |
|
||||||
|
| `communitymarket.bid` | Bid on auctions | true |
|
||||||
|
| `communitymarket.claim` | Claim items from storage | true |
|
||||||
|
| `communitymarket.withdraw` | Withdraw earnings | true |
|
||||||
|
| `communitymarket.admin` | Access admin functions | op |
|
||||||
|
| `communitymarket.admin.viewall` | View all listings/auctions | op |
|
||||||
|
| `communitymarket.admin.remove` | Remove any listing/auction | op |
|
||||||
|
| `communitymarket.admin.reload` | Reload configuration | op |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### config.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Language setting (available: en_US, pt_PT)
|
||||||
|
language: en_US
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
database:
|
||||||
|
type: sqlite # or "mysql"
|
||||||
|
sqlite:
|
||||||
|
file: database.db
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: communitymarket
|
||||||
|
username: root
|
||||||
|
password: ""
|
||||||
|
|
||||||
|
# Economy Settings
|
||||||
|
economy:
|
||||||
|
currency-format: "$#,##0.00"
|
||||||
|
currency-symbol: "$"
|
||||||
|
taxes:
|
||||||
|
market-tax: 5.0 # 5% tax on listings
|
||||||
|
auction-tax: 7.5 # 7.5% tax on auctions
|
||||||
|
|
||||||
|
# Market Settings
|
||||||
|
market:
|
||||||
|
max-listings-per-player: 20
|
||||||
|
listing-cooldown: 0 # seconds between listings
|
||||||
|
default-duration-hours: 168 # 7 days
|
||||||
|
min-price: 1.0
|
||||||
|
max-price: 1000000000.0
|
||||||
|
|
||||||
|
# Auction Settings
|
||||||
|
auction:
|
||||||
|
max-auctions-per-player: 10
|
||||||
|
min-duration-hours: 1
|
||||||
|
max-duration-hours: 168
|
||||||
|
anti-snipe:
|
||||||
|
enabled: true
|
||||||
|
trigger-seconds: 30
|
||||||
|
extension-seconds: 30
|
||||||
|
max-extensions: 10
|
||||||
|
|
||||||
|
# GUI Settings
|
||||||
|
gui:
|
||||||
|
items-per-page: 45
|
||||||
|
show-help-button: true # Set to false to hide help button in main menu
|
||||||
|
```
|
||||||
|
|
||||||
|
See the full `config.yml` for all options.
|
||||||
|
|
||||||
|
### Languages
|
||||||
|
|
||||||
|
CommunityMarket ships with two languages:
|
||||||
|
- **English (US)**: `en_US`
|
||||||
|
- **Portuguese (Portugal)**: `pt_PT`
|
||||||
|
|
||||||
|
Change the language in `config.yml`:
|
||||||
|
```yaml
|
||||||
|
language: pt_PT
|
||||||
|
```
|
||||||
|
|
||||||
|
You can create custom language files by copying an existing one in `plugins/CommunityMarket/lang/`.
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/henrique/CommunityMarket.git
|
||||||
|
cd CommunityMarket
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiled JAR will be in `target/CommunityMarket-1.0.0.jar`.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Q: The plugin says "No economy plugin found!"
|
||||||
|
**A:** Install Vault + an economy plugin (like EssentialsX) or just EssentialsX.
|
||||||
|
|
||||||
|
### Q: Can I use MySQL instead of SQLite?
|
||||||
|
**A:** Yes! Change `database.type` to `mysql` in config.yml and fill in your credentials.
|
||||||
|
|
||||||
|
### Q: How do I change the GUI titles?
|
||||||
|
**A:** Edit the language file in `plugins/CommunityMarket/lang/`.
|
||||||
|
|
||||||
|
### Q: Items aren't being removed when creating listings?
|
||||||
|
**A:** This is a known issue with some inventory plugins. Make sure you're running Paper 1.21.11.
|
||||||
|
|
||||||
|
### Q: How do taxes work?
|
||||||
|
**A:** When an item sells, the tax percentage is deducted from the seller's earnings. Buyers pay the listed price.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Issues**: [GitHub Issues](https://github.com/henrique/CommunityMarket/issues)
|
||||||
|
- **Discord**: Coming soon
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ for the Minecraft community
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="minecraft" name="Minecraft">
|
||||||
|
<configuration>
|
||||||
|
<autoDetectTypes>
|
||||||
|
<platformType>ADVENTURE</platformType>
|
||||||
|
</autoDetectTypes>
|
||||||
|
<projectReimportVersion>1</projectReimportVersion>
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?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/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>pt.henrique</groupId>
|
||||||
|
<artifactId>CommunityMarket</artifactId>
|
||||||
|
<name>CommunityMarket</name>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>A GUI-only marketplace plugin for Minecraft Paper servers</description>
|
||||||
|
<build>
|
||||||
|
<defaultGoal>clean package</defaultGoal>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
<finalName>${project.name}-${project.version}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>--enable-preview</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.zaxxer.hikari</pattern>
|
||||||
|
<shadedPattern>pt.henrique.communityMarket.lib.hikari</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc-repo</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>essentials-releases</id>
|
||||||
|
<url>https://repo.essentialsx.net/releases/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>1.21.4-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.MilkBowl</groupId>
|
||||||
|
<artifactId>VaultAPI</artifactId>
|
||||||
|
<version>1.7.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.essentialsx</groupId>
|
||||||
|
<artifactId>EssentialsX</artifactId>
|
||||||
|
<version>2.20.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?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>CommunityMarket</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>CommunityMarket</name>
|
||||||
|
<description>A GUI-only marketplace plugin for Minecraft Paper servers</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<defaultGoal>clean package</defaultGoal>
|
||||||
|
<finalName>${project.name}-${project.version}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>--enable-preview</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>com.zaxxer.hikari</pattern>
|
||||||
|
<shadedPattern>pt.henrique.communityMarket.lib.hikari</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc-repo</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>essentials-releases</id>
|
||||||
|
<url>https://repo.essentialsx.net/releases/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Paper API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>1.21.4-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Vault API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.MilkBowl</groupId>
|
||||||
|
<artifactId>VaultAPI</artifactId>
|
||||||
|
<version>1.7.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- EssentialsX API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.essentialsx</groupId>
|
||||||
|
<artifactId>EssentialsX</artifactId>
|
||||||
|
<version>2.20.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- HikariCP for connection pooling -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
<version>5.1.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package pt.henrique.communityMarket;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import pt.henrique.communityMarket.command.MarketCommand;
|
||||||
|
import pt.henrique.communityMarket.config.ConfigManager;
|
||||||
|
import pt.henrique.communityMarket.config.MessageManager;
|
||||||
|
import pt.henrique.communityMarket.db.DatabaseManager;
|
||||||
|
import pt.henrique.communityMarket.economy.EconomyManager;
|
||||||
|
import pt.henrique.communityMarket.gui.GuiManager;
|
||||||
|
import pt.henrique.communityMarket.listener.GuiListener;
|
||||||
|
import pt.henrique.communityMarket.listener.PlayerListener;
|
||||||
|
import pt.henrique.communityMarket.service.*;
|
||||||
|
import pt.henrique.communityMarket.task.AuctionTask;
|
||||||
|
import pt.henrique.communityMarket.task.ExpiredListingTask;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class CommunityMarket extends JavaPlugin {
|
||||||
|
|
||||||
|
private static CommunityMarket instance;
|
||||||
|
|
||||||
|
private ConfigManager configManager;
|
||||||
|
private MessageManager messageManager;
|
||||||
|
private DatabaseManager databaseManager;
|
||||||
|
private EconomyManager economyManager;
|
||||||
|
private GuiManager guiManager;
|
||||||
|
|
||||||
|
private ListingService listingService;
|
||||||
|
private AuctionService auctionService;
|
||||||
|
private ClaimService claimService;
|
||||||
|
private EarningsService earningsService;
|
||||||
|
private TransactionService transactionService;
|
||||||
|
|
||||||
|
private AuctionTask auctionTask;
|
||||||
|
private ExpiredListingTask expiredListingTask;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
instance = this;
|
||||||
|
|
||||||
|
// Load configurations
|
||||||
|
configManager = new ConfigManager(this);
|
||||||
|
messageManager = new MessageManager(this);
|
||||||
|
|
||||||
|
// Initialize economy
|
||||||
|
economyManager = new EconomyManager(this);
|
||||||
|
if (!economyManager.setupEconomy()) {
|
||||||
|
getLogger().severe("No economy plugin found! Please install Vault or EssentialsX.");
|
||||||
|
getLogger().severe("Disabling CommunityMarket...");
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getLogger().info("Economy provider: " + economyManager.getProviderName());
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
databaseManager = new DatabaseManager(this);
|
||||||
|
if (!databaseManager.initialize()) {
|
||||||
|
getLogger().severe("Failed to initialize database!");
|
||||||
|
getLogger().severe("Disabling CommunityMarket...");
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getLogger().info("Database initialized successfully.");
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
claimService = new ClaimService(this);
|
||||||
|
earningsService = new EarningsService(this);
|
||||||
|
listingService = new ListingService(this);
|
||||||
|
auctionService = new AuctionService(this);
|
||||||
|
transactionService = new TransactionService(this);
|
||||||
|
|
||||||
|
// Initialize GUI manager
|
||||||
|
guiManager = new GuiManager(this);
|
||||||
|
|
||||||
|
// Register command
|
||||||
|
MarketCommand marketCommand = new MarketCommand(this);
|
||||||
|
var command = getCommand("market");
|
||||||
|
if (command != null) {
|
||||||
|
command.setExecutor(marketCommand);
|
||||||
|
command.setTabCompleter(marketCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register listeners
|
||||||
|
getServer().getPluginManager().registerEvents(new GuiListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
|
||||||
|
|
||||||
|
// Start tasks
|
||||||
|
startTasks();
|
||||||
|
|
||||||
|
getLogger().info("CommunityMarket has been enabled!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
// Stop tasks
|
||||||
|
if (auctionTask != null) {
|
||||||
|
auctionTask.cancel();
|
||||||
|
}
|
||||||
|
if (expiredListingTask != null) {
|
||||||
|
expiredListingTask.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close GUI manager
|
||||||
|
if (guiManager != null) {
|
||||||
|
guiManager.closeAllGuis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close database
|
||||||
|
if (databaseManager != null) {
|
||||||
|
databaseManager.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogger().info("CommunityMarket has been disabled!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startTasks() {
|
||||||
|
// Auction check task
|
||||||
|
int auctionInterval = configManager.getAuctionCheckInterval() * 20; // Convert to ticks
|
||||||
|
auctionTask = new AuctionTask(this);
|
||||||
|
auctionTask.runTaskTimerAsynchronously(this, auctionInterval, auctionInterval);
|
||||||
|
|
||||||
|
// Expired listing check task
|
||||||
|
int expiredInterval = configManager.getExpiredCheckInterval() * 60 * 20; // Convert minutes to ticks
|
||||||
|
expiredListingTask = new ExpiredListingTask(this);
|
||||||
|
expiredListingTask.runTaskTimerAsynchronously(this, expiredInterval, expiredInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload() {
|
||||||
|
configManager.reload();
|
||||||
|
messageManager.reload();
|
||||||
|
getLogger().info("Configuration reloaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CommunityMarket getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigManager getConfigManager() {
|
||||||
|
return configManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageManager getMessageManager() {
|
||||||
|
return messageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseManager getDatabaseManager() {
|
||||||
|
return databaseManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyManager getEconomyManager() {
|
||||||
|
return economyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiManager getGuiManager() {
|
||||||
|
return guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListingService getListingService() {
|
||||||
|
return listingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuctionService getAuctionService() {
|
||||||
|
return auctionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimService getClaimService() {
|
||||||
|
return claimService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EarningsService getEarningsService() {
|
||||||
|
return earningsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionService getTransactionService() {
|
||||||
|
return transactionService;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package pt.henrique.communityMarket.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 org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The only command in the plugin: /market (alias: /cmarket)
|
||||||
|
* Opens the main market GUI. All other actions are done through GUIs.
|
||||||
|
*/
|
||||||
|
public class MarketCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public MarketCommand(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||||
|
@NotNull String label, @NotNull String[] args) {
|
||||||
|
// Only players can use this command
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage(plugin.getMessageManager().get("messages.player-only"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check basic permission
|
||||||
|
if (!player.hasPermission("communitymarket.use")) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the main market GUI
|
||||||
|
plugin.getGuiManager().openMainMenu(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||||
|
@NotNull String label, @NotNull String[] args) {
|
||||||
|
// No tab completions - everything is GUI-based
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
package pt.henrique.communityMarket.config;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the main plugin configuration
|
||||||
|
*/
|
||||||
|
public class ConfigManager {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private FileConfiguration config;
|
||||||
|
|
||||||
|
// Database settings
|
||||||
|
private String databaseType;
|
||||||
|
private String sqliteFile;
|
||||||
|
private String mysqlHost;
|
||||||
|
private int mysqlPort;
|
||||||
|
private String mysqlDatabase;
|
||||||
|
private String mysqlUsername;
|
||||||
|
private String mysqlPassword;
|
||||||
|
private int poolMaxSize;
|
||||||
|
private int poolMinIdle;
|
||||||
|
private long poolConnectionTimeout;
|
||||||
|
private long poolIdleTimeout;
|
||||||
|
private long poolMaxLifetime;
|
||||||
|
|
||||||
|
// Economy settings
|
||||||
|
private String currencyFormat;
|
||||||
|
private String currencySymbol;
|
||||||
|
private double marketTax;
|
||||||
|
private double auctionTax;
|
||||||
|
|
||||||
|
// Market settings
|
||||||
|
private int maxListingsPerPlayer;
|
||||||
|
private int listingCooldown;
|
||||||
|
private int defaultDurationHours;
|
||||||
|
private List<Integer> availableDurations;
|
||||||
|
private double minPrice;
|
||||||
|
private double maxPrice;
|
||||||
|
|
||||||
|
// Auction settings
|
||||||
|
private int maxAuctionsPerPlayer;
|
||||||
|
private int minDurationHours;
|
||||||
|
private int maxDurationHours;
|
||||||
|
private int defaultAuctionDurationHours;
|
||||||
|
private List<Integer> availableAuctionDurations;
|
||||||
|
private double minStartPrice;
|
||||||
|
private double maxStartPrice;
|
||||||
|
private double minBidIncrementPercent;
|
||||||
|
private double minBidIncrementAbsolute;
|
||||||
|
private boolean antiSnipeEnabled;
|
||||||
|
private int antiSnipeTriggerSeconds;
|
||||||
|
private int antiSnipeExtensionSeconds;
|
||||||
|
private int antiSnipeMaxExtensions;
|
||||||
|
|
||||||
|
// Blacklist
|
||||||
|
private Set<Material> blacklistedMaterials;
|
||||||
|
private List<String> blacklistedKeywords;
|
||||||
|
|
||||||
|
// GUI settings
|
||||||
|
private String mainMenuTitle;
|
||||||
|
private String browseMarketTitle;
|
||||||
|
private String browseAuctionsTitle;
|
||||||
|
private String createListingTitle;
|
||||||
|
private String createAuctionTitle;
|
||||||
|
private String myListingsTitle;
|
||||||
|
private String myAuctionsTitle;
|
||||||
|
private String claimTitle;
|
||||||
|
private String earningsTitle;
|
||||||
|
private String confirmTitle;
|
||||||
|
private String numberInputTitle;
|
||||||
|
private String adminTitle;
|
||||||
|
private int itemsPerPage;
|
||||||
|
private boolean helpButtonEnabled;
|
||||||
|
private String clickSound;
|
||||||
|
private String successSound;
|
||||||
|
private String errorSound;
|
||||||
|
private String purchaseSound;
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
private boolean notifyOnSale;
|
||||||
|
private boolean notifyOnOutbid;
|
||||||
|
private boolean notifyOnWin;
|
||||||
|
private boolean notifyOnExpire;
|
||||||
|
|
||||||
|
// Performance
|
||||||
|
private int cacheDuration;
|
||||||
|
private int auctionCheckInterval;
|
||||||
|
private int expiredCheckInterval;
|
||||||
|
|
||||||
|
public ConfigManager(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload() {
|
||||||
|
plugin.saveDefaultConfig();
|
||||||
|
plugin.reloadConfig();
|
||||||
|
config = plugin.getConfig();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSettings() {
|
||||||
|
// Database settings
|
||||||
|
databaseType = config.getString("database.type", "sqlite");
|
||||||
|
sqliteFile = config.getString("database.sqlite.file", "database.db");
|
||||||
|
mysqlHost = config.getString("database.mysql.host", "localhost");
|
||||||
|
mysqlPort = config.getInt("database.mysql.port", 3306);
|
||||||
|
mysqlDatabase = config.getString("database.mysql.database", "communitymarket");
|
||||||
|
mysqlUsername = config.getString("database.mysql.username", "root");
|
||||||
|
mysqlPassword = config.getString("database.mysql.password", "");
|
||||||
|
poolMaxSize = config.getInt("database.mysql.pool.maximum-pool-size", 10);
|
||||||
|
poolMinIdle = config.getInt("database.mysql.pool.minimum-idle", 2);
|
||||||
|
poolConnectionTimeout = config.getLong("database.mysql.pool.connection-timeout", 30000);
|
||||||
|
poolIdleTimeout = config.getLong("database.mysql.pool.idle-timeout", 600000);
|
||||||
|
poolMaxLifetime = config.getLong("database.mysql.pool.max-lifetime", 1800000);
|
||||||
|
|
||||||
|
// Economy settings
|
||||||
|
currencyFormat = config.getString("economy.currency-format", "$#,##0.00");
|
||||||
|
currencySymbol = config.getString("economy.currency-symbol", "$");
|
||||||
|
marketTax = config.getDouble("economy.taxes.market-tax", 5.0);
|
||||||
|
auctionTax = config.getDouble("economy.taxes.auction-tax", 7.5);
|
||||||
|
|
||||||
|
// Market settings
|
||||||
|
maxListingsPerPlayer = config.getInt("market.max-listings-per-player", 20);
|
||||||
|
listingCooldown = config.getInt("market.listing-cooldown", 0);
|
||||||
|
defaultDurationHours = config.getInt("market.default-duration-hours", 168);
|
||||||
|
availableDurations = config.getIntegerList("market.available-durations");
|
||||||
|
minPrice = config.getDouble("market.min-price", 1.0);
|
||||||
|
maxPrice = config.getDouble("market.max-price", 1000000000.0);
|
||||||
|
|
||||||
|
// Auction settings
|
||||||
|
maxAuctionsPerPlayer = config.getInt("auction.max-auctions-per-player", 10);
|
||||||
|
minDurationHours = config.getInt("auction.min-duration-hours", 1);
|
||||||
|
maxDurationHours = config.getInt("auction.max-duration-hours", 168);
|
||||||
|
defaultAuctionDurationHours = config.getInt("auction.default-duration-hours", 24);
|
||||||
|
availableAuctionDurations = config.getIntegerList("auction.available-durations");
|
||||||
|
minStartPrice = config.getDouble("auction.min-start-price", 1.0);
|
||||||
|
maxStartPrice = config.getDouble("auction.max-start-price", 1000000000.0);
|
||||||
|
minBidIncrementPercent = config.getDouble("auction.min-bid-increment-percent", 5.0);
|
||||||
|
minBidIncrementAbsolute = config.getDouble("auction.min-bid-increment-absolute", 1.0);
|
||||||
|
antiSnipeEnabled = config.getBoolean("auction.anti-snipe.enabled", true);
|
||||||
|
antiSnipeTriggerSeconds = config.getInt("auction.anti-snipe.trigger-seconds", 30);
|
||||||
|
antiSnipeExtensionSeconds = config.getInt("auction.anti-snipe.extension-seconds", 30);
|
||||||
|
antiSnipeMaxExtensions = config.getInt("auction.anti-snipe.max-extensions", 10);
|
||||||
|
|
||||||
|
// Blacklist
|
||||||
|
blacklistedMaterials = new HashSet<>();
|
||||||
|
for (String materialName : config.getStringList("blacklist.materials")) {
|
||||||
|
try {
|
||||||
|
Material material = Material.valueOf(materialName.toUpperCase());
|
||||||
|
blacklistedMaterials.add(material);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
plugin.getLogger().warning("Invalid material in blacklist: " + materialName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blacklistedKeywords = config.getStringList("blacklist.keywords");
|
||||||
|
|
||||||
|
// GUI settings
|
||||||
|
mainMenuTitle = config.getString("gui.main-menu-title", "&8&lCommunity Market");
|
||||||
|
browseMarketTitle = config.getString("gui.browse-market-title", "&8&lBrowse Market");
|
||||||
|
browseAuctionsTitle = config.getString("gui.browse-auctions-title", "&8&lBrowse Auctions");
|
||||||
|
createListingTitle = config.getString("gui.create-listing-title", "&8&lCreate Listing");
|
||||||
|
createAuctionTitle = config.getString("gui.create-auction-title", "&8&lCreate Auction");
|
||||||
|
myListingsTitle = config.getString("gui.my-listings-title", "&8&lMy Listings");
|
||||||
|
myAuctionsTitle = config.getString("gui.my-auctions-title", "&8&lMy Auctions");
|
||||||
|
claimTitle = config.getString("gui.claim-title", "&8&lClaim Items");
|
||||||
|
earningsTitle = config.getString("gui.earnings-title", "&8&lEarnings");
|
||||||
|
confirmTitle = config.getString("gui.confirm-title", "&8&lConfirm Action");
|
||||||
|
numberInputTitle = config.getString("gui.number-input-title", "&8&lEnter Amount");
|
||||||
|
adminTitle = config.getString("gui.admin-title", "&8&lAdmin Panel");
|
||||||
|
itemsPerPage = config.getInt("gui.items-per-page", 45);
|
||||||
|
helpButtonEnabled = config.getBoolean("gui.show-help-button", true);
|
||||||
|
clickSound = config.getString("gui.sounds.click", "UI_BUTTON_CLICK");
|
||||||
|
successSound = config.getString("gui.sounds.success", "ENTITY_PLAYER_LEVELUP");
|
||||||
|
errorSound = config.getString("gui.sounds.error", "ENTITY_VILLAGER_NO");
|
||||||
|
purchaseSound = config.getString("gui.sounds.purchase", "ENTITY_EXPERIENCE_ORB_PICKUP");
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
notifyOnSale = config.getBoolean("notifications.notify-on-sale", true);
|
||||||
|
notifyOnOutbid = config.getBoolean("notifications.notify-on-outbid", true);
|
||||||
|
notifyOnWin = config.getBoolean("notifications.notify-on-win", true);
|
||||||
|
notifyOnExpire = config.getBoolean("notifications.notify-on-expire", true);
|
||||||
|
|
||||||
|
// Performance
|
||||||
|
cacheDuration = config.getInt("performance.cache-duration", 30);
|
||||||
|
auctionCheckInterval = config.getInt("performance.auction-check-interval", 5);
|
||||||
|
expiredCheckInterval = config.getInt("performance.expired-check-interval", 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public String getDatabaseType() { return databaseType; }
|
||||||
|
public String getSqliteFile() { return sqliteFile; }
|
||||||
|
public String getMysqlHost() { return mysqlHost; }
|
||||||
|
public int getMysqlPort() { return mysqlPort; }
|
||||||
|
public String getMysqlDatabase() { return mysqlDatabase; }
|
||||||
|
public String getMysqlUsername() { return mysqlUsername; }
|
||||||
|
public String getMysqlPassword() { return mysqlPassword; }
|
||||||
|
public int getPoolMaxSize() { return poolMaxSize; }
|
||||||
|
public int getPoolMinIdle() { return poolMinIdle; }
|
||||||
|
public long getPoolConnectionTimeout() { return poolConnectionTimeout; }
|
||||||
|
public long getPoolIdleTimeout() { return poolIdleTimeout; }
|
||||||
|
public long getPoolMaxLifetime() { return poolMaxLifetime; }
|
||||||
|
|
||||||
|
public String getCurrencyFormat() { return currencyFormat; }
|
||||||
|
public String getCurrencySymbol() { return currencySymbol; }
|
||||||
|
public double getMarketTax() { return marketTax; }
|
||||||
|
public double getAuctionTax() { return auctionTax; }
|
||||||
|
|
||||||
|
public int getMaxListingsPerPlayer() { return maxListingsPerPlayer; }
|
||||||
|
public int getListingCooldown() { return listingCooldown; }
|
||||||
|
public int getDefaultDurationHours() { return defaultDurationHours; }
|
||||||
|
public List<Integer> getAvailableDurations() { return availableDurations; }
|
||||||
|
public double getMinPrice() { return minPrice; }
|
||||||
|
public double getMaxPrice() { return maxPrice; }
|
||||||
|
|
||||||
|
public int getMaxAuctionsPerPlayer() { return maxAuctionsPerPlayer; }
|
||||||
|
public int getMinDurationHours() { return minDurationHours; }
|
||||||
|
public int getMaxDurationHours() { return maxDurationHours; }
|
||||||
|
public int getDefaultAuctionDurationHours() { return defaultAuctionDurationHours; }
|
||||||
|
public List<Integer> getAvailableAuctionDurations() { return availableAuctionDurations; }
|
||||||
|
public double getMinStartPrice() { return minStartPrice; }
|
||||||
|
public double getMaxStartPrice() { return maxStartPrice; }
|
||||||
|
public double getMinBidIncrementPercent() { return minBidIncrementPercent; }
|
||||||
|
public double getMinBidIncrementAbsolute() { return minBidIncrementAbsolute; }
|
||||||
|
public boolean isAntiSnipeEnabled() { return antiSnipeEnabled; }
|
||||||
|
public int getAntiSnipeTriggerSeconds() { return antiSnipeTriggerSeconds; }
|
||||||
|
public int getAntiSnipeExtensionSeconds() { return antiSnipeExtensionSeconds; }
|
||||||
|
public int getAntiSnipeMaxExtensions() { return antiSnipeMaxExtensions; }
|
||||||
|
|
||||||
|
public Set<Material> getBlacklistedMaterials() { return blacklistedMaterials; }
|
||||||
|
public List<String> getBlacklistedKeywords() { return blacklistedKeywords; }
|
||||||
|
|
||||||
|
public String getMainMenuTitle() { return mainMenuTitle; }
|
||||||
|
public String getBrowseMarketTitle() { return browseMarketTitle; }
|
||||||
|
public String getBrowseAuctionsTitle() { return browseAuctionsTitle; }
|
||||||
|
public String getCreateListingTitle() { return createListingTitle; }
|
||||||
|
public String getCreateAuctionTitle() { return createAuctionTitle; }
|
||||||
|
public String getMyListingsTitle() { return myListingsTitle; }
|
||||||
|
public String getMyAuctionsTitle() { return myAuctionsTitle; }
|
||||||
|
public String getClaimTitle() { return claimTitle; }
|
||||||
|
public String getEarningsTitle() { return earningsTitle; }
|
||||||
|
public String getConfirmTitle() { return confirmTitle; }
|
||||||
|
public String getNumberInputTitle() { return numberInputTitle; }
|
||||||
|
public String getAdminTitle() { return adminTitle; }
|
||||||
|
public int getItemsPerPage() { return itemsPerPage; }
|
||||||
|
public boolean isHelpButtonEnabled() { return helpButtonEnabled; }
|
||||||
|
public String getClickSound() { return clickSound; }
|
||||||
|
public String getSuccessSound() { return successSound; }
|
||||||
|
public String getErrorSound() { return errorSound; }
|
||||||
|
public String getPurchaseSound() { return purchaseSound; }
|
||||||
|
|
||||||
|
public boolean isNotifyOnSale() { return notifyOnSale; }
|
||||||
|
public boolean isNotifyOnOutbid() { return notifyOnOutbid; }
|
||||||
|
public boolean isNotifyOnWin() { return notifyOnWin; }
|
||||||
|
public boolean isNotifyOnExpire() { return notifyOnExpire; }
|
||||||
|
|
||||||
|
public int getCacheDuration() { return cacheDuration; }
|
||||||
|
public int getAuctionCheckInterval() { return auctionCheckInterval; }
|
||||||
|
public int getExpiredCheckInterval() { return expiredCheckInterval; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a material is blacklisted
|
||||||
|
*/
|
||||||
|
public boolean isMaterialBlacklisted(Material material) {
|
||||||
|
return blacklistedMaterials.contains(material);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if text contains blacklisted keywords
|
||||||
|
*/
|
||||||
|
public boolean containsBlacklistedKeyword(String text) {
|
||||||
|
if (text == null) return false;
|
||||||
|
String lowerText = text.toLowerCase();
|
||||||
|
for (String keyword : blacklistedKeywords) {
|
||||||
|
if (lowerText.contains(keyword.toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package pt.henrique.communityMarket.config;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages plugin messages and localization.
|
||||||
|
* Supports multiple languages loaded from lang/ folder.
|
||||||
|
*/
|
||||||
|
public class MessageManager {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private FileConfiguration messagesConfig;
|
||||||
|
private final Map<String, String> messageCache;
|
||||||
|
private DecimalFormat currencyFormatter;
|
||||||
|
private String currentLanguage;
|
||||||
|
|
||||||
|
public MessageManager(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.messageCache = new HashMap<>();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload() {
|
||||||
|
messageCache.clear();
|
||||||
|
|
||||||
|
// Get language from config
|
||||||
|
currentLanguage = plugin.getConfig().getString("language", "en_US");
|
||||||
|
|
||||||
|
// Save default language files
|
||||||
|
saveDefaultLanguageFiles();
|
||||||
|
|
||||||
|
// Load the selected language file
|
||||||
|
File langFolder = new File(plugin.getDataFolder(), "lang");
|
||||||
|
File langFile = new File(langFolder, currentLanguage + ".yml");
|
||||||
|
|
||||||
|
if (!langFile.exists()) {
|
||||||
|
plugin.getLogger().warning("Language file not found: " + currentLanguage + ".yml, falling back to en_US");
|
||||||
|
langFile = new File(langFolder, "en_US.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesConfig = YamlConfiguration.loadConfiguration(langFile);
|
||||||
|
|
||||||
|
// Load defaults from jar as fallback
|
||||||
|
InputStream defaultStream = plugin.getResource("lang/en_US.yml");
|
||||||
|
if (defaultStream != null) {
|
||||||
|
YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(
|
||||||
|
new InputStreamReader(defaultStream, StandardCharsets.UTF_8));
|
||||||
|
messagesConfig.setDefaults(defaultConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup currency formatter
|
||||||
|
String format = plugin.getConfigManager().getCurrencyFormat();
|
||||||
|
try {
|
||||||
|
currencyFormatter = new DecimalFormat(format.replace("$", ""));
|
||||||
|
} catch (Exception e) {
|
||||||
|
currencyFormatter = new DecimalFormat("#,##0.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().info("Loaded language: " + currentLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDefaultLanguageFiles() {
|
||||||
|
File langFolder = new File(plugin.getDataFolder(), "lang");
|
||||||
|
if (!langFolder.exists()) {
|
||||||
|
langFolder.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save default language files if they don't exist
|
||||||
|
String[] languages = {"en_US.yml", "pt_PT.yml"};
|
||||||
|
for (String lang : languages) {
|
||||||
|
File langFile = new File(langFolder, lang);
|
||||||
|
if (!langFile.exists()) {
|
||||||
|
plugin.saveResource("lang/" + lang, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current language code
|
||||||
|
*/
|
||||||
|
public String getCurrentLanguage() {
|
||||||
|
return currentLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a raw message string from the config
|
||||||
|
*/
|
||||||
|
public String getRaw(String path) {
|
||||||
|
if (messageCache.containsKey(path)) {
|
||||||
|
return messageCache.get(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
String message = messagesConfig.getString(path, "&cMissing message: " + path);
|
||||||
|
messageCache.put(path, message);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a message as a Component
|
||||||
|
*/
|
||||||
|
public Component get(String path) {
|
||||||
|
return TextUtil.colorize(getRaw(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a message with placeholders replaced
|
||||||
|
*/
|
||||||
|
public Component get(String path, Map<String, String> placeholders) {
|
||||||
|
String message = getRaw(path);
|
||||||
|
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||||
|
message = message.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||||
|
}
|
||||||
|
return TextUtil.colorize(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a message with a single placeholder replaced
|
||||||
|
*/
|
||||||
|
public Component get(String path, String placeholder, String value) {
|
||||||
|
String message = getRaw(path).replace("{" + placeholder + "}", value);
|
||||||
|
return TextUtil.colorize(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a prefixed message
|
||||||
|
*/
|
||||||
|
public Component getPrefixed(String path) {
|
||||||
|
return TextUtil.colorize(getRaw("prefix") + getRaw(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a prefixed message with placeholders
|
||||||
|
*/
|
||||||
|
public Component getPrefixed(String path, Map<String, String> placeholders) {
|
||||||
|
String message = getRaw(path);
|
||||||
|
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||||
|
message = message.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||||
|
}
|
||||||
|
return TextUtil.colorize(getRaw("prefix") + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a prefixed message with a single placeholder
|
||||||
|
*/
|
||||||
|
public Component getPrefixed(String path, String placeholder, String value) {
|
||||||
|
String message = getRaw(path).replace("{" + placeholder + "}", value);
|
||||||
|
return TextUtil.colorize(getRaw("prefix") + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of messages from the config
|
||||||
|
*/
|
||||||
|
public List<String> getList(String path) {
|
||||||
|
return messagesConfig.getStringList(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of messages as Components
|
||||||
|
*/
|
||||||
|
public List<Component> getComponentList(String path) {
|
||||||
|
return getList(path).stream()
|
||||||
|
.map(TextUtil::colorize)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a currency amount
|
||||||
|
*/
|
||||||
|
public String formatCurrency(double amount) {
|
||||||
|
String symbol = plugin.getConfigManager().getCurrencySymbol();
|
||||||
|
return symbol + currencyFormatter.format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a button name from config
|
||||||
|
*/
|
||||||
|
public String getButton(String buttonKey) {
|
||||||
|
return getRaw("buttons." + buttonKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets button lore list from config
|
||||||
|
*/
|
||||||
|
public List<String> getLore(String loreKey) {
|
||||||
|
return getList("lore." + loreKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets lore with placeholders replaced
|
||||||
|
*/
|
||||||
|
public List<String> getLore(String loreKey, Map<String, String> placeholders) {
|
||||||
|
List<String> lore = getList("lore." + loreKey);
|
||||||
|
return lore.stream()
|
||||||
|
.map(line -> {
|
||||||
|
String result = line;
|
||||||
|
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||||
|
result = result.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets filter display name
|
||||||
|
*/
|
||||||
|
public String getFilter(String filterKey) {
|
||||||
|
return getRaw("filters." + filterKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets sort display name
|
||||||
|
*/
|
||||||
|
public String getSort(String sortKey) {
|
||||||
|
return getRaw("sort." + sortKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,987 @@
|
|||||||
|
package pt.henrique.communityMarket.db;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.*;
|
||||||
|
import pt.henrique.communityMarket.util.ItemSerializer;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages database connections and operations.
|
||||||
|
* Supports SQLite (default) and MySQL.
|
||||||
|
* All operations are asynchronous to prevent blocking the main thread.
|
||||||
|
*/
|
||||||
|
public class DatabaseManager {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private HikariDataSource dataSource;
|
||||||
|
private boolean isMySQL;
|
||||||
|
|
||||||
|
// Schema version for migrations
|
||||||
|
private static final int SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
|
public DatabaseManager(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the database connection pool and creates tables.
|
||||||
|
*
|
||||||
|
* @return true if successful
|
||||||
|
*/
|
||||||
|
public boolean initialize() {
|
||||||
|
try {
|
||||||
|
var config = plugin.getConfigManager();
|
||||||
|
isMySQL = "mysql".equalsIgnoreCase(config.getDatabaseType());
|
||||||
|
|
||||||
|
HikariConfig hikariConfig = new HikariConfig();
|
||||||
|
|
||||||
|
if (isMySQL) {
|
||||||
|
// MySQL configuration
|
||||||
|
hikariConfig.setJdbcUrl("jdbc:mysql://" + config.getMysqlHost() + ":" +
|
||||||
|
config.getMysqlPort() + "/" + config.getMysqlDatabase() +
|
||||||
|
"?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8");
|
||||||
|
hikariConfig.setUsername(config.getMysqlUsername());
|
||||||
|
hikariConfig.setPassword(config.getMysqlPassword());
|
||||||
|
hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
|
||||||
|
} else {
|
||||||
|
// SQLite configuration
|
||||||
|
File dataFolder = plugin.getDataFolder();
|
||||||
|
if (!dataFolder.exists()) {
|
||||||
|
dataFolder.mkdirs();
|
||||||
|
}
|
||||||
|
File dbFile = new File(dataFolder, config.getSqliteFile());
|
||||||
|
hikariConfig.setJdbcUrl("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||||
|
hikariConfig.setDriverClassName("org.sqlite.JDBC");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection pool settings
|
||||||
|
hikariConfig.setMaximumPoolSize(config.getPoolMaxSize());
|
||||||
|
hikariConfig.setMinimumIdle(config.getPoolMinIdle());
|
||||||
|
hikariConfig.setConnectionTimeout(config.getPoolConnectionTimeout());
|
||||||
|
hikariConfig.setIdleTimeout(config.getPoolIdleTimeout());
|
||||||
|
hikariConfig.setMaxLifetime(config.getPoolMaxLifetime());
|
||||||
|
hikariConfig.setPoolName("CommunityMarket-Pool");
|
||||||
|
|
||||||
|
dataSource = new HikariDataSource(hikariConfig);
|
||||||
|
|
||||||
|
// Create tables
|
||||||
|
createTables();
|
||||||
|
|
||||||
|
plugin.getLogger().info("Database connection established (" +
|
||||||
|
(isMySQL ? "MySQL" : "SQLite") + ")");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Failed to initialize database", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down the database connection pool.
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
if (dataSource != null && !dataSource.isClosed()) {
|
||||||
|
dataSource.close();
|
||||||
|
plugin.getLogger().info("Database connection closed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a connection from the pool.
|
||||||
|
*/
|
||||||
|
private Connection getConnection() throws SQLException {
|
||||||
|
return dataSource.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates all database tables.
|
||||||
|
*/
|
||||||
|
private void createTables() throws SQLException {
|
||||||
|
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||||
|
|
||||||
|
// Listings table
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS listings (
|
||||||
|
id INTEGER PRIMARY KEY %s,
|
||||||
|
seller_uuid VARCHAR(36) NOT NULL,
|
||||||
|
seller_name VARCHAR(16) NOT NULL,
|
||||||
|
item_data TEXT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
price DOUBLE NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
expires_at BIGINT,
|
||||||
|
buyer_uuid VARCHAR(36),
|
||||||
|
buyer_name VARCHAR(16),
|
||||||
|
sold_at BIGINT
|
||||||
|
)
|
||||||
|
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||||
|
|
||||||
|
// Auctions table
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS auctions (
|
||||||
|
id INTEGER PRIMARY KEY %s,
|
||||||
|
seller_uuid VARCHAR(36) NOT NULL,
|
||||||
|
seller_name VARCHAR(16) NOT NULL,
|
||||||
|
item_data TEXT NOT NULL,
|
||||||
|
start_price DOUBLE NOT NULL,
|
||||||
|
current_bid DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
highest_bidder_uuid VARCHAR(36),
|
||||||
|
highest_bidder_name VARCHAR(16),
|
||||||
|
bid_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
buyout_price DOUBLE,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
ends_at BIGINT NOT NULL,
|
||||||
|
extension_count INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||||
|
|
||||||
|
// Bids table (bid history)
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS bids (
|
||||||
|
id INTEGER PRIMARY KEY %s,
|
||||||
|
auction_id INTEGER NOT NULL,
|
||||||
|
bidder_uuid VARCHAR(36) NOT NULL,
|
||||||
|
bidder_name VARCHAR(16) NOT NULL,
|
||||||
|
amount DOUBLE NOT NULL,
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
FOREIGN KEY (auction_id) REFERENCES auctions(id)
|
||||||
|
)
|
||||||
|
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||||
|
|
||||||
|
// Claim storage table
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS claim_storage (
|
||||||
|
id INTEGER PRIMARY KEY %s,
|
||||||
|
player_uuid VARCHAR(36) NOT NULL,
|
||||||
|
item_data TEXT NOT NULL,
|
||||||
|
reason VARCHAR(50) NOT NULL,
|
||||||
|
source_info VARCHAR(100),
|
||||||
|
created_at BIGINT NOT NULL
|
||||||
|
)
|
||||||
|
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||||
|
|
||||||
|
// Pending earnings table
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS pending_earnings (
|
||||||
|
id INTEGER PRIMARY KEY %s,
|
||||||
|
player_uuid VARCHAR(36) NOT NULL,
|
||||||
|
amount DOUBLE NOT NULL,
|
||||||
|
source VARCHAR(100),
|
||||||
|
created_at BIGINT NOT NULL,
|
||||||
|
withdrawn BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
)
|
||||||
|
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||||
|
|
||||||
|
// Player data table (for cooldowns, etc.)
|
||||||
|
stmt.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS player_data (
|
||||||
|
player_uuid VARCHAR(36) PRIMARY KEY,
|
||||||
|
last_listing_time BIGINT,
|
||||||
|
preferred_language VARCHAR(10)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
|
||||||
|
// Create indexes for performance
|
||||||
|
try {
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS idx_listings_seller ON listings(seller_uuid)");
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS idx_listings_status ON listings(status)");
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS idx_auctions_seller ON auctions(seller_uuid)");
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS idx_auctions_status ON auctions(status)");
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS idx_claim_player ON claim_storage(player_uuid)");
|
||||||
|
stmt.execute("CREATE INDEX IF NOT EXISTS idx_earnings_player ON pending_earnings(player_uuid)");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// Indexes might already exist, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== LISTING OPERATIONS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new listing and returns its ID.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> createListing(Listing listing) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO listings (seller_uuid, seller_name, item_data, amount, price, status, created_at, expires_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
stmt.setString(1, listing.getSellerUuid().toString());
|
||||||
|
stmt.setString(2, listing.getSellerName());
|
||||||
|
stmt.setString(3, ItemSerializer.serialize(listing.getItem()));
|
||||||
|
stmt.setInt(4, listing.getAmount());
|
||||||
|
stmt.setDouble(5, listing.getPrice());
|
||||||
|
stmt.setString(6, listing.getStatus().name());
|
||||||
|
stmt.setLong(7, listing.getCreatedAt().toEpochMilli());
|
||||||
|
stmt.setLong(8, listing.getExpiresAt() != null ? listing.getExpiresAt().toEpochMilli() : 0);
|
||||||
|
|
||||||
|
stmt.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to create listing", e);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all active listings.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Listing>> getActiveListings() {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<Listing> listings = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM listings WHERE status = 'ACTIVE' ORDER BY created_at DESC";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||||
|
ResultSet rs = stmt.executeQuery()) {
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
Listing listing = mapListing(rs);
|
||||||
|
if (listing != null) {
|
||||||
|
listings.add(listing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get active listings", e);
|
||||||
|
}
|
||||||
|
return listings;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a listing by ID.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Optional<Listing>> getListing(int id) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "SELECT * FROM listings WHERE id = ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setInt(1, id);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return Optional.ofNullable(mapListing(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get listing", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all listings for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Listing>> getPlayerListings(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<Listing> listings = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM listings WHERE seller_uuid = ? AND status = 'ACTIVE' ORDER BY created_at DESC";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
Listing listing = mapListing(rs);
|
||||||
|
if (listing != null) {
|
||||||
|
listings.add(listing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get player listings", e);
|
||||||
|
}
|
||||||
|
return listings;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts active listings for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> countPlayerListings(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "SELECT COUNT(*) FROM listings WHERE seller_uuid = ? AND status = 'ACTIVE'";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to count player listings", e);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically purchases a listing.
|
||||||
|
* Returns true if successful (listing was still available).
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> purchaseListing(int listingId, UUID buyerUuid, String buyerName) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "UPDATE listings SET status = 'SOLD', buyer_uuid = ?, buyer_name = ?, sold_at = ? " +
|
||||||
|
"WHERE id = ? AND status = 'ACTIVE'";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, buyerUuid.toString());
|
||||||
|
stmt.setString(2, buyerName);
|
||||||
|
stmt.setLong(3, Instant.now().toEpochMilli());
|
||||||
|
stmt.setInt(4, listingId);
|
||||||
|
|
||||||
|
int updated = stmt.executeUpdate();
|
||||||
|
return updated > 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to purchase listing", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a listing's status.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> updateListingStatus(int listingId, Listing.ListingStatus status) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "UPDATE listings SET status = ? WHERE id = ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, status.name());
|
||||||
|
stmt.setInt(2, listingId);
|
||||||
|
|
||||||
|
return stmt.executeUpdate() > 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to update listing status", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets expired active listings.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Listing>> getExpiredListings() {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<Listing> listings = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM listings WHERE status = 'ACTIVE' AND expires_at > 0 AND expires_at < ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setLong(1, Instant.now().toEpochMilli());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
Listing listing = mapListing(rs);
|
||||||
|
if (listing != null) {
|
||||||
|
listings.add(listing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get expired listings", e);
|
||||||
|
}
|
||||||
|
return listings;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Listing mapListing(ResultSet rs) throws SQLException {
|
||||||
|
try {
|
||||||
|
Listing listing = new Listing();
|
||||||
|
listing.setId(rs.getInt("id"));
|
||||||
|
listing.setSellerUuid(UUID.fromString(rs.getString("seller_uuid")));
|
||||||
|
listing.setSellerName(rs.getString("seller_name"));
|
||||||
|
listing.setItem(ItemSerializer.deserialize(rs.getString("item_data")));
|
||||||
|
listing.setAmount(rs.getInt("amount"));
|
||||||
|
listing.setPrice(rs.getDouble("price"));
|
||||||
|
listing.setStatus(Listing.ListingStatus.valueOf(rs.getString("status")));
|
||||||
|
listing.setCreatedAt(Instant.ofEpochMilli(rs.getLong("created_at")));
|
||||||
|
long expiresAt = rs.getLong("expires_at");
|
||||||
|
if (expiresAt > 0) {
|
||||||
|
listing.setExpiresAt(Instant.ofEpochMilli(expiresAt));
|
||||||
|
}
|
||||||
|
return listing;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to map listing", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AUCTION OPERATIONS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new auction and returns its ID.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> createAuction(Auction auction) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO auctions (seller_uuid, seller_name, item_data, start_price, current_bid,
|
||||||
|
buyout_price, status, created_at, ends_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
stmt.setString(1, auction.getSellerUuid().toString());
|
||||||
|
stmt.setString(2, auction.getSellerName());
|
||||||
|
stmt.setString(3, ItemSerializer.serialize(auction.getItem()));
|
||||||
|
stmt.setDouble(4, auction.getStartPrice());
|
||||||
|
stmt.setDouble(5, 0);
|
||||||
|
if (auction.getBuyoutPrice() != null) {
|
||||||
|
stmt.setDouble(6, auction.getBuyoutPrice());
|
||||||
|
} else {
|
||||||
|
stmt.setNull(6, Types.DOUBLE);
|
||||||
|
}
|
||||||
|
stmt.setString(7, auction.getStatus().name());
|
||||||
|
stmt.setLong(8, auction.getCreatedAt().toEpochMilli());
|
||||||
|
stmt.setLong(9, auction.getEndsAt().toEpochMilli());
|
||||||
|
|
||||||
|
stmt.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to create auction", e);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all active auctions.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Auction>> getActiveAuctions() {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<Auction> auctions = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM auctions WHERE status = 'ACTIVE' ORDER BY ends_at ASC";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||||
|
ResultSet rs = stmt.executeQuery()) {
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
Auction auction = mapAuction(rs);
|
||||||
|
if (auction != null) {
|
||||||
|
auctions.add(auction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get active auctions", e);
|
||||||
|
}
|
||||||
|
return auctions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an auction by ID.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Optional<Auction>> getAuction(int id) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "SELECT * FROM auctions WHERE id = ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setInt(1, id);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return Optional.ofNullable(mapAuction(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get auction", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all auctions for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Auction>> getPlayerAuctions(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<Auction> auctions = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM auctions WHERE seller_uuid = ? AND status = 'ACTIVE' ORDER BY ends_at ASC";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
Auction auction = mapAuction(rs);
|
||||||
|
if (auction != null) {
|
||||||
|
auctions.add(auction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get player auctions", e);
|
||||||
|
}
|
||||||
|
return auctions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts active auctions for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> countPlayerAuctions(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "SELECT COUNT(*) FROM auctions WHERE seller_uuid = ? AND status = 'ACTIVE'";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to count player auctions", e);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places a bid on an auction.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> placeBid(int auctionId, UUID bidderUuid, String bidderName, double amount) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String updateSql = """
|
||||||
|
UPDATE auctions SET current_bid = ?, highest_bidder_uuid = ?, highest_bidder_name = ?,
|
||||||
|
bid_count = bid_count + 1
|
||||||
|
WHERE id = ? AND status = 'ACTIVE' AND current_bid < ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
String insertBidSql = """
|
||||||
|
INSERT INTO bids (auction_id, bidder_uuid, bidder_name, amount, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection()) {
|
||||||
|
conn.setAutoCommit(false);
|
||||||
|
|
||||||
|
try (PreparedStatement updateStmt = conn.prepareStatement(updateSql);
|
||||||
|
PreparedStatement insertStmt = conn.prepareStatement(insertBidSql)) {
|
||||||
|
|
||||||
|
updateStmt.setDouble(1, amount);
|
||||||
|
updateStmt.setString(2, bidderUuid.toString());
|
||||||
|
updateStmt.setString(3, bidderName);
|
||||||
|
updateStmt.setInt(4, auctionId);
|
||||||
|
updateStmt.setDouble(5, amount);
|
||||||
|
|
||||||
|
int updated = updateStmt.executeUpdate();
|
||||||
|
|
||||||
|
if (updated > 0) {
|
||||||
|
// Insert bid history
|
||||||
|
insertStmt.setInt(1, auctionId);
|
||||||
|
insertStmt.setString(2, bidderUuid.toString());
|
||||||
|
insertStmt.setString(3, bidderName);
|
||||||
|
insertStmt.setDouble(4, amount);
|
||||||
|
insertStmt.setLong(5, Instant.now().toEpochMilli());
|
||||||
|
insertStmt.executeUpdate();
|
||||||
|
|
||||||
|
conn.commit();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
conn.rollback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
conn.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to place bid", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an auction's status.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> updateAuctionStatus(int auctionId, Auction.AuctionStatus status) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "UPDATE auctions SET status = ? WHERE id = ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, status.name());
|
||||||
|
stmt.setInt(2, auctionId);
|
||||||
|
|
||||||
|
return stmt.executeUpdate() > 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to update auction status", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets auctions that have ended but are still active.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Auction>> getEndedAuctions() {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<Auction> auctions = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM auctions WHERE status = 'ACTIVE' AND ends_at < ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setLong(1, Instant.now().toEpochMilli());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
Auction auction = mapAuction(rs);
|
||||||
|
if (auction != null) {
|
||||||
|
auctions.add(auction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get ended auctions", e);
|
||||||
|
}
|
||||||
|
return auctions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Auction mapAuction(ResultSet rs) throws SQLException {
|
||||||
|
try {
|
||||||
|
Auction auction = new Auction();
|
||||||
|
auction.setId(rs.getInt("id"));
|
||||||
|
auction.setSellerUuid(UUID.fromString(rs.getString("seller_uuid")));
|
||||||
|
auction.setSellerName(rs.getString("seller_name"));
|
||||||
|
auction.setItem(ItemSerializer.deserialize(rs.getString("item_data")));
|
||||||
|
auction.setStartPrice(rs.getDouble("start_price"));
|
||||||
|
auction.setCurrentBid(rs.getDouble("current_bid"));
|
||||||
|
|
||||||
|
String highestBidder = rs.getString("highest_bidder_uuid");
|
||||||
|
if (highestBidder != null) {
|
||||||
|
auction.setHighestBidderUuid(UUID.fromString(highestBidder));
|
||||||
|
auction.setHighestBidderName(rs.getString("highest_bidder_name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
auction.setBidCount(rs.getInt("bid_count"));
|
||||||
|
|
||||||
|
double buyout = rs.getDouble("buyout_price");
|
||||||
|
if (!rs.wasNull()) {
|
||||||
|
auction.setBuyoutPrice(buyout);
|
||||||
|
}
|
||||||
|
|
||||||
|
auction.setStatus(Auction.AuctionStatus.valueOf(rs.getString("status")));
|
||||||
|
auction.setCreatedAt(Instant.ofEpochMilli(rs.getLong("created_at")));
|
||||||
|
auction.setEndsAt(Instant.ofEpochMilli(rs.getLong("ends_at")));
|
||||||
|
auction.setExtensionCount(rs.getInt("extension_count"));
|
||||||
|
|
||||||
|
return auction;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to map auction", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CLAIM STORAGE OPERATIONS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to claim storage.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> addClaimItem(ClaimItem claimItem) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO claim_storage (player_uuid, item_data, reason, source_info, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
stmt.setString(1, claimItem.getPlayerUuid().toString());
|
||||||
|
stmt.setString(2, ItemSerializer.serialize(claimItem.getItem()));
|
||||||
|
stmt.setString(3, claimItem.getReason().name());
|
||||||
|
stmt.setString(4, claimItem.getSourceInfo());
|
||||||
|
stmt.setLong(5, claimItem.getCreatedAt().toEpochMilli());
|
||||||
|
|
||||||
|
stmt.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to add claim item", e);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all claim items for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<ClaimItem>> getPlayerClaimItems(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<ClaimItem> items = new ArrayList<>();
|
||||||
|
String sql = "SELECT * FROM claim_storage WHERE player_uuid = ? ORDER BY created_at DESC";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
ClaimItem item = mapClaimItem(rs);
|
||||||
|
if (item != null) {
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get claim items", e);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts claim items for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> countPlayerClaimItems(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "SELECT COUNT(*) FROM claim_storage WHERE player_uuid = ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to count claim items", e);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a claim item.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> removeClaimItem(int id) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "DELETE FROM claim_storage WHERE id = ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setInt(1, id);
|
||||||
|
return stmt.executeUpdate() > 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to remove claim item", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClaimItem mapClaimItem(ResultSet rs) throws SQLException {
|
||||||
|
try {
|
||||||
|
ClaimItem item = new ClaimItem();
|
||||||
|
item.setId(rs.getInt("id"));
|
||||||
|
item.setPlayerUuid(UUID.fromString(rs.getString("player_uuid")));
|
||||||
|
item.setItem(ItemSerializer.deserialize(rs.getString("item_data")));
|
||||||
|
item.setReason(ClaimItem.ClaimReason.valueOf(rs.getString("reason")));
|
||||||
|
item.setSourceInfo(rs.getString("source_info"));
|
||||||
|
item.setCreatedAt(Instant.ofEpochMilli(rs.getLong("created_at")));
|
||||||
|
return item;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to map claim item", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== EARNINGS OPERATIONS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds pending earnings.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> addPendingEarnings(PendingEarnings earnings) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO pending_earnings (player_uuid, amount, source, created_at, withdrawn)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
stmt.setString(1, earnings.getPlayerUuid().toString());
|
||||||
|
stmt.setDouble(2, earnings.getAmount());
|
||||||
|
stmt.setString(3, earnings.getSource());
|
||||||
|
stmt.setLong(4, earnings.getCreatedAt().toEpochMilli());
|
||||||
|
stmt.setBoolean(5, false);
|
||||||
|
|
||||||
|
stmt.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to add pending earnings", e);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets total pending earnings for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Double> getPlayerPendingEarnings(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "SELECT SUM(amount) FROM pending_earnings WHERE player_uuid = ? AND withdrawn = FALSE";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getDouble(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get pending earnings", e);
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks all earnings as withdrawn for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> withdrawAllEarnings(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "UPDATE pending_earnings SET withdrawn = TRUE WHERE player_uuid = ? AND withdrawn = FALSE";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
return stmt.executeUpdate() > 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to withdraw earnings", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PLAYER DATA OPERATIONS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the last listing time for a player.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Optional<Instant>> getLastListingTime(UUID playerUuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
String sql = "SELECT last_listing_time FROM player_data WHERE player_uuid = ?";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long time = rs.getLong("last_listing_time");
|
||||||
|
if (!rs.wasNull() && time > 0) {
|
||||||
|
return Optional.of(Instant.ofEpochMilli(time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get last listing time", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the last listing time for a player.
|
||||||
|
*/
|
||||||
|
public void updateLastListingTime(UUID playerUuid) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
String sql = isMySQL
|
||||||
|
? "INSERT INTO player_data (player_uuid, last_listing_time) VALUES (?, ?) ON DUPLICATE KEY UPDATE last_listing_time = ?"
|
||||||
|
: "INSERT OR REPLACE INTO player_data (player_uuid, last_listing_time) VALUES (?, ?)";
|
||||||
|
|
||||||
|
try (Connection conn = getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
long now = Instant.now().toEpochMilli();
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
stmt.setLong(2, now);
|
||||||
|
if (isMySQL) {
|
||||||
|
stmt.setLong(3, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to update last listing time", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package pt.henrique.communityMarket.economy;
|
||||||
|
|
||||||
|
import net.milkbowl.vault.economy.Economy;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages economy operations with support for Vault and EssentialsX fallback.
|
||||||
|
* <p>
|
||||||
|
* Priority: Vault > EssentialsX
|
||||||
|
* If neither is available, the plugin will disable itself.
|
||||||
|
*/
|
||||||
|
public class EconomyManager {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private Economy vaultEconomy;
|
||||||
|
private com.earth2me.essentials.Essentials essentials;
|
||||||
|
private EconomyProvider provider = EconomyProvider.NONE;
|
||||||
|
|
||||||
|
public enum EconomyProvider {
|
||||||
|
VAULT,
|
||||||
|
ESSENTIALS,
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyManager(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to set up an economy provider.
|
||||||
|
* Tries Vault first, then EssentialsX.
|
||||||
|
*
|
||||||
|
* @return true if an economy provider was found
|
||||||
|
*/
|
||||||
|
public boolean setupEconomy() {
|
||||||
|
// Try Vault first
|
||||||
|
if (setupVault()) {
|
||||||
|
provider = EconomyProvider.VAULT;
|
||||||
|
plugin.getLogger().info("Using Vault as economy provider.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to EssentialsX
|
||||||
|
if (setupEssentials()) {
|
||||||
|
provider = EconomyProvider.ESSENTIALS;
|
||||||
|
plugin.getLogger().info("Using EssentialsX as economy provider.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().severe("No economy provider found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to hook into Vault economy
|
||||||
|
*/
|
||||||
|
private boolean setupVault() {
|
||||||
|
if (Bukkit.getPluginManager().getPlugin("Vault") == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
RegisteredServiceProvider<Economy> rsp = Bukkit.getServicesManager().getRegistration(Economy.class);
|
||||||
|
if (rsp == null) {
|
||||||
|
plugin.getLogger().warning("Vault found but no economy provider registered.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vaultEconomy = rsp.getProvider();
|
||||||
|
return vaultEconomy != null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to hook into Vault", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to hook into EssentialsX economy
|
||||||
|
*/
|
||||||
|
private boolean setupEssentials() {
|
||||||
|
if (Bukkit.getPluginManager().getPlugin("Essentials") == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
essentials = (com.earth2me.essentials.Essentials) Bukkit.getPluginManager().getPlugin("Essentials");
|
||||||
|
return essentials != null && essentials.isEnabled();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to hook into EssentialsX", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the active economy provider
|
||||||
|
*/
|
||||||
|
public String getProviderName() {
|
||||||
|
return switch (provider) {
|
||||||
|
case VAULT -> "Vault (" + (vaultEconomy != null ? vaultEconomy.getName() : "Unknown") + ")";
|
||||||
|
case ESSENTIALS -> "EssentialsX";
|
||||||
|
case NONE -> "None";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a player's current balance
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @return The player's balance
|
||||||
|
*/
|
||||||
|
public double getBalance(UUID playerUuid) {
|
||||||
|
OfflinePlayer player = Bukkit.getOfflinePlayer(playerUuid);
|
||||||
|
|
||||||
|
return switch (provider) {
|
||||||
|
case VAULT -> vaultEconomy.getBalance(player);
|
||||||
|
case ESSENTIALS -> {
|
||||||
|
try {
|
||||||
|
yield essentials.getUser(playerUuid).getMoney().doubleValue();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to get balance from EssentialsX", e);
|
||||||
|
yield 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case NONE -> 0.0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a player has at least the specified amount
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param amount The amount to check
|
||||||
|
* @return true if the player has enough money
|
||||||
|
*/
|
||||||
|
public boolean has(UUID playerUuid, double amount) {
|
||||||
|
return getBalance(playerUuid) >= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraws money from a player's account
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param amount The amount to withdraw
|
||||||
|
* @return true if successful
|
||||||
|
*/
|
||||||
|
public boolean withdraw(UUID playerUuid, double amount) {
|
||||||
|
if (amount <= 0) return true;
|
||||||
|
|
||||||
|
OfflinePlayer player = Bukkit.getOfflinePlayer(playerUuid);
|
||||||
|
|
||||||
|
return switch (provider) {
|
||||||
|
case VAULT -> {
|
||||||
|
if (!vaultEconomy.has(player, amount)) {
|
||||||
|
yield false;
|
||||||
|
}
|
||||||
|
yield vaultEconomy.withdrawPlayer(player, amount).transactionSuccess();
|
||||||
|
}
|
||||||
|
case ESSENTIALS -> {
|
||||||
|
try {
|
||||||
|
var user = essentials.getUser(playerUuid);
|
||||||
|
if (user.getMoney().doubleValue() < amount) {
|
||||||
|
yield false;
|
||||||
|
}
|
||||||
|
user.setMoney(user.getMoney().subtract(java.math.BigDecimal.valueOf(amount)));
|
||||||
|
yield true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to withdraw from EssentialsX", e);
|
||||||
|
yield false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case NONE -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposits money into a player's account
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param amount The amount to deposit
|
||||||
|
* @return true if successful
|
||||||
|
*/
|
||||||
|
public boolean deposit(UUID playerUuid, double amount) {
|
||||||
|
if (amount <= 0) return true;
|
||||||
|
|
||||||
|
OfflinePlayer player = Bukkit.getOfflinePlayer(playerUuid);
|
||||||
|
|
||||||
|
return switch (provider) {
|
||||||
|
case VAULT -> vaultEconomy.depositPlayer(player, amount).transactionSuccess();
|
||||||
|
case ESSENTIALS -> {
|
||||||
|
try {
|
||||||
|
var user = essentials.getUser(playerUuid);
|
||||||
|
user.setMoney(user.getMoney().add(java.math.BigDecimal.valueOf(amount)));
|
||||||
|
yield true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to deposit to EssentialsX", e);
|
||||||
|
yield false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case NONE -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transfers money between two players
|
||||||
|
*
|
||||||
|
* @param fromUuid The UUID of the payer
|
||||||
|
* @param toUuid The UUID of the receiver
|
||||||
|
* @param amount The amount to transfer
|
||||||
|
* @return true if successful
|
||||||
|
*/
|
||||||
|
public boolean transfer(UUID fromUuid, UUID toUuid, double amount) {
|
||||||
|
if (amount <= 0) return true;
|
||||||
|
|
||||||
|
// Withdraw first
|
||||||
|
if (!withdraw(fromUuid, amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then deposit - if this fails, refund the withdrawal
|
||||||
|
if (!deposit(toUuid, amount)) {
|
||||||
|
deposit(fromUuid, amount); // Attempt refund
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an amount according to the economy's formatting
|
||||||
|
*
|
||||||
|
* @param amount The amount to format
|
||||||
|
* @return Formatted currency string
|
||||||
|
*/
|
||||||
|
public String format(double amount) {
|
||||||
|
if (provider == EconomyProvider.VAULT && vaultEconomy != null) {
|
||||||
|
return vaultEconomy.format(amount);
|
||||||
|
}
|
||||||
|
return plugin.getMessageManager().formatCurrency(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the economy provider type
|
||||||
|
*/
|
||||||
|
public EconomyProvider getProvider() {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin panel GUI for moderating the marketplace.
|
||||||
|
* Provides access to view all listings/auctions, remove items, and reload config.
|
||||||
|
*/
|
||||||
|
public class AdminGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
|
||||||
|
private static final int VIEW_LISTINGS_SLOT = 20;
|
||||||
|
private static final int VIEW_AUCTIONS_SLOT = 24;
|
||||||
|
private static final int RELOAD_SLOT = 40;
|
||||||
|
private static final int BACK_SLOT = 49;
|
||||||
|
|
||||||
|
public AdminGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
if (!player.hasPermission("communitymarket.admin")) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.admin-panel"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Fill with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.RED_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin panel header
|
||||||
|
inventory.setItem(4, new ItemBuilder(Material.COMMAND_BLOCK)
|
||||||
|
.name("&c&lAdmin Panel")
|
||||||
|
.lore(
|
||||||
|
"&7Manage the marketplace.",
|
||||||
|
"&7Remove listings, cancel auctions,",
|
||||||
|
"&7and reload configuration."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// View all listings
|
||||||
|
inventory.setItem(VIEW_LISTINGS_SLOT, new ItemBuilder(Material.CHEST)
|
||||||
|
.name(msgManager.getButton("admin-view-listings"))
|
||||||
|
.lore(
|
||||||
|
"&7View all active listings",
|
||||||
|
"&7from all players.",
|
||||||
|
"",
|
||||||
|
"&cClick on items to remove them."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// View all auctions
|
||||||
|
inventory.setItem(VIEW_AUCTIONS_SLOT, new ItemBuilder(Material.GOLD_BLOCK)
|
||||||
|
.name(msgManager.getButton("admin-view-auctions"))
|
||||||
|
.lore(
|
||||||
|
"&7View all active auctions",
|
||||||
|
"&7from all players.",
|
||||||
|
"",
|
||||||
|
"&cClick on items to force-end them."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Reload config
|
||||||
|
inventory.setItem(RELOAD_SLOT, new ItemBuilder(Material.REPEATING_COMMAND_BLOCK)
|
||||||
|
.name(msgManager.getButton("admin-reload"))
|
||||||
|
.lore(
|
||||||
|
"&7Reload plugin configuration",
|
||||||
|
"&7and language files."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
switch (slot) {
|
||||||
|
case VIEW_LISTINGS_SLOT -> openAdminListings(player);
|
||||||
|
case VIEW_AUCTIONS_SLOT -> openAdminAuctions(player);
|
||||||
|
case RELOAD_SLOT -> {
|
||||||
|
plugin.reload();
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.admin-reload"));
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
}
|
||||||
|
case BACK_SLOT -> guiManager.openMainMenu(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAdminListings(Player player) {
|
||||||
|
// Opens browse market but with admin remove capability
|
||||||
|
new AdminListingsGui(plugin, guiManager).open(player, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAdminAuctions(Player player) {
|
||||||
|
new AdminAuctionsGui(plugin, guiManager).open(player, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Inner Admin GUIs ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin view of all listings with remove capability
|
||||||
|
*/
|
||||||
|
private static class AdminListingsGui implements MarketGui {
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private int page;
|
||||||
|
private java.util.List<pt.henrique.communityMarket.model.Listing> listings;
|
||||||
|
|
||||||
|
public AdminListingsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player, int page) {
|
||||||
|
this.player = player;
|
||||||
|
this.page = page;
|
||||||
|
|
||||||
|
plugin.getListingService().getActiveListings().thenAccept(loaded -> {
|
||||||
|
this.listings = loaded;
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.admin-listings"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
ItemStack filler = new ItemBuilder(Material.RED_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
int start = page * 45;
|
||||||
|
int end = Math.min(start + 45, listings.size());
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
var listing = listings.get(i);
|
||||||
|
ItemStack display = listing.getItem().clone();
|
||||||
|
inventory.setItem(i - start, new ItemBuilder(display)
|
||||||
|
.addLore(java.util.List.of(
|
||||||
|
"",
|
||||||
|
"&7Seller: &f" + listing.getSellerName(),
|
||||||
|
"&7Price: &a" + msgManager.formatCurrency(listing.getPrice()),
|
||||||
|
"&7ID: &f#" + listing.getId(),
|
||||||
|
"",
|
||||||
|
"&cClick to remove"
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory.setItem(49, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == 49) {
|
||||||
|
new AdminGui(plugin, guiManager).open(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot >= 0 && slot < 45 && slot + page * 45 < listings.size()) {
|
||||||
|
var listing = listings.get(slot + page * 45);
|
||||||
|
plugin.getListingService().cancelListing(listing.getId(), player.getUniqueId(), true)
|
||||||
|
.thenAccept(success -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (success) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||||
|
"messages.admin-listing-removed", "id", String.valueOf(listing.getId())));
|
||||||
|
}
|
||||||
|
open(player, page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.ADMIN_LISTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin view of all auctions with cancel capability
|
||||||
|
*/
|
||||||
|
private static class AdminAuctionsGui implements MarketGui {
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private int page;
|
||||||
|
private java.util.List<pt.henrique.communityMarket.model.Auction> auctions;
|
||||||
|
|
||||||
|
public AdminAuctionsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player, int page) {
|
||||||
|
this.player = player;
|
||||||
|
this.page = page;
|
||||||
|
|
||||||
|
plugin.getAuctionService().getActiveAuctions().thenAccept(loaded -> {
|
||||||
|
this.auctions = loaded;
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.admin-auctions"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
ItemStack filler = new ItemBuilder(Material.RED_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
int start = page * 45;
|
||||||
|
int end = Math.min(start + 45, auctions.size());
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
var auction = auctions.get(i);
|
||||||
|
ItemStack display = auction.getItem().clone();
|
||||||
|
String bidder = auction.getHighestBidderName() != null ? auction.getHighestBidderName() : "None";
|
||||||
|
inventory.setItem(i - start, new ItemBuilder(display)
|
||||||
|
.addLore(java.util.List.of(
|
||||||
|
"",
|
||||||
|
"&7Seller: &f" + auction.getSellerName(),
|
||||||
|
"&7Current Bid: &a" + msgManager.formatCurrency(auction.getCurrentBid()),
|
||||||
|
"&7Bidder: &f" + bidder,
|
||||||
|
"&7Bids: &f" + auction.getBidCount(),
|
||||||
|
"&7ID: &f#" + auction.getId(),
|
||||||
|
"",
|
||||||
|
"&cClick to force-end"
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory.setItem(49, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == 49) {
|
||||||
|
new AdminGui(plugin, guiManager).open(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot >= 0 && slot < 45 && slot + page * 45 < auctions.size()) {
|
||||||
|
var auction = auctions.get(slot + page * 45);
|
||||||
|
plugin.getAuctionService().cancelAuction(auction.getId(), player.getUniqueId(), true)
|
||||||
|
.thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||||
|
"messages.admin-auction-cancelled", "id", String.valueOf(auction.getId())));
|
||||||
|
open(player, page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.ADMIN_AUCTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.ClickType;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.Auction;
|
||||||
|
import pt.henrique.communityMarket.service.AuctionService;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for browsing active auctions.
|
||||||
|
* Left-click to bid, right-click to buyout (if available).
|
||||||
|
*/
|
||||||
|
public class BrowseAuctionsGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private int currentPage;
|
||||||
|
private List<Auction> auctions;
|
||||||
|
|
||||||
|
// Layout constants
|
||||||
|
private static final int ITEMS_PER_PAGE = 45;
|
||||||
|
private static final int PREV_PAGE_SLOT = 45;
|
||||||
|
private static final int INFO_SLOT = 49;
|
||||||
|
private static final int NEXT_PAGE_SLOT = 53;
|
||||||
|
private static final int BACK_SLOT = 48;
|
||||||
|
|
||||||
|
public BrowseAuctionsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player, int page) {
|
||||||
|
this.player = player;
|
||||||
|
this.currentPage = page;
|
||||||
|
|
||||||
|
// Load auctions asynchronously
|
||||||
|
plugin.getAuctionService().getActiveAuctions().thenAccept(loadedAuctions -> {
|
||||||
|
this.auctions = loadedAuctions;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String title = msgManager.getRaw("gui-titles.browse-auctions")
|
||||||
|
.replace("{page}", String.valueOf(currentPage + 1));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, TextUtil.colorizeToString(title));
|
||||||
|
|
||||||
|
// Fill bottom row
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add auctions
|
||||||
|
int startIndex = currentPage * ITEMS_PER_PAGE;
|
||||||
|
int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, auctions.size());
|
||||||
|
|
||||||
|
for (int i = startIndex; i < endIndex; i++) {
|
||||||
|
Auction auction = auctions.get(i);
|
||||||
|
int slot = i - startIndex;
|
||||||
|
inventory.setItem(slot, createAuctionItem(auction));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
if (currentPage > 0) {
|
||||||
|
inventory.setItem(PREV_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||||
|
.name(msgManager.getButton("previous-page"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalPages = (int) Math.ceil((double) auctions.size() / ITEMS_PER_PAGE);
|
||||||
|
if (currentPage < totalPages - 1) {
|
||||||
|
inventory.setItem(NEXT_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||||
|
.name(msgManager.getButton("next-page"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||||
|
.name("&ePage " + (currentPage + 1) + "/" + Math.max(1, totalPages))
|
||||||
|
.lore("&7Total auctions: &f" + auctions.size())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createAuctionItem(Auction auction) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
ItemStack display = auction.getItem().clone();
|
||||||
|
|
||||||
|
// Time remaining
|
||||||
|
Duration remaining = Duration.between(Instant.now(), auction.getEndsAt());
|
||||||
|
String ends = TextUtil.formatDuration(remaining);
|
||||||
|
|
||||||
|
// Current bidder
|
||||||
|
String bidder = auction.getHighestBidderName() != null ? auction.getHighestBidderName() : "&7None";
|
||||||
|
String currentBid = auction.getBidCount() > 0
|
||||||
|
? msgManager.formatCurrency(auction.getCurrentBid())
|
||||||
|
: msgManager.formatCurrency(auction.getStartPrice());
|
||||||
|
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add("");
|
||||||
|
for (String line : msgManager.getLore("auction-info", Map.of(
|
||||||
|
"seller", auction.getSellerName(),
|
||||||
|
"start_price", msgManager.formatCurrency(auction.getStartPrice()),
|
||||||
|
"current_bid", currentBid,
|
||||||
|
"bidder", bidder,
|
||||||
|
"bid_count", String.valueOf(auction.getBidCount()),
|
||||||
|
"ends", ends
|
||||||
|
))) {
|
||||||
|
lore.add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add buyout info if available
|
||||||
|
if (auction.hasBuyout()) {
|
||||||
|
lore.add("&7Buyout: &a" + msgManager.formatCurrency(auction.getBuyoutPrice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ItemBuilder(display)
|
||||||
|
.addLore(lore)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot == PREV_PAGE_SLOT && currentPage > 0) {
|
||||||
|
open(player, currentPage - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalPages = (int) Math.ceil((double) auctions.size() / ITEMS_PER_PAGE);
|
||||||
|
if (slot == NEXT_PAGE_SLOT && currentPage < totalPages - 1) {
|
||||||
|
open(player, currentPage + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on auction
|
||||||
|
if (slot >= 0 && slot < ITEMS_PER_PAGE) {
|
||||||
|
int auctionIndex = currentPage * ITEMS_PER_PAGE + slot;
|
||||||
|
if (auctionIndex < auctions.size()) {
|
||||||
|
Auction auction = auctions.get(auctionIndex);
|
||||||
|
|
||||||
|
// Right-click for buyout
|
||||||
|
if (event.getClick() == ClickType.RIGHT && auction.hasBuyout()) {
|
||||||
|
handleBuyout(player, auction);
|
||||||
|
} else {
|
||||||
|
// Left-click for bid
|
||||||
|
handleBid(player, auction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleBid(Player player, Auction auction) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Can't bid on own auction
|
||||||
|
if (auction.getSellerUuid().equals(player.getUniqueId())) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-own-item"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate minimum bid
|
||||||
|
double minBid = plugin.getAuctionService().calculateMinBid(auction);
|
||||||
|
|
||||||
|
// Open number input for bid amount
|
||||||
|
guiManager.openNumberInput(player, bidAmount -> {
|
||||||
|
if (bidAmount <= 0) {
|
||||||
|
guiManager.openBrowseAuctions(player, currentPage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place bid
|
||||||
|
plugin.getAuctionService().placeBid(auction.getId(), player, bidAmount)
|
||||||
|
.thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
switch (result) {
|
||||||
|
case SUCCESS -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-bid-placed", Map.of(
|
||||||
|
"amount", msgManager.formatCurrency(bidAmount),
|
||||||
|
"item", auction.getItem().getType().name()
|
||||||
|
)));
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
guiManager.openBrowseAuctions(player, currentPage);
|
||||||
|
}
|
||||||
|
case BID_TOO_LOW -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-bid-too-low",
|
||||||
|
"min", msgManager.formatCurrency(minBid)));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
case INSUFFICIENT_FUNDS -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-insufficient-funds",
|
||||||
|
"price", msgManager.formatCurrency(bidAmount)));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
guiManager.openBrowseAuctions(player, currentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, minBid, minBid, plugin.getConfigManager().getMaxPrice(),
|
||||||
|
msgManager.getRaw("gui-titles.number-input"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleBuyout(Player player, Auction auction) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
if (auction.getSellerUuid().equals(player.getUniqueId())) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-own-item"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] info = {
|
||||||
|
"&7Item: &f" + auction.getItem().getType().name(),
|
||||||
|
"&7Buyout Price: &a" + msgManager.formatCurrency(auction.getBuyoutPrice()),
|
||||||
|
"",
|
||||||
|
"&eClick to confirm buyout!"
|
||||||
|
};
|
||||||
|
|
||||||
|
guiManager.openConfirmation(player, confirmed -> {
|
||||||
|
if (confirmed) {
|
||||||
|
plugin.getAuctionService().buyout(auction.getId(), player)
|
||||||
|
.thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (result == AuctionService.BidResult.BUYOUT_SUCCESS) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-buyout", Map.of(
|
||||||
|
"item", auction.getItem().getType().name(),
|
||||||
|
"price", msgManager.formatCurrency(auction.getBuyoutPrice())
|
||||||
|
)));
|
||||||
|
playSound(player, plugin.getConfigManager().getPurchaseSound());
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
guiManager.openBrowseAuctions(player, currentPage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
guiManager.openBrowseAuctions(player, currentPage);
|
||||||
|
}
|
||||||
|
}, msgManager.getRaw("gui-titles.confirm-purchase"), info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.BROWSE_AUCTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.Listing;
|
||||||
|
import pt.henrique.communityMarket.service.ListingService;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for browsing the market's fixed-price listings.
|
||||||
|
* Features pagination, and click-to-buy functionality.
|
||||||
|
*/
|
||||||
|
public class BrowseMarketGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private int currentPage;
|
||||||
|
private List<Listing> listings;
|
||||||
|
|
||||||
|
// Layout constants
|
||||||
|
private static final int ITEMS_PER_PAGE = 45;
|
||||||
|
private static final int PREV_PAGE_SLOT = 45;
|
||||||
|
private static final int INFO_SLOT = 49;
|
||||||
|
private static final int NEXT_PAGE_SLOT = 53;
|
||||||
|
private static final int BACK_SLOT = 48;
|
||||||
|
private static final int FILTER_SLOT = 47;
|
||||||
|
private static final int SORT_SLOT = 51;
|
||||||
|
|
||||||
|
public BrowseMarketGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the browse market GUI for a player
|
||||||
|
*/
|
||||||
|
public void open(Player player, int page) {
|
||||||
|
this.player = player;
|
||||||
|
this.currentPage = page;
|
||||||
|
|
||||||
|
// Load listings asynchronously
|
||||||
|
plugin.getListingService().getActiveListings().thenAccept(loadedListings -> {
|
||||||
|
this.listings = loadedListings;
|
||||||
|
|
||||||
|
// Build GUI on main thread
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String title = msgManager.getRaw("gui-titles.browse-market")
|
||||||
|
.replace("{page}", String.valueOf(currentPage + 1));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, TextUtil.colorizeToString(title));
|
||||||
|
|
||||||
|
// Fill bottom row with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add listings to slots 0-44
|
||||||
|
int startIndex = currentPage * ITEMS_PER_PAGE;
|
||||||
|
int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, listings.size());
|
||||||
|
|
||||||
|
for (int i = startIndex; i < endIndex; i++) {
|
||||||
|
Listing listing = listings.get(i);
|
||||||
|
int slot = i - startIndex;
|
||||||
|
inventory.setItem(slot, createListingItem(listing));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation buttons
|
||||||
|
if (currentPage > 0) {
|
||||||
|
inventory.setItem(PREV_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||||
|
.name(msgManager.getButton("previous-page"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalPages = (int) Math.ceil((double) listings.size() / ITEMS_PER_PAGE);
|
||||||
|
if (currentPage < totalPages - 1) {
|
||||||
|
inventory.setItem(NEXT_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||||
|
.name(msgManager.getButton("next-page"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info/page indicator
|
||||||
|
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||||
|
.name("&ePage " + (currentPage + 1) + "/" + Math.max(1, totalPages))
|
||||||
|
.lore("&7Total listings: &f" + listings.size())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createListingItem(Listing listing) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
ItemStack display = listing.getItem().clone();
|
||||||
|
display.setAmount(listing.getAmount());
|
||||||
|
|
||||||
|
// Calculate time remaining
|
||||||
|
String expires;
|
||||||
|
if (listing.getExpiresAt() != null) {
|
||||||
|
Duration remaining = Duration.between(Instant.now(), listing.getExpiresAt());
|
||||||
|
expires = TextUtil.formatDuration(remaining);
|
||||||
|
} else {
|
||||||
|
expires = "Never";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add(""); // Empty line separator
|
||||||
|
for (String line : msgManager.getLore("listing-info", Map.of(
|
||||||
|
"seller", listing.getSellerName(),
|
||||||
|
"price", msgManager.formatCurrency(listing.getPrice()),
|
||||||
|
"amount", String.valueOf(listing.getAmount()),
|
||||||
|
"expires", expires
|
||||||
|
))) {
|
||||||
|
lore.add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ItemBuilder(display)
|
||||||
|
.addLore(lore)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
// Bottom row navigation
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot == PREV_PAGE_SLOT && currentPage > 0) {
|
||||||
|
open(player, currentPage - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalPages = (int) Math.ceil((double) listings.size() / ITEMS_PER_PAGE);
|
||||||
|
if (slot == NEXT_PAGE_SLOT && currentPage < totalPages - 1) {
|
||||||
|
open(player, currentPage + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on a listing (slots 0-44)
|
||||||
|
if (slot >= 0 && slot < ITEMS_PER_PAGE) {
|
||||||
|
int listingIndex = currentPage * ITEMS_PER_PAGE + slot;
|
||||||
|
if (listingIndex < listings.size()) {
|
||||||
|
Listing listing = listings.get(listingIndex);
|
||||||
|
handleListingClick(player, listing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleListingClick(Player player, Listing listing) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Can't buy own listing
|
||||||
|
if (listing.getSellerUuid().equals(player.getUniqueId())) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-own-item"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show confirmation
|
||||||
|
double tax = plugin.getTransactionService().calculateListingTax(listing.getPrice());
|
||||||
|
String[] info = {
|
||||||
|
"&7Item: &f" + listing.getItem().getType().name() + " x" + listing.getAmount(),
|
||||||
|
"&7Seller: &f" + listing.getSellerName(),
|
||||||
|
"&7Price: &a" + msgManager.formatCurrency(listing.getPrice()),
|
||||||
|
"",
|
||||||
|
"&eClick to confirm purchase!"
|
||||||
|
};
|
||||||
|
|
||||||
|
guiManager.openConfirmation(player, confirmed -> {
|
||||||
|
if (confirmed) {
|
||||||
|
// Attempt purchase
|
||||||
|
plugin.getListingService().purchaseListing(listing.getId(), player)
|
||||||
|
.thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
switch (result) {
|
||||||
|
case SUCCESS -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-purchased", Map.of(
|
||||||
|
"item", listing.getItem().getType().name(),
|
||||||
|
"amount", String.valueOf(listing.getAmount()),
|
||||||
|
"price", msgManager.formatCurrency(listing.getPrice())
|
||||||
|
)));
|
||||||
|
playSound(player, plugin.getConfigManager().getPurchaseSound());
|
||||||
|
guiManager.openBrowseMarket(player, currentPage);
|
||||||
|
}
|
||||||
|
case INSUFFICIENT_FUNDS -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-insufficient-funds",
|
||||||
|
"price", msgManager.formatCurrency(listing.getPrice())));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
case ALREADY_SOLD, NOT_FOUND -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
guiManager.openBrowseMarket(player, currentPage);
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
guiManager.openBrowseMarket(player, currentPage);
|
||||||
|
}
|
||||||
|
}, msgManager.getRaw("gui-titles.confirm-purchase"), info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.BROWSE_MARKET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.ClaimItem;
|
||||||
|
import pt.henrique.communityMarket.service.ClaimService;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for claiming items from expired listings, won auctions, etc.
|
||||||
|
* Click on an item to claim it, or use "Claim All" button.
|
||||||
|
*/
|
||||||
|
public class ClaimGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private List<ClaimItem> claimItems;
|
||||||
|
|
||||||
|
private static final int BACK_SLOT = 49;
|
||||||
|
private static final int CLAIM_ALL_SLOT = 45;
|
||||||
|
|
||||||
|
public ClaimGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
plugin.getClaimService().getPlayerClaimItems(player.getUniqueId())
|
||||||
|
.thenAccept(items -> {
|
||||||
|
this.claimItems = items;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.claim-items"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
// Fill bottom row
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add claim items
|
||||||
|
for (int i = 0; i < Math.min(claimItems.size(), 45); i++) {
|
||||||
|
ClaimItem item = claimItems.get(i);
|
||||||
|
inventory.setItem(i, createClaimItemDisplay(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim All button
|
||||||
|
if (!claimItems.isEmpty()) {
|
||||||
|
inventory.setItem(CLAIM_ALL_SLOT, new ItemBuilder(Material.HOPPER)
|
||||||
|
.name(msgManager.getButton("claim-all"))
|
||||||
|
.lore("&7Claim all items at once")
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Show empty message if no items
|
||||||
|
if (claimItems.isEmpty()) {
|
||||||
|
inventory.setItem(22, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name("&cNo items to claim")
|
||||||
|
.lore("&7Items from expired listings,",
|
||||||
|
"&7won auctions, etc. appear here.")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createClaimItemDisplay(ClaimItem claimItem) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
ItemStack display = claimItem.getItem().clone();
|
||||||
|
|
||||||
|
String age = TextUtil.formatDuration(
|
||||||
|
Duration.between(claimItem.getCreatedAt(), Instant.now())) + " ago";
|
||||||
|
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add("");
|
||||||
|
lore.addAll(msgManager.getLore("claim-item-info", Map.of(
|
||||||
|
"reason", claimItem.getReason().getDisplayName(),
|
||||||
|
"source", claimItem.getSourceInfo() != null ? claimItem.getSourceInfo() : "Unknown",
|
||||||
|
"date", age
|
||||||
|
)));
|
||||||
|
|
||||||
|
return new ItemBuilder(display)
|
||||||
|
.addLore(lore)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot == CLAIM_ALL_SLOT && !claimItems.isEmpty()) {
|
||||||
|
claimAll(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on item to claim
|
||||||
|
if (slot >= 0 && slot < 45 && slot < claimItems.size()) {
|
||||||
|
ClaimItem item = claimItems.get(slot);
|
||||||
|
claimSingle(player, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void claimSingle(Player player, ClaimItem item) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
plugin.getClaimService().claimItem(item.getId(), player)
|
||||||
|
.thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
switch (result) {
|
||||||
|
case SUCCESS -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.claim-success"));
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
open(player); // Refresh
|
||||||
|
}
|
||||||
|
case INVENTORY_FULL -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.claim-inventory-full"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.claim-empty"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
open(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void claimAll(Player player) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
plugin.getClaimService().claimAll(player)
|
||||||
|
.thenAccept(count -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (count > 0) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.claim-all-success",
|
||||||
|
"count", String.valueOf(count)));
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.claim-empty"));
|
||||||
|
}
|
||||||
|
open(player); // Refresh
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
try {
|
||||||
|
Sound sound = Sound.valueOf(soundName);
|
||||||
|
player.playSound(player.getLocation(), sound, 0.5f, 1.0f);
|
||||||
|
} catch (IllegalArgumentException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.CLAIM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirmation dialog GUI for important actions like purchases and cancellations.
|
||||||
|
*/
|
||||||
|
public class ConfirmationGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private final ConfirmCallback callback;
|
||||||
|
private final String title;
|
||||||
|
private final String[] infoLines;
|
||||||
|
|
||||||
|
private Inventory inventory;
|
||||||
|
|
||||||
|
private static final int INFO_SLOT = 13;
|
||||||
|
private static final int CONFIRM_SLOT = 29;
|
||||||
|
private static final int CANCEL_SLOT = 33;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ConfirmCallback {
|
||||||
|
void onComplete(boolean confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfirmationGui(CommunityMarket plugin, GuiManager guiManager,
|
||||||
|
ConfirmCallback callback, String title, String... infoLines) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
this.callback = callback;
|
||||||
|
this.title = title;
|
||||||
|
this.infoLines = infoLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
inventory = Bukkit.createInventory(this, 45, TextUtil.colorizeToString(title));
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Fill with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 45; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info display
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
for (String line : infoLines) {
|
||||||
|
lore.add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||||
|
.name("&e&lConfirm Action")
|
||||||
|
.lore(lore)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Confirm button
|
||||||
|
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||||
|
.name(msgManager.getButton("confirm"))
|
||||||
|
.lore("&aClick to confirm")
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Cancel button
|
||||||
|
inventory.setItem(CANCEL_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||||
|
.name(msgManager.getButton("cancel"))
|
||||||
|
.lore("&cClick to cancel")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == CONFIRM_SLOT) {
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
player.closeInventory();
|
||||||
|
callback.onComplete(true);
|
||||||
|
} else if (slot == CANCEL_SLOT) {
|
||||||
|
player.closeInventory();
|
||||||
|
callback.onComplete(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.CONFIRMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||||
|
import pt.henrique.communityMarket.util.SoundUtil;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for setting up a new auction.
|
||||||
|
* This GUI is opened AFTER the player has selected an item and quantity.
|
||||||
|
* All elements are merged: start price, buyout, and duration are single clickable items.
|
||||||
|
*
|
||||||
|
* Layout (54-slot chest):
|
||||||
|
* ┌─────────────────────────────────────────────────────┐
|
||||||
|
* │ . . . . INFO . . . . │ Row 0 │
|
||||||
|
* │ . . . . ITEM . . . . │ Row 1: Item │
|
||||||
|
* │ . . . . . . . . . │ Row 2 │
|
||||||
|
* │ . START . BUYOUT . DURATION . │ Row 3: Setup │
|
||||||
|
* │ . . . . . . . . . │ Row 4 │
|
||||||
|
* │ BACK . . . CONFIRM . . . .│ Row 5: Action│
|
||||||
|
* └─────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
public class CreateAuctionGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
|
||||||
|
// Selected item info (from ItemSelectionGui -> QuantitySelectGui)
|
||||||
|
private int sourceInventorySlot = -1;
|
||||||
|
private ItemStack selectedItem = null;
|
||||||
|
|
||||||
|
// Auction settings
|
||||||
|
private double startPrice;
|
||||||
|
private Double buyoutPrice = null;
|
||||||
|
private int durationHours;
|
||||||
|
|
||||||
|
// ==================== LAYOUT CONSTANTS ====================
|
||||||
|
private static final int INFO_SLOT = 4; // Top center
|
||||||
|
private static final int ITEM_DISPLAY_SLOT = 13; // Center
|
||||||
|
|
||||||
|
// Row 3: Merged elements (single slot each)
|
||||||
|
private static final int START_PRICE_SLOT = 28; // Start price (display + click)
|
||||||
|
private static final int BUYOUT_SLOT = 31; // Buyout (display + click)
|
||||||
|
private static final int DURATION_SLOT = 34; // Duration (display + click)
|
||||||
|
|
||||||
|
private static final int BACK_SLOT = 45; // Bottom-left
|
||||||
|
private static final int CONFIRM_SLOT = 49; // Bottom-center
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
public CreateAuctionGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the auction creation GUI with a pre-selected item and quantity.
|
||||||
|
* Called from QuantitySelectGui or ItemSelectionGui (for unstackable items).
|
||||||
|
*
|
||||||
|
* @param player The player
|
||||||
|
* @param inventorySlot The slot in the player's inventory where the item is
|
||||||
|
* @param item A clone of the selected item with the desired quantity
|
||||||
|
*/
|
||||||
|
public void openWithItem(Player player, int inventorySlot, ItemStack item) {
|
||||||
|
this.player = player;
|
||||||
|
this.sourceInventorySlot = inventorySlot;
|
||||||
|
this.selectedItem = item;
|
||||||
|
this.durationHours = plugin.getConfigManager().getDefaultAuctionDurationHours();
|
||||||
|
this.startPrice = plugin.getConfigManager().getMinStartPrice();
|
||||||
|
this.buyoutPrice = null;
|
||||||
|
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-auction"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use openWithItem instead. This opens item selection first.
|
||||||
|
*/
|
||||||
|
public void open(Player player) {
|
||||||
|
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Fill with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info panel
|
||||||
|
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.OAK_SIGN)
|
||||||
|
.name("&6&lCreate Auction")
|
||||||
|
.lore(
|
||||||
|
"&7Set starting price, optional buyout,",
|
||||||
|
"&7and duration for your auction.",
|
||||||
|
"",
|
||||||
|
"&7Tax on sale: &f" + plugin.getConfigManager().getAuctionTax() + "%"
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Selected item display
|
||||||
|
if (selectedItem != null) {
|
||||||
|
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(selectedItem.clone())
|
||||||
|
.addLore(List.of(
|
||||||
|
"",
|
||||||
|
"&7Quantity: &f" + selectedItem.getAmount(),
|
||||||
|
"&eThis item will be auctioned"
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merged start price element
|
||||||
|
updateStartPriceElement();
|
||||||
|
|
||||||
|
// Merged buyout element
|
||||||
|
updateBuyoutElement();
|
||||||
|
|
||||||
|
// Merged duration element
|
||||||
|
updateDurationElement();
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.lore("&7Return to item selection")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Confirm button
|
||||||
|
updateConfirmButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the merged start price element (displays + clickable)
|
||||||
|
*/
|
||||||
|
private void updateStartPriceElement() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
inventory.setItem(START_PRICE_SLOT, new ItemBuilder(Material.GOLD_INGOT)
|
||||||
|
.name("&6Starting Price: " + msgManager.formatCurrency(startPrice))
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Minimum bid to start",
|
||||||
|
"&7the auction.",
|
||||||
|
"",
|
||||||
|
"&eClick to change"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the merged buyout element (displays + clickable)
|
||||||
|
*/
|
||||||
|
private void updateBuyoutElement() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
if (buyoutPrice != null) {
|
||||||
|
inventory.setItem(BUYOUT_SLOT, new ItemBuilder(Material.DIAMOND)
|
||||||
|
.name("&bBuyout: " + msgManager.formatCurrency(buyoutPrice))
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Instant purchase price.",
|
||||||
|
"",
|
||||||
|
"&eLeft-click to change",
|
||||||
|
"&cRight-click to remove"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
inventory.setItem(BUYOUT_SLOT, new ItemBuilder(Material.DIAMOND)
|
||||||
|
.name("&bBuyout: &7Not set")
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Optional instant purchase",
|
||||||
|
"&7price for your auction.",
|
||||||
|
"",
|
||||||
|
"&eClick to set buyout price"
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the merged duration element (displays + clickable)
|
||||||
|
*/
|
||||||
|
private void updateDurationElement() {
|
||||||
|
String durationText = formatDuration(durationHours);
|
||||||
|
|
||||||
|
inventory.setItem(DURATION_SLOT, new ItemBuilder(Material.CLOCK)
|
||||||
|
.name("&eDuration: " + durationText)
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Auction ends after this time.",
|
||||||
|
"",
|
||||||
|
"&eClick to change duration"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDuration(int hours) {
|
||||||
|
if (hours >= 24) {
|
||||||
|
int days = hours / 24;
|
||||||
|
return days + " day" + (days > 1 ? "s" : "");
|
||||||
|
} else {
|
||||||
|
return hours + " hour" + (hours > 1 ? "s" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConfirmButton() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||||
|
.name(msgManager.getButton("confirm"))
|
||||||
|
.lore(
|
||||||
|
"&7Item: &f" + selectedItem.getType().name() + " x" + selectedItem.getAmount(),
|
||||||
|
"&7Start: &a" + msgManager.formatCurrency(startPrice),
|
||||||
|
buyoutPrice != null ? "&7Buyout: &b" + msgManager.formatCurrency(buyoutPrice) : "&7Buyout: &7None",
|
||||||
|
"&7Duration: &e" + formatDuration(durationHours),
|
||||||
|
"",
|
||||||
|
"&aClick to create auction!"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
switch (slot) {
|
||||||
|
case START_PRICE_SLOT -> {
|
||||||
|
guiManager.openNumberInput(player, newPrice -> {
|
||||||
|
if (newPrice > 0) {
|
||||||
|
this.startPrice = newPrice;
|
||||||
|
// Ensure buyout is higher than start
|
||||||
|
if (buyoutPrice != null && buyoutPrice <= startPrice) {
|
||||||
|
buyoutPrice = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reopenGui();
|
||||||
|
}, startPrice, plugin.getConfigManager().getMinStartPrice(),
|
||||||
|
plugin.getConfigManager().getMaxStartPrice(),
|
||||||
|
msgManager.getRaw("gui-titles.number-input"));
|
||||||
|
}
|
||||||
|
case BUYOUT_SLOT -> {
|
||||||
|
if (event.isRightClick() && buyoutPrice != null) {
|
||||||
|
// Remove buyout
|
||||||
|
buyoutPrice = null;
|
||||||
|
updateBuyoutElement();
|
||||||
|
updateConfirmButton();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
} else {
|
||||||
|
// Set buyout
|
||||||
|
double defaultBuyout = buyoutPrice != null ? buyoutPrice : startPrice * 2;
|
||||||
|
guiManager.openNumberInput(player, newPrice -> {
|
||||||
|
if (newPrice > startPrice) {
|
||||||
|
this.buyoutPrice = newPrice;
|
||||||
|
}
|
||||||
|
reopenGui();
|
||||||
|
}, defaultBuyout, startPrice + 1, plugin.getConfigManager().getMaxPrice(),
|
||||||
|
msgManager.getRaw("gui-titles.number-input"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case DURATION_SLOT -> {
|
||||||
|
List<Integer> durations = plugin.getConfigManager().getAvailableAuctionDurations();
|
||||||
|
if (!durations.isEmpty()) {
|
||||||
|
int currentIndex = durations.indexOf(durationHours);
|
||||||
|
int nextIndex = (currentIndex + 1) % durations.size();
|
||||||
|
durationHours = durations.get(nextIndex);
|
||||||
|
updateDurationElement();
|
||||||
|
updateConfirmButton();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case CONFIRM_SLOT -> {
|
||||||
|
confirmAuction(player);
|
||||||
|
}
|
||||||
|
case BACK_SLOT -> {
|
||||||
|
// Go back to item selection
|
||||||
|
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmAuction(Player player) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Verify item still exists in player's inventory with sufficient quantity
|
||||||
|
int available = InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||||
|
|
||||||
|
if (available < selectedItem.getAmount()) {
|
||||||
|
if (available < 1) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.item-no-longer-available"));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.quantity-changed"));
|
||||||
|
}
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate item again
|
||||||
|
var validation = plugin.getTransactionService().validateItem(selectedItem);
|
||||||
|
if (!validation.isValid()) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages." + validation.getErrorKey()));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the auction
|
||||||
|
ItemStack auctionItem = selectedItem.clone();
|
||||||
|
|
||||||
|
plugin.getTransactionService().createAuctionTransaction(
|
||||||
|
player, auctionItem, startPrice, buyoutPrice, durationHours
|
||||||
|
).thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
// Remove items from inventory AFTER successful creation
|
||||||
|
InventoryUtil.removeItems(player, auctionItem, auctionItem.getAmount());
|
||||||
|
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-created",
|
||||||
|
"id", String.valueOf(result.getId())));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
player.closeInventory();
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages." + result.getErrorKey()));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reopenGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-auction"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowsItemMovement() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.CREATE_AUCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||||
|
import pt.henrique.communityMarket.util.SoundUtil;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for setting up a new fixed-price listing.
|
||||||
|
* This GUI is opened AFTER the player has selected an item and quantity.
|
||||||
|
* All elements are merged: price and duration are single clickable items.
|
||||||
|
*
|
||||||
|
* Layout (54-slot chest):
|
||||||
|
* ┌─────────────────────────────────────────────────────┐
|
||||||
|
* │ . . . . INFO . . . . │ Row 0 │
|
||||||
|
* │ . . . . ITEM . . . . │ Row 1: Item │
|
||||||
|
* │ . . . . . . . . . │ Row 2 │
|
||||||
|
* │ . . PRICE . . . DURATION │ Row 3: Setup │
|
||||||
|
* │ . . . . . . . . . │ Row 4 │
|
||||||
|
* │ BACK . . . CONFIRM . . . .│ Row 5: Action│
|
||||||
|
* └─────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
public class CreateListingGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
|
||||||
|
// Selected item info (from ItemSelectionGui -> QuantitySelectGui)
|
||||||
|
private int sourceInventorySlot = -1;
|
||||||
|
private ItemStack selectedItem = null;
|
||||||
|
|
||||||
|
// Listing settings
|
||||||
|
private double price;
|
||||||
|
private int durationHours;
|
||||||
|
|
||||||
|
// ==================== LAYOUT CONSTANTS ====================
|
||||||
|
private static final int INFO_SLOT = 4; // Top center
|
||||||
|
private static final int ITEM_DISPLAY_SLOT = 13; // Center row 1
|
||||||
|
|
||||||
|
// Row 3: Merged elements (single slot each)
|
||||||
|
private static final int PRICE_SLOT = 29; // Price (display + click to adjust)
|
||||||
|
private static final int DURATION_SLOT = 33; // Duration (display + click to cycle)
|
||||||
|
|
||||||
|
private static final int BACK_SLOT = 45; // Bottom-left
|
||||||
|
private static final int CONFIRM_SLOT = 49; // Bottom-center
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
public CreateListingGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the listing creation GUI with a pre-selected item and quantity.
|
||||||
|
* Called from QuantitySelectGui or ItemSelectionGui (for unstackable items).
|
||||||
|
*
|
||||||
|
* @param player The player
|
||||||
|
* @param inventorySlot The slot in the player's inventory where the item is
|
||||||
|
* @param item A clone of the selected item with the desired quantity
|
||||||
|
*/
|
||||||
|
public void openWithItem(Player player, int inventorySlot, ItemStack item) {
|
||||||
|
this.player = player;
|
||||||
|
this.sourceInventorySlot = inventorySlot;
|
||||||
|
this.selectedItem = item;
|
||||||
|
this.durationHours = plugin.getConfigManager().getDefaultDurationHours();
|
||||||
|
this.price = plugin.getConfigManager().getMinPrice();
|
||||||
|
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-listing"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use openWithItem instead. This opens item selection first.
|
||||||
|
*/
|
||||||
|
public void open(Player player) {
|
||||||
|
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Fill with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info panel
|
||||||
|
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.OAK_SIGN)
|
||||||
|
.name("&6&lCreate Listing")
|
||||||
|
.lore(
|
||||||
|
"&7Set a price and duration",
|
||||||
|
"&7for your listing.",
|
||||||
|
"",
|
||||||
|
"&7Tax: &f" + plugin.getConfigManager().getMarketTax() + "%"
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Selected item display
|
||||||
|
if (selectedItem != null) {
|
||||||
|
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(selectedItem.clone())
|
||||||
|
.addLore(List.of(
|
||||||
|
"",
|
||||||
|
"&7Quantity: &f" + selectedItem.getAmount(),
|
||||||
|
"&eThis item will be listed"
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merged price element
|
||||||
|
updatePriceElement();
|
||||||
|
|
||||||
|
// Merged duration element
|
||||||
|
updateDurationElement();
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.lore("&7Return to item selection")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Confirm button
|
||||||
|
updateConfirmButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the merged price element (displays current price + clickable to change)
|
||||||
|
*/
|
||||||
|
private void updatePriceElement() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
double tax = plugin.getTransactionService().calculateListingTax(price);
|
||||||
|
double earnings = price - tax;
|
||||||
|
|
||||||
|
inventory.setItem(PRICE_SLOT, new ItemBuilder(Material.GOLD_INGOT)
|
||||||
|
.name("&6Price: " + msgManager.formatCurrency(price))
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Tax (" + plugin.getConfigManager().getMarketTax() + "%): &c" + msgManager.formatCurrency(tax),
|
||||||
|
"&7You receive: &a" + msgManager.formatCurrency(earnings),
|
||||||
|
"",
|
||||||
|
"&eClick to change price"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the merged duration element (displays current duration + clickable to cycle)
|
||||||
|
*/
|
||||||
|
private void updateDurationElement() {
|
||||||
|
String durationText = formatDuration(durationHours);
|
||||||
|
|
||||||
|
inventory.setItem(DURATION_SLOT, new ItemBuilder(Material.CLOCK)
|
||||||
|
.name("&eDuration: " + durationText)
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Listing expires after this time",
|
||||||
|
"",
|
||||||
|
"&eClick to change duration"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDuration(int hours) {
|
||||||
|
if (hours >= 24) {
|
||||||
|
int days = hours / 24;
|
||||||
|
return days + " day" + (days > 1 ? "s" : "");
|
||||||
|
} else {
|
||||||
|
return hours + " hour" + (hours > 1 ? "s" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConfirmButton() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
double tax = plugin.getTransactionService().calculateListingTax(price);
|
||||||
|
double earnings = price - tax;
|
||||||
|
|
||||||
|
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||||
|
.name(msgManager.getButton("confirm"))
|
||||||
|
.lore(
|
||||||
|
"&7Item: &f" + selectedItem.getType().name() + " x" + selectedItem.getAmount(),
|
||||||
|
"&7Price: &a" + msgManager.formatCurrency(price),
|
||||||
|
"&7You receive: &a" + msgManager.formatCurrency(earnings),
|
||||||
|
"&7Duration: &e" + formatDuration(durationHours),
|
||||||
|
"",
|
||||||
|
"&aClick to create listing!"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
switch (slot) {
|
||||||
|
case PRICE_SLOT -> {
|
||||||
|
// Open number input for price
|
||||||
|
guiManager.openNumberInput(player, newPrice -> {
|
||||||
|
if (newPrice > 0) {
|
||||||
|
this.price = newPrice;
|
||||||
|
}
|
||||||
|
reopenGui();
|
||||||
|
}, price, plugin.getConfigManager().getMinPrice(),
|
||||||
|
plugin.getConfigManager().getMaxPrice(),
|
||||||
|
msgManager.getRaw("gui-titles.number-input"));
|
||||||
|
}
|
||||||
|
case DURATION_SLOT -> {
|
||||||
|
// Cycle through available durations
|
||||||
|
List<Integer> durations = plugin.getConfigManager().getAvailableDurations();
|
||||||
|
if (!durations.isEmpty()) {
|
||||||
|
int currentIndex = durations.indexOf(durationHours);
|
||||||
|
int nextIndex = (currentIndex + 1) % durations.size();
|
||||||
|
durationHours = durations.get(nextIndex);
|
||||||
|
updateDurationElement();
|
||||||
|
updateConfirmButton();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case CONFIRM_SLOT -> {
|
||||||
|
confirmListing(player);
|
||||||
|
}
|
||||||
|
case BACK_SLOT -> {
|
||||||
|
// Go back to item selection
|
||||||
|
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmListing(Player player) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Verify item still exists in player's inventory with sufficient quantity
|
||||||
|
int available = InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||||
|
|
||||||
|
if (available < selectedItem.getAmount()) {
|
||||||
|
if (available < 1) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.item-no-longer-available"));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.quantity-changed"));
|
||||||
|
}
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate item again
|
||||||
|
var validation = plugin.getTransactionService().validateItem(selectedItem);
|
||||||
|
if (!validation.isValid()) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages." + validation.getErrorKey()));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the listing
|
||||||
|
int amount = selectedItem.getAmount();
|
||||||
|
ItemStack listItem = selectedItem.clone();
|
||||||
|
|
||||||
|
plugin.getTransactionService().createListingTransaction(
|
||||||
|
player, listItem, amount, price, durationHours
|
||||||
|
).thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
// Remove items from inventory AFTER successful creation
|
||||||
|
InventoryUtil.removeItems(player, listItem, amount);
|
||||||
|
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-created",
|
||||||
|
"id", String.valueOf(result.getId())));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
player.closeInventory();
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages." + result.getErrorKey()));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reopenGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-listing"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowsItemMovement() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.CREATE_LISTING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for viewing and withdrawing pending earnings from sales.
|
||||||
|
*/
|
||||||
|
public class EarningsGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private double pendingAmount;
|
||||||
|
|
||||||
|
private static final int EARNINGS_DISPLAY_SLOT = 13;
|
||||||
|
private static final int WITHDRAW_SLOT = 31;
|
||||||
|
private static final int BACK_SLOT = 49;
|
||||||
|
|
||||||
|
public EarningsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
plugin.getEarningsService().getPendingEarnings(player.getUniqueId())
|
||||||
|
.thenAccept(amount -> {
|
||||||
|
this.pendingAmount = amount;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.earnings"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
// Fill with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Earnings display
|
||||||
|
inventory.setItem(EARNINGS_DISPLAY_SLOT, new ItemBuilder(Material.EMERALD_BLOCK)
|
||||||
|
.name("&a&lPending Earnings")
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Total: &a" + msgManager.formatCurrency(pendingAmount),
|
||||||
|
"",
|
||||||
|
"&7This is money from your sales",
|
||||||
|
"&7waiting to be withdrawn."
|
||||||
|
)
|
||||||
|
.glow(pendingAmount > 0)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Withdraw button
|
||||||
|
if (pendingAmount > 0) {
|
||||||
|
inventory.setItem(WITHDRAW_SLOT, new ItemBuilder(Material.GOLD_BLOCK)
|
||||||
|
.name(msgManager.getButton("withdraw"))
|
||||||
|
.lore(
|
||||||
|
"&7Click to withdraw all earnings",
|
||||||
|
"&7Amount: &a" + msgManager.formatCurrency(pendingAmount)
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
inventory.setItem(WITHDRAW_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name("&cNo Earnings")
|
||||||
|
.lore("&7You have no pending earnings to withdraw.")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot == WITHDRAW_SLOT && pendingAmount > 0) {
|
||||||
|
withdrawEarnings(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withdrawEarnings(Player player) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
plugin.getEarningsService().withdrawAll(player)
|
||||||
|
.thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
double newBalance = plugin.getEconomyManager().getBalance(player.getUniqueId());
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.earnings-withdrawn",
|
||||||
|
java.util.Map.of(
|
||||||
|
"amount", msgManager.formatCurrency(result.getAmount()),
|
||||||
|
"balance", msgManager.formatCurrency(newBalance)
|
||||||
|
)));
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
open(player); // Refresh
|
||||||
|
} else {
|
||||||
|
if ("no_earnings".equals(result.getError())) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.earnings-empty"));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.earnings-empty"));
|
||||||
|
}
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.EARNINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages all GUI instances and player GUI states.
|
||||||
|
* Central hub for opening, closing, and tracking GUI interactions.
|
||||||
|
*/
|
||||||
|
public class GuiManager {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
// Track open GUIs per player
|
||||||
|
private final Map<UUID, MarketGui> openGuis = new HashMap<>();
|
||||||
|
|
||||||
|
// GUI builders/templates
|
||||||
|
private final MainMenuGui mainMenuGui;
|
||||||
|
private final BrowseMarketGui browseMarketGui;
|
||||||
|
private final BrowseAuctionsGui browseAuctionsGui;
|
||||||
|
private final CreateListingGui createListingGui;
|
||||||
|
private final CreateAuctionGui createAuctionGui;
|
||||||
|
private final MyListingsGui myListingsGui;
|
||||||
|
private final MyAuctionsGui myAuctionsGui;
|
||||||
|
private final ClaimGui claimGui;
|
||||||
|
private final EarningsGui earningsGui;
|
||||||
|
private final HelpGui helpGui;
|
||||||
|
private final AdminGui adminGui;
|
||||||
|
|
||||||
|
public GuiManager(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
|
||||||
|
// Initialize GUI handlers
|
||||||
|
this.mainMenuGui = new MainMenuGui(plugin, this);
|
||||||
|
this.browseMarketGui = new BrowseMarketGui(plugin, this);
|
||||||
|
this.browseAuctionsGui = new BrowseAuctionsGui(plugin, this);
|
||||||
|
this.createListingGui = new CreateListingGui(plugin, this);
|
||||||
|
this.createAuctionGui = new CreateAuctionGui(plugin, this);
|
||||||
|
this.myListingsGui = new MyListingsGui(plugin, this);
|
||||||
|
this.myAuctionsGui = new MyAuctionsGui(plugin, this);
|
||||||
|
this.claimGui = new ClaimGui(plugin, this);
|
||||||
|
this.earningsGui = new EarningsGui(plugin, this);
|
||||||
|
this.helpGui = new HelpGui(plugin, this);
|
||||||
|
this.adminGui = new AdminGui(plugin, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the main menu for a player
|
||||||
|
*/
|
||||||
|
public void openMainMenu(Player player) {
|
||||||
|
mainMenuGui.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the browse market GUI
|
||||||
|
*/
|
||||||
|
public void openBrowseMarket(Player player, int page) {
|
||||||
|
browseMarketGui.open(player, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the browse auctions GUI
|
||||||
|
*/
|
||||||
|
public void openBrowseAuctions(Player player, int page) {
|
||||||
|
browseAuctionsGui.open(player, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the create listing flow (starts with item selection)
|
||||||
|
*/
|
||||||
|
public void openCreateListing(Player player) {
|
||||||
|
// Open item selection first, which will then open CreateListingGui
|
||||||
|
new ItemSelectionGui(plugin, this, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the create auction flow (starts with item selection)
|
||||||
|
*/
|
||||||
|
public void openCreateAuction(Player player) {
|
||||||
|
// Open item selection first, which will then open CreateAuctionGui
|
||||||
|
new ItemSelectionGui(plugin, this, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the player's listings GUI
|
||||||
|
*/
|
||||||
|
public void openMyListings(Player player) {
|
||||||
|
myListingsGui.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the player's auctions GUI
|
||||||
|
*/
|
||||||
|
public void openMyAuctions(Player player) {
|
||||||
|
myAuctionsGui.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the claim GUI
|
||||||
|
*/
|
||||||
|
public void openClaim(Player player) {
|
||||||
|
claimGui.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the earnings GUI
|
||||||
|
*/
|
||||||
|
public void openEarnings(Player player) {
|
||||||
|
earningsGui.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the help GUI
|
||||||
|
*/
|
||||||
|
public void openHelp(Player player) {
|
||||||
|
helpGui.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the admin panel
|
||||||
|
*/
|
||||||
|
public void openAdmin(Player player) {
|
||||||
|
adminGui.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a number input GUI
|
||||||
|
*/
|
||||||
|
public void openNumberInput(Player player, NumberInputGui.NumberInputCallback callback,
|
||||||
|
double currentValue, double minValue, double maxValue, String title) {
|
||||||
|
new NumberInputGui(plugin, this, callback, currentValue, minValue, maxValue, title).open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a confirmation GUI
|
||||||
|
*/
|
||||||
|
public void openConfirmation(Player player, ConfirmationGui.ConfirmCallback callback,
|
||||||
|
String title, String... infoLines) {
|
||||||
|
new ConfirmationGui(plugin, this, callback, title, infoLines).open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an open GUI for a player
|
||||||
|
*/
|
||||||
|
public void registerGui(UUID playerUuid, MarketGui gui) {
|
||||||
|
openGuis.put(playerUuid, gui);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a GUI when closed
|
||||||
|
*/
|
||||||
|
public void unregisterGui(UUID playerUuid) {
|
||||||
|
openGuis.remove(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the currently open GUI for a player
|
||||||
|
*/
|
||||||
|
public MarketGui getOpenGui(UUID playerUuid) {
|
||||||
|
return openGuis.get(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a player has a market GUI open
|
||||||
|
*/
|
||||||
|
public boolean hasGuiOpen(UUID playerUuid) {
|
||||||
|
return openGuis.containsKey(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all open market GUIs (used on plugin disable)
|
||||||
|
*/
|
||||||
|
public void closeAllGuis() {
|
||||||
|
for (UUID uuid : openGuis.keySet()) {
|
||||||
|
Player player = Bukkit.getPlayer(uuid);
|
||||||
|
if (player != null && player.isOnline()) {
|
||||||
|
player.closeInventory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openGuis.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the plugin instance
|
||||||
|
*/
|
||||||
|
public CommunityMarket getPlugin() {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters for GUI handlers
|
||||||
|
public MainMenuGui getMainMenuGui() { return mainMenuGui; }
|
||||||
|
public BrowseMarketGui getBrowseMarketGui() { return browseMarketGui; }
|
||||||
|
public BrowseAuctionsGui getBrowseAuctionsGui() { return browseAuctionsGui; }
|
||||||
|
public CreateListingGui getCreateListingGui() { return createListingGui; }
|
||||||
|
public CreateAuctionGui getCreateAuctionGui() { return createAuctionGui; }
|
||||||
|
public MyListingsGui getMyListingsGui() { return myListingsGui; }
|
||||||
|
public MyAuctionsGui getMyAuctionsGui() { return myAuctionsGui; }
|
||||||
|
public ClaimGui getClaimGui() { return claimGui; }
|
||||||
|
public EarningsGui getEarningsGui() { return earningsGui; }
|
||||||
|
public HelpGui getHelpGui() { return helpGui; }
|
||||||
|
public AdminGui getAdminGui() { return adminGui; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Help GUI showing how to use the marketplace.
|
||||||
|
*/
|
||||||
|
public class HelpGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
|
||||||
|
private static final int BACK_SLOT = 49;
|
||||||
|
|
||||||
|
public HelpGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.help"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Fill with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help content
|
||||||
|
List<String> helpContent = msgManager.getList("help.content");
|
||||||
|
|
||||||
|
// Main help book
|
||||||
|
inventory.setItem(4, new ItemBuilder(Material.WRITTEN_BOOK)
|
||||||
|
.name(msgManager.getRaw("help.title"))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Feature explanations
|
||||||
|
inventory.setItem(19, new ItemBuilder(Material.CHEST)
|
||||||
|
.name("&aBrowse Market")
|
||||||
|
.lore(
|
||||||
|
"&7View all fixed-price listings",
|
||||||
|
"&7from other players.",
|
||||||
|
"",
|
||||||
|
"&7Click on items to purchase them."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(20, new ItemBuilder(Material.GOLD_INGOT)
|
||||||
|
.name("&6Browse Auctions")
|
||||||
|
.lore(
|
||||||
|
"&7View all active auctions.",
|
||||||
|
"",
|
||||||
|
"&eLeft-click &7to place a bid",
|
||||||
|
"&eRight-click &7to buyout (if available)"
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(21, new ItemBuilder(Material.WRITABLE_BOOK)
|
||||||
|
.name("&eCreate Listing")
|
||||||
|
.lore(
|
||||||
|
"&7Sell items at a fixed price.",
|
||||||
|
"",
|
||||||
|
"&71. Place your item in the slot",
|
||||||
|
"&72. Set the price",
|
||||||
|
"&73. Choose duration",
|
||||||
|
"&74. Click confirm"
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(22, new ItemBuilder(Material.GOLDEN_HELMET)
|
||||||
|
.name("&eCreate Auction")
|
||||||
|
.lore(
|
||||||
|
"&7Auction items to the highest bidder.",
|
||||||
|
"",
|
||||||
|
"&71. Place your item in the slot",
|
||||||
|
"&72. Set starting price",
|
||||||
|
"&73. Optionally set buyout",
|
||||||
|
"&74. Choose duration",
|
||||||
|
"&75. Click confirm"
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(23, new ItemBuilder(Material.BOOK)
|
||||||
|
.name("&bMy Listings")
|
||||||
|
.lore(
|
||||||
|
"&7View your active listings.",
|
||||||
|
"",
|
||||||
|
"&7Click on a listing to cancel it.",
|
||||||
|
"&7Cancelled items go to claim storage."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(24, new ItemBuilder(Material.CLOCK)
|
||||||
|
.name("&bMy Auctions")
|
||||||
|
.lore(
|
||||||
|
"&7View your active auctions.",
|
||||||
|
"",
|
||||||
|
"&7You can only cancel auctions",
|
||||||
|
"&7that have no bids yet."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(25, new ItemBuilder(Material.ENDER_CHEST)
|
||||||
|
.name("&dClaim Items")
|
||||||
|
.lore(
|
||||||
|
"&7Collect items waiting for you:",
|
||||||
|
"",
|
||||||
|
"&7- Expired listings",
|
||||||
|
"&7- Cancelled listings",
|
||||||
|
"&7- Won auctions",
|
||||||
|
"&7- Auction refunds"
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(31, new ItemBuilder(Material.EMERALD)
|
||||||
|
.name("&aEarnings")
|
||||||
|
.lore(
|
||||||
|
"&7Withdraw money from sales.",
|
||||||
|
"",
|
||||||
|
"&7When you sell something, the money",
|
||||||
|
"&7goes to pending earnings first.",
|
||||||
|
"&7Withdraw it here."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Tax info
|
||||||
|
inventory.setItem(40, new ItemBuilder(Material.GOLD_NUGGET)
|
||||||
|
.name("&6Tax Information")
|
||||||
|
.lore(
|
||||||
|
"&7Market Tax: &f" + plugin.getConfigManager().getMarketTax() + "%",
|
||||||
|
"&7Auction Tax: &f" + plugin.getConfigManager().getAuctionTax() + "%",
|
||||||
|
"",
|
||||||
|
"&7Taxes are deducted from seller earnings."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.HELP;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.PlayerInventory;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||||
|
import pt.henrique.communityMarket.util.SoundUtil;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for selecting an item from the player's inventory to list or auction.
|
||||||
|
* Displays a mirror view of the player's inventory as clickable icons.
|
||||||
|
* This replaces the "drag item into slot" workflow for better UX.
|
||||||
|
*/
|
||||||
|
public class ItemSelectionGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private final SelectionMode mode;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
|
||||||
|
// Maps GUI slot -> player inventory slot for tracking selections
|
||||||
|
private final Map<Integer, Integer> slotMapping = new HashMap<>();
|
||||||
|
|
||||||
|
// Layout: 54-slot chest
|
||||||
|
// Rows 0-3 (slots 0-35): Player inventory mirror
|
||||||
|
// Row 4 (slots 36-44): Hotbar mirror
|
||||||
|
// Row 5 (slots 45-53): Navigation/info bar
|
||||||
|
|
||||||
|
private static final int INFO_SLOT = 49;
|
||||||
|
private static final int BACK_SLOT = 45;
|
||||||
|
|
||||||
|
public enum SelectionMode {
|
||||||
|
LISTING,
|
||||||
|
AUCTION
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemSelectionGui(CommunityMarket plugin, GuiManager guiManager, SelectionMode mode) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
this.mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the item selection GUI for a player
|
||||||
|
*/
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String titleKey = mode == SelectionMode.LISTING
|
||||||
|
? "gui-titles.select-item-listing"
|
||||||
|
: "gui-titles.select-item-auction";
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw(titleKey));
|
||||||
|
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
slotMapping.clear();
|
||||||
|
|
||||||
|
// Fill bottom row with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerInventory playerInv = player.getInventory();
|
||||||
|
|
||||||
|
// Mirror player's main inventory (slots 9-35 in player inv -> slots 0-26 in GUI)
|
||||||
|
for (int i = 9; i < 36; i++) {
|
||||||
|
ItemStack item = playerInv.getItem(i);
|
||||||
|
int guiSlot = i - 9; // Maps 9-35 to 0-26
|
||||||
|
|
||||||
|
if (item != null && !item.getType().isAir()) {
|
||||||
|
// Check if item can be listed
|
||||||
|
if (canBeSelected(item)) {
|
||||||
|
inventory.setItem(guiSlot, createSelectableItem(item));
|
||||||
|
slotMapping.put(guiSlot, i);
|
||||||
|
} else {
|
||||||
|
// Show as blocked/unavailable
|
||||||
|
inventory.setItem(guiSlot, createBlockedItem(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror hotbar (slots 0-8 in player inv -> slots 27-35 in GUI)
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
ItemStack item = playerInv.getItem(i);
|
||||||
|
int guiSlot = 27 + i; // Maps 0-8 to 27-35
|
||||||
|
|
||||||
|
if (item != null && !item.getType().isAir()) {
|
||||||
|
if (canBeSelected(item)) {
|
||||||
|
inventory.setItem(guiSlot, createSelectableItem(item));
|
||||||
|
slotMapping.put(guiSlot, i);
|
||||||
|
} else {
|
||||||
|
inventory.setItem(guiSlot, createBlockedItem(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info display
|
||||||
|
String modeText = mode == SelectionMode.LISTING ? "&eListing" : "&6Auction";
|
||||||
|
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||||
|
.name("&fSelect an Item")
|
||||||
|
.lore(
|
||||||
|
"&7Click on an item from your",
|
||||||
|
"&7inventory to create a " + modeText + "&7.",
|
||||||
|
"",
|
||||||
|
"&7Blacklisted items are shown in red."
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.lore("&7Return to main menu")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an item can be selected for listing/auction
|
||||||
|
*/
|
||||||
|
private boolean canBeSelected(ItemStack item) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check material blacklist
|
||||||
|
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check keyword blacklist in name/lore
|
||||||
|
var validation = plugin.getTransactionService().validateItem(item);
|
||||||
|
return validation.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a display item with selection lore
|
||||||
|
*/
|
||||||
|
private ItemStack createSelectableItem(ItemStack original) {
|
||||||
|
return new ItemBuilder(original.clone())
|
||||||
|
.addLore(java.util.List.of(
|
||||||
|
"",
|
||||||
|
"&a► Click to select"
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a blocked/unavailable item display
|
||||||
|
*/
|
||||||
|
private ItemStack createBlockedItem(ItemStack original) {
|
||||||
|
return new ItemBuilder(Material.BARRIER)
|
||||||
|
.name("&c" + original.getType().name())
|
||||||
|
.lore(
|
||||||
|
"&7This item cannot be listed.",
|
||||||
|
"&cBlacklisted or invalid."
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if clicked slot contains a selectable item
|
||||||
|
if (slotMapping.containsKey(slot)) {
|
||||||
|
int playerInvSlot = slotMapping.get(slot);
|
||||||
|
ItemStack selectedItem = player.getInventory().getItem(playerInvSlot);
|
||||||
|
|
||||||
|
// Verify item still exists
|
||||||
|
if (selectedItem == null || selectedItem.getType().isAir()) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.invalid-item"));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
open(player); // Refresh
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify item is still valid
|
||||||
|
if (!canBeSelected(selectedItem)) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.blacklisted-item"));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
|
||||||
|
// Check if item is stackable (max stack size > 1)
|
||||||
|
int maxStackSize = selectedItem.getMaxStackSize();
|
||||||
|
int availableQuantity = InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||||
|
|
||||||
|
if (maxStackSize > 1 && availableQuantity > 1) {
|
||||||
|
// Stackable item with more than 1 available - show quantity selector
|
||||||
|
new QuantitySelectGui(plugin, guiManager, quantity -> {
|
||||||
|
if (quantity > 0) {
|
||||||
|
// Proceed with the selected quantity
|
||||||
|
ItemStack itemWithQuantity = selectedItem.clone();
|
||||||
|
itemWithQuantity.setAmount(quantity);
|
||||||
|
|
||||||
|
if (mode == SelectionMode.LISTING) {
|
||||||
|
guiManager.getCreateListingGui().openWithItem(player, playerInvSlot, itemWithQuantity);
|
||||||
|
} else {
|
||||||
|
guiManager.getCreateAuctionGui().openWithItem(player, playerInvSlot, itemWithQuantity);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User cancelled - go back to item selection
|
||||||
|
open(player);
|
||||||
|
}
|
||||||
|
}, selectedItem, availableQuantity).open(player);
|
||||||
|
} else {
|
||||||
|
// Unstackable item or only 1 available - skip quantity selection
|
||||||
|
ItemStack singleItem = selectedItem.clone();
|
||||||
|
singleItem.setAmount(1);
|
||||||
|
|
||||||
|
if (mode == SelectionMode.LISTING) {
|
||||||
|
guiManager.getCreateListingGui().openWithItem(player, playerInvSlot, singleItem);
|
||||||
|
} else {
|
||||||
|
guiManager.getCreateAuctionGui().openWithItem(player, playerInvSlot, singleItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.ITEM_SELECTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.SoundUtil;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main menu GUI - the hub of the marketplace.
|
||||||
|
* Contains buttons for all major features.
|
||||||
|
*/
|
||||||
|
public class MainMenuGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
|
||||||
|
// Slot positions for buttons
|
||||||
|
private static final int SLOT_BROWSE_MARKET = 10;
|
||||||
|
private static final int SLOT_BROWSE_AUCTIONS = 12;
|
||||||
|
private static final int SLOT_CREATE_LISTING = 14;
|
||||||
|
private static final int SLOT_CREATE_AUCTION = 16;
|
||||||
|
private static final int SLOT_MY_LISTINGS = 28;
|
||||||
|
private static final int SLOT_MY_AUCTIONS = 30;
|
||||||
|
private static final int SLOT_CLAIM = 32;
|
||||||
|
private static final int SLOT_EARNINGS = 34;
|
||||||
|
private static final int SLOT_HELP = 40;
|
||||||
|
private static final int SLOT_ADMIN = 44;
|
||||||
|
|
||||||
|
public MainMenuGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the main menu for a player
|
||||||
|
*/
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
String title = TextUtil.colorizeToString(
|
||||||
|
plugin.getMessageManager().getRaw("gui-titles.main-menu"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
// Fill background with glass panes
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE)
|
||||||
|
.name(" ")
|
||||||
|
.build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build menu items asynchronously (to get counts from DB)
|
||||||
|
buildMenuAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildMenuAsync() {
|
||||||
|
// Get player stats for lore
|
||||||
|
var listingCountFuture = plugin.getListingService().countPlayerListings(player.getUniqueId());
|
||||||
|
var auctionCountFuture = plugin.getAuctionService().countPlayerAuctions(player.getUniqueId());
|
||||||
|
var claimCountFuture = plugin.getClaimService().countPlayerClaimItems(player.getUniqueId());
|
||||||
|
var earningsFuture = plugin.getEarningsService().getPendingEarnings(player.getUniqueId());
|
||||||
|
|
||||||
|
// When all futures complete, build the GUI
|
||||||
|
listingCountFuture.thenCombine(auctionCountFuture, (listingCount, auctionCount) ->
|
||||||
|
claimCountFuture.thenCombine(earningsFuture, (claimCount, earnings) -> {
|
||||||
|
// Run on main thread
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildMenu(listingCount, auctionCount, claimCount, earnings);
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildMenu(int listingCount, int auctionCount, int claimCount, double earnings) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
var configManager = plugin.getConfigManager();
|
||||||
|
|
||||||
|
// Browse Market
|
||||||
|
inventory.setItem(SLOT_BROWSE_MARKET, new ItemBuilder(Material.CHEST)
|
||||||
|
.name(msgManager.getButton("browse-market"))
|
||||||
|
.lore(msgManager.getLore("browse-market"))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Browse Auctions
|
||||||
|
inventory.setItem(SLOT_BROWSE_AUCTIONS, new ItemBuilder(Material.GOLD_INGOT)
|
||||||
|
.name(msgManager.getButton("browse-auctions"))
|
||||||
|
.lore(msgManager.getLore("browse-auctions"))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Create Listing
|
||||||
|
inventory.setItem(SLOT_CREATE_LISTING, new ItemBuilder(Material.WRITABLE_BOOK)
|
||||||
|
.name(msgManager.getButton("create-listing"))
|
||||||
|
.lore(msgManager.getLore("create-listing", Map.of(
|
||||||
|
"tax", String.valueOf(configManager.getMarketTax())
|
||||||
|
)))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Create Auction
|
||||||
|
inventory.setItem(SLOT_CREATE_AUCTION, new ItemBuilder(Material.GOLDEN_HELMET)
|
||||||
|
.name(msgManager.getButton("create-auction"))
|
||||||
|
.lore(msgManager.getLore("create-auction", Map.of(
|
||||||
|
"tax", String.valueOf(configManager.getAuctionTax())
|
||||||
|
)))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// My Listings
|
||||||
|
inventory.setItem(SLOT_MY_LISTINGS, new ItemBuilder(Material.BOOK)
|
||||||
|
.name(msgManager.getButton("my-listings"))
|
||||||
|
.lore(msgManager.getLore("my-listings", Map.of(
|
||||||
|
"count", String.valueOf(listingCount),
|
||||||
|
"max", String.valueOf(configManager.getMaxListingsPerPlayer())
|
||||||
|
)))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// My Auctions
|
||||||
|
inventory.setItem(SLOT_MY_AUCTIONS, new ItemBuilder(Material.CLOCK)
|
||||||
|
.name(msgManager.getButton("my-auctions"))
|
||||||
|
.lore(msgManager.getLore("my-auctions", Map.of(
|
||||||
|
"count", String.valueOf(auctionCount),
|
||||||
|
"max", String.valueOf(configManager.getMaxAuctionsPerPlayer())
|
||||||
|
)))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Claim Items
|
||||||
|
inventory.setItem(SLOT_CLAIM, new ItemBuilder(Material.ENDER_CHEST)
|
||||||
|
.name(msgManager.getButton("claim-items"))
|
||||||
|
.lore(msgManager.getLore("claim-items", Map.of(
|
||||||
|
"count", String.valueOf(claimCount)
|
||||||
|
)))
|
||||||
|
.glow(claimCount > 0) // Glow if there are items to claim
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Earnings
|
||||||
|
inventory.setItem(SLOT_EARNINGS, new ItemBuilder(Material.EMERALD)
|
||||||
|
.name(msgManager.getButton("earnings"))
|
||||||
|
.lore(msgManager.getLore("earnings", Map.of(
|
||||||
|
"amount", msgManager.formatCurrency(earnings)
|
||||||
|
)))
|
||||||
|
.glow(earnings > 0) // Glow if there are earnings
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Help (only if enabled in config)
|
||||||
|
if (configManager.isHelpButtonEnabled()) {
|
||||||
|
inventory.setItem(SLOT_HELP, new ItemBuilder(Material.OAK_SIGN)
|
||||||
|
.name(msgManager.getButton("help"))
|
||||||
|
.lore(msgManager.getLore("help"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
// If help is disabled, the slot stays as glass pane (already filled)
|
||||||
|
|
||||||
|
// Admin button (only if player has permission)
|
||||||
|
if (player.hasPermission("communitymarket.admin")) {
|
||||||
|
inventory.setItem(SLOT_ADMIN, new ItemBuilder(Material.COMMAND_BLOCK)
|
||||||
|
.name(msgManager.getButton("admin"))
|
||||||
|
.lore(msgManager.getLore("admin"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
if (event.getCurrentItem() == null) return;
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
switch (slot) {
|
||||||
|
case SLOT_BROWSE_MARKET -> {
|
||||||
|
if (player.hasPermission("communitymarket.buy")) {
|
||||||
|
guiManager.openBrowseMarket(player, 0);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SLOT_BROWSE_AUCTIONS -> {
|
||||||
|
if (player.hasPermission("communitymarket.bid")) {
|
||||||
|
guiManager.openBrowseAuctions(player, 0);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SLOT_CREATE_LISTING -> {
|
||||||
|
if (player.hasPermission("communitymarket.sell")) {
|
||||||
|
guiManager.openCreateListing(player);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SLOT_CREATE_AUCTION -> {
|
||||||
|
if (player.hasPermission("communitymarket.auction")) {
|
||||||
|
guiManager.openCreateAuction(player);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SLOT_MY_LISTINGS -> guiManager.openMyListings(player);
|
||||||
|
case SLOT_MY_AUCTIONS -> guiManager.openMyAuctions(player);
|
||||||
|
case SLOT_CLAIM -> {
|
||||||
|
if (player.hasPermission("communitymarket.claim")) {
|
||||||
|
guiManager.openClaim(player);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SLOT_EARNINGS -> {
|
||||||
|
if (player.hasPermission("communitymarket.withdraw")) {
|
||||||
|
guiManager.openEarnings(player);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SLOT_HELP -> {
|
||||||
|
if (plugin.getConfigManager().isHelpButtonEnabled()) {
|
||||||
|
guiManager.openHelp(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SLOT_ADMIN -> {
|
||||||
|
if (player.hasPermission("communitymarket.admin")) {
|
||||||
|
guiManager.openAdmin(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.MAIN_MENU;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @org.jetbrains.annotations.NotNull Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base interface for all market GUI screens.
|
||||||
|
* Implements InventoryHolder to allow identification of market inventories.
|
||||||
|
*/
|
||||||
|
public interface MarketGui extends InventoryHolder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the GUI type identifier
|
||||||
|
*/
|
||||||
|
GuiType getType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a click event in this GUI
|
||||||
|
*
|
||||||
|
* @param event The click event
|
||||||
|
*/
|
||||||
|
void handleClick(InventoryClickEvent event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a drag event in this GUI
|
||||||
|
*
|
||||||
|
* @param event The drag event
|
||||||
|
*/
|
||||||
|
default void handleDrag(InventoryDragEvent event) {
|
||||||
|
// By default, cancel all drags to prevent item manipulation
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the inventory being closed
|
||||||
|
*
|
||||||
|
* @param event The close event
|
||||||
|
*/
|
||||||
|
default void handleClose(InventoryCloseEvent event) {
|
||||||
|
// Default: do nothing special
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if items can be moved in this GUI
|
||||||
|
* Most GUIs should return false, but create listing/auction GUIs need true
|
||||||
|
*/
|
||||||
|
default boolean allowsItemMovement() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum of all GUI types for identification
|
||||||
|
*/
|
||||||
|
enum GuiType {
|
||||||
|
MAIN_MENU,
|
||||||
|
BROWSE_MARKET,
|
||||||
|
BROWSE_AUCTIONS,
|
||||||
|
CREATE_LISTING,
|
||||||
|
CREATE_AUCTION,
|
||||||
|
ITEM_SELECTION,
|
||||||
|
QUANTITY_SELECT,
|
||||||
|
MY_LISTINGS,
|
||||||
|
MY_AUCTIONS,
|
||||||
|
CLAIM,
|
||||||
|
EARNINGS,
|
||||||
|
HELP,
|
||||||
|
ADMIN,
|
||||||
|
ADMIN_LISTINGS,
|
||||||
|
ADMIN_AUCTIONS,
|
||||||
|
NUMBER_INPUT,
|
||||||
|
CONFIRMATION,
|
||||||
|
LISTING_DETAILS,
|
||||||
|
AUCTION_DETAILS,
|
||||||
|
DURATION_SELECT,
|
||||||
|
FILTER_MENU,
|
||||||
|
SORT_MENU
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.Auction;
|
||||||
|
import pt.henrique.communityMarket.service.AuctionService;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for viewing and managing the player's own auctions.
|
||||||
|
* Click to cancel (only if no bids).
|
||||||
|
*/
|
||||||
|
public class MyAuctionsGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private List<Auction> auctions;
|
||||||
|
|
||||||
|
private static final int BACK_SLOT = 49;
|
||||||
|
|
||||||
|
public MyAuctionsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
plugin.getAuctionService().getPlayerAuctions(player.getUniqueId())
|
||||||
|
.thenAccept(loadedAuctions -> {
|
||||||
|
this.auctions = loadedAuctions;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.my-auctions"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
// Fill bottom
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add auctions
|
||||||
|
for (int i = 0; i < Math.min(auctions.size(), 45); i++) {
|
||||||
|
Auction auction = auctions.get(i);
|
||||||
|
inventory.setItem(i, createAuctionItem(auction));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createAuctionItem(Auction auction) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
ItemStack display = auction.getItem().clone();
|
||||||
|
|
||||||
|
Duration remaining = Duration.between(Instant.now(), auction.getEndsAt());
|
||||||
|
String ends = TextUtil.formatDuration(remaining);
|
||||||
|
|
||||||
|
String bidder = auction.getHighestBidderName() != null ? auction.getHighestBidderName() : "&7None";
|
||||||
|
String currentBid = auction.getBidCount() > 0
|
||||||
|
? msgManager.formatCurrency(auction.getCurrentBid())
|
||||||
|
: "&7No bids";
|
||||||
|
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add("");
|
||||||
|
lore.addAll(msgManager.getLore("my-auction-info", Map.of(
|
||||||
|
"start_price", msgManager.formatCurrency(auction.getStartPrice()),
|
||||||
|
"current_bid", currentBid,
|
||||||
|
"bidder", bidder,
|
||||||
|
"bid_count", String.valueOf(auction.getBidCount()),
|
||||||
|
"ends", ends
|
||||||
|
)));
|
||||||
|
|
||||||
|
// Show if cancellable
|
||||||
|
if (auction.getBidCount() == 0) {
|
||||||
|
lore.add("");
|
||||||
|
lore.add("&aClick to cancel");
|
||||||
|
} else {
|
||||||
|
lore.add("");
|
||||||
|
lore.add("&cCannot cancel - has bids");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ItemBuilder(display)
|
||||||
|
.addLore(lore)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on auction to cancel
|
||||||
|
if (slot >= 0 && slot < 45 && slot < auctions.size()) {
|
||||||
|
Auction auction = auctions.get(slot);
|
||||||
|
|
||||||
|
if (auction.getBidCount() > 0) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.auction-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmCancel(player, auction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmCancel(Player player, Auction auction) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String[] info = {
|
||||||
|
"&7Item: &f" + auction.getItem().getType().name(),
|
||||||
|
"&7Starting Price: &a" + msgManager.formatCurrency(auction.getStartPrice()),
|
||||||
|
"",
|
||||||
|
"&cThis will cancel your auction.",
|
||||||
|
"&cThe item will be moved to claim storage."
|
||||||
|
};
|
||||||
|
|
||||||
|
guiManager.openConfirmation(player, confirmed -> {
|
||||||
|
if (confirmed) {
|
||||||
|
plugin.getAuctionService().cancelAuction(auction.getId(), player.getUniqueId(), false)
|
||||||
|
.thenAccept(result -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (result == AuctionService.CancelResult.SUCCESS) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-cancelled"));
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
} else if (result == AuctionService.CancelResult.HAS_BIDS) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
open(player); // Refresh
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
open(player);
|
||||||
|
}
|
||||||
|
}, msgManager.getRaw("gui-titles.confirm-cancel"), info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.MY_AUCTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.Listing;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for viewing and managing the player's own listings.
|
||||||
|
* Click on a listing to cancel it.
|
||||||
|
*/
|
||||||
|
public class MyListingsGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private List<Listing> listings;
|
||||||
|
|
||||||
|
private static final int BACK_SLOT = 49;
|
||||||
|
|
||||||
|
public MyListingsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
plugin.getListingService().getPlayerListings(player.getUniqueId())
|
||||||
|
.thenAccept(loadedListings -> {
|
||||||
|
this.listings = loadedListings;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.my-listings"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
// Fill bottom
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 45; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add listings
|
||||||
|
for (int i = 0; i < Math.min(listings.size(), 45); i++) {
|
||||||
|
Listing listing = listings.get(i);
|
||||||
|
inventory.setItem(i, createListingItem(listing));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack createListingItem(Listing listing) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
ItemStack display = listing.getItem().clone();
|
||||||
|
display.setAmount(listing.getAmount());
|
||||||
|
|
||||||
|
String expires;
|
||||||
|
if (listing.getExpiresAt() != null) {
|
||||||
|
Duration remaining = Duration.between(Instant.now(), listing.getExpiresAt());
|
||||||
|
expires = TextUtil.formatDuration(remaining);
|
||||||
|
} else {
|
||||||
|
expires = "Never";
|
||||||
|
}
|
||||||
|
|
||||||
|
String created = TextUtil.formatDuration(
|
||||||
|
Duration.between(listing.getCreatedAt(), Instant.now())) + " ago";
|
||||||
|
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add("");
|
||||||
|
lore.addAll(msgManager.getLore("my-listing-info", Map.of(
|
||||||
|
"price", msgManager.formatCurrency(listing.getPrice()),
|
||||||
|
"amount", String.valueOf(listing.getAmount()),
|
||||||
|
"created", created,
|
||||||
|
"expires", expires
|
||||||
|
)));
|
||||||
|
|
||||||
|
return new ItemBuilder(display)
|
||||||
|
.addLore(lore)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
if (slot == BACK_SLOT) {
|
||||||
|
guiManager.openMainMenu(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on listing to cancel
|
||||||
|
if (slot >= 0 && slot < 45 && slot < listings.size()) {
|
||||||
|
Listing listing = listings.get(slot);
|
||||||
|
confirmCancel(player, listing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmCancel(Player player, Listing listing) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
String[] info = {
|
||||||
|
"&7Item: &f" + listing.getItem().getType().name() + " x" + listing.getAmount(),
|
||||||
|
"&7Price: &a" + msgManager.formatCurrency(listing.getPrice()),
|
||||||
|
"",
|
||||||
|
"&cThis will cancel your listing.",
|
||||||
|
"&cThe item will be moved to claim storage."
|
||||||
|
};
|
||||||
|
|
||||||
|
guiManager.openConfirmation(player, confirmed -> {
|
||||||
|
if (confirmed) {
|
||||||
|
plugin.getListingService().cancelListing(listing.getId(), player.getUniqueId(), false)
|
||||||
|
.thenAccept(success -> {
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
if (success) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-cancelled"));
|
||||||
|
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
} else {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.listing-not-found"));
|
||||||
|
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
}
|
||||||
|
open(player); // Refresh
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
open(player);
|
||||||
|
}
|
||||||
|
}, msgManager.getRaw("gui-titles.confirm-cancel"), info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playSound(Player player, String soundName) {
|
||||||
|
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.MY_LISTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.SoundUtil;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI-based numeric input to replace chat input.
|
||||||
|
* Players can increment/decrement values using buttons.
|
||||||
|
*
|
||||||
|
* Layout (54-slot chest):
|
||||||
|
* ┌─────────────────────────────────────────────────────┐
|
||||||
|
* │ . . . . . . . . . │ Row 0: Empty │
|
||||||
|
* │ . . . . [DISPLAY] . . . │ Row 1: Value │
|
||||||
|
* │ . . . . . . . . . │ Row 2: Empty │
|
||||||
|
* │ -1K -100 -10 -1 . +1 +10 +100 +1K│ Row 3: Adjust │
|
||||||
|
* │ . MIN . . . . . MAX . │ Row 4: Presets│
|
||||||
|
* │ BACK . . . CONFIRM . . . │ Row 5: Actions│
|
||||||
|
* └─────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
public class NumberInputGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private final NumberInputCallback callback;
|
||||||
|
private final double minValue;
|
||||||
|
private final double maxValue;
|
||||||
|
private final String title;
|
||||||
|
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private double currentValue;
|
||||||
|
|
||||||
|
// ==================== LAYOUT CONSTANTS ====================
|
||||||
|
// Row 1: Current value display (center)
|
||||||
|
private static final int DISPLAY_SLOT = 13;
|
||||||
|
|
||||||
|
// Row 3: Decrease buttons (LEFT side) - slots 27-30
|
||||||
|
private static final int SUB_1000_SLOT = 27; // -1,000
|
||||||
|
private static final int SUB_100_SLOT = 28; // -100
|
||||||
|
private static final int SUB_10_SLOT = 29; // -10
|
||||||
|
private static final int SUB_1_SLOT = 30; // -1
|
||||||
|
|
||||||
|
// Row 3: Increase buttons (RIGHT side) - slots 32-35
|
||||||
|
private static final int ADD_1_SLOT = 32; // +1
|
||||||
|
private static final int ADD_10_SLOT = 33; // +10
|
||||||
|
private static final int ADD_100_SLOT = 34; // +100
|
||||||
|
private static final int ADD_1000_SLOT = 35; // +1,000
|
||||||
|
|
||||||
|
// Row 4: Preset buttons
|
||||||
|
private static final int SET_MIN_SLOT = 37; // Set to minimum
|
||||||
|
private static final int SET_MAX_SLOT = 43; // Set to maximum
|
||||||
|
|
||||||
|
// Row 5: Action buttons
|
||||||
|
private static final int BACK_SLOT = 45; // Cancel/Back (bottom-left)
|
||||||
|
private static final int CONFIRM_SLOT = 49; // Confirm (bottom-center)
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface NumberInputCallback {
|
||||||
|
void onComplete(double value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberInputGui(CommunityMarket plugin, GuiManager guiManager,
|
||||||
|
NumberInputCallback callback, double currentValue,
|
||||||
|
double minValue, double maxValue, String title) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
this.callback = callback;
|
||||||
|
this.currentValue = currentValue;
|
||||||
|
this.minValue = minValue;
|
||||||
|
this.maxValue = maxValue;
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
inventory = Bukkit.createInventory(this, 54, TextUtil.colorizeToString(title));
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Fill all slots with glass panes
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current value display (center, row 1)
|
||||||
|
updateDisplay();
|
||||||
|
|
||||||
|
// === DECREASE BUTTONS (LEFT SIDE) ===
|
||||||
|
inventory.setItem(SUB_1000_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-1,000")
|
||||||
|
.lore("&7Click: &c-1,000", "&7Shift-click: &c-10,000")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SUB_100_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-100")
|
||||||
|
.lore("&7Click: &c-100", "&7Shift-click: &c-1,000")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SUB_10_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-10")
|
||||||
|
.lore("&7Click: &c-10", "&7Shift-click: &c-100")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SUB_1_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-1")
|
||||||
|
.lore("&7Click: &c-1", "&7Shift-click: &c-10")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// === INCREASE BUTTONS (RIGHT SIDE) ===
|
||||||
|
inventory.setItem(ADD_1_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+1")
|
||||||
|
.lore("&7Click: &a+1", "&7Shift-click: &a+10")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(ADD_10_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+10")
|
||||||
|
.lore("&7Click: &a+10", "&7Shift-click: &a+100")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(ADD_100_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+100")
|
||||||
|
.lore("&7Click: &a+100", "&7Shift-click: &a+1,000")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(ADD_1000_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+1,000")
|
||||||
|
.lore("&7Click: &a+1,000", "&7Shift-click: &a+10,000")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// === PRESET BUTTONS ===
|
||||||
|
inventory.setItem(SET_MIN_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||||
|
.name("&6Set Minimum")
|
||||||
|
.lore("&7Set to: &f" + msgManager.formatCurrency(minValue))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SET_MAX_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||||
|
.name("&6Set Maximum")
|
||||||
|
.lore("&7Set to: &f" + msgManager.formatCurrency(maxValue))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// === ACTION BUTTONS ===
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||||
|
.name(msgManager.getButton("cancel"))
|
||||||
|
.lore("&7Cancel and go back")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||||
|
.name(msgManager.getButton("confirm"))
|
||||||
|
.lore("&7Confirm: &a" + msgManager.formatCurrency(currentValue))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDisplay() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
inventory.setItem(DISPLAY_SLOT, new ItemBuilder(Material.GOLD_INGOT)
|
||||||
|
.name("&6&l" + msgManager.formatCurrency(currentValue))
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Minimum: &f" + msgManager.formatCurrency(minValue),
|
||||||
|
"&7Maximum: &f" + msgManager.formatCurrency(maxValue),
|
||||||
|
"",
|
||||||
|
"&eUse buttons to adjust"
|
||||||
|
)
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Also update confirm button lore
|
||||||
|
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||||
|
.name(plugin.getMessageManager().getButton("confirm"))
|
||||||
|
.lore("&7Confirm: &a" + msgManager.formatCurrency(currentValue))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
boolean shift = event.isShiftClick();
|
||||||
|
|
||||||
|
// Multiplier for shift-click (10x)
|
||||||
|
double multiplier = shift ? 10 : 1;
|
||||||
|
|
||||||
|
switch (slot) {
|
||||||
|
// Decrease buttons
|
||||||
|
case SUB_1000_SLOT -> adjustValue(-1000 * multiplier);
|
||||||
|
case SUB_100_SLOT -> adjustValue(-100 * multiplier);
|
||||||
|
case SUB_10_SLOT -> adjustValue(-10 * multiplier);
|
||||||
|
case SUB_1_SLOT -> adjustValue(-1 * multiplier);
|
||||||
|
|
||||||
|
// Increase buttons
|
||||||
|
case ADD_1_SLOT -> adjustValue(1 * multiplier);
|
||||||
|
case ADD_10_SLOT -> adjustValue(10 * multiplier);
|
||||||
|
case ADD_100_SLOT -> adjustValue(100 * multiplier);
|
||||||
|
case ADD_1000_SLOT -> adjustValue(1000 * multiplier);
|
||||||
|
|
||||||
|
// Preset buttons
|
||||||
|
case SET_MIN_SLOT -> {
|
||||||
|
currentValue = minValue;
|
||||||
|
updateDisplay();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
}
|
||||||
|
case SET_MAX_SLOT -> {
|
||||||
|
currentValue = maxValue;
|
||||||
|
updateDisplay();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
case CONFIRM_SLOT -> {
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
player.closeInventory();
|
||||||
|
callback.onComplete(currentValue);
|
||||||
|
}
|
||||||
|
case BACK_SLOT -> {
|
||||||
|
player.closeInventory();
|
||||||
|
callback.onComplete(-1); // Signal cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustValue(double delta) {
|
||||||
|
double newValue = currentValue + delta;
|
||||||
|
|
||||||
|
// Clamp to min/max
|
||||||
|
newValue = Math.max(minValue, Math.min(maxValue, newValue));
|
||||||
|
|
||||||
|
// Round to 2 decimal places
|
||||||
|
currentValue = Math.round(newValue * 100.0) / 100.0;
|
||||||
|
|
||||||
|
updateDisplay();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound(), 0.3f, 1.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.NUMBER_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package pt.henrique.communityMarket.gui;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||||
|
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||||
|
import pt.henrique.communityMarket.util.SoundUtil;
|
||||||
|
import pt.henrique.communityMarket.util.TextUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI for selecting quantity of an item to list or auction.
|
||||||
|
* Only shown for stackable items; unstackable items skip this step.
|
||||||
|
*
|
||||||
|
* Layout (54-slot chest):
|
||||||
|
* ┌─────────────────────────────────────────────────────┐
|
||||||
|
* │ . . . . INFO . . . . │ Row 0 │
|
||||||
|
* │ . . . . ITEM . . . . │ Row 1: Item │
|
||||||
|
* │ . . . . DISPLAY . . . . │ Row 2: Qty │
|
||||||
|
* │ -64 -32 -16 -1 . +1 +16 +32 +64 │ Row 3: Adjust│
|
||||||
|
* │ . MIN . . . . . MAX . │ Row 4: Preset│
|
||||||
|
* │ BACK . . . CONFIRM . . . .│ Row 5: Action│
|
||||||
|
* └─────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
public class QuantitySelectGui implements MarketGui {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private final QuantityCallback callback;
|
||||||
|
private final ItemStack selectedItem;
|
||||||
|
private final int maxQuantity;
|
||||||
|
|
||||||
|
private Inventory inventory;
|
||||||
|
private Player player;
|
||||||
|
private int currentQuantity;
|
||||||
|
|
||||||
|
// ==================== LAYOUT CONSTANTS ====================
|
||||||
|
private static final int INFO_SLOT = 4; // Top center info
|
||||||
|
private static final int ITEM_DISPLAY_SLOT = 13; // Item preview
|
||||||
|
private static final int QUANTITY_DISPLAY_SLOT = 22; // Current quantity display
|
||||||
|
|
||||||
|
// Row 3: Decrease buttons (LEFT side) - slots 27-30
|
||||||
|
private static final int SUB_64_SLOT = 27; // -64
|
||||||
|
private static final int SUB_32_SLOT = 28; // -32
|
||||||
|
private static final int SUB_16_SLOT = 29; // -16
|
||||||
|
private static final int SUB_1_SLOT = 30; // -1
|
||||||
|
|
||||||
|
// Row 3: Increase buttons (RIGHT side) - slots 32-35
|
||||||
|
private static final int ADD_1_SLOT = 32; // +1
|
||||||
|
private static final int ADD_16_SLOT = 33; // +16
|
||||||
|
private static final int ADD_32_SLOT = 34; // +32
|
||||||
|
private static final int ADD_64_SLOT = 35; // +64
|
||||||
|
|
||||||
|
// Row 4: Preset buttons
|
||||||
|
private static final int SET_MIN_SLOT = 37; // Set to 1
|
||||||
|
private static final int SET_MAX_SLOT = 43; // Set to max
|
||||||
|
|
||||||
|
// Row 5: Action buttons
|
||||||
|
private static final int BACK_SLOT = 45; // Cancel/Back
|
||||||
|
private static final int CONFIRM_SLOT = 49; // Confirm
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface QuantityCallback {
|
||||||
|
void onComplete(int quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuantitySelectGui(CommunityMarket plugin, GuiManager guiManager,
|
||||||
|
QuantityCallback callback, ItemStack selectedItem,
|
||||||
|
int maxQuantity) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
this.callback = callback;
|
||||||
|
this.selectedItem = selectedItem;
|
||||||
|
this.maxQuantity = maxQuantity;
|
||||||
|
this.currentQuantity = Math.min(selectedItem.getAmount(), maxQuantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.quantity-select"));
|
||||||
|
inventory = Bukkit.createInventory(this, 54, title);
|
||||||
|
|
||||||
|
buildGui();
|
||||||
|
player.openInventory(inventory);
|
||||||
|
guiManager.registerGui(player.getUniqueId(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildGui() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Fill with glass
|
||||||
|
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||||
|
for (int i = 0; i < 54; i++) {
|
||||||
|
inventory.setItem(i, filler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info panel
|
||||||
|
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.OAK_SIGN)
|
||||||
|
.name("&6&lSelect Quantity")
|
||||||
|
.lore(
|
||||||
|
"&7Choose how many items",
|
||||||
|
"&7you want to sell.",
|
||||||
|
"",
|
||||||
|
"&7Available: &f" + maxQuantity
|
||||||
|
)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Item preview
|
||||||
|
ItemStack displayItem = selectedItem.clone();
|
||||||
|
displayItem.setAmount(currentQuantity);
|
||||||
|
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(displayItem)
|
||||||
|
.addLore(java.util.List.of(
|
||||||
|
"",
|
||||||
|
"&7Selected: &f" + currentQuantity
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Quantity display
|
||||||
|
updateQuantityDisplay();
|
||||||
|
|
||||||
|
// === DECREASE BUTTONS (LEFT SIDE) ===
|
||||||
|
inventory.setItem(SUB_64_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-64")
|
||||||
|
.lore("&7Click: &c-64")
|
||||||
|
.amount(64)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SUB_32_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-32")
|
||||||
|
.lore("&7Click: &c-32")
|
||||||
|
.amount(32)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SUB_16_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-16")
|
||||||
|
.lore("&7Click: &c-16")
|
||||||
|
.amount(16)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SUB_1_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||||
|
.name("&c-1")
|
||||||
|
.lore("&7Click: &c-1")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// === INCREASE BUTTONS (RIGHT SIDE) ===
|
||||||
|
inventory.setItem(ADD_1_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+1")
|
||||||
|
.lore("&7Click: &a+1")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(ADD_16_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+16")
|
||||||
|
.lore("&7Click: &a+16")
|
||||||
|
.amount(16)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(ADD_32_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+32")
|
||||||
|
.lore("&7Click: &a+32")
|
||||||
|
.amount(32)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(ADD_64_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||||
|
.name("&a+64")
|
||||||
|
.lore("&7Click: &a+64")
|
||||||
|
.amount(64)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// === PRESET BUTTONS ===
|
||||||
|
inventory.setItem(SET_MIN_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||||
|
.name("&6Set Minimum")
|
||||||
|
.lore("&7Set to: &f1")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
inventory.setItem(SET_MAX_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||||
|
.name("&6Set Maximum")
|
||||||
|
.lore("&7Set to: &f" + maxQuantity)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// === ACTION BUTTONS ===
|
||||||
|
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||||
|
.name(msgManager.getButton("back"))
|
||||||
|
.lore("&7Return to item selection")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
updateConfirmButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateQuantityDisplay() {
|
||||||
|
inventory.setItem(QUANTITY_DISPLAY_SLOT, new ItemBuilder(Material.PAPER)
|
||||||
|
.name("&6&lQuantity: " + currentQuantity)
|
||||||
|
.lore(
|
||||||
|
"",
|
||||||
|
"&7Minimum: &f1",
|
||||||
|
"&7Maximum: &f" + maxQuantity,
|
||||||
|
"",
|
||||||
|
"&eUse buttons to adjust"
|
||||||
|
)
|
||||||
|
.amount(Math.min(currentQuantity, 64))
|
||||||
|
.glow()
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Update item display amount
|
||||||
|
ItemStack displayItem = selectedItem.clone();
|
||||||
|
displayItem.setAmount(Math.min(currentQuantity, 64));
|
||||||
|
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(displayItem)
|
||||||
|
.addLore(java.util.List.of(
|
||||||
|
"",
|
||||||
|
"&7Selected: &f" + currentQuantity
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConfirmButton() {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||||
|
.name(msgManager.getButton("confirm"))
|
||||||
|
.lore("&7Quantity: &a" + currentQuantity)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleClick(InventoryClickEvent event) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
Player player = (Player) event.getWhoClicked();
|
||||||
|
int slot = event.getRawSlot();
|
||||||
|
|
||||||
|
switch (slot) {
|
||||||
|
// Decrease buttons
|
||||||
|
case SUB_64_SLOT -> adjustQuantity(-64);
|
||||||
|
case SUB_32_SLOT -> adjustQuantity(-32);
|
||||||
|
case SUB_16_SLOT -> adjustQuantity(-16);
|
||||||
|
case SUB_1_SLOT -> adjustQuantity(-1);
|
||||||
|
|
||||||
|
// Increase buttons
|
||||||
|
case ADD_1_SLOT -> adjustQuantity(1);
|
||||||
|
case ADD_16_SLOT -> adjustQuantity(16);
|
||||||
|
case ADD_32_SLOT -> adjustQuantity(32);
|
||||||
|
case ADD_64_SLOT -> adjustQuantity(64);
|
||||||
|
|
||||||
|
// Preset buttons
|
||||||
|
case SET_MIN_SLOT -> {
|
||||||
|
currentQuantity = 1;
|
||||||
|
updateQuantityDisplay();
|
||||||
|
updateConfirmButton();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
}
|
||||||
|
case SET_MAX_SLOT -> {
|
||||||
|
currentQuantity = maxQuantity;
|
||||||
|
updateQuantityDisplay();
|
||||||
|
updateConfirmButton();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
case CONFIRM_SLOT -> {
|
||||||
|
// Validate quantity is still valid
|
||||||
|
int currentAvailable = countAvailableItems(player);
|
||||||
|
if (currentQuantity > currentAvailable) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.quantity-changed"));
|
||||||
|
currentQuantity = Math.min(currentQuantity, currentAvailable);
|
||||||
|
if (currentQuantity < 1) {
|
||||||
|
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.item-no-longer-available"));
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
player.closeInventory();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateQuantityDisplay();
|
||||||
|
updateConfirmButton();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||||
|
player.closeInventory();
|
||||||
|
callback.onComplete(currentQuantity);
|
||||||
|
}
|
||||||
|
case BACK_SLOT -> {
|
||||||
|
player.closeInventory();
|
||||||
|
callback.onComplete(-1); // Signal cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustQuantity(int delta) {
|
||||||
|
int newQuantity = currentQuantity + delta;
|
||||||
|
|
||||||
|
// Clamp to 1..maxQuantity
|
||||||
|
newQuantity = Math.max(1, Math.min(maxQuantity, newQuantity));
|
||||||
|
|
||||||
|
currentQuantity = newQuantity;
|
||||||
|
updateQuantityDisplay();
|
||||||
|
updateConfirmButton();
|
||||||
|
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound(), 0.3f, 1.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts how many of the selected item the player currently has.
|
||||||
|
* Uses strict item comparison including all metadata.
|
||||||
|
*/
|
||||||
|
private int countAvailableItems(Player player) {
|
||||||
|
return InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GuiType getType() {
|
||||||
|
return GuiType.QUANTITY_SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package pt.henrique.communityMarket.listener;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.inventory.*;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.gui.MarketGui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for all GUI-related inventory events.
|
||||||
|
* Handles clicks, drags, and closes for market GUIs.
|
||||||
|
* Implements security measures to prevent item duplication exploits.
|
||||||
|
*/
|
||||||
|
public class GuiListener implements Listener {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public GuiListener(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all click events in market GUIs.
|
||||||
|
* Security: Validates all click actions and prevents item manipulation.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.HIGH)
|
||||||
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
|
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||||
|
|
||||||
|
Inventory topInventory = event.getView().getTopInventory();
|
||||||
|
InventoryHolder holder = topInventory.getHolder();
|
||||||
|
|
||||||
|
// Check if this is a market GUI
|
||||||
|
if (!(holder instanceof MarketGui marketGui)) return;
|
||||||
|
|
||||||
|
// Get the clicked inventory
|
||||||
|
Inventory clickedInventory = event.getClickedInventory();
|
||||||
|
|
||||||
|
// Handle different GUI types
|
||||||
|
if (marketGui.allowsItemMovement()) {
|
||||||
|
// GUIs like CreateListing allow item placement in specific slots
|
||||||
|
handleItemMovementGui(event, marketGui);
|
||||||
|
} else {
|
||||||
|
// Standard GUIs don't allow any item movement
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always delegate to the GUI's click handler
|
||||||
|
marketGui.handleClick(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles GUIs that allow item movement (like create listing/auction).
|
||||||
|
* Only allows items in designated slots.
|
||||||
|
*/
|
||||||
|
private void handleItemMovementGui(InventoryClickEvent event, MarketGui marketGui) {
|
||||||
|
// Cancel by default - the GUI handler will un-cancel for specific slots
|
||||||
|
ClickType clickType = event.getClick();
|
||||||
|
|
||||||
|
// Block potentially exploitative click types
|
||||||
|
switch (clickType) {
|
||||||
|
case DOUBLE_CLICK -> {
|
||||||
|
// Prevent collecting items from market GUI via double-click
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
case NUMBER_KEY -> {
|
||||||
|
// Block number key swaps to prevent inventory tricks
|
||||||
|
if (event.getRawSlot() < event.getView().getTopInventory().getSize()) {
|
||||||
|
// Allow in player inventory, cancel in market GUI
|
||||||
|
// The specific GUI handler will manage this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SWAP_OFFHAND -> {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
// Let the GUI handler decide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles drag events - prevents dragging items across market GUIs.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.HIGH)
|
||||||
|
public void onInventoryDrag(InventoryDragEvent event) {
|
||||||
|
if (!(event.getWhoClicked() instanceof Player)) return;
|
||||||
|
|
||||||
|
Inventory topInventory = event.getView().getTopInventory();
|
||||||
|
InventoryHolder holder = topInventory.getHolder();
|
||||||
|
|
||||||
|
if (!(holder instanceof MarketGui marketGui)) return;
|
||||||
|
|
||||||
|
// Check if any dragged slots are in the top inventory
|
||||||
|
int topSize = topInventory.getSize();
|
||||||
|
boolean affectsTop = event.getRawSlots().stream()
|
||||||
|
.anyMatch(slot -> slot < topSize);
|
||||||
|
|
||||||
|
if (affectsTop) {
|
||||||
|
// Delegate to GUI handler (most will cancel)
|
||||||
|
marketGui.handleDrag(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles inventory close events.
|
||||||
|
* Ensures items are returned and GUI state is cleaned up.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onInventoryClose(InventoryCloseEvent event) {
|
||||||
|
if (!(event.getPlayer() instanceof Player player)) return;
|
||||||
|
|
||||||
|
Inventory inventory = event.getInventory();
|
||||||
|
InventoryHolder holder = inventory.getHolder();
|
||||||
|
|
||||||
|
if (!(holder instanceof MarketGui marketGui)) return;
|
||||||
|
|
||||||
|
// Notify the GUI of the close
|
||||||
|
marketGui.handleClose(event);
|
||||||
|
|
||||||
|
// Unregister from GUI manager
|
||||||
|
plugin.getGuiManager().unregisterGui(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents moving items out of market GUIs via shift-click from player inventory.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.HIGH)
|
||||||
|
public void onInventoryMoveItem(InventoryMoveItemEvent event) {
|
||||||
|
// This handles hopper/dropper interactions - cancel if involves market GUI
|
||||||
|
if (event.getSource().getHolder() instanceof MarketGui ||
|
||||||
|
event.getDestination().getHolder() instanceof MarketGui) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package pt.henrique.communityMarket.listener;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for player join/quit events.
|
||||||
|
* Notifies players of pending items and earnings on join.
|
||||||
|
*/
|
||||||
|
public class PlayerListener implements Listener {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public PlayerListener(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies players of pending claims and earnings when they join.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Delay notification slightly to ensure player is fully loaded
|
||||||
|
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||||
|
if (!player.isOnline()) return;
|
||||||
|
|
||||||
|
notifyPendingItems(player);
|
||||||
|
}, 40L); // 2 second delay
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up when a player quits.
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Close any open market GUI to prevent issues
|
||||||
|
if (plugin.getGuiManager().hasGuiOpen(player.getUniqueId())) {
|
||||||
|
player.closeInventory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies a player about pending claims and earnings.
|
||||||
|
*/
|
||||||
|
private void notifyPendingItems(Player player) {
|
||||||
|
var msgManager = plugin.getMessageManager();
|
||||||
|
|
||||||
|
// Check for pending claim items
|
||||||
|
plugin.getClaimService().countPlayerClaimItems(player.getUniqueId())
|
||||||
|
.thenAccept(claimCount -> {
|
||||||
|
if (claimCount > 0) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
if (player.isOnline()) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.claim-items",
|
||||||
|
java.util.Map.of("count", String.valueOf(claimCount))));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for pending earnings
|
||||||
|
plugin.getEarningsService().getPendingEarnings(player.getUniqueId())
|
||||||
|
.thenAccept(earnings -> {
|
||||||
|
if (earnings > 0) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
if (player.isOnline()) {
|
||||||
|
player.sendMessage(msgManager.getPrefixed("messages.earnings-balance",
|
||||||
|
"amount", msgManager.formatCurrency(earnings)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package pt.henrique.communityMarket.model;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an auction listing
|
||||||
|
*/
|
||||||
|
public class Auction {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private UUID sellerUuid;
|
||||||
|
private String sellerName;
|
||||||
|
private ItemStack item;
|
||||||
|
private double startPrice;
|
||||||
|
private double currentBid;
|
||||||
|
private UUID highestBidderUuid;
|
||||||
|
private String highestBidderName;
|
||||||
|
private int bidCount;
|
||||||
|
private Double buyoutPrice; // nullable
|
||||||
|
private Instant createdAt;
|
||||||
|
private Instant endsAt;
|
||||||
|
private int extensionCount;
|
||||||
|
private AuctionStatus status;
|
||||||
|
|
||||||
|
public Auction() {
|
||||||
|
this.status = AuctionStatus.ACTIVE;
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
this.bidCount = 0;
|
||||||
|
this.extensionCount = 0;
|
||||||
|
this.currentBid = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Auction(UUID sellerUuid, String sellerName, ItemStack item, double startPrice, Double buyoutPrice, Instant endsAt) {
|
||||||
|
this();
|
||||||
|
this.sellerUuid = sellerUuid;
|
||||||
|
this.sellerName = sellerName;
|
||||||
|
this.item = item;
|
||||||
|
this.startPrice = startPrice;
|
||||||
|
this.buyoutPrice = buyoutPrice;
|
||||||
|
this.endsAt = endsAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getSellerUuid() {
|
||||||
|
return sellerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSellerUuid(UUID sellerUuid) {
|
||||||
|
this.sellerUuid = sellerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSellerName() {
|
||||||
|
return sellerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSellerName(String sellerName) {
|
||||||
|
this.sellerName = sellerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack getItem() {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItem(ItemStack item) {
|
||||||
|
this.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getStartPrice() {
|
||||||
|
return startPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartPrice(double startPrice) {
|
||||||
|
this.startPrice = startPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCurrentBid() {
|
||||||
|
return currentBid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentBid(double currentBid) {
|
||||||
|
this.currentBid = currentBid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getHighestBidderUuid() {
|
||||||
|
return highestBidderUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHighestBidderUuid(UUID highestBidderUuid) {
|
||||||
|
this.highestBidderUuid = highestBidderUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHighestBidderName() {
|
||||||
|
return highestBidderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHighestBidderName(String highestBidderName) {
|
||||||
|
this.highestBidderName = highestBidderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBidCount() {
|
||||||
|
return bidCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBidCount(int bidCount) {
|
||||||
|
this.bidCount = bidCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getBuyoutPrice() {
|
||||||
|
return buyoutPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuyoutPrice(Double buyoutPrice) {
|
||||||
|
this.buyoutPrice = buyoutPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasBuyout() {
|
||||||
|
return buyoutPrice != null && buyoutPrice > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getEndsAt() {
|
||||||
|
return endsAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndsAt(Instant endsAt) {
|
||||||
|
this.endsAt = endsAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExtensionCount() {
|
||||||
|
return extensionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExtensionCount(int extensionCount) {
|
||||||
|
this.extensionCount = extensionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementExtensionCount() {
|
||||||
|
this.extensionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuctionStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(AuctionStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnded() {
|
||||||
|
return Instant.now().isAfter(endsAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return status == AuctionStatus.ACTIVE && !isEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasBids() {
|
||||||
|
return bidCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getEffectivePrice() {
|
||||||
|
return hasBids() ? currentBid : startPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMinimumBid(double minIncrementPercent, double minIncrementAbsolute) {
|
||||||
|
if (!hasBids()) {
|
||||||
|
return startPrice;
|
||||||
|
}
|
||||||
|
double percentIncrement = currentBid * (minIncrementPercent / 100.0);
|
||||||
|
return currentBid + Math.max(percentIncrement, minIncrementAbsolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AuctionStatus {
|
||||||
|
ACTIVE,
|
||||||
|
ENDED,
|
||||||
|
SOLD,
|
||||||
|
CANCELLED,
|
||||||
|
EXPIRED,
|
||||||
|
NO_BIDS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package pt.henrique.communityMarket.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a bid on an auction
|
||||||
|
*/
|
||||||
|
public class Bid {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private int auctionId;
|
||||||
|
private UUID bidderUuid;
|
||||||
|
private String bidderName;
|
||||||
|
private double amount;
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
public Bid() {
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bid(int auctionId, UUID bidderUuid, String bidderName, double amount) {
|
||||||
|
this();
|
||||||
|
this.auctionId = auctionId;
|
||||||
|
this.bidderUuid = bidderUuid;
|
||||||
|
this.bidderName = bidderName;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAuctionId() {
|
||||||
|
return auctionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuctionId(int auctionId) {
|
||||||
|
this.auctionId = auctionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getBidderUuid() {
|
||||||
|
return bidderUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBidderUuid(UUID bidderUuid) {
|
||||||
|
this.bidderUuid = bidderUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBidderName() {
|
||||||
|
return bidderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBidderName(String bidderName) {
|
||||||
|
this.bidderName = bidderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(double amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package pt.henrique.communityMarket.model;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an item in claim storage waiting to be claimed by a player
|
||||||
|
*/
|
||||||
|
public class ClaimItem {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private UUID playerUuid;
|
||||||
|
private ItemStack item;
|
||||||
|
private ClaimReason reason;
|
||||||
|
private String sourceInfo; // Additional info like listing ID, auction ID, etc.
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
public ClaimItem() {
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimItem(UUID playerUuid, ItemStack item, ClaimReason reason, String sourceInfo) {
|
||||||
|
this();
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.item = item;
|
||||||
|
this.reason = reason;
|
||||||
|
this.sourceInfo = sourceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPlayerUuid() {
|
||||||
|
return playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerUuid(UUID playerUuid) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack getItem() {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItem(ItemStack item) {
|
||||||
|
this.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimReason getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReason(ClaimReason reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSourceInfo() {
|
||||||
|
return sourceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourceInfo(String sourceInfo) {
|
||||||
|
this.sourceInfo = sourceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ClaimReason {
|
||||||
|
EXPIRED_LISTING("Expired Listing"),
|
||||||
|
CANCELLED_LISTING("Cancelled Listing"),
|
||||||
|
WON_AUCTION("Won Auction"),
|
||||||
|
AUCTION_NO_BIDS("Auction Ended (No Bids)"),
|
||||||
|
CANCELLED_AUCTION("Cancelled Auction"),
|
||||||
|
PURCHASE_FULL_INVENTORY("Purchase (Inventory Full)"),
|
||||||
|
OUTBID_REFUND("Outbid Refund"),
|
||||||
|
ADMIN_RETURN("Admin Return");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
ClaimReason(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package pt.henrique.communityMarket.model;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a fixed-price market listing
|
||||||
|
*/
|
||||||
|
public class Listing {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private UUID sellerUuid;
|
||||||
|
private String sellerName;
|
||||||
|
private ItemStack item;
|
||||||
|
private int amount;
|
||||||
|
private double price;
|
||||||
|
private Instant createdAt;
|
||||||
|
private Instant expiresAt;
|
||||||
|
private ListingStatus status;
|
||||||
|
|
||||||
|
public Listing() {
|
||||||
|
this.status = ListingStatus.ACTIVE;
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Listing(UUID sellerUuid, String sellerName, ItemStack item, int amount, double price, Instant expiresAt) {
|
||||||
|
this();
|
||||||
|
this.sellerUuid = sellerUuid;
|
||||||
|
this.sellerName = sellerName;
|
||||||
|
this.item = item;
|
||||||
|
this.amount = amount;
|
||||||
|
this.price = price;
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getSellerUuid() {
|
||||||
|
return sellerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSellerUuid(UUID sellerUuid) {
|
||||||
|
this.sellerUuid = sellerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSellerName() {
|
||||||
|
return sellerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSellerName(String sellerName) {
|
||||||
|
this.sellerName = sellerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack getItem() {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItem(ItemStack item) {
|
||||||
|
this.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(int amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrice() {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrice(double price) {
|
||||||
|
this.price = price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getExpiresAt() {
|
||||||
|
return expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpiresAt(Instant expiresAt) {
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListingStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(ListingStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return expiresAt != null && Instant.now().isAfter(expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return status == ListingStatus.ACTIVE && !isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ListingStatus {
|
||||||
|
ACTIVE,
|
||||||
|
SOLD,
|
||||||
|
EXPIRED,
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package pt.henrique.communityMarket.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents pending earnings that a player can withdraw
|
||||||
|
*/
|
||||||
|
public class PendingEarnings {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
private UUID playerUuid;
|
||||||
|
private double amount;
|
||||||
|
private String source; // e.g., "Listing #123", "Auction #456"
|
||||||
|
private Instant createdAt;
|
||||||
|
private boolean withdrawn;
|
||||||
|
|
||||||
|
public PendingEarnings() {
|
||||||
|
this.createdAt = Instant.now();
|
||||||
|
this.withdrawn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PendingEarnings(UUID playerUuid, double amount, String source) {
|
||||||
|
this();
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.amount = amount;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPlayerUuid() {
|
||||||
|
return playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerUuid(UUID playerUuid) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(double amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSource() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSource(String source) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWithdrawn() {
|
||||||
|
return withdrawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWithdrawn(boolean withdrawn) {
|
||||||
|
this.withdrawn = withdrawn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
package pt.henrique.communityMarket.service;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.Auction;
|
||||||
|
import pt.henrique.communityMarket.model.Bid;
|
||||||
|
import pt.henrique.communityMarket.model.ClaimItem;
|
||||||
|
import pt.henrique.communityMarket.model.PendingEarnings;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service layer for managing auctions.
|
||||||
|
* Handles creation, bidding, buyout, and auction end processing.
|
||||||
|
*/
|
||||||
|
public class AuctionService {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
// Simple cache for active auctions
|
||||||
|
private List<Auction> cachedAuctions;
|
||||||
|
private long cacheExpiry = 0;
|
||||||
|
|
||||||
|
// Track pending operations to prevent race conditions
|
||||||
|
private final Map<Integer, Boolean> pendingBids = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public AuctionService(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new auction
|
||||||
|
*
|
||||||
|
* @param player The seller
|
||||||
|
* @param item The item to auction
|
||||||
|
* @param startPrice Starting bid price
|
||||||
|
* @param buyoutPrice Optional buyout price (null for no buyout)
|
||||||
|
* @param durationHours Duration in hours
|
||||||
|
* @return CompletableFuture with the auction ID or -1 if failed
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> createAuction(Player player, ItemStack item, double startPrice,
|
||||||
|
Double buyoutPrice, int durationHours) {
|
||||||
|
// Validate
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
return CompletableFuture.completedFuture(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||||
|
return CompletableFuture.completedFuture(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create auction object
|
||||||
|
Auction auction = new Auction(
|
||||||
|
player.getUniqueId(),
|
||||||
|
player.getName(),
|
||||||
|
item.clone(),
|
||||||
|
startPrice,
|
||||||
|
buyoutPrice,
|
||||||
|
Instant.now().plusSeconds(durationHours * 3600L)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
return plugin.getDatabaseManager().createAuction(auction)
|
||||||
|
.thenApply(id -> {
|
||||||
|
if (id > 0) {
|
||||||
|
invalidateCache();
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all active auctions with caching
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Auction>> getActiveAuctions() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (cachedAuctions != null && now < cacheExpiry) {
|
||||||
|
return CompletableFuture.completedFuture(cachedAuctions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.getDatabaseManager().getActiveAuctions()
|
||||||
|
.thenApply(auctions -> {
|
||||||
|
cachedAuctions = auctions;
|
||||||
|
cacheExpiry = System.currentTimeMillis() +
|
||||||
|
(plugin.getConfigManager().getCacheDuration() * 1000L);
|
||||||
|
return auctions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a specific auction by ID
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Optional<Auction>> getAuction(int id) {
|
||||||
|
return plugin.getDatabaseManager().getAuction(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all auctions for a specific player
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Auction>> getPlayerAuctions(UUID playerUuid) {
|
||||||
|
return plugin.getDatabaseManager().getPlayerAuctions(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts active auctions for a player
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> countPlayerAuctions(UUID playerUuid) {
|
||||||
|
return plugin.getDatabaseManager().countPlayerAuctions(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a player can create a new auction (not at limit)
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> canCreateAuction(UUID playerUuid) {
|
||||||
|
int maxAuctions = plugin.getConfigManager().getMaxAuctionsPerPlayer();
|
||||||
|
|
||||||
|
return countPlayerAuctions(playerUuid)
|
||||||
|
.thenApply(count -> count < maxAuctions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the minimum bid for an auction
|
||||||
|
*
|
||||||
|
* @param auction The auction
|
||||||
|
* @return Minimum bid amount
|
||||||
|
*/
|
||||||
|
public double calculateMinBid(Auction auction) {
|
||||||
|
return auction.getMinimumBid(
|
||||||
|
plugin.getConfigManager().getMinBidIncrementPercent(),
|
||||||
|
plugin.getConfigManager().getMinBidIncrementAbsolute()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places a bid on an auction
|
||||||
|
*
|
||||||
|
* @param auctionId The auction to bid on
|
||||||
|
* @param bidder The bidder
|
||||||
|
* @param amount The bid amount
|
||||||
|
* @return CompletableFuture with bid result
|
||||||
|
*/
|
||||||
|
public CompletableFuture<BidResult> placeBid(int auctionId, Player bidder, double amount) {
|
||||||
|
// Check if already processing this auction
|
||||||
|
if (pendingBids.putIfAbsent(auctionId, true) != null) {
|
||||||
|
return CompletableFuture.completedFuture(BidResult.ALREADY_PROCESSING);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getAuction(auctionId)
|
||||||
|
.thenCompose(optAuction -> {
|
||||||
|
if (optAuction.isEmpty()) {
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
return CompletableFuture.completedFuture(BidResult.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
Auction auction = optAuction.get();
|
||||||
|
|
||||||
|
// Can't bid on own auction
|
||||||
|
if (auction.getSellerUuid().equals(bidder.getUniqueId())) {
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
return CompletableFuture.completedFuture(BidResult.OWN_AUCTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bid amount
|
||||||
|
double minBid = calculateMinBid(auction);
|
||||||
|
if (amount < minBid) {
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
return CompletableFuture.completedFuture(BidResult.BID_TOO_LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bidder funds
|
||||||
|
if (!plugin.getEconomyManager().has(bidder.getUniqueId(), amount)) {
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
return CompletableFuture.completedFuture(BidResult.INSUFFICIENT_FUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store previous bidder info for refund
|
||||||
|
UUID previousBidder = auction.getHighestBidderUuid();
|
||||||
|
double previousBid = auction.getCurrentBid();
|
||||||
|
|
||||||
|
// Withdraw from bidder first
|
||||||
|
if (!plugin.getEconomyManager().withdraw(bidder.getUniqueId(), amount)) {
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
return CompletableFuture.completedFuture(BidResult.ECONOMY_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place bid in database
|
||||||
|
return plugin.getDatabaseManager().placeBid(auctionId, bidder.getUniqueId(), bidder.getName(), amount)
|
||||||
|
.thenApply(success -> {
|
||||||
|
if (!success) {
|
||||||
|
// Refund bidder
|
||||||
|
plugin.getEconomyManager().deposit(bidder.getUniqueId(), amount);
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
return BidResult.AUCTION_ENDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refund previous bidder
|
||||||
|
if (previousBidder != null && previousBid > 0) {
|
||||||
|
plugin.getEconomyManager().deposit(previousBidder, previousBid);
|
||||||
|
|
||||||
|
// Notify previous bidder if online
|
||||||
|
if (plugin.getConfigManager().isNotifyOnOutbid()) {
|
||||||
|
Player prevBidderPlayer = Bukkit.getPlayer(previousBidder);
|
||||||
|
if (prevBidderPlayer != null && prevBidderPlayer.isOnline()) {
|
||||||
|
Map<String, String> placeholders = Map.of(
|
||||||
|
"item", auction.getItem().getType().name(),
|
||||||
|
"amount", plugin.getMessageManager().formatCurrency(amount),
|
||||||
|
"bidder", bidder.getName()
|
||||||
|
);
|
||||||
|
prevBidderPlayer.sendMessage(plugin.getMessageManager()
|
||||||
|
.getPrefixed("messages.auction-outbid", placeholders));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateCache();
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
return BidResult.SUCCESS;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
pendingBids.remove(auctionId);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buys out an auction immediately
|
||||||
|
*
|
||||||
|
* @param auctionId The auction to buyout
|
||||||
|
* @param buyer The buyer
|
||||||
|
* @return CompletableFuture with buyout result
|
||||||
|
*/
|
||||||
|
public CompletableFuture<BidResult> buyout(int auctionId, Player buyer) {
|
||||||
|
return getAuction(auctionId)
|
||||||
|
.thenCompose(optAuction -> {
|
||||||
|
if (optAuction.isEmpty()) {
|
||||||
|
return CompletableFuture.completedFuture(BidResult.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
Auction auction = optAuction.get();
|
||||||
|
|
||||||
|
// Check if buyout is available
|
||||||
|
if (!auction.hasBuyout()) {
|
||||||
|
return CompletableFuture.completedFuture(BidResult.NO_BUYOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't buyout own auction
|
||||||
|
if (auction.getSellerUuid().equals(buyer.getUniqueId())) {
|
||||||
|
return CompletableFuture.completedFuture(BidResult.OWN_AUCTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check buyer funds
|
||||||
|
double buyoutPrice = auction.getBuyoutPrice();
|
||||||
|
if (!plugin.getEconomyManager().has(buyer.getUniqueId(), buyoutPrice)) {
|
||||||
|
return CompletableFuture.completedFuture(BidResult.INSUFFICIENT_FUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place bid at buyout price (this will end the auction)
|
||||||
|
return placeBid(auctionId, buyer, buyoutPrice)
|
||||||
|
.thenCompose(result -> {
|
||||||
|
if (result == BidResult.SUCCESS) {
|
||||||
|
// Immediately end the auction
|
||||||
|
return processAuctionEnd(auctionId)
|
||||||
|
.thenApply(v -> BidResult.BUYOUT_SUCCESS);
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels an auction (only if no bids)
|
||||||
|
*
|
||||||
|
* @param auctionId The auction to cancel
|
||||||
|
* @param playerUuid The player attempting to cancel
|
||||||
|
* @param isAdmin Whether this is an admin action
|
||||||
|
* @return CompletableFuture with success status
|
||||||
|
*/
|
||||||
|
public CompletableFuture<CancelResult> cancelAuction(int auctionId, UUID playerUuid, boolean isAdmin) {
|
||||||
|
return getAuction(auctionId)
|
||||||
|
.thenCompose(optAuction -> {
|
||||||
|
if (optAuction.isEmpty()) {
|
||||||
|
return CompletableFuture.completedFuture(CancelResult.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
Auction auction = optAuction.get();
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!isAdmin && !auction.getSellerUuid().equals(playerUuid)) {
|
||||||
|
return CompletableFuture.completedFuture(CancelResult.NOT_OWNER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can only cancel if no bids (unless admin)
|
||||||
|
if (!isAdmin && auction.getBidCount() > 0) {
|
||||||
|
return CompletableFuture.completedFuture(CancelResult.HAS_BIDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If admin cancelling with bids, refund highest bidder
|
||||||
|
if (isAdmin && auction.getBidCount() > 0 && auction.getHighestBidderUuid() != null) {
|
||||||
|
plugin.getEconomyManager().deposit(auction.getHighestBidderUuid(), auction.getCurrentBid());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
return plugin.getDatabaseManager().updateAuctionStatus(auctionId, Auction.AuctionStatus.CANCELLED)
|
||||||
|
.thenApply(success -> {
|
||||||
|
if (success) {
|
||||||
|
// Return item to claim storage
|
||||||
|
ClaimItem claimItem = new ClaimItem(
|
||||||
|
auction.getSellerUuid(),
|
||||||
|
auction.getItem().clone(),
|
||||||
|
ClaimItem.ClaimReason.CANCELLED_AUCTION,
|
||||||
|
"Auction #" + auctionId
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||||
|
invalidateCache();
|
||||||
|
return CancelResult.SUCCESS;
|
||||||
|
}
|
||||||
|
return CancelResult.FAILED;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes ended auctions - delivers items and handles payments
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> processEndedAuctions() {
|
||||||
|
return plugin.getDatabaseManager().getEndedAuctions()
|
||||||
|
.thenAccept(auctions -> {
|
||||||
|
for (Auction auction : auctions) {
|
||||||
|
processAuctionEnd(auction.getId());
|
||||||
|
}
|
||||||
|
if (!auctions.isEmpty()) {
|
||||||
|
invalidateCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a single auction end
|
||||||
|
*/
|
||||||
|
private CompletableFuture<Void> processAuctionEnd(int auctionId) {
|
||||||
|
return getAuction(auctionId)
|
||||||
|
.thenAccept(optAuction -> {
|
||||||
|
if (optAuction.isEmpty()) return;
|
||||||
|
|
||||||
|
Auction auction = optAuction.get();
|
||||||
|
|
||||||
|
// Update status first
|
||||||
|
Auction.AuctionStatus newStatus = auction.getBidCount() > 0
|
||||||
|
? Auction.AuctionStatus.SOLD
|
||||||
|
: Auction.AuctionStatus.EXPIRED;
|
||||||
|
|
||||||
|
plugin.getDatabaseManager().updateAuctionStatus(auctionId, newStatus);
|
||||||
|
|
||||||
|
if (auction.getBidCount() > 0 && auction.getHighestBidderUuid() != null) {
|
||||||
|
// Auction has a winner
|
||||||
|
double winningBid = auction.getCurrentBid();
|
||||||
|
double tax = plugin.getConfigManager().getAuctionTax();
|
||||||
|
double sellerEarnings = winningBid * (1 - tax / 100);
|
||||||
|
|
||||||
|
// Add pending earnings for seller
|
||||||
|
PendingEarnings earnings = new PendingEarnings(
|
||||||
|
auction.getSellerUuid(),
|
||||||
|
sellerEarnings,
|
||||||
|
"Auction #" + auctionId
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addPendingEarnings(earnings);
|
||||||
|
|
||||||
|
// Add item to winner's claim storage
|
||||||
|
ClaimItem claimItem = new ClaimItem(
|
||||||
|
auction.getHighestBidderUuid(),
|
||||||
|
auction.getItem().clone(),
|
||||||
|
ClaimItem.ClaimReason.WON_AUCTION,
|
||||||
|
"Auction #" + auctionId
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||||
|
|
||||||
|
// Notify winner if online
|
||||||
|
if (plugin.getConfigManager().isNotifyOnWin()) {
|
||||||
|
Player winner = Bukkit.getPlayer(auction.getHighestBidderUuid());
|
||||||
|
if (winner != null && winner.isOnline()) {
|
||||||
|
Map<String, String> placeholders = Map.of(
|
||||||
|
"item", auction.getItem().getType().name(),
|
||||||
|
"price", plugin.getMessageManager().formatCurrency(winningBid)
|
||||||
|
);
|
||||||
|
winner.sendMessage(plugin.getMessageManager()
|
||||||
|
.getPrefixed("messages.auction-ended-winner", placeholders));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify seller if online
|
||||||
|
if (plugin.getConfigManager().isNotifyOnSale()) {
|
||||||
|
Player seller = Bukkit.getPlayer(auction.getSellerUuid());
|
||||||
|
if (seller != null && seller.isOnline()) {
|
||||||
|
Map<String, String> placeholders = Map.of(
|
||||||
|
"item", auction.getItem().getType().name(),
|
||||||
|
"winner", auction.getHighestBidderName(),
|
||||||
|
"price", plugin.getMessageManager().formatCurrency(sellerEarnings)
|
||||||
|
);
|
||||||
|
seller.sendMessage(plugin.getMessageManager()
|
||||||
|
.getPrefixed("messages.auction-ended-seller", placeholders));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No bids - return item to seller
|
||||||
|
ClaimItem claimItem = new ClaimItem(
|
||||||
|
auction.getSellerUuid(),
|
||||||
|
auction.getItem().clone(),
|
||||||
|
ClaimItem.ClaimReason.AUCTION_NO_BIDS,
|
||||||
|
"Auction #" + auctionId
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||||
|
|
||||||
|
// Notify seller if online
|
||||||
|
if (plugin.getConfigManager().isNotifyOnExpire()) {
|
||||||
|
Player seller = Bukkit.getPlayer(auction.getSellerUuid());
|
||||||
|
if (seller != null && seller.isOnline()) {
|
||||||
|
seller.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||||
|
"messages.auction-ended-no-bids",
|
||||||
|
"item", auction.getItem().getType().name()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the auction cache
|
||||||
|
*/
|
||||||
|
public void invalidateCache() {
|
||||||
|
cachedAuctions = null;
|
||||||
|
cacheExpiry = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a bid attempt
|
||||||
|
*/
|
||||||
|
public enum BidResult {
|
||||||
|
SUCCESS,
|
||||||
|
BUYOUT_SUCCESS,
|
||||||
|
NOT_FOUND,
|
||||||
|
AUCTION_ENDED,
|
||||||
|
OWN_AUCTION,
|
||||||
|
BID_TOO_LOW,
|
||||||
|
INSUFFICIENT_FUNDS,
|
||||||
|
ECONOMY_ERROR,
|
||||||
|
ALREADY_PROCESSING,
|
||||||
|
NO_BUYOUT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a cancel attempt
|
||||||
|
*/
|
||||||
|
public enum CancelResult {
|
||||||
|
SUCCESS,
|
||||||
|
NOT_FOUND,
|
||||||
|
NOT_OWNER,
|
||||||
|
HAS_BIDS,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package pt.henrique.communityMarket.service;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.ClaimItem;
|
||||||
|
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service layer for managing claim storage.
|
||||||
|
* Players claim items from expired listings, won auctions, etc.
|
||||||
|
*/
|
||||||
|
public class ClaimService {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public ClaimService(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all pending claim items for a player
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @return CompletableFuture with list of claim items
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<ClaimItem>> getPlayerClaimItems(UUID playerUuid) {
|
||||||
|
return plugin.getDatabaseManager().getPlayerClaimItems(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts pending claim items for a player
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @return CompletableFuture with count
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> countPlayerClaimItems(UUID playerUuid) {
|
||||||
|
return plugin.getDatabaseManager().countPlayerClaimItems(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims a single item
|
||||||
|
*
|
||||||
|
* @param claimId The claim item ID
|
||||||
|
* @param player The player claiming
|
||||||
|
* @return CompletableFuture with claim result
|
||||||
|
*/
|
||||||
|
public CompletableFuture<ClaimResult> claimItem(int claimId, Player player) {
|
||||||
|
return plugin.getDatabaseManager().getPlayerClaimItems(player.getUniqueId())
|
||||||
|
.thenCompose(items -> {
|
||||||
|
// Find the specific item
|
||||||
|
ClaimItem claimItem = items.stream()
|
||||||
|
.filter(i -> i.getId() == claimId)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (claimItem == null) {
|
||||||
|
return CompletableFuture.completedFuture(ClaimResult.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check inventory space
|
||||||
|
if (!InventoryUtil.hasSpace(player, claimItem.getItem())) {
|
||||||
|
return CompletableFuture.completedFuture(ClaimResult.INVENTORY_FULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from database first
|
||||||
|
return plugin.getDatabaseManager().removeClaimItem(claimId)
|
||||||
|
.thenApply(success -> {
|
||||||
|
if (!success) {
|
||||||
|
return ClaimResult.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give item to player on main thread
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
ItemStack leftover = InventoryUtil.giveItem(player, claimItem.getItem());
|
||||||
|
if (leftover != null) {
|
||||||
|
// This shouldn't happen since we checked space, but just in case
|
||||||
|
plugin.getDatabaseManager().addClaimItem(new ClaimItem(
|
||||||
|
player.getUniqueId(),
|
||||||
|
leftover,
|
||||||
|
claimItem.getReason(),
|
||||||
|
claimItem.getSourceInfo()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ClaimResult.SUCCESS;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims all items for a player
|
||||||
|
*
|
||||||
|
* @param player The player claiming
|
||||||
|
* @return CompletableFuture with number of items claimed
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> claimAll(Player player) {
|
||||||
|
return plugin.getDatabaseManager().getPlayerClaimItems(player.getUniqueId())
|
||||||
|
.thenApply(items -> {
|
||||||
|
int claimed = 0;
|
||||||
|
|
||||||
|
for (ClaimItem item : items) {
|
||||||
|
// Check if player has space
|
||||||
|
if (!InventoryUtil.hasSpace(player, item.getItem())) {
|
||||||
|
break; // Stop claiming if inventory is full
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from database
|
||||||
|
boolean removed = plugin.getDatabaseManager().removeClaimItem(item.getId()).join();
|
||||||
|
if (removed) {
|
||||||
|
// Give item on main thread
|
||||||
|
ItemStack itemToGive = item.getItem();
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
InventoryUtil.giveItem(player, itemToGive);
|
||||||
|
});
|
||||||
|
claimed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return claimed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to a player's claim storage
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param item The item to add
|
||||||
|
* @param reason The reason for the claim
|
||||||
|
* @param sourceInfo Additional info about the source
|
||||||
|
* @return CompletableFuture with the claim ID
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> addClaimItem(UUID playerUuid, ItemStack item, ClaimItem.ClaimReason reason, String sourceInfo) {
|
||||||
|
ClaimItem claimItem = new ClaimItem(playerUuid, item, reason, sourceInfo);
|
||||||
|
return plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a claim attempt
|
||||||
|
*/
|
||||||
|
public enum ClaimResult {
|
||||||
|
SUCCESS,
|
||||||
|
NOT_FOUND,
|
||||||
|
INVENTORY_FULL,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package pt.henrique.communityMarket.service;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service layer for managing player earnings.
|
||||||
|
* Handles pending earnings from sales and withdrawals.
|
||||||
|
*/
|
||||||
|
public class EarningsService {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public EarningsService(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total pending earnings for a player
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @return CompletableFuture with the total pending amount
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Double> getPendingEarnings(UUID playerUuid) {
|
||||||
|
return plugin.getDatabaseManager().getPlayerPendingEarnings(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraws all pending earnings for a player
|
||||||
|
*
|
||||||
|
* @param player The player withdrawing
|
||||||
|
* @return CompletableFuture with the withdrawal result
|
||||||
|
*/
|
||||||
|
public CompletableFuture<WithdrawResult> withdrawAll(Player player) {
|
||||||
|
return getPendingEarnings(player.getUniqueId())
|
||||||
|
.thenCompose(amount -> {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return CompletableFuture.completedFuture(WithdrawResult.NO_EARNINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark earnings as withdrawn in database first
|
||||||
|
return plugin.getDatabaseManager().withdrawAllEarnings(player.getUniqueId())
|
||||||
|
.thenApply(success -> {
|
||||||
|
if (!success) {
|
||||||
|
return WithdrawResult.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deposit to player's economy account
|
||||||
|
if (!plugin.getEconomyManager().deposit(player.getUniqueId(), amount)) {
|
||||||
|
// Economy failed - this is a problem
|
||||||
|
// The earnings are marked as withdrawn but money wasn't given
|
||||||
|
plugin.getLogger().severe("Failed to deposit " + amount + " to " + player.getName() + " after marking earnings as withdrawn!");
|
||||||
|
return WithdrawResult.ECONOMY_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WithdrawResult.success(amount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds pending earnings for a player
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param amount The amount to add
|
||||||
|
* @param source Description of the source (e.g., "Listing #123")
|
||||||
|
* @return CompletableFuture with the earnings ID
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> addEarnings(UUID playerUuid, double amount, String source) {
|
||||||
|
var earnings = new pt.henrique.communityMarket.model.PendingEarnings(playerUuid, amount, source);
|
||||||
|
return plugin.getDatabaseManager().addPendingEarnings(earnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a withdrawal attempt
|
||||||
|
*/
|
||||||
|
public static class WithdrawResult {
|
||||||
|
private final boolean success;
|
||||||
|
private final double amount;
|
||||||
|
private final String error;
|
||||||
|
|
||||||
|
private WithdrawResult(boolean success, double amount, String error) {
|
||||||
|
this.success = success;
|
||||||
|
this.amount = amount;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WithdrawResult success(double amount) {
|
||||||
|
return new WithdrawResult(true, amount, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final WithdrawResult NO_EARNINGS = new WithdrawResult(false, 0, "no_earnings");
|
||||||
|
public static final WithdrawResult FAILED = new WithdrawResult(false, 0, "failed");
|
||||||
|
public static final WithdrawResult ECONOMY_ERROR = new WithdrawResult(false, 0, "economy_error");
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError() {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
package pt.henrique.communityMarket.service;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
import pt.henrique.communityMarket.model.ClaimItem;
|
||||||
|
import pt.henrique.communityMarket.model.Listing;
|
||||||
|
import pt.henrique.communityMarket.model.PendingEarnings;
|
||||||
|
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service layer for managing fixed-price market listings.
|
||||||
|
* Handles creation, purchase, cancellation, and expiration of listings.
|
||||||
|
*/
|
||||||
|
public class ListingService {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
// Simple cache for active listings
|
||||||
|
private List<Listing> cachedListings;
|
||||||
|
private long cacheExpiry = 0;
|
||||||
|
|
||||||
|
// Track pending operations to prevent double-purchases
|
||||||
|
private final Map<Integer, Boolean> pendingPurchases = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public ListingService(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new listing for a player
|
||||||
|
*
|
||||||
|
* @param player The seller
|
||||||
|
* @param item The item to sell
|
||||||
|
* @param amount Amount of items
|
||||||
|
* @param price Total price for all items
|
||||||
|
* @param durationHours Duration in hours
|
||||||
|
* @return CompletableFuture with the listing ID or -1 if failed
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> createListing(Player player, ItemStack item, int amount, double price, int durationHours) {
|
||||||
|
// Validate
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
return CompletableFuture.completedFuture(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||||
|
return CompletableFuture.completedFuture(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create listing object
|
||||||
|
Listing listing = new Listing(
|
||||||
|
player.getUniqueId(),
|
||||||
|
player.getName(),
|
||||||
|
item.clone(),
|
||||||
|
amount,
|
||||||
|
price,
|
||||||
|
Instant.now().plusSeconds(durationHours * 3600L)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
return plugin.getDatabaseManager().createListing(listing)
|
||||||
|
.thenApply(id -> {
|
||||||
|
if (id > 0) {
|
||||||
|
invalidateCache();
|
||||||
|
// Update cooldown
|
||||||
|
plugin.getDatabaseManager().updateLastListingTime(player.getUniqueId());
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all active listings with caching
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Listing>> getActiveListings() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Return cached if still valid
|
||||||
|
if (cachedListings != null && now < cacheExpiry) {
|
||||||
|
return CompletableFuture.completedFuture(cachedListings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.getDatabaseManager().getActiveListings()
|
||||||
|
.thenApply(listings -> {
|
||||||
|
cachedListings = listings;
|
||||||
|
cacheExpiry = System.currentTimeMillis() +
|
||||||
|
(plugin.getConfigManager().getCacheDuration() * 1000L);
|
||||||
|
return listings;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a specific listing by ID
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Optional<Listing>> getListing(int id) {
|
||||||
|
return plugin.getDatabaseManager().getListing(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all listings for a specific player
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Listing>> getPlayerListings(UUID playerUuid) {
|
||||||
|
return plugin.getDatabaseManager().getPlayerListings(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts active listings for a player
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Integer> countPlayerListings(UUID playerUuid) {
|
||||||
|
return plugin.getDatabaseManager().countPlayerListings(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a player can create a new listing (not at limit and no cooldown)
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> canCreateListing(UUID playerUuid) {
|
||||||
|
int maxListings = plugin.getConfigManager().getMaxListingsPerPlayer();
|
||||||
|
|
||||||
|
return countPlayerListings(playerUuid)
|
||||||
|
.thenCompose(count -> {
|
||||||
|
if (count >= maxListings) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cooldown = plugin.getConfigManager().getListingCooldown();
|
||||||
|
if (cooldown <= 0) {
|
||||||
|
return CompletableFuture.completedFuture(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.getDatabaseManager().getLastListingTime(playerUuid)
|
||||||
|
.thenApply(lastTime -> {
|
||||||
|
if (lastTime.isEmpty()) return true;
|
||||||
|
return Instant.now().isAfter(lastTime.get().plusSeconds(cooldown));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the remaining cooldown time in seconds
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Long> getRemainingCooldown(UUID playerUuid) {
|
||||||
|
int cooldown = plugin.getConfigManager().getListingCooldown();
|
||||||
|
if (cooldown <= 0) {
|
||||||
|
return CompletableFuture.completedFuture(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.getDatabaseManager().getLastListingTime(playerUuid)
|
||||||
|
.thenApply(lastTime -> {
|
||||||
|
if (lastTime.isEmpty()) return 0L;
|
||||||
|
long remaining = lastTime.get().plusSeconds(cooldown).getEpochSecond() - Instant.now().getEpochSecond();
|
||||||
|
return Math.max(0, remaining);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to purchase a listing.
|
||||||
|
* This is an atomic operation that prevents double-purchases.
|
||||||
|
*
|
||||||
|
* @param listingId The listing to purchase
|
||||||
|
* @param buyer The buyer
|
||||||
|
* @return CompletableFuture with success status
|
||||||
|
*/
|
||||||
|
public CompletableFuture<PurchaseResult> purchaseListing(int listingId, Player buyer) {
|
||||||
|
// Check if already processing this listing
|
||||||
|
if (pendingPurchases.putIfAbsent(listingId, true) != null) {
|
||||||
|
return CompletableFuture.completedFuture(PurchaseResult.ALREADY_PROCESSING);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getListing(listingId)
|
||||||
|
.thenCompose(optListing -> {
|
||||||
|
if (optListing.isEmpty()) {
|
||||||
|
pendingPurchases.remove(listingId);
|
||||||
|
return CompletableFuture.completedFuture(PurchaseResult.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
Listing listing = optListing.get();
|
||||||
|
|
||||||
|
// Can't buy own listing
|
||||||
|
if (listing.getSellerUuid().equals(buyer.getUniqueId())) {
|
||||||
|
pendingPurchases.remove(listingId);
|
||||||
|
return CompletableFuture.completedFuture(PurchaseResult.OWN_LISTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check buyer funds
|
||||||
|
if (!plugin.getEconomyManager().has(buyer.getUniqueId(), listing.getPrice())) {
|
||||||
|
pendingPurchases.remove(listingId);
|
||||||
|
return CompletableFuture.completedFuture(PurchaseResult.INSUFFICIENT_FUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt atomic purchase in DB
|
||||||
|
return plugin.getDatabaseManager().purchaseListing(listingId, buyer.getUniqueId(), buyer.getName())
|
||||||
|
.thenApply(success -> {
|
||||||
|
if (!success) {
|
||||||
|
pendingPurchases.remove(listingId);
|
||||||
|
return PurchaseResult.ALREADY_SOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Withdraw from buyer
|
||||||
|
if (!plugin.getEconomyManager().withdraw(buyer.getUniqueId(), listing.getPrice())) {
|
||||||
|
// Rollback DB change
|
||||||
|
plugin.getDatabaseManager().updateListingStatus(listingId, Listing.ListingStatus.ACTIVE);
|
||||||
|
pendingPurchases.remove(listingId);
|
||||||
|
return PurchaseResult.ECONOMY_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate seller earnings after tax
|
||||||
|
double tax = plugin.getConfigManager().getMarketTax();
|
||||||
|
double sellerEarnings = listing.getPrice() * (1 - tax / 100);
|
||||||
|
|
||||||
|
// Add pending earnings for seller
|
||||||
|
PendingEarnings earnings = new PendingEarnings(
|
||||||
|
listing.getSellerUuid(),
|
||||||
|
sellerEarnings,
|
||||||
|
"Listing #" + listingId
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addPendingEarnings(earnings);
|
||||||
|
|
||||||
|
// Give item to buyer or add to claim storage
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
ItemStack item = listing.getItem().clone();
|
||||||
|
item.setAmount(listing.getAmount());
|
||||||
|
ItemStack leftover = InventoryUtil.giveItem(buyer, item);
|
||||||
|
|
||||||
|
if (leftover != null) {
|
||||||
|
// Couldn't fit in inventory, add to claim storage
|
||||||
|
ClaimItem claimItem = new ClaimItem(
|
||||||
|
buyer.getUniqueId(),
|
||||||
|
leftover,
|
||||||
|
ClaimItem.ClaimReason.PURCHASE_FULL_INVENTORY,
|
||||||
|
"Listing #" + listingId
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||||
|
buyer.sendMessage(plugin.getMessageManager().getPrefixed("messages.claim-inventory-full"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify seller if online
|
||||||
|
if (plugin.getConfigManager().isNotifyOnSale()) {
|
||||||
|
Player seller = Bukkit.getPlayer(listing.getSellerUuid());
|
||||||
|
if (seller != null && seller.isOnline()) {
|
||||||
|
Map<String, String> placeholders = Map.of(
|
||||||
|
"item", listing.getItem().getType().name(),
|
||||||
|
"amount", String.valueOf(listing.getAmount()),
|
||||||
|
"buyer", buyer.getName(),
|
||||||
|
"price", plugin.getMessageManager().formatCurrency(listing.getPrice())
|
||||||
|
);
|
||||||
|
seller.sendMessage(plugin.getMessageManager().getPrefixed("messages.listing-sold", placeholders));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateCache();
|
||||||
|
pendingPurchases.remove(listingId);
|
||||||
|
return PurchaseResult.SUCCESS;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
pendingPurchases.remove(listingId);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a listing and returns the item to the seller's claim storage
|
||||||
|
*
|
||||||
|
* @param listingId The listing to cancel
|
||||||
|
* @param playerUuid The player attempting to cancel (must be seller or admin)
|
||||||
|
* @param isAdmin Whether this is an admin action
|
||||||
|
* @return CompletableFuture with success status
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> cancelListing(int listingId, UUID playerUuid, boolean isAdmin) {
|
||||||
|
return getListing(listingId)
|
||||||
|
.thenCompose(optListing -> {
|
||||||
|
if (optListing.isEmpty()) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Listing listing = optListing.get();
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!isAdmin && !listing.getSellerUuid().equals(playerUuid)) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
return plugin.getDatabaseManager().updateListingStatus(listingId, Listing.ListingStatus.CANCELLED)
|
||||||
|
.thenApply(success -> {
|
||||||
|
if (success) {
|
||||||
|
// Return item to claim storage
|
||||||
|
ItemStack item = listing.getItem().clone();
|
||||||
|
item.setAmount(listing.getAmount());
|
||||||
|
ClaimItem claimItem = new ClaimItem(
|
||||||
|
listing.getSellerUuid(),
|
||||||
|
item,
|
||||||
|
ClaimItem.ClaimReason.CANCELLED_LISTING,
|
||||||
|
"Listing #" + listingId
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||||
|
invalidateCache();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes expired listings - moves items to claim storage
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> processExpiredListings() {
|
||||||
|
return plugin.getDatabaseManager().getExpiredListings()
|
||||||
|
.thenAccept(listings -> {
|
||||||
|
for (Listing listing : listings) {
|
||||||
|
// Update status to expired
|
||||||
|
plugin.getDatabaseManager().updateListingStatus(listing.getId(), Listing.ListingStatus.EXPIRED)
|
||||||
|
.thenAccept(success -> {
|
||||||
|
if (success) {
|
||||||
|
// Return item to claim storage
|
||||||
|
ItemStack item = listing.getItem().clone();
|
||||||
|
item.setAmount(listing.getAmount());
|
||||||
|
ClaimItem claimItem = new ClaimItem(
|
||||||
|
listing.getSellerUuid(),
|
||||||
|
item,
|
||||||
|
ClaimItem.ClaimReason.EXPIRED_LISTING,
|
||||||
|
"Listing #" + listing.getId()
|
||||||
|
);
|
||||||
|
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||||
|
|
||||||
|
// Notify seller if online
|
||||||
|
if (plugin.getConfigManager().isNotifyOnExpire()) {
|
||||||
|
Player seller = Bukkit.getPlayer(listing.getSellerUuid());
|
||||||
|
if (seller != null && seller.isOnline()) {
|
||||||
|
seller.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||||
|
"messages.listing-expired",
|
||||||
|
"id", String.valueOf(listing.getId())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!listings.isEmpty()) {
|
||||||
|
invalidateCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the listing cache
|
||||||
|
*/
|
||||||
|
public void invalidateCache() {
|
||||||
|
cachedListings = null;
|
||||||
|
cacheExpiry = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a purchase attempt
|
||||||
|
*/
|
||||||
|
public enum PurchaseResult {
|
||||||
|
SUCCESS,
|
||||||
|
NOT_FOUND,
|
||||||
|
ALREADY_SOLD,
|
||||||
|
OWN_LISTING,
|
||||||
|
INSUFFICIENT_FUNDS,
|
||||||
|
ECONOMY_ERROR,
|
||||||
|
ALREADY_PROCESSING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
package pt.henrique.communityMarket.service;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service layer for handling marketplace transactions.
|
||||||
|
* Provides a unified interface for complex operations that
|
||||||
|
* involve multiple services (purchases, bids, etc.)
|
||||||
|
*/
|
||||||
|
public class TransactionService {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public TransactionService(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an item before it can be listed or auctioned.
|
||||||
|
* Checks material blacklist, keywords in name/lore, and other restrictions.
|
||||||
|
*
|
||||||
|
* @param item The item to validate
|
||||||
|
* @return ValidationResult with success status and error message if applicable
|
||||||
|
*/
|
||||||
|
public ValidationResult validateItem(ItemStack item) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
return new ValidationResult(false, "invalid-item");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check material blacklist
|
||||||
|
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||||
|
return new ValidationResult(false, "blacklisted-item");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for blacklisted keywords in item name/lore
|
||||||
|
if (item.hasItemMeta()) {
|
||||||
|
var meta = item.getItemMeta();
|
||||||
|
|
||||||
|
// Check display name
|
||||||
|
if (meta.hasDisplayName()) {
|
||||||
|
String displayName = meta.getDisplayName();
|
||||||
|
if (plugin.getConfigManager().containsBlacklistedKeyword(displayName)) {
|
||||||
|
return new ValidationResult(false, "blacklisted-content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check lore
|
||||||
|
if (meta.hasLore()) {
|
||||||
|
for (String loreLine : meta.getLore()) {
|
||||||
|
if (plugin.getConfigManager().containsBlacklistedKeyword(loreLine)) {
|
||||||
|
return new ValidationResult(false, "blacklisted-content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValidationResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a price for a listing
|
||||||
|
*
|
||||||
|
* @param price The price to validate
|
||||||
|
* @return true if the price is within allowed range
|
||||||
|
*/
|
||||||
|
public boolean validateListingPrice(double price) {
|
||||||
|
double min = plugin.getConfigManager().getMinPrice();
|
||||||
|
double max = plugin.getConfigManager().getMaxPrice();
|
||||||
|
return price >= min && price <= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a starting price for an auction
|
||||||
|
*
|
||||||
|
* @param price The price to validate
|
||||||
|
* @return true if the price is within allowed range
|
||||||
|
*/
|
||||||
|
public boolean validateAuctionStartPrice(double price) {
|
||||||
|
double min = plugin.getConfigManager().getMinStartPrice();
|
||||||
|
double max = plugin.getConfigManager().getMaxStartPrice();
|
||||||
|
return price >= min && price <= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an auction duration
|
||||||
|
*
|
||||||
|
* @param hours The duration in hours
|
||||||
|
* @return true if the duration is within allowed range
|
||||||
|
*/
|
||||||
|
public boolean validateAuctionDuration(int hours) {
|
||||||
|
int min = plugin.getConfigManager().getMinDurationHours();
|
||||||
|
int max = plugin.getConfigManager().getMaxDurationHours();
|
||||||
|
return hours >= min && hours <= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the tax amount for a listing sale
|
||||||
|
*
|
||||||
|
* @param salePrice The sale price
|
||||||
|
* @return The tax amount
|
||||||
|
*/
|
||||||
|
public double calculateListingTax(double salePrice) {
|
||||||
|
double taxPercent = plugin.getConfigManager().getMarketTax();
|
||||||
|
return salePrice * (taxPercent / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the seller's earnings after tax for a listing
|
||||||
|
*
|
||||||
|
* @param salePrice The sale price
|
||||||
|
* @return The amount the seller receives
|
||||||
|
*/
|
||||||
|
public double calculateListingEarnings(double salePrice) {
|
||||||
|
return salePrice - calculateListingTax(salePrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the tax amount for an auction sale
|
||||||
|
*
|
||||||
|
* @param salePrice The final sale price
|
||||||
|
* @return The tax amount
|
||||||
|
*/
|
||||||
|
public double calculateAuctionTax(double salePrice) {
|
||||||
|
double taxPercent = plugin.getConfigManager().getAuctionTax();
|
||||||
|
return salePrice * (taxPercent / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the seller's earnings after tax for an auction
|
||||||
|
*
|
||||||
|
* @param salePrice The final sale price
|
||||||
|
* @return The amount the seller receives
|
||||||
|
*/
|
||||||
|
public double calculateAuctionEarnings(double salePrice) {
|
||||||
|
return salePrice - calculateAuctionTax(salePrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a listing with full validation and item removal
|
||||||
|
*
|
||||||
|
* @param player The seller
|
||||||
|
* @param item The item to list
|
||||||
|
* @param amount Amount to list
|
||||||
|
* @param price Total price
|
||||||
|
* @param durationHours Duration in hours
|
||||||
|
* @return CompletableFuture with transaction result
|
||||||
|
*/
|
||||||
|
public CompletableFuture<TransactionResult> createListingTransaction(
|
||||||
|
Player player, ItemStack item, int amount, double price, int durationHours) {
|
||||||
|
|
||||||
|
// Validate item
|
||||||
|
ValidationResult validation = validateItem(item);
|
||||||
|
if (!validation.isValid()) {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
TransactionResult.failed(validation.getErrorKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate price
|
||||||
|
if (!validateListingPrice(price)) {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
TransactionResult.failed("invalid-price"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check listing limit
|
||||||
|
return plugin.getListingService().canCreateListing(player.getUniqueId())
|
||||||
|
.thenCompose(canCreate -> {
|
||||||
|
if (!canCreate) {
|
||||||
|
// Check if it's cooldown or limit
|
||||||
|
return plugin.getListingService().getRemainingCooldown(player.getUniqueId())
|
||||||
|
.thenApply(cooldown -> {
|
||||||
|
if (cooldown > 0) {
|
||||||
|
return TransactionResult.failed("listing-cooldown");
|
||||||
|
}
|
||||||
|
return TransactionResult.failed("listing-limit-reached");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the listing
|
||||||
|
ItemStack listItem = item.clone();
|
||||||
|
listItem.setAmount(amount);
|
||||||
|
|
||||||
|
return plugin.getListingService().createListing(player, listItem, amount, price, durationHours)
|
||||||
|
.thenApply(id -> {
|
||||||
|
if (id > 0) {
|
||||||
|
return TransactionResult.success(id);
|
||||||
|
}
|
||||||
|
return TransactionResult.failed("failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an auction with full validation and item removal
|
||||||
|
*
|
||||||
|
* @param player The seller
|
||||||
|
* @param item The item to auction
|
||||||
|
* @param startPrice Starting bid
|
||||||
|
* @param buyoutPrice Optional buyout price
|
||||||
|
* @param durationHours Duration in hours
|
||||||
|
* @return CompletableFuture with transaction result
|
||||||
|
*/
|
||||||
|
public CompletableFuture<TransactionResult> createAuctionTransaction(
|
||||||
|
Player player, ItemStack item, double startPrice, Double buyoutPrice, int durationHours) {
|
||||||
|
|
||||||
|
// Validate item
|
||||||
|
ValidationResult validation = validateItem(item);
|
||||||
|
if (!validation.isValid()) {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
TransactionResult.failed(validation.getErrorKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prices
|
||||||
|
if (!validateAuctionStartPrice(startPrice)) {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
TransactionResult.failed("invalid-price"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buyoutPrice != null && buyoutPrice <= startPrice) {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
TransactionResult.failed("invalid-price"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate duration
|
||||||
|
if (!validateAuctionDuration(durationHours)) {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
TransactionResult.failed("invalid-duration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auction limit
|
||||||
|
return plugin.getAuctionService().canCreateAuction(player.getUniqueId())
|
||||||
|
.thenCompose(canCreate -> {
|
||||||
|
if (!canCreate) {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
TransactionResult.failed("auction-limit-reached"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the auction
|
||||||
|
return plugin.getAuctionService().createAuction(
|
||||||
|
player, item.clone(), startPrice, buyoutPrice, durationHours)
|
||||||
|
.thenApply(id -> {
|
||||||
|
if (id > 0) {
|
||||||
|
return TransactionResult.success(id);
|
||||||
|
}
|
||||||
|
return TransactionResult.failed("failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of item validation
|
||||||
|
*/
|
||||||
|
public record ValidationResult(boolean isValid, String errorKey) {
|
||||||
|
public String getErrorKey() {
|
||||||
|
return errorKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a transaction
|
||||||
|
*/
|
||||||
|
public static class TransactionResult {
|
||||||
|
private final boolean success;
|
||||||
|
private final int id;
|
||||||
|
private final String errorKey;
|
||||||
|
|
||||||
|
private TransactionResult(boolean success, int id, String errorKey) {
|
||||||
|
this.success = success;
|
||||||
|
this.id = id;
|
||||||
|
this.errorKey = errorKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransactionResult success(int id) {
|
||||||
|
return new TransactionResult(true, id, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransactionResult failed(String errorKey) {
|
||||||
|
return new TransactionResult(false, -1, errorKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorKey() {
|
||||||
|
return errorKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package pt.henrique.communityMarket.task;
|
||||||
|
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodic task that checks for ended auctions and processes them.
|
||||||
|
* Runs asynchronously to avoid blocking the main thread.
|
||||||
|
*/
|
||||||
|
public class AuctionTask extends BukkitRunnable {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public AuctionTask(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Process ended auctions
|
||||||
|
plugin.getAuctionService().processEndedAuctions()
|
||||||
|
.exceptionally(ex -> {
|
||||||
|
plugin.getLogger().warning("Error processing ended auctions: " + ex.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package pt.henrique.communityMarket.task;
|
||||||
|
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodic task that checks for expired listings and moves items to claim storage.
|
||||||
|
* Runs asynchronously to avoid blocking the main thread.
|
||||||
|
*/
|
||||||
|
public class ExpiredListingTask extends BukkitRunnable {
|
||||||
|
|
||||||
|
private final CommunityMarket plugin;
|
||||||
|
|
||||||
|
public ExpiredListingTask(CommunityMarket plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Process expired listings
|
||||||
|
plugin.getListingService().processExpiredListings()
|
||||||
|
.exceptionally(ex -> {
|
||||||
|
plugin.getLogger().warning("Error processing expired listings: " + ex.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package pt.henrique.communityMarket.util;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import pt.henrique.communityMarket.CommunityMarket;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for inventory operations
|
||||||
|
*/
|
||||||
|
public class InventoryUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a player has space for an item in their inventory
|
||||||
|
*
|
||||||
|
* @param player The player
|
||||||
|
* @param item The item to check
|
||||||
|
* @return True if the player has space
|
||||||
|
*/
|
||||||
|
public static boolean hasSpace(Player player, ItemStack item) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to add a clone and see if anything remains
|
||||||
|
ItemStack clone = item.clone();
|
||||||
|
Map<Integer, ItemStack> leftover = new HashMap<>();
|
||||||
|
|
||||||
|
// Check each slot
|
||||||
|
for (int i = 0; i < 36; i++) {
|
||||||
|
ItemStack slot = player.getInventory().getItem(i);
|
||||||
|
|
||||||
|
if (slot == null || slot.getType().isAir()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot.isSimilar(clone) && slot.getAmount() < slot.getMaxStackSize()) {
|
||||||
|
int canAdd = slot.getMaxStackSize() - slot.getAmount();
|
||||||
|
if (canAdd >= clone.getAmount()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives an item to a player, returning any items that couldn't fit
|
||||||
|
*
|
||||||
|
* @param player The player
|
||||||
|
* @param item The item to give
|
||||||
|
* @return Items that couldn't fit, or null if all items were added
|
||||||
|
*/
|
||||||
|
public static ItemStack giveItem(Player player, ItemStack item) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap<Integer, ItemStack> leftover = player.getInventory().addItem(item.clone());
|
||||||
|
|
||||||
|
if (leftover.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftover.values().iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a specific amount of an item from a player's inventory
|
||||||
|
*
|
||||||
|
* @param player The player
|
||||||
|
* @param item The item type to remove
|
||||||
|
* @param amount The amount to remove
|
||||||
|
* @return True if the full amount was removed
|
||||||
|
*/
|
||||||
|
public static boolean removeItem(Player player, ItemStack item, int amount) {
|
||||||
|
if (item == null || amount <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack toRemove = item.clone();
|
||||||
|
toRemove.setAmount(amount);
|
||||||
|
|
||||||
|
HashMap<Integer, ItemStack> notRemoved = player.getInventory().removeItem(toRemove);
|
||||||
|
|
||||||
|
return notRemoved.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts how many of a specific item a player has
|
||||||
|
*
|
||||||
|
* @param player The player
|
||||||
|
* @param item The item type to count
|
||||||
|
* @return Total count
|
||||||
|
*/
|
||||||
|
public static int countItem(Player player, ItemStack item) {
|
||||||
|
if (item == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (ItemStack slot : player.getInventory().getContents()) {
|
||||||
|
if (slot != null && slot.isSimilar(item)) {
|
||||||
|
count += slot.getAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts how many similar items exist in an inventory.
|
||||||
|
* Uses strict comparison including all metadata (material, name, lore,
|
||||||
|
* enchantments, custom model data, etc.).
|
||||||
|
*
|
||||||
|
* @param inventory The inventory to search
|
||||||
|
* @param item The item to match against
|
||||||
|
* @return Total count of matching items
|
||||||
|
*/
|
||||||
|
public static int countSimilarItems(org.bukkit.inventory.Inventory inventory, ItemStack item) {
|
||||||
|
if (item == null || inventory == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (ItemStack slot : inventory.getContents()) {
|
||||||
|
if (slot != null && slot.isSimilar(item)) {
|
||||||
|
count += slot.getAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a display name for an item, using material name if custom name is not set
|
||||||
|
*
|
||||||
|
* @param item The item
|
||||||
|
* @return Display name
|
||||||
|
*/
|
||||||
|
public static String getDisplayName(ItemStack item) {
|
||||||
|
if (item == null) {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null && meta.hasDisplayName()) {
|
||||||
|
return TextUtil.stripColor(meta.getDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatMaterialName(item.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a material name into a readable string
|
||||||
|
*
|
||||||
|
* @param material The material
|
||||||
|
* @return Formatted name (e.g., "Diamond Sword")
|
||||||
|
*/
|
||||||
|
public static String formatMaterialName(Material material) {
|
||||||
|
String name = material.name().replace("_", " ").toLowerCase();
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
boolean capitalizeNext = true;
|
||||||
|
|
||||||
|
for (char c : name.toCharArray()) {
|
||||||
|
if (c == ' ') {
|
||||||
|
result.append(c);
|
||||||
|
capitalizeNext = true;
|
||||||
|
} else if (capitalizeNext) {
|
||||||
|
result.append(Character.toUpperCase(c));
|
||||||
|
capitalizeNext = false;
|
||||||
|
} else {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the category of an item for filtering purposes
|
||||||
|
*
|
||||||
|
* @param item The item
|
||||||
|
* @return Category name
|
||||||
|
*/
|
||||||
|
public static ItemCategory getCategory(ItemStack item) {
|
||||||
|
if (item == null) {
|
||||||
|
return ItemCategory.MISC;
|
||||||
|
}
|
||||||
|
|
||||||
|
Material material = item.getType();
|
||||||
|
String name = material.name();
|
||||||
|
|
||||||
|
// Check for enchantments first
|
||||||
|
if (item.hasItemMeta() && item.getItemMeta().hasEnchants()) {
|
||||||
|
return ItemCategory.ENCHANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weapons
|
||||||
|
if (name.endsWith("_SWORD") || name.endsWith("_AXE") || name.equals("BOW") ||
|
||||||
|
name.equals("CROSSBOW") || name.equals("TRIDENT") || name.equals("MACE")) {
|
||||||
|
return ItemCategory.WEAPONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Armor
|
||||||
|
if (name.endsWith("_HELMET") || name.endsWith("_CHESTPLATE") ||
|
||||||
|
name.endsWith("_LEGGINGS") || name.endsWith("_BOOTS") ||
|
||||||
|
name.equals("SHIELD") || name.equals("ELYTRA")) {
|
||||||
|
return ItemCategory.ARMOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
if (name.endsWith("_PICKAXE") || name.endsWith("_SHOVEL") ||
|
||||||
|
name.endsWith("_HOE") || name.equals("FISHING_ROD") ||
|
||||||
|
name.equals("FLINT_AND_STEEL") || name.equals("SHEARS")) {
|
||||||
|
return ItemCategory.TOOLS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks
|
||||||
|
if (material.isBlock()) {
|
||||||
|
return ItemCategory.BLOCKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Food
|
||||||
|
if (material.isEdible()) {
|
||||||
|
return ItemCategory.FOOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potions
|
||||||
|
if (name.contains("POTION") || name.equals("DRAGON_BREATH")) {
|
||||||
|
return ItemCategory.POTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ItemCategory.MISC;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ItemCategory {
|
||||||
|
ALL("All Items", Material.CHEST),
|
||||||
|
WEAPONS("Weapons", Material.DIAMOND_SWORD),
|
||||||
|
ARMOR("Armor", Material.DIAMOND_CHESTPLATE),
|
||||||
|
TOOLS("Tools", Material.DIAMOND_PICKAXE),
|
||||||
|
BLOCKS("Blocks", Material.GRASS_BLOCK),
|
||||||
|
FOOD("Food", Material.GOLDEN_APPLE),
|
||||||
|
POTIONS("Potions", Material.POTION),
|
||||||
|
ENCHANTED("Enchanted", Material.ENCHANTED_BOOK),
|
||||||
|
MISC("Miscellaneous", Material.ENDER_PEARL);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final Material icon;
|
||||||
|
|
||||||
|
ItemCategory(String displayName, Material icon) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Material getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package pt.henrique.communityMarket.util;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.enchantments.Enchantment;
|
||||||
|
import org.bukkit.inventory.ItemFlag;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fluent builder for creating ItemStacks with custom properties.
|
||||||
|
* Used throughout the GUI system to create menu items.
|
||||||
|
*/
|
||||||
|
public class ItemBuilder {
|
||||||
|
|
||||||
|
private final ItemStack item;
|
||||||
|
private final ItemMeta meta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ItemBuilder with the specified material
|
||||||
|
*/
|
||||||
|
public ItemBuilder(Material material) {
|
||||||
|
this(material, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ItemBuilder with the specified material and amount
|
||||||
|
*/
|
||||||
|
public ItemBuilder(Material material, int amount) {
|
||||||
|
this.item = new ItemStack(material, amount);
|
||||||
|
this.meta = item.getItemMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ItemBuilder from an existing ItemStack
|
||||||
|
*/
|
||||||
|
public ItemBuilder(ItemStack item) {
|
||||||
|
this.item = item.clone();
|
||||||
|
this.meta = this.item.getItemMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the display name using legacy color codes
|
||||||
|
*/
|
||||||
|
public ItemBuilder name(String name) {
|
||||||
|
if (meta != null) {
|
||||||
|
meta.displayName(TextUtil.colorize(name));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the display name using a Component
|
||||||
|
*/
|
||||||
|
public ItemBuilder name(Component name) {
|
||||||
|
if (meta != null) {
|
||||||
|
meta.displayName(name);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the lore using a list of strings with legacy color codes
|
||||||
|
*/
|
||||||
|
public ItemBuilder lore(List<String> lore) {
|
||||||
|
if (meta != null && lore != null) {
|
||||||
|
List<Component> components = lore.stream()
|
||||||
|
.map(TextUtil::colorize)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
meta.lore(components);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the lore using varargs strings
|
||||||
|
*/
|
||||||
|
public ItemBuilder lore(String... lore) {
|
||||||
|
return lore(List.of(lore));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a line to the existing lore
|
||||||
|
*/
|
||||||
|
public ItemBuilder addLore(String line) {
|
||||||
|
if (meta != null) {
|
||||||
|
List<Component> existingLore = meta.lore();
|
||||||
|
List<Component> newLore = existingLore != null ? new ArrayList<>(existingLore) : new ArrayList<>();
|
||||||
|
newLore.add(TextUtil.colorize(line));
|
||||||
|
meta.lore(newLore);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple lines to the existing lore
|
||||||
|
*/
|
||||||
|
public ItemBuilder addLore(List<String> lines) {
|
||||||
|
if (meta != null && lines != null) {
|
||||||
|
List<Component> existingLore = meta.lore();
|
||||||
|
List<Component> newLore = existingLore != null ? new ArrayList<>(existingLore) : new ArrayList<>();
|
||||||
|
for (String line : lines) {
|
||||||
|
newLore.add(TextUtil.colorize(line));
|
||||||
|
}
|
||||||
|
meta.lore(newLore);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the item amount
|
||||||
|
*/
|
||||||
|
public ItemBuilder amount(int amount) {
|
||||||
|
item.setAmount(Math.min(64, Math.max(1, amount)));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an enchantment glow effect (uses LUCK with HIDE_ENCHANTS flag)
|
||||||
|
*/
|
||||||
|
public ItemBuilder glow(boolean glow) {
|
||||||
|
if (glow && meta != null) {
|
||||||
|
meta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||||
|
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always adds glow effect
|
||||||
|
*/
|
||||||
|
public ItemBuilder glow() {
|
||||||
|
return glow(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an enchantment
|
||||||
|
*/
|
||||||
|
public ItemBuilder enchant(Enchantment enchantment, int level) {
|
||||||
|
if (meta != null) {
|
||||||
|
meta.addEnchant(enchantment, level, true);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides all item flags (enchants, attributes, etc.)
|
||||||
|
*/
|
||||||
|
public ItemBuilder hideFlags() {
|
||||||
|
if (meta != null) {
|
||||||
|
meta.addItemFlags(ItemFlag.values());
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds specific item flags
|
||||||
|
*/
|
||||||
|
public ItemBuilder addFlags(ItemFlag... flags) {
|
||||||
|
if (meta != null) {
|
||||||
|
meta.addItemFlags(flags);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the item as unbreakable
|
||||||
|
*/
|
||||||
|
public ItemBuilder unbreakable(boolean unbreakable) {
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setUnbreakable(unbreakable);
|
||||||
|
if (unbreakable) {
|
||||||
|
meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets custom model data
|
||||||
|
*/
|
||||||
|
public ItemBuilder customModelData(int data) {
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setCustomModelData(data);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns the final ItemStack
|
||||||
|
*/
|
||||||
|
public ItemStack build() {
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy of an ItemStack with modified lore
|
||||||
|
*/
|
||||||
|
public static ItemStack withLore(ItemStack original, List<String> lore) {
|
||||||
|
return new ItemBuilder(original).lore(lore).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy of an ItemStack with additional lore appended
|
||||||
|
*/
|
||||||
|
public static ItemStack appendLore(ItemStack original, List<String> additionalLore) {
|
||||||
|
return new ItemBuilder(original).addLore(additionalLore).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package pt.henrique.communityMarket.util;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||||
|
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for serializing and deserializing ItemStacks to/from Base64 strings
|
||||||
|
*/
|
||||||
|
public class ItemSerializer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes an ItemStack to a Base64 encoded string
|
||||||
|
*
|
||||||
|
* @param item The ItemStack to serialize
|
||||||
|
* @return Base64 encoded string representation
|
||||||
|
* @throws IOException If serialization fails
|
||||||
|
*/
|
||||||
|
public static String serialize(ItemStack item) throws IOException {
|
||||||
|
if (item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
|
||||||
|
|
||||||
|
dataOutput.writeObject(item);
|
||||||
|
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes a Base64 encoded string to an ItemStack
|
||||||
|
*
|
||||||
|
* @param data The Base64 encoded string
|
||||||
|
* @return The deserialized ItemStack
|
||||||
|
* @throws IOException If deserialization fails
|
||||||
|
* @throws ClassNotFoundException If the class is not found
|
||||||
|
*/
|
||||||
|
public static ItemStack deserialize(String data) throws IOException, ClassNotFoundException {
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = Base64.getDecoder().decode(data);
|
||||||
|
|
||||||
|
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||||
|
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) {
|
||||||
|
|
||||||
|
return (ItemStack) dataInput.readObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely deserializes an ItemStack, returning null if any error occurs
|
||||||
|
*
|
||||||
|
* @param data The Base64 encoded string
|
||||||
|
* @return The deserialized ItemStack or null if failed
|
||||||
|
*/
|
||||||
|
public static ItemStack deserializeSafe(String data) {
|
||||||
|
try {
|
||||||
|
return deserialize(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package pt.henrique.communityMarket.util;
|
||||||
|
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.Registry;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for playing sounds.
|
||||||
|
* Handles Sound API changes gracefully for Paper 1.21+.
|
||||||
|
*/
|
||||||
|
public class SoundUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays a sound to a player by name.
|
||||||
|
*
|
||||||
|
* @param player The player
|
||||||
|
* @param soundName The sound name (e.g., "UI_BUTTON_CLICK" or "ui.button.click")
|
||||||
|
* @param volume Volume (0.0 - 1.0)
|
||||||
|
* @param pitch Pitch (0.5 - 2.0)
|
||||||
|
*/
|
||||||
|
public static void playSound(Player player, String soundName, float volume, float pitch) {
|
||||||
|
if (player == null || soundName == null || soundName.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Sound sound = findSound(soundName);
|
||||||
|
if (sound != null) {
|
||||||
|
player.playSound(player.getLocation(), sound, volume, pitch);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Sound not found or error playing, ignore silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays a sound with default volume and pitch.
|
||||||
|
*/
|
||||||
|
public static void playSound(Player player, String soundName) {
|
||||||
|
playSound(player, soundName, 0.5f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a Sound by name using the Registry API (Paper 1.21+).
|
||||||
|
* Supports both formats: "UI_BUTTON_CLICK" and "ui.button.click"
|
||||||
|
*/
|
||||||
|
private static Sound findSound(String name) {
|
||||||
|
if (name == null) return null;
|
||||||
|
|
||||||
|
// Convert legacy format (UI_BUTTON_CLICK) to namespaced format (ui.button.click)
|
||||||
|
String key = name.toLowerCase().replace("_", ".");
|
||||||
|
|
||||||
|
// Try direct lookup with the converted key
|
||||||
|
Sound sound = Registry.SOUNDS.get(NamespacedKey.minecraft(key));
|
||||||
|
if (sound != null) {
|
||||||
|
return sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try some common mappings for legacy sound names
|
||||||
|
String mappedKey = mapLegacySoundName(name);
|
||||||
|
if (mappedKey != null) {
|
||||||
|
sound = Registry.SOUNDS.get(NamespacedKey.minecraft(mappedKey));
|
||||||
|
if (sound != null) {
|
||||||
|
return sound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try the original name as-is (lowercase)
|
||||||
|
return Registry.SOUNDS.get(NamespacedKey.minecraft(name.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps legacy enum-style sound names to their registry keys.
|
||||||
|
*/
|
||||||
|
private static String mapLegacySoundName(String legacyName) {
|
||||||
|
if (legacyName == null) return null;
|
||||||
|
|
||||||
|
String upper = legacyName.toUpperCase();
|
||||||
|
|
||||||
|
return switch (upper) {
|
||||||
|
case "UI_BUTTON_CLICK" -> "ui.button.click";
|
||||||
|
case "ENTITY_PLAYER_LEVELUP" -> "entity.player.levelup";
|
||||||
|
case "ENTITY_VILLAGER_NO" -> "entity.villager.no";
|
||||||
|
case "ENTITY_EXPERIENCE_ORB_PICKUP" -> "entity.experience_orb.pickup";
|
||||||
|
case "BLOCK_NOTE_BLOCK_PLING" -> "block.note_block.pling";
|
||||||
|
case "ENTITY_ITEM_PICKUP" -> "entity.item.pickup";
|
||||||
|
case "BLOCK_CHEST_OPEN" -> "block.chest.open";
|
||||||
|
case "BLOCK_CHEST_CLOSE" -> "block.chest.close";
|
||||||
|
case "ENTITY_ARROW_HIT_PLAYER" -> "entity.arrow.hit_player";
|
||||||
|
case "BLOCK_ANVIL_USE" -> "block.anvil.use";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common sounds for easy access
|
||||||
|
*/
|
||||||
|
public static void playClickSound(Player player) {
|
||||||
|
playSound(player, "ui.button.click", 0.5f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void playSuccessSound(Player player) {
|
||||||
|
playSound(player, "entity.player.levelup", 0.5f, 1.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void playErrorSound(Player player) {
|
||||||
|
playSound(player, "entity.villager.no", 0.5f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void playPurchaseSound(Player player) {
|
||||||
|
playSound(player, "entity.experience_orb.pickup", 0.5f, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package pt.henrique.communityMarket.util;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for text formatting and color code handling
|
||||||
|
*/
|
||||||
|
public class TextUtil {
|
||||||
|
|
||||||
|
private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})");
|
||||||
|
private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacyAmpersand();
|
||||||
|
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colorizes a string using legacy & color codes
|
||||||
|
*
|
||||||
|
* @param text The text to colorize
|
||||||
|
* @return Colorized Component
|
||||||
|
*/
|
||||||
|
public static Component colorize(String text) {
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
return Component.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex codes like &#FFFFFF to adventure format
|
||||||
|
text = HEX_PATTERN.matcher(text).replaceAll("<#$1>");
|
||||||
|
|
||||||
|
// First try legacy format, then MiniMessage
|
||||||
|
try {
|
||||||
|
Component component = LEGACY_SERIALIZER.deserialize(text);
|
||||||
|
// Remove italic decoration that gets added by default
|
||||||
|
return component.decoration(TextDecoration.ITALIC, false);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return MINI_MESSAGE.deserialize(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colorizes a string and returns the legacy string representation
|
||||||
|
*
|
||||||
|
* @param text The text to colorize
|
||||||
|
* @return Colorized string with § codes
|
||||||
|
*/
|
||||||
|
public static String colorizeToString(String text) {
|
||||||
|
return LegacyComponentSerializer.legacySection().serialize(colorize(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips color codes from a string
|
||||||
|
*
|
||||||
|
* @param text The text to strip
|
||||||
|
* @return Text without color codes
|
||||||
|
*/
|
||||||
|
public static String stripColor(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return text.replaceAll("(?i)[&§][0-9a-fk-or]", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a duration into a human-readable string
|
||||||
|
*
|
||||||
|
* @param duration The duration to format
|
||||||
|
* @return Formatted string (e.g., "2d 5h 30m")
|
||||||
|
*/
|
||||||
|
public static String formatDuration(Duration duration) {
|
||||||
|
if (duration.isNegative() || duration.isZero()) {
|
||||||
|
return "Expired";
|
||||||
|
}
|
||||||
|
|
||||||
|
long days = duration.toDays();
|
||||||
|
long hours = duration.toHoursPart();
|
||||||
|
long minutes = duration.toMinutesPart();
|
||||||
|
long seconds = duration.toSecondsPart();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
sb.append(days).append("d ");
|
||||||
|
}
|
||||||
|
if (hours > 0) {
|
||||||
|
sb.append(hours).append("h ");
|
||||||
|
}
|
||||||
|
if (minutes > 0 && days == 0) {
|
||||||
|
sb.append(minutes).append("m ");
|
||||||
|
}
|
||||||
|
if (seconds > 0 && days == 0 && hours == 0) {
|
||||||
|
sb.append(seconds).append("s");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a duration from now until a future instant
|
||||||
|
*
|
||||||
|
* @param future The future instant
|
||||||
|
* @return Formatted duration string
|
||||||
|
*/
|
||||||
|
public static String formatTimeUntil(Instant future) {
|
||||||
|
if (future == null) {
|
||||||
|
return "Never";
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration duration = Duration.between(Instant.now(), future);
|
||||||
|
return formatDuration(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a duration is considered "ending soon" (less than 5 minutes)
|
||||||
|
*
|
||||||
|
* @param endsAt The end time
|
||||||
|
* @return True if ending soon
|
||||||
|
*/
|
||||||
|
public static boolean isEndingSoon(Instant endsAt) {
|
||||||
|
if (endsAt == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Duration remaining = Duration.between(Instant.now(), endsAt);
|
||||||
|
return !remaining.isNegative() && remaining.toMinutes() < 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncates a string to a maximum length
|
||||||
|
*
|
||||||
|
* @param text The text to truncate
|
||||||
|
* @param maxLength Maximum length
|
||||||
|
* @return Truncated string with "..." if needed
|
||||||
|
*/
|
||||||
|
public static String truncate(String text, int maxLength) {
|
||||||
|
if (text == null || text.length() <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return text.substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# ============================================
|
||||||
|
# CommunityMarket Configuration
|
||||||
|
# A GUI-only marketplace plugin
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Language setting (available: en_US, pt_PT)
|
||||||
|
language: en_US
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
database:
|
||||||
|
# Type: sqlite or mysql
|
||||||
|
type: sqlite
|
||||||
|
|
||||||
|
sqlite:
|
||||||
|
file: database.db
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: communitymarket
|
||||||
|
username: root
|
||||||
|
password: ""
|
||||||
|
pool:
|
||||||
|
maximum-pool-size: 10
|
||||||
|
minimum-idle: 2
|
||||||
|
connection-timeout: 30000
|
||||||
|
idle-timeout: 600000
|
||||||
|
max-lifetime: 1800000
|
||||||
|
|
||||||
|
# Economy Settings
|
||||||
|
economy:
|
||||||
|
# Currency display format (uses Java DecimalFormat)
|
||||||
|
currency-format: "$#,##0.00"
|
||||||
|
currency-symbol: "$"
|
||||||
|
|
||||||
|
taxes:
|
||||||
|
# Tax percentage for fixed-price market sales (seller pays)
|
||||||
|
market-tax: 5.0
|
||||||
|
# Tax percentage for auction sales (seller pays)
|
||||||
|
auction-tax: 7.5
|
||||||
|
|
||||||
|
# Fixed-Price Market Settings
|
||||||
|
market:
|
||||||
|
# Maximum active listings per player
|
||||||
|
max-listings-per-player: 20
|
||||||
|
# Cooldown between creating listings (seconds, 0 = disabled)
|
||||||
|
listing-cooldown: 0
|
||||||
|
# Default listing duration in hours
|
||||||
|
default-duration-hours: 168
|
||||||
|
# Available durations for players to choose (in hours)
|
||||||
|
available-durations:
|
||||||
|
- 24
|
||||||
|
- 72
|
||||||
|
- 168
|
||||||
|
- 336
|
||||||
|
# Price limits
|
||||||
|
min-price: 1.0
|
||||||
|
max-price: 1000000000.0
|
||||||
|
|
||||||
|
# Auction Settings
|
||||||
|
auction:
|
||||||
|
# Maximum active auctions per player
|
||||||
|
max-auctions-per-player: 10
|
||||||
|
# Duration limits (hours)
|
||||||
|
min-duration-hours: 1
|
||||||
|
max-duration-hours: 168
|
||||||
|
default-duration-hours: 24
|
||||||
|
# Available durations for players to choose (in hours)
|
||||||
|
available-durations:
|
||||||
|
- 1
|
||||||
|
- 6
|
||||||
|
- 12
|
||||||
|
- 24
|
||||||
|
- 48
|
||||||
|
- 72
|
||||||
|
- 168
|
||||||
|
# Price limits for starting bid
|
||||||
|
min-start-price: 1.0
|
||||||
|
max-start-price: 1000000000.0
|
||||||
|
# Minimum bid increment (percentage of current bid)
|
||||||
|
min-bid-increment-percent: 5.0
|
||||||
|
# Minimum absolute bid increment
|
||||||
|
min-bid-increment-absolute: 1.0
|
||||||
|
|
||||||
|
# Anti-snipe protection
|
||||||
|
anti-snipe:
|
||||||
|
enabled: true
|
||||||
|
# If bid placed within this many seconds of end, extend auction
|
||||||
|
trigger-seconds: 30
|
||||||
|
# How many seconds to extend
|
||||||
|
extension-seconds: 30
|
||||||
|
# Maximum number of extensions (0 = unlimited)
|
||||||
|
max-extensions: 10
|
||||||
|
|
||||||
|
# Item Blacklist
|
||||||
|
blacklist:
|
||||||
|
# Materials that cannot be listed/auctioned
|
||||||
|
materials:
|
||||||
|
- BARRIER
|
||||||
|
- COMMAND_BLOCK
|
||||||
|
- CHAIN_COMMAND_BLOCK
|
||||||
|
- REPEATING_COMMAND_BLOCK
|
||||||
|
- COMMAND_BLOCK_MINECART
|
||||||
|
- STRUCTURE_BLOCK
|
||||||
|
- STRUCTURE_VOID
|
||||||
|
- JIGSAW
|
||||||
|
- DEBUG_STICK
|
||||||
|
- KNOWLEDGE_BOOK
|
||||||
|
- SPAWNER
|
||||||
|
- BEDROCK
|
||||||
|
# Items with these keywords in their name/lore are blocked
|
||||||
|
keywords:
|
||||||
|
- "admin"
|
||||||
|
- "illegal"
|
||||||
|
- "exploit"
|
||||||
|
|
||||||
|
# GUI Settings
|
||||||
|
gui:
|
||||||
|
# Items displayed per page in browse views
|
||||||
|
items-per-page: 45
|
||||||
|
|
||||||
|
# Whether to show the Help button in the main menu
|
||||||
|
# Set to false to hide it (slot will be filled with glass)
|
||||||
|
show-help-button: true
|
||||||
|
|
||||||
|
# Sound effects (use Bukkit Sound enum names)
|
||||||
|
sounds:
|
||||||
|
click: UI_BUTTON_CLICK
|
||||||
|
success: ENTITY_PLAYER_LEVELUP
|
||||||
|
error: ENTITY_VILLAGER_NO
|
||||||
|
purchase: ENTITY_EXPERIENCE_ORB_PICKUP
|
||||||
|
|
||||||
|
# Notification Settings
|
||||||
|
notifications:
|
||||||
|
# Notify seller when their item sells
|
||||||
|
notify-on-sale: true
|
||||||
|
# Notify bidder when they are outbid
|
||||||
|
notify-on-outbid: true
|
||||||
|
# Notify winner when they win an auction
|
||||||
|
notify-on-win: true
|
||||||
|
# Notify seller when their listing expires
|
||||||
|
notify-on-expire: true
|
||||||
|
|
||||||
|
# Performance Settings
|
||||||
|
performance:
|
||||||
|
# How often to cache listings/auctions (seconds)
|
||||||
|
cache-duration: 30
|
||||||
|
# How often to check for ended auctions (seconds)
|
||||||
|
auction-check-interval: 5
|
||||||
|
# How often to check for expired listings (minutes)
|
||||||
|
expired-check-interval: 5
|
||||||
|
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
# ============================================
|
||||||
|
# CommunityMarket Language File - English (US)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# General
|
||||||
|
prefix: "&8[&6Market&8] &r"
|
||||||
|
|
||||||
|
# General Messages
|
||||||
|
messages:
|
||||||
|
no-permission: "&cYou don't have permission to do that."
|
||||||
|
player-only: "&cThis command can only be used by players."
|
||||||
|
reload-success: "&aConfiguration reloaded successfully!"
|
||||||
|
economy-not-found: "&cNo economy plugin found! Market disabled."
|
||||||
|
|
||||||
|
# Listing Messages
|
||||||
|
listing-created: "&aListing created successfully! ID: #{id}"
|
||||||
|
listing-cancelled: "&aListing cancelled. Item returned to claim storage."
|
||||||
|
listing-expired: "&eYour listing #{id} has expired. Item moved to claim storage."
|
||||||
|
listing-purchased: "&aYou purchased {item} x{amount} for {price}!"
|
||||||
|
listing-sold: "&aYour {item} x{amount} was sold to {buyer} for {price}!"
|
||||||
|
listing-limit-reached: "&cYou've reached the maximum number of listings ({max})."
|
||||||
|
listing-cooldown: "&cPlease wait {time} before creating another listing."
|
||||||
|
listing-not-found: "&cListing not found or no longer available."
|
||||||
|
listing-own-item: "&cYou cannot buy your own listing."
|
||||||
|
listing-insufficient-funds: "&cYou don't have enough money. Required: {price}"
|
||||||
|
|
||||||
|
# Auction Messages
|
||||||
|
auction-created: "&aAuction created successfully! ID: #{id}"
|
||||||
|
auction-cancelled: "&aAuction cancelled. Item returned to claim storage."
|
||||||
|
auction-ended-winner: "&aCongratulations! You won the auction for {item}! Paid: {price}"
|
||||||
|
auction-ended-seller: "&aYour auction for {item} ended! Winner: {winner}, Earned: {price}"
|
||||||
|
auction-ended-no-bids: "&eYour auction for {item} ended with no bids. Item moved to claim storage."
|
||||||
|
auction-limit-reached: "&cYou've reached the maximum number of auctions ({max})."
|
||||||
|
auction-not-found: "&cAuction not found or no longer available."
|
||||||
|
auction-own-item: "&cYou cannot bid on your own auction."
|
||||||
|
auction-bid-placed: "&aYou placed a bid of {amount} on {item}!"
|
||||||
|
auction-outbid: "&eYou've been outbid on {item}! New bid: {amount} by {bidder}"
|
||||||
|
auction-bid-too-low: "&cBid too low! Minimum bid: {min}"
|
||||||
|
auction-insufficient-funds: "&cYou don't have enough money. Required: {price}"
|
||||||
|
auction-buyout: "&aYou bought out the auction for {item} for {price}!"
|
||||||
|
auction-extended: "&eAuction extended by {seconds}s due to anti-snipe protection."
|
||||||
|
|
||||||
|
# Claim Messages
|
||||||
|
claim-success: "&aItem claimed successfully!"
|
||||||
|
claim-empty: "&eYou have no items to claim."
|
||||||
|
claim-inventory-full: "&cYour inventory is full! Please make space."
|
||||||
|
claim-all-success: "&aClaimed {count} items!"
|
||||||
|
|
||||||
|
# Earnings Messages
|
||||||
|
earnings-withdrawn: "&aWithdrew {amount}! New balance: {balance}"
|
||||||
|
earnings-empty: "&eYou have no pending earnings."
|
||||||
|
earnings-balance: "&aYour pending earnings: {amount}"
|
||||||
|
|
||||||
|
# Item Validation
|
||||||
|
invalid-item: "&cPlease select a valid item."
|
||||||
|
item-no-longer-available: "&cThe selected item is no longer in your inventory."
|
||||||
|
item-changed: "&cThe selected item has changed. Please select again."
|
||||||
|
quantity-changed: "&cThe available quantity has changed. Please verify and try again."
|
||||||
|
blacklisted-item: "&cThis item type is not allowed on the market."
|
||||||
|
blacklisted-content: "&cThis item contains blacklisted content."
|
||||||
|
invalid-price: "&cInvalid price. Range: {min} - {max}"
|
||||||
|
invalid-amount: "&cInvalid amount."
|
||||||
|
invalid-duration: "&cInvalid duration."
|
||||||
|
|
||||||
|
# Admin Messages
|
||||||
|
admin-listing-removed: "&aListing #{id} removed by admin."
|
||||||
|
admin-auction-cancelled: "&aAuction #{id} cancelled by admin."
|
||||||
|
admin-reload: "&aConfiguration reloaded."
|
||||||
|
|
||||||
|
# GUI Titles (support color codes)
|
||||||
|
gui-titles:
|
||||||
|
main-menu: "&8&lCommunity Market"
|
||||||
|
browse-market: "&8&lBrowse Market &7(Page {page})"
|
||||||
|
browse-auctions: "&8&lBrowse Auctions &7(Page {page})"
|
||||||
|
create-listing: "&8&lCreate Listing"
|
||||||
|
create-auction: "&8&lCreate Auction"
|
||||||
|
select-item-listing: "&8&lSelect Item to Sell"
|
||||||
|
select-item-auction: "&8&lSelect Item to Auction"
|
||||||
|
quantity-select: "&8&lSelect Quantity"
|
||||||
|
my-listings: "&8&lMy Listings"
|
||||||
|
my-auctions: "&8&lMy Auctions"
|
||||||
|
claim-items: "&8&lClaim Items"
|
||||||
|
earnings: "&8&lEarnings"
|
||||||
|
confirm-purchase: "&8&lConfirm Purchase"
|
||||||
|
confirm-bid: "&8&lConfirm Bid"
|
||||||
|
confirm-cancel: "&8&lConfirm Cancellation"
|
||||||
|
number-input: "&8&lEnter Amount"
|
||||||
|
admin-panel: "&8&lAdmin Panel"
|
||||||
|
admin-listings: "&8&lAll Listings"
|
||||||
|
admin-auctions: "&8&lAll Auctions"
|
||||||
|
listing-details: "&8&lListing Details"
|
||||||
|
auction-details: "&8&lAuction Details"
|
||||||
|
filter-menu: "&8&lFilter Options"
|
||||||
|
sort-menu: "&8&lSort Options"
|
||||||
|
duration-select: "&8&lSelect Duration"
|
||||||
|
help: "&8&lHelp"
|
||||||
|
|
||||||
|
# Button Names
|
||||||
|
buttons:
|
||||||
|
# Main Menu
|
||||||
|
browse-market: "&aBrowse Market"
|
||||||
|
browse-auctions: "&6Browse Auctions"
|
||||||
|
create-listing: "&eCreate Listing"
|
||||||
|
create-auction: "&eCreate Auction"
|
||||||
|
my-listings: "&bMy Listings"
|
||||||
|
my-auctions: "&bMy Auctions"
|
||||||
|
claim-items: "&dClaim Items"
|
||||||
|
earnings: "&aEarnings"
|
||||||
|
help: "&fHelp"
|
||||||
|
admin: "&cAdmin Panel"
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
next-page: "&aNext Page →"
|
||||||
|
previous-page: "&a← Previous Page"
|
||||||
|
back: "&cBack"
|
||||||
|
close: "&cClose"
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
confirm: "&aConfirm"
|
||||||
|
cancel: "&cCancel"
|
||||||
|
buy: "&aBuy Now"
|
||||||
|
bid: "&6Place Bid"
|
||||||
|
buyout: "&eBuyout"
|
||||||
|
claim: "&aClaim"
|
||||||
|
claim-all: "&aClaim All"
|
||||||
|
withdraw: "&aWithdraw All"
|
||||||
|
remove: "&cRemove Listing"
|
||||||
|
cancel-auction: "&cCancel Auction"
|
||||||
|
|
||||||
|
# Number Input
|
||||||
|
add-1: "&a+1"
|
||||||
|
add-10: "&a+10"
|
||||||
|
add-100: "&a+100"
|
||||||
|
add-1000: "&a+1,000"
|
||||||
|
subtract-1: "&c-1"
|
||||||
|
subtract-10: "&c-10"
|
||||||
|
subtract-100: "&c-100"
|
||||||
|
subtract-1000: "&c-1,000"
|
||||||
|
set-min: "&eSet Min"
|
||||||
|
set-max: "&eSet Max"
|
||||||
|
custom-amount: "&bCustom Amount"
|
||||||
|
|
||||||
|
# Filters & Sort
|
||||||
|
filter: "&eFilter"
|
||||||
|
sort: "&eSort"
|
||||||
|
search: "&eSearch"
|
||||||
|
clear-filter: "&cClear Filters"
|
||||||
|
|
||||||
|
# Duration
|
||||||
|
duration-1h: "&e1 Hour"
|
||||||
|
duration-6h: "&e6 Hours"
|
||||||
|
duration-12h: "&e12 Hours"
|
||||||
|
duration-24h: "&e24 Hours"
|
||||||
|
duration-48h: "&e48 Hours"
|
||||||
|
duration-72h: "&e3 Days"
|
||||||
|
duration-168h: "&e7 Days"
|
||||||
|
duration-336h: "&e14 Days"
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
admin-view-listings: "&aView All Listings"
|
||||||
|
admin-view-auctions: "&6View All Auctions"
|
||||||
|
admin-reload: "&eReload Config"
|
||||||
|
|
||||||
|
# Button Lore (descriptions)
|
||||||
|
lore:
|
||||||
|
browse-market:
|
||||||
|
- "&7Browse all fixed-price"
|
||||||
|
- "&7listings from players."
|
||||||
|
- ""
|
||||||
|
- "&eClick to browse!"
|
||||||
|
browse-auctions:
|
||||||
|
- "&7Browse all active auctions"
|
||||||
|
- "&7and place bids."
|
||||||
|
- ""
|
||||||
|
- "&eClick to browse!"
|
||||||
|
create-listing:
|
||||||
|
- "&7Sell items at a fixed price."
|
||||||
|
- "&7Tax: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClick to create!"
|
||||||
|
create-auction:
|
||||||
|
- "&7Auction items to the"
|
||||||
|
- "&7highest bidder."
|
||||||
|
- "&7Tax: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClick to create!"
|
||||||
|
my-listings:
|
||||||
|
- "&7View and manage your"
|
||||||
|
- "&7active listings."
|
||||||
|
- ""
|
||||||
|
- "&7Active: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to view!"
|
||||||
|
my-auctions:
|
||||||
|
- "&7View and manage your"
|
||||||
|
- "&7active auctions."
|
||||||
|
- ""
|
||||||
|
- "&7Active: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to view!"
|
||||||
|
claim-items:
|
||||||
|
- "&7Claim items from expired"
|
||||||
|
- "&7listings or won auctions."
|
||||||
|
- ""
|
||||||
|
- "&7Pending: &f{count}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to claim!"
|
||||||
|
earnings:
|
||||||
|
- "&7View and withdraw your"
|
||||||
|
- "&7pending earnings from sales."
|
||||||
|
- ""
|
||||||
|
- "&7Pending: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to view!"
|
||||||
|
help:
|
||||||
|
- "&7Learn how to use the"
|
||||||
|
- "&7Community Market."
|
||||||
|
- ""
|
||||||
|
- "&eClick for help!"
|
||||||
|
admin:
|
||||||
|
- "&cAdmin Panel"
|
||||||
|
- "&7Manage listings and auctions."
|
||||||
|
- ""
|
||||||
|
- "&eClick to open!"
|
||||||
|
|
||||||
|
# Listing Info
|
||||||
|
listing-info:
|
||||||
|
- "&7Seller: &f{seller}"
|
||||||
|
- "&7Price: &a{price}"
|
||||||
|
- "&7Amount: &f{amount}"
|
||||||
|
- "&7Expires: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&eLeft-click to buy!"
|
||||||
|
|
||||||
|
# Auction Info
|
||||||
|
auction-info:
|
||||||
|
- "&7Seller: &f{seller}"
|
||||||
|
- "&7Starting bid: &a{start_price}"
|
||||||
|
- "&7Current bid: &a{current_bid}"
|
||||||
|
- "&7Bidder: &f{bidder}"
|
||||||
|
- "&7Bids: &f{bid_count}"
|
||||||
|
- "&7Ends: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&eLeft-click to bid!"
|
||||||
|
- "&eRight-click to buyout!"
|
||||||
|
|
||||||
|
# My Listing Info
|
||||||
|
my-listing-info:
|
||||||
|
- "&7Price: &a{price}"
|
||||||
|
- "&7Amount: &f{amount}"
|
||||||
|
- "&7Created: &f{created}"
|
||||||
|
- "&7Expires: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&cClick to cancel"
|
||||||
|
|
||||||
|
# My Auction Info
|
||||||
|
my-auction-info:
|
||||||
|
- "&7Starting bid: &a{start_price}"
|
||||||
|
- "&7Current bid: &a{current_bid}"
|
||||||
|
- "&7Bidder: &f{bidder}"
|
||||||
|
- "&7Bids: &f{bid_count}"
|
||||||
|
- "&7Ends: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&cClick to cancel (if no bids)"
|
||||||
|
|
||||||
|
# Confirm Purchase
|
||||||
|
confirm-purchase-info:
|
||||||
|
- "&7You are purchasing:"
|
||||||
|
- "&f{item} x{amount}"
|
||||||
|
- ""
|
||||||
|
- "&7Price: &a{price}"
|
||||||
|
- "&7Tax: &e{tax}"
|
||||||
|
- "&7Total: &a{total}"
|
||||||
|
- ""
|
||||||
|
- "&aClick to confirm!"
|
||||||
|
|
||||||
|
# Confirm Bid
|
||||||
|
confirm-bid-info:
|
||||||
|
- "&7You are bidding on:"
|
||||||
|
- "&f{item}"
|
||||||
|
- ""
|
||||||
|
- "&7Your bid: &a{bid}"
|
||||||
|
- "&7Current high: &e{current}"
|
||||||
|
- ""
|
||||||
|
- "&aClick to confirm!"
|
||||||
|
|
||||||
|
# Claim Item
|
||||||
|
claim-item-info:
|
||||||
|
- "&7Reason: &f{reason}"
|
||||||
|
- "&7From: &f{source}"
|
||||||
|
- "&7Date: &f{date}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to claim!"
|
||||||
|
|
||||||
|
# Earnings Info
|
||||||
|
earnings-info:
|
||||||
|
- "&7Your pending earnings"
|
||||||
|
- "&7from market sales."
|
||||||
|
- ""
|
||||||
|
- "&7Total: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&aClick to withdraw!"
|
||||||
|
|
||||||
|
# Number Input Info
|
||||||
|
current-value:
|
||||||
|
- "&7Current: &a{value}"
|
||||||
|
|
||||||
|
# Filter Options
|
||||||
|
filters:
|
||||||
|
all: "&fAll Items"
|
||||||
|
weapons: "&cWeapons"
|
||||||
|
armor: "&bArmor"
|
||||||
|
tools: "&eTools"
|
||||||
|
blocks: "&7Blocks"
|
||||||
|
food: "&6Food"
|
||||||
|
potions: "&dPotions"
|
||||||
|
materials: "&aMaterials"
|
||||||
|
enchanted: "&5Enchanted Items"
|
||||||
|
misc: "&8Miscellaneous"
|
||||||
|
|
||||||
|
# Sort Options
|
||||||
|
sort:
|
||||||
|
newest: "&aNewest First"
|
||||||
|
oldest: "&eOldest First"
|
||||||
|
price-low: "&aPrice: Low to High"
|
||||||
|
price-high: "&cPrice: High to Low"
|
||||||
|
ending-soon: "&6Ending Soon"
|
||||||
|
most-bids: "&bMost Bids"
|
||||||
|
|
||||||
|
# Time Formats
|
||||||
|
time:
|
||||||
|
expired: "&cExpired"
|
||||||
|
days: "{d}d"
|
||||||
|
hours: "{h}h"
|
||||||
|
minutes: "{m}m"
|
||||||
|
seconds: "{s}s"
|
||||||
|
|
||||||
|
# Help Content
|
||||||
|
help:
|
||||||
|
title: "&6&lCommunity Market Help"
|
||||||
|
content:
|
||||||
|
- "&eBrowse Market &7- View and buy fixed-price listings"
|
||||||
|
- "&eBrowse Auctions &7- View and bid on auctions"
|
||||||
|
- "&eCreate Listing &7- Sell items at a fixed price"
|
||||||
|
- "&eCreate Auction &7- Auction items to highest bidder"
|
||||||
|
- "&eMy Listings &7- Manage your active listings"
|
||||||
|
- "&eMy Auctions &7- Manage your active auctions"
|
||||||
|
- "&eClaim Items &7- Collect unsold/won items"
|
||||||
|
- "&eEarnings &7- Withdraw money from sales"
|
||||||
|
- ""
|
||||||
|
- "&7&oTip: All actions are done through GUIs!"
|
||||||
|
- "&7&oJust click on buttons to navigate."
|
||||||
|
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
# ============================================
|
||||||
|
# CommunityMarket Ficheiro de Idioma - Português (Portugal)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Geral
|
||||||
|
prefix: "&8[&6Mercado&8] &r"
|
||||||
|
|
||||||
|
# Mensagens Gerais
|
||||||
|
messages:
|
||||||
|
no-permission: "&cNão tens permissão para fazer isso."
|
||||||
|
player-only: "&cEste comando só pode ser usado por jogadores."
|
||||||
|
reload-success: "&aConfiguração recarregada com sucesso!"
|
||||||
|
economy-not-found: "&cNenhum plugin de economia encontrado! Mercado desativado."
|
||||||
|
|
||||||
|
# Mensagens de Anúncios
|
||||||
|
listing-created: "&aAnúncio criado com sucesso! ID: #{id}"
|
||||||
|
listing-cancelled: "&aAnúncio cancelado. Item devolvido ao armazém."
|
||||||
|
listing-expired: "&eO teu anúncio #{id} expirou. Item movido para o armazém."
|
||||||
|
listing-purchased: "&aCompraste {item} x{amount} por {price}!"
|
||||||
|
listing-sold: "&aO teu {item} x{amount} foi vendido a {buyer} por {price}!"
|
||||||
|
listing-limit-reached: "&cAtingiste o número máximo de anúncios ({max})."
|
||||||
|
listing-cooldown: "&cPor favor aguarda {time} antes de criar outro anúncio."
|
||||||
|
listing-not-found: "&cAnúncio não encontrado ou já não está disponível."
|
||||||
|
listing-own-item: "&cNão podes comprar o teu próprio anúncio."
|
||||||
|
listing-insufficient-funds: "&cNão tens dinheiro suficiente. Necessário: {price}"
|
||||||
|
|
||||||
|
# Mensagens de Leilões
|
||||||
|
auction-created: "&aLeilão criado com sucesso! ID: #{id}"
|
||||||
|
auction-cancelled: "&aLeilão cancelado. Item devolvido ao armazém."
|
||||||
|
auction-ended-winner: "&aParabéns! Ganhaste o leilão por {item}! Pagaste: {price}"
|
||||||
|
auction-ended-seller: "&aO teu leilão por {item} terminou! Vencedor: {winner}, Ganhaste: {price}"
|
||||||
|
auction-ended-no-bids: "&eO teu leilão por {item} terminou sem licitações. Item movido para o armazém."
|
||||||
|
auction-limit-reached: "&cAtingiste o número máximo de leilões ({max})."
|
||||||
|
auction-not-found: "&cLeilão não encontrado ou já não está disponível."
|
||||||
|
auction-own-item: "&cNão podes licitar no teu próprio leilão."
|
||||||
|
auction-bid-placed: "&aFizeste uma licitação de {amount} em {item}!"
|
||||||
|
auction-outbid: "&eForam feitas licitações superiores à tua em {item}! Nova licitação: {amount} por {bidder}"
|
||||||
|
auction-bid-too-low: "&cLicitação muito baixa! Mínimo: {min}"
|
||||||
|
auction-insufficient-funds: "&cNão tens dinheiro suficiente. Necessário: {price}"
|
||||||
|
auction-buyout: "&aCompraste o leilão de {item} por {price}!"
|
||||||
|
auction-extended: "&eLeilão prolongado por {seconds}s devido à proteção anti-snipe."
|
||||||
|
|
||||||
|
# Mensagens de Reclamação
|
||||||
|
claim-success: "&aItem reclamado com sucesso!"
|
||||||
|
claim-empty: "&eNão tens itens para reclamar."
|
||||||
|
claim-inventory-full: "&cO teu inventário está cheio! Por favor liberta espaço."
|
||||||
|
claim-all-success: "&aReclamaste {count} itens!"
|
||||||
|
|
||||||
|
# Mensagens de Ganhos
|
||||||
|
earnings-withdrawn: "&aLevantaste {amount}! Novo saldo: {balance}"
|
||||||
|
earnings-empty: "&eNão tens ganhos pendentes."
|
||||||
|
earnings-balance: "&aOs teus ganhos pendentes: {amount}"
|
||||||
|
|
||||||
|
# Validação de Itens
|
||||||
|
invalid-item: "&cPor favor seleciona um item válido."
|
||||||
|
item-no-longer-available: "&cO item selecionado já não está no teu inventário."
|
||||||
|
item-changed: "&cO item selecionado foi alterado. Por favor seleciona novamente."
|
||||||
|
quantity-changed: "&cA quantidade disponível foi alterada. Por favor verifica e tenta novamente."
|
||||||
|
blacklisted-item: "&cEste tipo de item não é permitido no mercado."
|
||||||
|
blacklisted-content: "&cEste item contém conteúdo bloqueado."
|
||||||
|
invalid-price: "&cPreço inválido. Intervalo: {min} - {max}"
|
||||||
|
invalid-amount: "&cQuantidade inválida."
|
||||||
|
invalid-duration: "&cDuração inválida."
|
||||||
|
|
||||||
|
# Mensagens de Admin
|
||||||
|
admin-listing-removed: "&aAnúncio #{id} removido pelo admin."
|
||||||
|
admin-auction-cancelled: "&aLeilão #{id} cancelado pelo admin."
|
||||||
|
admin-reload: "&aConfiguração recarregada."
|
||||||
|
|
||||||
|
# Títulos GUI (suportam códigos de cor)
|
||||||
|
gui-titles:
|
||||||
|
main-menu: "&8&lMercado Comunitário"
|
||||||
|
browse-market: "&8&lExplorar Mercado &7(Página {page})"
|
||||||
|
browse-auctions: "&8&lExplorar Leilões &7(Página {page})"
|
||||||
|
create-listing: "&8&lCriar Anúncio"
|
||||||
|
create-auction: "&8&lCriar Leilão"
|
||||||
|
select-item-listing: "&8&lSelecionar Item para Vender"
|
||||||
|
select-item-auction: "&8&lSelecionar Item para Leilão"
|
||||||
|
quantity-select: "&8&lSelecionar Quantidade"
|
||||||
|
my-listings: "&8&lOs Meus Anúncios"
|
||||||
|
my-auctions: "&8&lOs Meus Leilões"
|
||||||
|
claim-items: "&8&lReclamar Itens"
|
||||||
|
earnings: "&8&lGanhos"
|
||||||
|
confirm-purchase: "&8&lConfirmar Compra"
|
||||||
|
confirm-bid: "&8&lConfirmar Licitação"
|
||||||
|
confirm-cancel: "&8&lConfirmar Cancelamento"
|
||||||
|
number-input: "&8&lIntroduzir Valor"
|
||||||
|
admin-panel: "&8&lPainel de Admin"
|
||||||
|
admin-listings: "&8&lTodos os Anúncios"
|
||||||
|
admin-auctions: "&8&lTodos os Leilões"
|
||||||
|
listing-details: "&8&lDetalhes do Anúncio"
|
||||||
|
auction-details: "&8&lDetalhes do Leilão"
|
||||||
|
filter-menu: "&8&lOpções de Filtro"
|
||||||
|
sort-menu: "&8&lOpções de Ordenação"
|
||||||
|
duration-select: "&8&lSelecionar Duração"
|
||||||
|
help: "&8&lAjuda"
|
||||||
|
|
||||||
|
# Nomes dos Botões
|
||||||
|
buttons:
|
||||||
|
# Menu Principal
|
||||||
|
browse-market: "&aExplorar Mercado"
|
||||||
|
browse-auctions: "&6Explorar Leilões"
|
||||||
|
create-listing: "&eCriar Anúncio"
|
||||||
|
create-auction: "&eCriar Leilão"
|
||||||
|
my-listings: "&bOs Meus Anúncios"
|
||||||
|
my-auctions: "&bOs Meus Leilões"
|
||||||
|
claim-items: "&dReclamar Itens"
|
||||||
|
earnings: "&aGanhos"
|
||||||
|
help: "&fAjuda"
|
||||||
|
admin: "&cPainel de Admin"
|
||||||
|
|
||||||
|
# Navegação
|
||||||
|
next-page: "&aPágina Seguinte →"
|
||||||
|
previous-page: "&a← Página Anterior"
|
||||||
|
back: "&cVoltar"
|
||||||
|
close: "&cFechar"
|
||||||
|
|
||||||
|
# Ações
|
||||||
|
confirm: "&aConfirmar"
|
||||||
|
cancel: "&cCancelar"
|
||||||
|
buy: "&aComprar Agora"
|
||||||
|
bid: "&6Fazer Licitação"
|
||||||
|
buyout: "&eCompra Imediata"
|
||||||
|
claim: "&aReclamar"
|
||||||
|
claim-all: "&aReclamar Tudo"
|
||||||
|
withdraw: "&aLevantar Tudo"
|
||||||
|
remove: "&cRemover Anúncio"
|
||||||
|
cancel-auction: "&cCancelar Leilão"
|
||||||
|
|
||||||
|
# Entrada Numérica
|
||||||
|
add-1: "&a+1"
|
||||||
|
add-10: "&a+10"
|
||||||
|
add-100: "&a+100"
|
||||||
|
add-1000: "&a+1.000"
|
||||||
|
subtract-1: "&c-1"
|
||||||
|
subtract-10: "&c-10"
|
||||||
|
subtract-100: "&c-100"
|
||||||
|
subtract-1000: "&c-1.000"
|
||||||
|
set-min: "&eDefinir Mín"
|
||||||
|
set-max: "&eDefinir Máx"
|
||||||
|
custom-amount: "&bValor Personalizado"
|
||||||
|
|
||||||
|
# Filtros e Ordenação
|
||||||
|
filter: "&eFiltro"
|
||||||
|
sort: "&eOrdenar"
|
||||||
|
search: "&ePesquisar"
|
||||||
|
clear-filter: "&cLimpar Filtros"
|
||||||
|
|
||||||
|
# Duração
|
||||||
|
duration-1h: "&e1 Hora"
|
||||||
|
duration-6h: "&e6 Horas"
|
||||||
|
duration-12h: "&e12 Horas"
|
||||||
|
duration-24h: "&e24 Horas"
|
||||||
|
duration-48h: "&e48 Horas"
|
||||||
|
duration-72h: "&e3 Dias"
|
||||||
|
duration-168h: "&e7 Dias"
|
||||||
|
duration-336h: "&e14 Dias"
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
admin-view-listings: "&aVer Todos os Anúncios"
|
||||||
|
admin-view-auctions: "&6Ver Todos os Leilões"
|
||||||
|
admin-reload: "&eRecarregar Config"
|
||||||
|
|
||||||
|
# Lore dos Botões (descrições)
|
||||||
|
lore:
|
||||||
|
browse-market:
|
||||||
|
- "&7Explora todos os anúncios"
|
||||||
|
- "&7de preço fixo dos jogadores."
|
||||||
|
- ""
|
||||||
|
- "&eClica para explorar!"
|
||||||
|
browse-auctions:
|
||||||
|
- "&7Explora todos os leilões ativos"
|
||||||
|
- "&7e faz licitações."
|
||||||
|
- ""
|
||||||
|
- "&eClica para explorar!"
|
||||||
|
create-listing:
|
||||||
|
- "&7Vende itens a um preço fixo."
|
||||||
|
- "&7Taxa: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClica para criar!"
|
||||||
|
create-auction:
|
||||||
|
- "&7Leiloa itens ao"
|
||||||
|
- "&7maior licitador."
|
||||||
|
- "&7Taxa: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClica para criar!"
|
||||||
|
my-listings:
|
||||||
|
- "&7Vê e gere os teus"
|
||||||
|
- "&7anúncios ativos."
|
||||||
|
- ""
|
||||||
|
- "&7Ativos: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para ver!"
|
||||||
|
my-auctions:
|
||||||
|
- "&7Vê e gere os teus"
|
||||||
|
- "&7leilões ativos."
|
||||||
|
- ""
|
||||||
|
- "&7Ativos: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para ver!"
|
||||||
|
claim-items:
|
||||||
|
- "&7Reclama itens de anúncios"
|
||||||
|
- "&7expirados ou leilões ganhos."
|
||||||
|
- ""
|
||||||
|
- "&7Pendentes: &f{count}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para reclamar!"
|
||||||
|
earnings:
|
||||||
|
- "&7Vê e levanta os teus"
|
||||||
|
- "&7ganhos pendentes das vendas."
|
||||||
|
- ""
|
||||||
|
- "&7Pendente: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para ver!"
|
||||||
|
help:
|
||||||
|
- "&7Aprende a usar o"
|
||||||
|
- "&7Mercado Comunitário."
|
||||||
|
- ""
|
||||||
|
- "&eClica para ajuda!"
|
||||||
|
admin:
|
||||||
|
- "&cPainel de Admin"
|
||||||
|
- "&7Gere anúncios e leilões."
|
||||||
|
- ""
|
||||||
|
- "&eClica para abrir!"
|
||||||
|
|
||||||
|
# Info do Anúncio
|
||||||
|
listing-info:
|
||||||
|
- "&7Vendedor: &f{seller}"
|
||||||
|
- "&7Preço: &a{price}"
|
||||||
|
- "&7Quantidade: &f{amount}"
|
||||||
|
- "&7Expira: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&eClique esquerdo para comprar!"
|
||||||
|
|
||||||
|
# Info do Leilão
|
||||||
|
auction-info:
|
||||||
|
- "&7Vendedor: &f{seller}"
|
||||||
|
- "&7Licitação inicial: &a{start_price}"
|
||||||
|
- "&7Licitação atual: &a{current_bid}"
|
||||||
|
- "&7Licitador: &f{bidder}"
|
||||||
|
- "&7Licitações: &f{bid_count}"
|
||||||
|
- "&7Termina: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&eClique esquerdo para licitar!"
|
||||||
|
- "&eClique direito para compra imediata!"
|
||||||
|
|
||||||
|
# Info do Meu Anúncio
|
||||||
|
my-listing-info:
|
||||||
|
- "&7Preço: &a{price}"
|
||||||
|
- "&7Quantidade: &f{amount}"
|
||||||
|
- "&7Criado: &f{created}"
|
||||||
|
- "&7Expira: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&cClica para cancelar"
|
||||||
|
|
||||||
|
# Info do Meu Leilão
|
||||||
|
my-auction-info:
|
||||||
|
- "&7Licitação inicial: &a{start_price}"
|
||||||
|
- "&7Licitação atual: &a{current_bid}"
|
||||||
|
- "&7Licitador: &f{bidder}"
|
||||||
|
- "&7Licitações: &f{bid_count}"
|
||||||
|
- "&7Termina: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&cClica para cancelar (sem licitações)"
|
||||||
|
|
||||||
|
# Confirmar Compra
|
||||||
|
confirm-purchase-info:
|
||||||
|
- "&7Estás a comprar:"
|
||||||
|
- "&f{item} x{amount}"
|
||||||
|
- ""
|
||||||
|
- "&7Preço: &a{price}"
|
||||||
|
- "&7Taxa: &e{tax}"
|
||||||
|
- "&7Total: &a{total}"
|
||||||
|
- ""
|
||||||
|
- "&aClica para confirmar!"
|
||||||
|
|
||||||
|
# Confirmar Licitação
|
||||||
|
confirm-bid-info:
|
||||||
|
- "&7Estás a licitar em:"
|
||||||
|
- "&f{item}"
|
||||||
|
- ""
|
||||||
|
- "&7A tua licitação: &a{bid}"
|
||||||
|
- "&7Atual mais alta: &e{current}"
|
||||||
|
- ""
|
||||||
|
- "&aClica para confirmar!"
|
||||||
|
|
||||||
|
# Reclamar Item
|
||||||
|
claim-item-info:
|
||||||
|
- "&7Razão: &f{reason}"
|
||||||
|
- "&7De: &f{source}"
|
||||||
|
- "&7Data: &f{date}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para reclamar!"
|
||||||
|
|
||||||
|
# Info de Ganhos
|
||||||
|
earnings-info:
|
||||||
|
- "&7Os teus ganhos pendentes"
|
||||||
|
- "&7das vendas no mercado."
|
||||||
|
- ""
|
||||||
|
- "&7Total: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&aClica para levantar!"
|
||||||
|
|
||||||
|
# Info de Entrada Numérica
|
||||||
|
current-value:
|
||||||
|
- "&7Atual: &a{value}"
|
||||||
|
|
||||||
|
# Opções de Filtro
|
||||||
|
filters:
|
||||||
|
all: "&fTodos os Itens"
|
||||||
|
weapons: "&cArmas"
|
||||||
|
armor: "&bArmadura"
|
||||||
|
tools: "&eFerramentas"
|
||||||
|
blocks: "&7Blocos"
|
||||||
|
food: "&6Comida"
|
||||||
|
potions: "&dPoções"
|
||||||
|
materials: "&aMateriais"
|
||||||
|
enchanted: "&5Itens Encantados"
|
||||||
|
misc: "&8Diversos"
|
||||||
|
|
||||||
|
# Opções de Ordenação
|
||||||
|
sort:
|
||||||
|
newest: "&aMais Recente"
|
||||||
|
oldest: "&eMais Antigo"
|
||||||
|
price-low: "&aPreço: Menor para Maior"
|
||||||
|
price-high: "&cPreço: Maior para Menor"
|
||||||
|
ending-soon: "&6A Terminar Em Breve"
|
||||||
|
most-bids: "&bMais Licitações"
|
||||||
|
|
||||||
|
# Formatos de Tempo
|
||||||
|
time:
|
||||||
|
expired: "&cExpirado"
|
||||||
|
days: "{d}d"
|
||||||
|
hours: "{h}h"
|
||||||
|
minutes: "{m}m"
|
||||||
|
seconds: "{s}s"
|
||||||
|
|
||||||
|
# Conteúdo de Ajuda
|
||||||
|
help:
|
||||||
|
title: "&6&lAjuda do Mercado Comunitário"
|
||||||
|
content:
|
||||||
|
- "&eExplorar Mercado &7- Ver e comprar anúncios de preço fixo"
|
||||||
|
- "&eExplorar Leilões &7- Ver e licitar em leilões"
|
||||||
|
- "&eCriar Anúncio &7- Vender itens a um preço fixo"
|
||||||
|
- "&eCriar Leilão &7- Leiloar itens ao maior licitador"
|
||||||
|
- "&eOs Meus Anúncios &7- Gerir os teus anúncios ativos"
|
||||||
|
- "&eOs Meus Leilões &7- Gerir os teus leilões ativos"
|
||||||
|
- "&eReclamar Itens &7- Recolher itens não vendidos/ganhos"
|
||||||
|
- "&eGanhos &7- Levantar dinheiro das vendas"
|
||||||
|
- ""
|
||||||
|
- "&7&oDica: Todas as ações são feitas através de GUIs!"
|
||||||
|
- "&7&oBasta clicar nos botões para navegar."
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
name: CommunityMarket
|
||||||
|
version: '${project.version}'
|
||||||
|
main: pt.henrique.communityMarket.CommunityMarket
|
||||||
|
api-version: '1.21'
|
||||||
|
description: A GUI-only marketplace plugin for fixed-price listings and auctions
|
||||||
|
author: Henrique
|
||||||
|
website: https://github.com/henrique/CommunityMarket
|
||||||
|
|
||||||
|
# Soft dependencies - plugin will detect and use these if available
|
||||||
|
softdepend:
|
||||||
|
- Vault
|
||||||
|
- Essentials
|
||||||
|
|
||||||
|
load: POSTWORLD
|
||||||
|
|
||||||
|
commands:
|
||||||
|
market:
|
||||||
|
description: Opens the Community Market main menu
|
||||||
|
usage: /<command>
|
||||||
|
aliases: [cmarket]
|
||||||
|
permission: communitymarket.use
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
communitymarket.*:
|
||||||
|
description: Grants all CommunityMarket permissions
|
||||||
|
default: op
|
||||||
|
children:
|
||||||
|
communitymarket.use: true
|
||||||
|
communitymarket.sell: true
|
||||||
|
communitymarket.auction: true
|
||||||
|
communitymarket.buy: true
|
||||||
|
communitymarket.bid: true
|
||||||
|
communitymarket.claim: true
|
||||||
|
communitymarket.withdraw: true
|
||||||
|
communitymarket.admin: true
|
||||||
|
|
||||||
|
communitymarket.use:
|
||||||
|
description: Allows access to the market GUI
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.sell:
|
||||||
|
description: Allows creating fixed-price listings
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.auction:
|
||||||
|
description: Allows creating auctions
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.buy:
|
||||||
|
description: Allows purchasing from the market
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.bid:
|
||||||
|
description: Allows bidding on auctions
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.claim:
|
||||||
|
description: Allows claiming items from storage
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.withdraw:
|
||||||
|
description: Allows withdrawing earnings
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.admin:
|
||||||
|
description: Allows access to admin functions
|
||||||
|
default: op
|
||||||
|
children:
|
||||||
|
communitymarket.admin.viewall: true
|
||||||
|
communitymarket.admin.remove: true
|
||||||
|
communitymarket.admin.reload: true
|
||||||
|
|
||||||
|
communitymarket.admin.viewall:
|
||||||
|
description: Allows viewing all listings/auctions
|
||||||
|
default: op
|
||||||
|
|
||||||
|
communitymarket.admin.remove:
|
||||||
|
description: Allows removing any listing or auction
|
||||||
|
default: op
|
||||||
|
|
||||||
|
communitymarket.admin.reload:
|
||||||
|
description: Allows reloading configuration
|
||||||
|
default: op
|
||||||
Binary file not shown.
@@ -0,0 +1,152 @@
|
|||||||
|
# ============================================
|
||||||
|
# CommunityMarket Configuration
|
||||||
|
# A GUI-only marketplace plugin
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Language setting (available: en_US, pt_PT)
|
||||||
|
language: en_US
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
database:
|
||||||
|
# Type: sqlite or mysql
|
||||||
|
type: sqlite
|
||||||
|
|
||||||
|
sqlite:
|
||||||
|
file: database.db
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: communitymarket
|
||||||
|
username: root
|
||||||
|
password: ""
|
||||||
|
pool:
|
||||||
|
maximum-pool-size: 10
|
||||||
|
minimum-idle: 2
|
||||||
|
connection-timeout: 30000
|
||||||
|
idle-timeout: 600000
|
||||||
|
max-lifetime: 1800000
|
||||||
|
|
||||||
|
# Economy Settings
|
||||||
|
economy:
|
||||||
|
# Currency display format (uses Java DecimalFormat)
|
||||||
|
currency-format: "$#,##0.00"
|
||||||
|
currency-symbol: "$"
|
||||||
|
|
||||||
|
taxes:
|
||||||
|
# Tax percentage for fixed-price market sales (seller pays)
|
||||||
|
market-tax: 5.0
|
||||||
|
# Tax percentage for auction sales (seller pays)
|
||||||
|
auction-tax: 7.5
|
||||||
|
|
||||||
|
# Fixed-Price Market Settings
|
||||||
|
market:
|
||||||
|
# Maximum active listings per player
|
||||||
|
max-listings-per-player: 20
|
||||||
|
# Cooldown between creating listings (seconds, 0 = disabled)
|
||||||
|
listing-cooldown: 0
|
||||||
|
# Default listing duration in hours
|
||||||
|
default-duration-hours: 168
|
||||||
|
# Available durations for players to choose (in hours)
|
||||||
|
available-durations:
|
||||||
|
- 24
|
||||||
|
- 72
|
||||||
|
- 168
|
||||||
|
- 336
|
||||||
|
# Price limits
|
||||||
|
min-price: 1.0
|
||||||
|
max-price: 1000000000.0
|
||||||
|
|
||||||
|
# Auction Settings
|
||||||
|
auction:
|
||||||
|
# Maximum active auctions per player
|
||||||
|
max-auctions-per-player: 10
|
||||||
|
# Duration limits (hours)
|
||||||
|
min-duration-hours: 1
|
||||||
|
max-duration-hours: 168
|
||||||
|
default-duration-hours: 24
|
||||||
|
# Available durations for players to choose (in hours)
|
||||||
|
available-durations:
|
||||||
|
- 1
|
||||||
|
- 6
|
||||||
|
- 12
|
||||||
|
- 24
|
||||||
|
- 48
|
||||||
|
- 72
|
||||||
|
- 168
|
||||||
|
# Price limits for starting bid
|
||||||
|
min-start-price: 1.0
|
||||||
|
max-start-price: 1000000000.0
|
||||||
|
# Minimum bid increment (percentage of current bid)
|
||||||
|
min-bid-increment-percent: 5.0
|
||||||
|
# Minimum absolute bid increment
|
||||||
|
min-bid-increment-absolute: 1.0
|
||||||
|
|
||||||
|
# Anti-snipe protection
|
||||||
|
anti-snipe:
|
||||||
|
enabled: true
|
||||||
|
# If bid placed within this many seconds of end, extend auction
|
||||||
|
trigger-seconds: 30
|
||||||
|
# How many seconds to extend
|
||||||
|
extension-seconds: 30
|
||||||
|
# Maximum number of extensions (0 = unlimited)
|
||||||
|
max-extensions: 10
|
||||||
|
|
||||||
|
# Item Blacklist
|
||||||
|
blacklist:
|
||||||
|
# Materials that cannot be listed/auctioned
|
||||||
|
materials:
|
||||||
|
- BARRIER
|
||||||
|
- COMMAND_BLOCK
|
||||||
|
- CHAIN_COMMAND_BLOCK
|
||||||
|
- REPEATING_COMMAND_BLOCK
|
||||||
|
- COMMAND_BLOCK_MINECART
|
||||||
|
- STRUCTURE_BLOCK
|
||||||
|
- STRUCTURE_VOID
|
||||||
|
- JIGSAW
|
||||||
|
- DEBUG_STICK
|
||||||
|
- KNOWLEDGE_BOOK
|
||||||
|
- SPAWNER
|
||||||
|
- BEDROCK
|
||||||
|
# Items with these keywords in their name/lore are blocked
|
||||||
|
keywords:
|
||||||
|
- "admin"
|
||||||
|
- "illegal"
|
||||||
|
- "exploit"
|
||||||
|
|
||||||
|
# GUI Settings
|
||||||
|
gui:
|
||||||
|
# Items displayed per page in browse views
|
||||||
|
items-per-page: 45
|
||||||
|
|
||||||
|
# Whether to show the Help button in the main menu
|
||||||
|
# Set to false to hide it (slot will be filled with glass)
|
||||||
|
show-help-button: true
|
||||||
|
|
||||||
|
# Sound effects (use Bukkit Sound enum names)
|
||||||
|
sounds:
|
||||||
|
click: UI_BUTTON_CLICK
|
||||||
|
success: ENTITY_PLAYER_LEVELUP
|
||||||
|
error: ENTITY_VILLAGER_NO
|
||||||
|
purchase: ENTITY_EXPERIENCE_ORB_PICKUP
|
||||||
|
|
||||||
|
# Notification Settings
|
||||||
|
notifications:
|
||||||
|
# Notify seller when their item sells
|
||||||
|
notify-on-sale: true
|
||||||
|
# Notify bidder when they are outbid
|
||||||
|
notify-on-outbid: true
|
||||||
|
# Notify winner when they win an auction
|
||||||
|
notify-on-win: true
|
||||||
|
# Notify seller when their listing expires
|
||||||
|
notify-on-expire: true
|
||||||
|
|
||||||
|
# Performance Settings
|
||||||
|
performance:
|
||||||
|
# How often to cache listings/auctions (seconds)
|
||||||
|
cache-duration: 30
|
||||||
|
# How often to check for ended auctions (seconds)
|
||||||
|
auction-check-interval: 5
|
||||||
|
# How often to check for expired listings (minutes)
|
||||||
|
expired-check-interval: 5
|
||||||
|
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
# ============================================
|
||||||
|
# CommunityMarket Language File - English (US)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# General
|
||||||
|
prefix: "&8[&6Market&8] &r"
|
||||||
|
|
||||||
|
# General Messages
|
||||||
|
messages:
|
||||||
|
no-permission: "&cYou don't have permission to do that."
|
||||||
|
player-only: "&cThis command can only be used by players."
|
||||||
|
reload-success: "&aConfiguration reloaded successfully!"
|
||||||
|
economy-not-found: "&cNo economy plugin found! Market disabled."
|
||||||
|
|
||||||
|
# Listing Messages
|
||||||
|
listing-created: "&aListing created successfully! ID: #{id}"
|
||||||
|
listing-cancelled: "&aListing cancelled. Item returned to claim storage."
|
||||||
|
listing-expired: "&eYour listing #{id} has expired. Item moved to claim storage."
|
||||||
|
listing-purchased: "&aYou purchased {item} x{amount} for {price}!"
|
||||||
|
listing-sold: "&aYour {item} x{amount} was sold to {buyer} for {price}!"
|
||||||
|
listing-limit-reached: "&cYou've reached the maximum number of listings ({max})."
|
||||||
|
listing-cooldown: "&cPlease wait {time} before creating another listing."
|
||||||
|
listing-not-found: "&cListing not found or no longer available."
|
||||||
|
listing-own-item: "&cYou cannot buy your own listing."
|
||||||
|
listing-insufficient-funds: "&cYou don't have enough money. Required: {price}"
|
||||||
|
|
||||||
|
# Auction Messages
|
||||||
|
auction-created: "&aAuction created successfully! ID: #{id}"
|
||||||
|
auction-cancelled: "&aAuction cancelled. Item returned to claim storage."
|
||||||
|
auction-ended-winner: "&aCongratulations! You won the auction for {item}! Paid: {price}"
|
||||||
|
auction-ended-seller: "&aYour auction for {item} ended! Winner: {winner}, Earned: {price}"
|
||||||
|
auction-ended-no-bids: "&eYour auction for {item} ended with no bids. Item moved to claim storage."
|
||||||
|
auction-limit-reached: "&cYou've reached the maximum number of auctions ({max})."
|
||||||
|
auction-not-found: "&cAuction not found or no longer available."
|
||||||
|
auction-own-item: "&cYou cannot bid on your own auction."
|
||||||
|
auction-bid-placed: "&aYou placed a bid of {amount} on {item}!"
|
||||||
|
auction-outbid: "&eYou've been outbid on {item}! New bid: {amount} by {bidder}"
|
||||||
|
auction-bid-too-low: "&cBid too low! Minimum bid: {min}"
|
||||||
|
auction-insufficient-funds: "&cYou don't have enough money. Required: {price}"
|
||||||
|
auction-buyout: "&aYou bought out the auction for {item} for {price}!"
|
||||||
|
auction-extended: "&eAuction extended by {seconds}s due to anti-snipe protection."
|
||||||
|
|
||||||
|
# Claim Messages
|
||||||
|
claim-success: "&aItem claimed successfully!"
|
||||||
|
claim-empty: "&eYou have no items to claim."
|
||||||
|
claim-inventory-full: "&cYour inventory is full! Please make space."
|
||||||
|
claim-all-success: "&aClaimed {count} items!"
|
||||||
|
|
||||||
|
# Earnings Messages
|
||||||
|
earnings-withdrawn: "&aWithdrew {amount}! New balance: {balance}"
|
||||||
|
earnings-empty: "&eYou have no pending earnings."
|
||||||
|
earnings-balance: "&aYour pending earnings: {amount}"
|
||||||
|
|
||||||
|
# Item Validation
|
||||||
|
invalid-item: "&cPlease select a valid item."
|
||||||
|
item-no-longer-available: "&cThe selected item is no longer in your inventory."
|
||||||
|
item-changed: "&cThe selected item has changed. Please select again."
|
||||||
|
blacklisted-item: "&cThis item type is not allowed on the market."
|
||||||
|
blacklisted-content: "&cThis item contains blacklisted content."
|
||||||
|
invalid-price: "&cInvalid price. Range: {min} - {max}"
|
||||||
|
invalid-amount: "&cInvalid amount."
|
||||||
|
invalid-duration: "&cInvalid duration."
|
||||||
|
|
||||||
|
# Admin Messages
|
||||||
|
admin-listing-removed: "&aListing #{id} removed by admin."
|
||||||
|
admin-auction-cancelled: "&aAuction #{id} cancelled by admin."
|
||||||
|
admin-reload: "&aConfiguration reloaded."
|
||||||
|
|
||||||
|
# GUI Titles (support color codes)
|
||||||
|
gui-titles:
|
||||||
|
main-menu: "&8&lCommunity Market"
|
||||||
|
browse-market: "&8&lBrowse Market &7(Page {page})"
|
||||||
|
browse-auctions: "&8&lBrowse Auctions &7(Page {page})"
|
||||||
|
create-listing: "&8&lCreate Listing"
|
||||||
|
create-auction: "&8&lCreate Auction"
|
||||||
|
select-item-listing: "&8&lSelect Item to Sell"
|
||||||
|
select-item-auction: "&8&lSelect Item to Auction"
|
||||||
|
my-listings: "&8&lMy Listings"
|
||||||
|
my-auctions: "&8&lMy Auctions"
|
||||||
|
claim-items: "&8&lClaim Items"
|
||||||
|
earnings: "&8&lEarnings"
|
||||||
|
confirm-purchase: "&8&lConfirm Purchase"
|
||||||
|
confirm-bid: "&8&lConfirm Bid"
|
||||||
|
confirm-cancel: "&8&lConfirm Cancellation"
|
||||||
|
number-input: "&8&lEnter Amount"
|
||||||
|
admin-panel: "&8&lAdmin Panel"
|
||||||
|
admin-listings: "&8&lAll Listings"
|
||||||
|
admin-auctions: "&8&lAll Auctions"
|
||||||
|
listing-details: "&8&lListing Details"
|
||||||
|
auction-details: "&8&lAuction Details"
|
||||||
|
filter-menu: "&8&lFilter Options"
|
||||||
|
sort-menu: "&8&lSort Options"
|
||||||
|
duration-select: "&8&lSelect Duration"
|
||||||
|
help: "&8&lHelp"
|
||||||
|
|
||||||
|
# Button Names
|
||||||
|
buttons:
|
||||||
|
# Main Menu
|
||||||
|
browse-market: "&aBrowse Market"
|
||||||
|
browse-auctions: "&6Browse Auctions"
|
||||||
|
create-listing: "&eCreate Listing"
|
||||||
|
create-auction: "&eCreate Auction"
|
||||||
|
my-listings: "&bMy Listings"
|
||||||
|
my-auctions: "&bMy Auctions"
|
||||||
|
claim-items: "&dClaim Items"
|
||||||
|
earnings: "&aEarnings"
|
||||||
|
help: "&fHelp"
|
||||||
|
admin: "&cAdmin Panel"
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
next-page: "&aNext Page →"
|
||||||
|
previous-page: "&a← Previous Page"
|
||||||
|
back: "&cBack"
|
||||||
|
close: "&cClose"
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
confirm: "&aConfirm"
|
||||||
|
cancel: "&cCancel"
|
||||||
|
buy: "&aBuy Now"
|
||||||
|
bid: "&6Place Bid"
|
||||||
|
buyout: "&eBuyout"
|
||||||
|
claim: "&aClaim"
|
||||||
|
claim-all: "&aClaim All"
|
||||||
|
withdraw: "&aWithdraw All"
|
||||||
|
remove: "&cRemove Listing"
|
||||||
|
cancel-auction: "&cCancel Auction"
|
||||||
|
|
||||||
|
# Number Input
|
||||||
|
add-1: "&a+1"
|
||||||
|
add-10: "&a+10"
|
||||||
|
add-100: "&a+100"
|
||||||
|
add-1000: "&a+1,000"
|
||||||
|
subtract-1: "&c-1"
|
||||||
|
subtract-10: "&c-10"
|
||||||
|
subtract-100: "&c-100"
|
||||||
|
subtract-1000: "&c-1,000"
|
||||||
|
set-min: "&eSet Min"
|
||||||
|
set-max: "&eSet Max"
|
||||||
|
custom-amount: "&bCustom Amount"
|
||||||
|
|
||||||
|
# Filters & Sort
|
||||||
|
filter: "&eFilter"
|
||||||
|
sort: "&eSort"
|
||||||
|
search: "&eSearch"
|
||||||
|
clear-filter: "&cClear Filters"
|
||||||
|
|
||||||
|
# Duration
|
||||||
|
duration-1h: "&e1 Hour"
|
||||||
|
duration-6h: "&e6 Hours"
|
||||||
|
duration-12h: "&e12 Hours"
|
||||||
|
duration-24h: "&e24 Hours"
|
||||||
|
duration-48h: "&e48 Hours"
|
||||||
|
duration-72h: "&e3 Days"
|
||||||
|
duration-168h: "&e7 Days"
|
||||||
|
duration-336h: "&e14 Days"
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
admin-view-listings: "&aView All Listings"
|
||||||
|
admin-view-auctions: "&6View All Auctions"
|
||||||
|
admin-reload: "&eReload Config"
|
||||||
|
|
||||||
|
# Button Lore (descriptions)
|
||||||
|
lore:
|
||||||
|
browse-market:
|
||||||
|
- "&7Browse all fixed-price"
|
||||||
|
- "&7listings from players."
|
||||||
|
- ""
|
||||||
|
- "&eClick to browse!"
|
||||||
|
browse-auctions:
|
||||||
|
- "&7Browse all active auctions"
|
||||||
|
- "&7and place bids."
|
||||||
|
- ""
|
||||||
|
- "&eClick to browse!"
|
||||||
|
create-listing:
|
||||||
|
- "&7Sell items at a fixed price."
|
||||||
|
- "&7Tax: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClick to create!"
|
||||||
|
create-auction:
|
||||||
|
- "&7Auction items to the"
|
||||||
|
- "&7highest bidder."
|
||||||
|
- "&7Tax: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClick to create!"
|
||||||
|
my-listings:
|
||||||
|
- "&7View and manage your"
|
||||||
|
- "&7active listings."
|
||||||
|
- ""
|
||||||
|
- "&7Active: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to view!"
|
||||||
|
my-auctions:
|
||||||
|
- "&7View and manage your"
|
||||||
|
- "&7active auctions."
|
||||||
|
- ""
|
||||||
|
- "&7Active: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to view!"
|
||||||
|
claim-items:
|
||||||
|
- "&7Claim items from expired"
|
||||||
|
- "&7listings or won auctions."
|
||||||
|
- ""
|
||||||
|
- "&7Pending: &f{count}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to claim!"
|
||||||
|
earnings:
|
||||||
|
- "&7View and withdraw your"
|
||||||
|
- "&7pending earnings from sales."
|
||||||
|
- ""
|
||||||
|
- "&7Pending: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to view!"
|
||||||
|
help:
|
||||||
|
- "&7Learn how to use the"
|
||||||
|
- "&7Community Market."
|
||||||
|
- ""
|
||||||
|
- "&eClick for help!"
|
||||||
|
admin:
|
||||||
|
- "&cAdmin Panel"
|
||||||
|
- "&7Manage listings and auctions."
|
||||||
|
- ""
|
||||||
|
- "&eClick to open!"
|
||||||
|
|
||||||
|
# Listing Info
|
||||||
|
listing-info:
|
||||||
|
- "&7Seller: &f{seller}"
|
||||||
|
- "&7Price: &a{price}"
|
||||||
|
- "&7Amount: &f{amount}"
|
||||||
|
- "&7Expires: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&eLeft-click to buy!"
|
||||||
|
|
||||||
|
# Auction Info
|
||||||
|
auction-info:
|
||||||
|
- "&7Seller: &f{seller}"
|
||||||
|
- "&7Starting bid: &a{start_price}"
|
||||||
|
- "&7Current bid: &a{current_bid}"
|
||||||
|
- "&7Bidder: &f{bidder}"
|
||||||
|
- "&7Bids: &f{bid_count}"
|
||||||
|
- "&7Ends: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&eLeft-click to bid!"
|
||||||
|
- "&eRight-click to buyout!"
|
||||||
|
|
||||||
|
# My Listing Info
|
||||||
|
my-listing-info:
|
||||||
|
- "&7Price: &a{price}"
|
||||||
|
- "&7Amount: &f{amount}"
|
||||||
|
- "&7Created: &f{created}"
|
||||||
|
- "&7Expires: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&cClick to cancel"
|
||||||
|
|
||||||
|
# My Auction Info
|
||||||
|
my-auction-info:
|
||||||
|
- "&7Starting bid: &a{start_price}"
|
||||||
|
- "&7Current bid: &a{current_bid}"
|
||||||
|
- "&7Bidder: &f{bidder}"
|
||||||
|
- "&7Bids: &f{bid_count}"
|
||||||
|
- "&7Ends: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&cClick to cancel (if no bids)"
|
||||||
|
|
||||||
|
# Confirm Purchase
|
||||||
|
confirm-purchase-info:
|
||||||
|
- "&7You are purchasing:"
|
||||||
|
- "&f{item} x{amount}"
|
||||||
|
- ""
|
||||||
|
- "&7Price: &a{price}"
|
||||||
|
- "&7Tax: &e{tax}"
|
||||||
|
- "&7Total: &a{total}"
|
||||||
|
- ""
|
||||||
|
- "&aClick to confirm!"
|
||||||
|
|
||||||
|
# Confirm Bid
|
||||||
|
confirm-bid-info:
|
||||||
|
- "&7You are bidding on:"
|
||||||
|
- "&f{item}"
|
||||||
|
- ""
|
||||||
|
- "&7Your bid: &a{bid}"
|
||||||
|
- "&7Current high: &e{current}"
|
||||||
|
- ""
|
||||||
|
- "&aClick to confirm!"
|
||||||
|
|
||||||
|
# Claim Item
|
||||||
|
claim-item-info:
|
||||||
|
- "&7Reason: &f{reason}"
|
||||||
|
- "&7From: &f{source}"
|
||||||
|
- "&7Date: &f{date}"
|
||||||
|
- ""
|
||||||
|
- "&eClick to claim!"
|
||||||
|
|
||||||
|
# Earnings Info
|
||||||
|
earnings-info:
|
||||||
|
- "&7Your pending earnings"
|
||||||
|
- "&7from market sales."
|
||||||
|
- ""
|
||||||
|
- "&7Total: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&aClick to withdraw!"
|
||||||
|
|
||||||
|
# Number Input Info
|
||||||
|
current-value:
|
||||||
|
- "&7Current: &a{value}"
|
||||||
|
|
||||||
|
# Filter Options
|
||||||
|
filters:
|
||||||
|
all: "&fAll Items"
|
||||||
|
weapons: "&cWeapons"
|
||||||
|
armor: "&bArmor"
|
||||||
|
tools: "&eTools"
|
||||||
|
blocks: "&7Blocks"
|
||||||
|
food: "&6Food"
|
||||||
|
potions: "&dPotions"
|
||||||
|
materials: "&aMaterials"
|
||||||
|
enchanted: "&5Enchanted Items"
|
||||||
|
misc: "&8Miscellaneous"
|
||||||
|
|
||||||
|
# Sort Options
|
||||||
|
sort:
|
||||||
|
newest: "&aNewest First"
|
||||||
|
oldest: "&eOldest First"
|
||||||
|
price-low: "&aPrice: Low to High"
|
||||||
|
price-high: "&cPrice: High to Low"
|
||||||
|
ending-soon: "&6Ending Soon"
|
||||||
|
most-bids: "&bMost Bids"
|
||||||
|
|
||||||
|
# Time Formats
|
||||||
|
time:
|
||||||
|
expired: "&cExpired"
|
||||||
|
days: "{d}d"
|
||||||
|
hours: "{h}h"
|
||||||
|
minutes: "{m}m"
|
||||||
|
seconds: "{s}s"
|
||||||
|
|
||||||
|
# Help Content
|
||||||
|
help:
|
||||||
|
title: "&6&lCommunity Market Help"
|
||||||
|
content:
|
||||||
|
- "&eBrowse Market &7- View and buy fixed-price listings"
|
||||||
|
- "&eBrowse Auctions &7- View and bid on auctions"
|
||||||
|
- "&eCreate Listing &7- Sell items at a fixed price"
|
||||||
|
- "&eCreate Auction &7- Auction items to highest bidder"
|
||||||
|
- "&eMy Listings &7- Manage your active listings"
|
||||||
|
- "&eMy Auctions &7- Manage your active auctions"
|
||||||
|
- "&eClaim Items &7- Collect unsold/won items"
|
||||||
|
- "&eEarnings &7- Withdraw money from sales"
|
||||||
|
- ""
|
||||||
|
- "&7&oTip: All actions are done through GUIs!"
|
||||||
|
- "&7&oJust click on buttons to navigate."
|
||||||
|
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
# ============================================
|
||||||
|
# CommunityMarket Ficheiro de Idioma - Português (Portugal)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Geral
|
||||||
|
prefix: "&8[&6Mercado&8] &r"
|
||||||
|
|
||||||
|
# Mensagens Gerais
|
||||||
|
messages:
|
||||||
|
no-permission: "&cNão tens permissão para fazer isso."
|
||||||
|
player-only: "&cEste comando só pode ser usado por jogadores."
|
||||||
|
reload-success: "&aConfiguração recarregada com sucesso!"
|
||||||
|
economy-not-found: "&cNenhum plugin de economia encontrado! Mercado desativado."
|
||||||
|
|
||||||
|
# Mensagens de Anúncios
|
||||||
|
listing-created: "&aAnúncio criado com sucesso! ID: #{id}"
|
||||||
|
listing-cancelled: "&aAnúncio cancelado. Item devolvido ao armazém."
|
||||||
|
listing-expired: "&eO teu anúncio #{id} expirou. Item movido para o armazém."
|
||||||
|
listing-purchased: "&aCompraste {item} x{amount} por {price}!"
|
||||||
|
listing-sold: "&aO teu {item} x{amount} foi vendido a {buyer} por {price}!"
|
||||||
|
listing-limit-reached: "&cAtingiste o número máximo de anúncios ({max})."
|
||||||
|
listing-cooldown: "&cPor favor aguarda {time} antes de criar outro anúncio."
|
||||||
|
listing-not-found: "&cAnúncio não encontrado ou já não está disponível."
|
||||||
|
listing-own-item: "&cNão podes comprar o teu próprio anúncio."
|
||||||
|
listing-insufficient-funds: "&cNão tens dinheiro suficiente. Necessário: {price}"
|
||||||
|
|
||||||
|
# Mensagens de Leilões
|
||||||
|
auction-created: "&aLeilão criado com sucesso! ID: #{id}"
|
||||||
|
auction-cancelled: "&aLeilão cancelado. Item devolvido ao armazém."
|
||||||
|
auction-ended-winner: "&aParabéns! Ganhaste o leilão por {item}! Pagaste: {price}"
|
||||||
|
auction-ended-seller: "&aO teu leilão por {item} terminou! Vencedor: {winner}, Ganhaste: {price}"
|
||||||
|
auction-ended-no-bids: "&eO teu leilão por {item} terminou sem licitações. Item movido para o armazém."
|
||||||
|
auction-limit-reached: "&cAtingiste o número máximo de leilões ({max})."
|
||||||
|
auction-not-found: "&cLeilão não encontrado ou já não está disponível."
|
||||||
|
auction-own-item: "&cNão podes licitar no teu próprio leilão."
|
||||||
|
auction-bid-placed: "&aFizeste uma licitação de {amount} em {item}!"
|
||||||
|
auction-outbid: "&eForam feitas licitações superiores à tua em {item}! Nova licitação: {amount} por {bidder}"
|
||||||
|
auction-bid-too-low: "&cLicitação muito baixa! Mínimo: {min}"
|
||||||
|
auction-insufficient-funds: "&cNão tens dinheiro suficiente. Necessário: {price}"
|
||||||
|
auction-buyout: "&aCompraste o leilão de {item} por {price}!"
|
||||||
|
auction-extended: "&eLeilão prolongado por {seconds}s devido à proteção anti-snipe."
|
||||||
|
|
||||||
|
# Mensagens de Reclamação
|
||||||
|
claim-success: "&aItem reclamado com sucesso!"
|
||||||
|
claim-empty: "&eNão tens itens para reclamar."
|
||||||
|
claim-inventory-full: "&cO teu inventário está cheio! Por favor liberta espaço."
|
||||||
|
claim-all-success: "&aReclamaste {count} itens!"
|
||||||
|
|
||||||
|
# Mensagens de Ganhos
|
||||||
|
earnings-withdrawn: "&aLevantaste {amount}! Novo saldo: {balance}"
|
||||||
|
earnings-empty: "&eNão tens ganhos pendentes."
|
||||||
|
earnings-balance: "&aOs teus ganhos pendentes: {amount}"
|
||||||
|
|
||||||
|
# Validação de Itens
|
||||||
|
invalid-item: "&cPor favor seleciona um item válido."
|
||||||
|
item-no-longer-available: "&cO item selecionado já não está no teu inventário."
|
||||||
|
item-changed: "&cO item selecionado foi alterado. Por favor seleciona novamente."
|
||||||
|
blacklisted-item: "&cEste tipo de item não é permitido no mercado."
|
||||||
|
blacklisted-content: "&cEste item contém conteúdo bloqueado."
|
||||||
|
invalid-price: "&cPreço inválido. Intervalo: {min} - {max}"
|
||||||
|
invalid-amount: "&cQuantidade inválida."
|
||||||
|
invalid-duration: "&cDuração inválida."
|
||||||
|
|
||||||
|
# Mensagens de Admin
|
||||||
|
admin-listing-removed: "&aAnúncio #{id} removido pelo admin."
|
||||||
|
admin-auction-cancelled: "&aLeilão #{id} cancelado pelo admin."
|
||||||
|
admin-reload: "&aConfiguração recarregada."
|
||||||
|
|
||||||
|
# Títulos GUI (suportam códigos de cor)
|
||||||
|
gui-titles:
|
||||||
|
main-menu: "&8&lMercado Comunitário"
|
||||||
|
browse-market: "&8&lExplorar Mercado &7(Página {page})"
|
||||||
|
browse-auctions: "&8&lExplorar Leilões &7(Página {page})"
|
||||||
|
create-listing: "&8&lCriar Anúncio"
|
||||||
|
create-auction: "&8&lCriar Leilão"
|
||||||
|
select-item-listing: "&8&lSelecionar Item para Vender"
|
||||||
|
select-item-auction: "&8&lSelecionar Item para Leilão"
|
||||||
|
my-listings: "&8&lOs Meus Anúncios"
|
||||||
|
my-auctions: "&8&lOs Meus Leilões"
|
||||||
|
claim-items: "&8&lReclamar Itens"
|
||||||
|
earnings: "&8&lGanhos"
|
||||||
|
confirm-purchase: "&8&lConfirmar Compra"
|
||||||
|
confirm-bid: "&8&lConfirmar Licitação"
|
||||||
|
confirm-cancel: "&8&lConfirmar Cancelamento"
|
||||||
|
number-input: "&8&lIntroduzir Valor"
|
||||||
|
admin-panel: "&8&lPainel de Admin"
|
||||||
|
admin-listings: "&8&lTodos os Anúncios"
|
||||||
|
admin-auctions: "&8&lTodos os Leilões"
|
||||||
|
listing-details: "&8&lDetalhes do Anúncio"
|
||||||
|
auction-details: "&8&lDetalhes do Leilão"
|
||||||
|
filter-menu: "&8&lOpções de Filtro"
|
||||||
|
sort-menu: "&8&lOpções de Ordenação"
|
||||||
|
duration-select: "&8&lSelecionar Duração"
|
||||||
|
help: "&8&lAjuda"
|
||||||
|
|
||||||
|
# Nomes dos Botões
|
||||||
|
buttons:
|
||||||
|
# Menu Principal
|
||||||
|
browse-market: "&aExplorar Mercado"
|
||||||
|
browse-auctions: "&6Explorar Leilões"
|
||||||
|
create-listing: "&eCriar Anúncio"
|
||||||
|
create-auction: "&eCriar Leilão"
|
||||||
|
my-listings: "&bOs Meus Anúncios"
|
||||||
|
my-auctions: "&bOs Meus Leilões"
|
||||||
|
claim-items: "&dReclamar Itens"
|
||||||
|
earnings: "&aGanhos"
|
||||||
|
help: "&fAjuda"
|
||||||
|
admin: "&cPainel de Admin"
|
||||||
|
|
||||||
|
# Navegação
|
||||||
|
next-page: "&aPágina Seguinte →"
|
||||||
|
previous-page: "&a← Página Anterior"
|
||||||
|
back: "&cVoltar"
|
||||||
|
close: "&cFechar"
|
||||||
|
|
||||||
|
# Ações
|
||||||
|
confirm: "&aConfirmar"
|
||||||
|
cancel: "&cCancelar"
|
||||||
|
buy: "&aComprar Agora"
|
||||||
|
bid: "&6Fazer Licitação"
|
||||||
|
buyout: "&eCompra Imediata"
|
||||||
|
claim: "&aReclamar"
|
||||||
|
claim-all: "&aReclamar Tudo"
|
||||||
|
withdraw: "&aLevantar Tudo"
|
||||||
|
remove: "&cRemover Anúncio"
|
||||||
|
cancel-auction: "&cCancelar Leilão"
|
||||||
|
|
||||||
|
# Entrada Numérica
|
||||||
|
add-1: "&a+1"
|
||||||
|
add-10: "&a+10"
|
||||||
|
add-100: "&a+100"
|
||||||
|
add-1000: "&a+1.000"
|
||||||
|
subtract-1: "&c-1"
|
||||||
|
subtract-10: "&c-10"
|
||||||
|
subtract-100: "&c-100"
|
||||||
|
subtract-1000: "&c-1.000"
|
||||||
|
set-min: "&eDefinir Mín"
|
||||||
|
set-max: "&eDefinir Máx"
|
||||||
|
custom-amount: "&bValor Personalizado"
|
||||||
|
|
||||||
|
# Filtros e Ordenação
|
||||||
|
filter: "&eFiltro"
|
||||||
|
sort: "&eOrdenar"
|
||||||
|
search: "&ePesquisar"
|
||||||
|
clear-filter: "&cLimpar Filtros"
|
||||||
|
|
||||||
|
# Duração
|
||||||
|
duration-1h: "&e1 Hora"
|
||||||
|
duration-6h: "&e6 Horas"
|
||||||
|
duration-12h: "&e12 Horas"
|
||||||
|
duration-24h: "&e24 Horas"
|
||||||
|
duration-48h: "&e48 Horas"
|
||||||
|
duration-72h: "&e3 Dias"
|
||||||
|
duration-168h: "&e7 Dias"
|
||||||
|
duration-336h: "&e14 Dias"
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
admin-view-listings: "&aVer Todos os Anúncios"
|
||||||
|
admin-view-auctions: "&6Ver Todos os Leilões"
|
||||||
|
admin-reload: "&eRecarregar Config"
|
||||||
|
|
||||||
|
# Lore dos Botões (descrições)
|
||||||
|
lore:
|
||||||
|
browse-market:
|
||||||
|
- "&7Explora todos os anúncios"
|
||||||
|
- "&7de preço fixo dos jogadores."
|
||||||
|
- ""
|
||||||
|
- "&eClica para explorar!"
|
||||||
|
browse-auctions:
|
||||||
|
- "&7Explora todos os leilões ativos"
|
||||||
|
- "&7e faz licitações."
|
||||||
|
- ""
|
||||||
|
- "&eClica para explorar!"
|
||||||
|
create-listing:
|
||||||
|
- "&7Vende itens a um preço fixo."
|
||||||
|
- "&7Taxa: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClica para criar!"
|
||||||
|
create-auction:
|
||||||
|
- "&7Leiloa itens ao"
|
||||||
|
- "&7maior licitador."
|
||||||
|
- "&7Taxa: &f{tax}%"
|
||||||
|
- ""
|
||||||
|
- "&eClica para criar!"
|
||||||
|
my-listings:
|
||||||
|
- "&7Vê e gere os teus"
|
||||||
|
- "&7anúncios ativos."
|
||||||
|
- ""
|
||||||
|
- "&7Ativos: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para ver!"
|
||||||
|
my-auctions:
|
||||||
|
- "&7Vê e gere os teus"
|
||||||
|
- "&7leilões ativos."
|
||||||
|
- ""
|
||||||
|
- "&7Ativos: &f{count}/{max}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para ver!"
|
||||||
|
claim-items:
|
||||||
|
- "&7Reclama itens de anúncios"
|
||||||
|
- "&7expirados ou leilões ganhos."
|
||||||
|
- ""
|
||||||
|
- "&7Pendentes: &f{count}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para reclamar!"
|
||||||
|
earnings:
|
||||||
|
- "&7Vê e levanta os teus"
|
||||||
|
- "&7ganhos pendentes das vendas."
|
||||||
|
- ""
|
||||||
|
- "&7Pendente: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para ver!"
|
||||||
|
help:
|
||||||
|
- "&7Aprende a usar o"
|
||||||
|
- "&7Mercado Comunitário."
|
||||||
|
- ""
|
||||||
|
- "&eClica para ajuda!"
|
||||||
|
admin:
|
||||||
|
- "&cPainel de Admin"
|
||||||
|
- "&7Gere anúncios e leilões."
|
||||||
|
- ""
|
||||||
|
- "&eClica para abrir!"
|
||||||
|
|
||||||
|
# Info do Anúncio
|
||||||
|
listing-info:
|
||||||
|
- "&7Vendedor: &f{seller}"
|
||||||
|
- "&7Preço: &a{price}"
|
||||||
|
- "&7Quantidade: &f{amount}"
|
||||||
|
- "&7Expira: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&eClique esquerdo para comprar!"
|
||||||
|
|
||||||
|
# Info do Leilão
|
||||||
|
auction-info:
|
||||||
|
- "&7Vendedor: &f{seller}"
|
||||||
|
- "&7Licitação inicial: &a{start_price}"
|
||||||
|
- "&7Licitação atual: &a{current_bid}"
|
||||||
|
- "&7Licitador: &f{bidder}"
|
||||||
|
- "&7Licitações: &f{bid_count}"
|
||||||
|
- "&7Termina: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&eClique esquerdo para licitar!"
|
||||||
|
- "&eClique direito para compra imediata!"
|
||||||
|
|
||||||
|
# Info do Meu Anúncio
|
||||||
|
my-listing-info:
|
||||||
|
- "&7Preço: &a{price}"
|
||||||
|
- "&7Quantidade: &f{amount}"
|
||||||
|
- "&7Criado: &f{created}"
|
||||||
|
- "&7Expira: &f{expires}"
|
||||||
|
- ""
|
||||||
|
- "&cClica para cancelar"
|
||||||
|
|
||||||
|
# Info do Meu Leilão
|
||||||
|
my-auction-info:
|
||||||
|
- "&7Licitação inicial: &a{start_price}"
|
||||||
|
- "&7Licitação atual: &a{current_bid}"
|
||||||
|
- "&7Licitador: &f{bidder}"
|
||||||
|
- "&7Licitações: &f{bid_count}"
|
||||||
|
- "&7Termina: &f{ends}"
|
||||||
|
- ""
|
||||||
|
- "&cClica para cancelar (sem licitações)"
|
||||||
|
|
||||||
|
# Confirmar Compra
|
||||||
|
confirm-purchase-info:
|
||||||
|
- "&7Estás a comprar:"
|
||||||
|
- "&f{item} x{amount}"
|
||||||
|
- ""
|
||||||
|
- "&7Preço: &a{price}"
|
||||||
|
- "&7Taxa: &e{tax}"
|
||||||
|
- "&7Total: &a{total}"
|
||||||
|
- ""
|
||||||
|
- "&aClica para confirmar!"
|
||||||
|
|
||||||
|
# Confirmar Licitação
|
||||||
|
confirm-bid-info:
|
||||||
|
- "&7Estás a licitar em:"
|
||||||
|
- "&f{item}"
|
||||||
|
- ""
|
||||||
|
- "&7A tua licitação: &a{bid}"
|
||||||
|
- "&7Atual mais alta: &e{current}"
|
||||||
|
- ""
|
||||||
|
- "&aClica para confirmar!"
|
||||||
|
|
||||||
|
# Reclamar Item
|
||||||
|
claim-item-info:
|
||||||
|
- "&7Razão: &f{reason}"
|
||||||
|
- "&7De: &f{source}"
|
||||||
|
- "&7Data: &f{date}"
|
||||||
|
- ""
|
||||||
|
- "&eClica para reclamar!"
|
||||||
|
|
||||||
|
# Info de Ganhos
|
||||||
|
earnings-info:
|
||||||
|
- "&7Os teus ganhos pendentes"
|
||||||
|
- "&7das vendas no mercado."
|
||||||
|
- ""
|
||||||
|
- "&7Total: &a{amount}"
|
||||||
|
- ""
|
||||||
|
- "&aClica para levantar!"
|
||||||
|
|
||||||
|
# Info de Entrada Numérica
|
||||||
|
current-value:
|
||||||
|
- "&7Atual: &a{value}"
|
||||||
|
|
||||||
|
# Opções de Filtro
|
||||||
|
filters:
|
||||||
|
all: "&fTodos os Itens"
|
||||||
|
weapons: "&cArmas"
|
||||||
|
armor: "&bArmadura"
|
||||||
|
tools: "&eFerramentas"
|
||||||
|
blocks: "&7Blocos"
|
||||||
|
food: "&6Comida"
|
||||||
|
potions: "&dPoções"
|
||||||
|
materials: "&aMateriais"
|
||||||
|
enchanted: "&5Itens Encantados"
|
||||||
|
misc: "&8Diversos"
|
||||||
|
|
||||||
|
# Opções de Ordenação
|
||||||
|
sort:
|
||||||
|
newest: "&aMais Recente"
|
||||||
|
oldest: "&eMais Antigo"
|
||||||
|
price-low: "&aPreço: Menor para Maior"
|
||||||
|
price-high: "&cPreço: Maior para Menor"
|
||||||
|
ending-soon: "&6A Terminar Em Breve"
|
||||||
|
most-bids: "&bMais Licitações"
|
||||||
|
|
||||||
|
# Formatos de Tempo
|
||||||
|
time:
|
||||||
|
expired: "&cExpirado"
|
||||||
|
days: "{d}d"
|
||||||
|
hours: "{h}h"
|
||||||
|
minutes: "{m}m"
|
||||||
|
seconds: "{s}s"
|
||||||
|
|
||||||
|
# Conteúdo de Ajuda
|
||||||
|
help:
|
||||||
|
title: "&6&lAjuda do Mercado Comunitário"
|
||||||
|
content:
|
||||||
|
- "&eExplorar Mercado &7- Ver e comprar anúncios de preço fixo"
|
||||||
|
- "&eExplorar Leilões &7- Ver e licitar em leilões"
|
||||||
|
- "&eCriar Anúncio &7- Vender itens a um preço fixo"
|
||||||
|
- "&eCriar Leilão &7- Leiloar itens ao maior licitador"
|
||||||
|
- "&eOs Meus Anúncios &7- Gerir os teus anúncios ativos"
|
||||||
|
- "&eOs Meus Leilões &7- Gerir os teus leilões ativos"
|
||||||
|
- "&eReclamar Itens &7- Recolher itens não vendidos/ganhos"
|
||||||
|
- "&eGanhos &7- Levantar dinheiro das vendas"
|
||||||
|
- ""
|
||||||
|
- "&7&oDica: Todas as ações são feitas através de GUIs!"
|
||||||
|
- "&7&oBasta clicar nos botões para navegar."
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
name: CommunityMarket
|
||||||
|
version: '1.0.0'
|
||||||
|
main: pt.henrique.communityMarket.CommunityMarket
|
||||||
|
api-version: '1.21'
|
||||||
|
description: A GUI-only marketplace plugin for fixed-price listings and auctions
|
||||||
|
author: Henrique
|
||||||
|
website: https://github.com/henrique/CommunityMarket
|
||||||
|
|
||||||
|
# Soft dependencies - plugin will detect and use these if available
|
||||||
|
softdepend:
|
||||||
|
- Vault
|
||||||
|
- Essentials
|
||||||
|
|
||||||
|
load: POSTWORLD
|
||||||
|
|
||||||
|
commands:
|
||||||
|
market:
|
||||||
|
description: Opens the Community Market main menu
|
||||||
|
usage: /<command>
|
||||||
|
aliases: [cmarket]
|
||||||
|
permission: communitymarket.use
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
communitymarket.*:
|
||||||
|
description: Grants all CommunityMarket permissions
|
||||||
|
default: op
|
||||||
|
children:
|
||||||
|
communitymarket.use: true
|
||||||
|
communitymarket.sell: true
|
||||||
|
communitymarket.auction: true
|
||||||
|
communitymarket.buy: true
|
||||||
|
communitymarket.bid: true
|
||||||
|
communitymarket.claim: true
|
||||||
|
communitymarket.withdraw: true
|
||||||
|
communitymarket.admin: true
|
||||||
|
|
||||||
|
communitymarket.use:
|
||||||
|
description: Allows access to the market GUI
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.sell:
|
||||||
|
description: Allows creating fixed-price listings
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.auction:
|
||||||
|
description: Allows creating auctions
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.buy:
|
||||||
|
description: Allows purchasing from the market
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.bid:
|
||||||
|
description: Allows bidding on auctions
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.claim:
|
||||||
|
description: Allows claiming items from storage
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.withdraw:
|
||||||
|
description: Allows withdrawing earnings
|
||||||
|
default: true
|
||||||
|
|
||||||
|
communitymarket.admin:
|
||||||
|
description: Allows access to admin functions
|
||||||
|
default: op
|
||||||
|
children:
|
||||||
|
communitymarket.admin.viewall: true
|
||||||
|
communitymarket.admin.remove: true
|
||||||
|
communitymarket.admin.reload: true
|
||||||
|
|
||||||
|
communitymarket.admin.viewall:
|
||||||
|
description: Allows viewing all listings/auctions
|
||||||
|
default: op
|
||||||
|
|
||||||
|
communitymarket.admin.remove:
|
||||||
|
description: Allows removing any listing or auction
|
||||||
|
default: op
|
||||||
|
|
||||||
|
communitymarket.admin.reload:
|
||||||
|
description: Allows reloading configuration
|
||||||
|
default: op
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user