commit a3fcb844a62e9aa9384cba9ebbee07343cb6c220 Author: henriquescrrrr Date: Wed Jan 14 00:04:03 2026 +0000 Initial Realease diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -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/ diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..a4aa64a --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..da1acce --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7ace097 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d16871e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2e53c0 --- /dev/null +++ b/README.md @@ -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 + diff --git a/communitymarket.iml b/communitymarket.iml new file mode 100644 index 0000000..a376b96 --- /dev/null +++ b/communitymarket.iml @@ -0,0 +1,13 @@ + + + + + + + ADVENTURE + + 1 + + + + \ No newline at end of file diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..64288af --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + pt.henrique + CommunityMarket + CommunityMarket + 1.0.0 + A GUI-only marketplace plugin for Minecraft Paper servers + + clean package + + + true + src/main/resources + + + ${project.name}-${project.version} + + + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + --enable-preview + + + + + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + com.zaxxer.hikari + pt.henrique.communityMarket.lib.hikari + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + jitpack.io + https://jitpack.io + + + essentials-releases + https://repo.essentialsx.net/releases/ + + + + + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT + provided + + + com.github.MilkBowl + VaultAPI + 1.7.1 + provided + + + net.essentialsx + EssentialsX + 2.20.1 + provided + + + + 21 + UTF-8 + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..97a43a8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + pt.henrique + CommunityMarket + 1.0.0 + jar + + CommunityMarket + A GUI-only marketplace plugin for Minecraft Paper servers + + + 21 + UTF-8 + + + + clean package + ${project.name}-${project.version} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + --enable-preview + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + com.zaxxer.hikari + pt.henrique.communityMarket.lib.hikari + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + src/main/resources + true + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + jitpack.io + https://jitpack.io + + + essentials-releases + https://repo.essentialsx.net/releases/ + + + + + + + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT + provided + + + + + com.github.MilkBowl + VaultAPI + 1.7.1 + provided + + + + + net.essentialsx + EssentialsX + 2.20.1 + provided + + + + + com.zaxxer + HikariCP + 5.1.0 + compile + + + diff --git a/src/main/java/pt/henrique/communityMarket/CommunityMarket.java b/src/main/java/pt/henrique/communityMarket/CommunityMarket.java new file mode 100644 index 0000000..ca85bd1 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/CommunityMarket.java @@ -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; + } +} diff --git a/src/main/java/pt/henrique/communityMarket/command/MarketCommand.java b/src/main/java/pt/henrique/communityMarket/command/MarketCommand.java new file mode 100644 index 0000000..c65dd2e --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/command/MarketCommand.java @@ -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 onTabComplete(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + // No tab completions - everything is GUI-based + return Collections.emptyList(); + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/config/ConfigManager.java b/src/main/java/pt/henrique/communityMarket/config/ConfigManager.java new file mode 100644 index 0000000..0393458 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/config/ConfigManager.java @@ -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 availableDurations; + private double minPrice; + private double maxPrice; + + // Auction settings + private int maxAuctionsPerPlayer; + private int minDurationHours; + private int maxDurationHours; + private int defaultAuctionDurationHours; + private List 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 blacklistedMaterials; + private List 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 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 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 getBlacklistedMaterials() { return blacklistedMaterials; } + public List 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/config/MessageManager.java b/src/main/java/pt/henrique/communityMarket/config/MessageManager.java new file mode 100644 index 0000000..2efa189 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/config/MessageManager.java @@ -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 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 placeholders) { + String message = getRaw(path); + for (Map.Entry 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 placeholders) { + String message = getRaw(path); + for (Map.Entry 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 getList(String path) { + return messagesConfig.getStringList(path); + } + + /** + * Gets a list of messages as Components + */ + public List 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 getLore(String loreKey) { + return getList("lore." + loreKey); + } + + /** + * Gets lore with placeholders replaced + */ + public List getLore(String loreKey, Map placeholders) { + List lore = getList("lore." + loreKey); + return lore.stream() + .map(line -> { + String result = line; + for (Map.Entry 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); + } +} diff --git a/src/main/java/pt/henrique/communityMarket/db/DatabaseManager.java b/src/main/java/pt/henrique/communityMarket/db/DatabaseManager.java new file mode 100644 index 0000000..821f316 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/db/DatabaseManager.java @@ -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 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> getActiveListings() { + return CompletableFuture.supplyAsync(() -> { + List 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> 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> getPlayerListings(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> { + List 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 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 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 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> getExpiredListings() { + return CompletableFuture.supplyAsync(() -> { + List 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 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> getActiveAuctions() { + return CompletableFuture.supplyAsync(() -> { + List 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> 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> getPlayerAuctions(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> { + List 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 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 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 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> getEndedAuctions() { + return CompletableFuture.supplyAsync(() -> { + List 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 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> getPlayerClaimItems(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> { + List 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 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 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 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 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 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> 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); + } + }); + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/economy/EconomyManager.java b/src/main/java/pt/henrique/communityMarket/economy/EconomyManager.java new file mode 100644 index 0000000..7eac470 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/economy/EconomyManager.java @@ -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. + *

+ * 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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/AdminGui.java b/src/main/java/pt/henrique/communityMarket/gui/AdminGui.java new file mode 100644 index 0000000..e41f34e --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/AdminGui.java @@ -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 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 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; + } + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/BrowseAuctionsGui.java b/src/main/java/pt/henrique/communityMarket/gui/BrowseAuctionsGui.java new file mode 100644 index 0000000..c7bd6bc --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/BrowseAuctionsGui.java @@ -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 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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/BrowseMarketGui.java b/src/main/java/pt/henrique/communityMarket/gui/BrowseMarketGui.java new file mode 100644 index 0000000..8a52de9 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/BrowseMarketGui.java @@ -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

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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/ClaimGui.java b/src/main/java/pt/henrique/communityMarket/gui/ClaimGui.java new file mode 100644 index 0000000..709b564 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/ClaimGui.java @@ -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 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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/ConfirmationGui.java b/src/main/java/pt/henrique/communityMarket/gui/ConfirmationGui.java new file mode 100644 index 0000000..dfb9301 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/ConfirmationGui.java @@ -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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/CreateAuctionGui.java b/src/main/java/pt/henrique/communityMarket/gui/CreateAuctionGui.java new file mode 100644 index 0000000..2aa0a62 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/CreateAuctionGui.java @@ -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 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; + } +} diff --git a/src/main/java/pt/henrique/communityMarket/gui/CreateListingGui.java b/src/main/java/pt/henrique/communityMarket/gui/CreateListingGui.java new file mode 100644 index 0000000..27120f3 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/CreateListingGui.java @@ -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 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; + } +} diff --git a/src/main/java/pt/henrique/communityMarket/gui/EarningsGui.java b/src/main/java/pt/henrique/communityMarket/gui/EarningsGui.java new file mode 100644 index 0000000..cf7920d --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/EarningsGui.java @@ -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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/GuiManager.java b/src/main/java/pt/henrique/communityMarket/gui/GuiManager.java new file mode 100644 index 0000000..0a69526 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/GuiManager.java @@ -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 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; } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/HelpGui.java b/src/main/java/pt/henrique/communityMarket/gui/HelpGui.java new file mode 100644 index 0000000..563de04 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/HelpGui.java @@ -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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/ItemSelectionGui.java b/src/main/java/pt/henrique/communityMarket/gui/ItemSelectionGui.java new file mode 100644 index 0000000..4f4f456 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/ItemSelectionGui.java @@ -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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/MainMenuGui.java b/src/main/java/pt/henrique/communityMarket/gui/MainMenuGui.java new file mode 100644 index 0000000..c4f73fe --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/MainMenuGui.java @@ -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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/MarketGui.java b/src/main/java/pt/henrique/communityMarket/gui/MarketGui.java new file mode 100644 index 0000000..2078bcc --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/MarketGui.java @@ -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 + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/MyAuctionsGui.java b/src/main/java/pt/henrique/communityMarket/gui/MyAuctionsGui.java new file mode 100644 index 0000000..0274ff4 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/MyAuctionsGui.java @@ -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 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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/MyListingsGui.java b/src/main/java/pt/henrique/communityMarket/gui/MyListingsGui.java new file mode 100644 index 0000000..deb382c --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/MyListingsGui.java @@ -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 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 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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/gui/NumberInputGui.java b/src/main/java/pt/henrique/communityMarket/gui/NumberInputGui.java new file mode 100644 index 0000000..844f4f0 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/NumberInputGui.java @@ -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; + } +} diff --git a/src/main/java/pt/henrique/communityMarket/gui/QuantitySelectGui.java b/src/main/java/pt/henrique/communityMarket/gui/QuantitySelectGui.java new file mode 100644 index 0000000..157162b --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/gui/QuantitySelectGui.java @@ -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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/listener/GuiListener.java b/src/main/java/pt/henrique/communityMarket/listener/GuiListener.java new file mode 100644 index 0000000..21b46e5 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/listener/GuiListener.java @@ -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); + } + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/listener/PlayerListener.java b/src/main/java/pt/henrique/communityMarket/listener/PlayerListener.java new file mode 100644 index 0000000..dfdf6d5 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/listener/PlayerListener.java @@ -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))); + } + }); + } + }); + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/model/Auction.java b/src/main/java/pt/henrique/communityMarket/model/Auction.java new file mode 100644 index 0000000..296b85a --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/model/Auction.java @@ -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 + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/model/Bid.java b/src/main/java/pt/henrique/communityMarket/model/Bid.java new file mode 100644 index 0000000..d6a2ae0 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/model/Bid.java @@ -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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/model/ClaimItem.java b/src/main/java/pt/henrique/communityMarket/model/ClaimItem.java new file mode 100644 index 0000000..64a20d8 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/model/ClaimItem.java @@ -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; + } + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/model/Listing.java b/src/main/java/pt/henrique/communityMarket/model/Listing.java new file mode 100644 index 0000000..2698725 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/model/Listing.java @@ -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 + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/model/PendingEarnings.java b/src/main/java/pt/henrique/communityMarket/model/PendingEarnings.java new file mode 100644 index 0000000..8b6bed2 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/model/PendingEarnings.java @@ -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; + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/service/AuctionService.java b/src/main/java/pt/henrique/communityMarket/service/AuctionService.java new file mode 100644 index 0000000..3b0eef4 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/service/AuctionService.java @@ -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 cachedAuctions; + private long cacheExpiry = 0; + + // Track pending operations to prevent race conditions + private final Map 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 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> 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> getAuction(int id) { + return plugin.getDatabaseManager().getAuction(id); + } + + /** + * Gets all auctions for a specific player + */ + public CompletableFuture> getPlayerAuctions(UUID playerUuid) { + return plugin.getDatabaseManager().getPlayerAuctions(playerUuid); + } + + /** + * Counts active auctions for a player + */ + public CompletableFuture countPlayerAuctions(UUID playerUuid) { + return plugin.getDatabaseManager().countPlayerAuctions(playerUuid); + } + + /** + * Checks if a player can create a new auction (not at limit) + */ + public CompletableFuture 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 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 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 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 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 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 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 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 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 + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/service/ClaimService.java b/src/main/java/pt/henrique/communityMarket/service/ClaimService.java new file mode 100644 index 0000000..ed5e404 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/service/ClaimService.java @@ -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> 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 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 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 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 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 + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/service/EarningsService.java b/src/main/java/pt/henrique/communityMarket/service/EarningsService.java new file mode 100644 index 0000000..d1940b5 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/service/EarningsService.java @@ -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 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 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 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; + } + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/service/ListingService.java b/src/main/java/pt/henrique/communityMarket/service/ListingService.java new file mode 100644 index 0000000..d749efa --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/service/ListingService.java @@ -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 cachedListings; + private long cacheExpiry = 0; + + // Track pending operations to prevent double-purchases + private final Map 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 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> 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> getListing(int id) { + return plugin.getDatabaseManager().getListing(id); + } + + /** + * Gets all listings for a specific player + */ + public CompletableFuture> getPlayerListings(UUID playerUuid) { + return plugin.getDatabaseManager().getPlayerListings(playerUuid); + } + + /** + * Counts active listings for a player + */ + public CompletableFuture 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 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 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 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 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 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 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 + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/service/TransactionService.java b/src/main/java/pt/henrique/communityMarket/service/TransactionService.java new file mode 100644 index 0000000..8eac484 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/service/TransactionService.java @@ -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 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 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; + } + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/task/AuctionTask.java b/src/main/java/pt/henrique/communityMarket/task/AuctionTask.java new file mode 100644 index 0000000..a7c7088 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/task/AuctionTask.java @@ -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; + }); + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/task/ExpiredListingTask.java b/src/main/java/pt/henrique/communityMarket/task/ExpiredListingTask.java new file mode 100644 index 0000000..9dbd24a --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/task/ExpiredListingTask.java @@ -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; + }); + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/util/InventoryUtil.java b/src/main/java/pt/henrique/communityMarket/util/InventoryUtil.java new file mode 100644 index 0000000..cdabd8a --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/util/InventoryUtil.java @@ -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 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 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 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; + } + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/util/ItemBuilder.java b/src/main/java/pt/henrique/communityMarket/util/ItemBuilder.java new file mode 100644 index 0000000..279359e --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/util/ItemBuilder.java @@ -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 lore) { + if (meta != null && lore != null) { + List 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 existingLore = meta.lore(); + List 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 lines) { + if (meta != null && lines != null) { + List existingLore = meta.lore(); + List 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 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 additionalLore) { + return new ItemBuilder(original).addLore(additionalLore).build(); + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/util/ItemSerializer.java b/src/main/java/pt/henrique/communityMarket/util/ItemSerializer.java new file mode 100644 index 0000000..e9d71e5 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/util/ItemSerializer.java @@ -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; + } + } +} + diff --git a/src/main/java/pt/henrique/communityMarket/util/SoundUtil.java b/src/main/java/pt/henrique/communityMarket/util/SoundUtil.java new file mode 100644 index 0000000..f1c11a6 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/util/SoundUtil.java @@ -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); + } +} diff --git a/src/main/java/pt/henrique/communityMarket/util/TextUtil.java b/src/main/java/pt/henrique/communityMarket/util/TextUtil.java new file mode 100644 index 0000000..c622882 --- /dev/null +++ b/src/main/java/pt/henrique/communityMarket/util/TextUtil.java @@ -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) + "..."; + } +} + diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..1bd7524 --- /dev/null +++ b/src/main/resources/config.yml @@ -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 + diff --git a/src/main/resources/lang/en_US.yml b/src/main/resources/lang/en_US.yml new file mode 100644 index 0000000..a4d4306 --- /dev/null +++ b/src/main/resources/lang/en_US.yml @@ -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." + diff --git a/src/main/resources/lang/pt_PT.yml b/src/main/resources/lang/pt_PT.yml new file mode 100644 index 0000000..6b1d6b8 --- /dev/null +++ b/src/main/resources/lang/pt_PT.yml @@ -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." + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..c99954b --- /dev/null +++ b/src/main/resources/plugin.yml @@ -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: / + 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 diff --git a/target/CommunityMarket-1.0.0.jar b/target/CommunityMarket-1.0.0.jar new file mode 100644 index 0000000..47fbee6 Binary files /dev/null and b/target/CommunityMarket-1.0.0.jar differ diff --git a/target/classes/config.yml b/target/classes/config.yml new file mode 100644 index 0000000..1bd7524 --- /dev/null +++ b/target/classes/config.yml @@ -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 + diff --git a/target/classes/lang/en_US.yml b/target/classes/lang/en_US.yml new file mode 100644 index 0000000..a814f97 --- /dev/null +++ b/target/classes/lang/en_US.yml @@ -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." + diff --git a/target/classes/lang/pt_PT.yml b/target/classes/lang/pt_PT.yml new file mode 100644 index 0000000..3735aed --- /dev/null +++ b/target/classes/lang/pt_PT.yml @@ -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." + diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml new file mode 100644 index 0000000..844bcb7 --- /dev/null +++ b/target/classes/plugin.yml @@ -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: / + 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 diff --git a/target/classes/pt/henrique/communityMarket/CommunityMarket.class b/target/classes/pt/henrique/communityMarket/CommunityMarket.class new file mode 100644 index 0000000..6faa8a5 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/CommunityMarket.class differ diff --git a/target/classes/pt/henrique/communityMarket/command/MarketCommand.class b/target/classes/pt/henrique/communityMarket/command/MarketCommand.class new file mode 100644 index 0000000..d14c080 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/command/MarketCommand.class differ diff --git a/target/classes/pt/henrique/communityMarket/config/ConfigManager.class b/target/classes/pt/henrique/communityMarket/config/ConfigManager.class new file mode 100644 index 0000000..e35a8e1 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/config/ConfigManager.class differ diff --git a/target/classes/pt/henrique/communityMarket/config/MessageManager.class b/target/classes/pt/henrique/communityMarket/config/MessageManager.class new file mode 100644 index 0000000..d65cc1b Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/config/MessageManager.class differ diff --git a/target/classes/pt/henrique/communityMarket/db/DatabaseManager.class b/target/classes/pt/henrique/communityMarket/db/DatabaseManager.class new file mode 100644 index 0000000..3ef9cb6 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/db/DatabaseManager.class differ diff --git a/target/classes/pt/henrique/communityMarket/economy/EconomyManager$EconomyProvider.class b/target/classes/pt/henrique/communityMarket/economy/EconomyManager$EconomyProvider.class new file mode 100644 index 0000000..5a6bb57 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/economy/EconomyManager$EconomyProvider.class differ diff --git a/target/classes/pt/henrique/communityMarket/economy/EconomyManager.class b/target/classes/pt/henrique/communityMarket/economy/EconomyManager.class new file mode 100644 index 0000000..f2bb469 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/economy/EconomyManager.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/AdminGui$AdminAuctionsGui.class b/target/classes/pt/henrique/communityMarket/gui/AdminGui$AdminAuctionsGui.class new file mode 100644 index 0000000..12e28bd Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/AdminGui$AdminAuctionsGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/AdminGui$AdminListingsGui.class b/target/classes/pt/henrique/communityMarket/gui/AdminGui$AdminListingsGui.class new file mode 100644 index 0000000..36848ad Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/AdminGui$AdminListingsGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/AdminGui.class b/target/classes/pt/henrique/communityMarket/gui/AdminGui.class new file mode 100644 index 0000000..154db21 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/AdminGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/BrowseAuctionsGui$1.class b/target/classes/pt/henrique/communityMarket/gui/BrowseAuctionsGui$1.class new file mode 100644 index 0000000..4d68a81 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/BrowseAuctionsGui$1.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/BrowseAuctionsGui.class b/target/classes/pt/henrique/communityMarket/gui/BrowseAuctionsGui.class new file mode 100644 index 0000000..67a1a02 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/BrowseAuctionsGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/BrowseMarketGui$1.class b/target/classes/pt/henrique/communityMarket/gui/BrowseMarketGui$1.class new file mode 100644 index 0000000..e317f6c Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/BrowseMarketGui$1.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/BrowseMarketGui.class b/target/classes/pt/henrique/communityMarket/gui/BrowseMarketGui.class new file mode 100644 index 0000000..8466800 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/BrowseMarketGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/ClaimGui$1.class b/target/classes/pt/henrique/communityMarket/gui/ClaimGui$1.class new file mode 100644 index 0000000..b49505f Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/ClaimGui$1.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/ClaimGui.class b/target/classes/pt/henrique/communityMarket/gui/ClaimGui.class new file mode 100644 index 0000000..51172c6 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/ClaimGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/ConfirmationGui$ConfirmCallback.class b/target/classes/pt/henrique/communityMarket/gui/ConfirmationGui$ConfirmCallback.class new file mode 100644 index 0000000..3b64abd Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/ConfirmationGui$ConfirmCallback.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/ConfirmationGui.class b/target/classes/pt/henrique/communityMarket/gui/ConfirmationGui.class new file mode 100644 index 0000000..a35cd01 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/ConfirmationGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/CreateAuctionGui.class b/target/classes/pt/henrique/communityMarket/gui/CreateAuctionGui.class new file mode 100644 index 0000000..620f6fa Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/CreateAuctionGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/CreateListingGui.class b/target/classes/pt/henrique/communityMarket/gui/CreateListingGui.class new file mode 100644 index 0000000..0f6ee4e Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/CreateListingGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/EarningsGui.class b/target/classes/pt/henrique/communityMarket/gui/EarningsGui.class new file mode 100644 index 0000000..6a5a6e1 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/EarningsGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/GuiManager.class b/target/classes/pt/henrique/communityMarket/gui/GuiManager.class new file mode 100644 index 0000000..4e854ad Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/GuiManager.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/HelpGui.class b/target/classes/pt/henrique/communityMarket/gui/HelpGui.class new file mode 100644 index 0000000..a531317 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/HelpGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/ItemSelectionGui$SelectionMode.class b/target/classes/pt/henrique/communityMarket/gui/ItemSelectionGui$SelectionMode.class new file mode 100644 index 0000000..ecc0753 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/ItemSelectionGui$SelectionMode.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/ItemSelectionGui.class b/target/classes/pt/henrique/communityMarket/gui/ItemSelectionGui.class new file mode 100644 index 0000000..f3bc99f Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/ItemSelectionGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/MainMenuGui.class b/target/classes/pt/henrique/communityMarket/gui/MainMenuGui.class new file mode 100644 index 0000000..0a650c7 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/MainMenuGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/MarketGui$GuiType.class b/target/classes/pt/henrique/communityMarket/gui/MarketGui$GuiType.class new file mode 100644 index 0000000..5e0e7d6 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/MarketGui$GuiType.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/MarketGui.class b/target/classes/pt/henrique/communityMarket/gui/MarketGui.class new file mode 100644 index 0000000..36ea701 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/MarketGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/MyAuctionsGui.class b/target/classes/pt/henrique/communityMarket/gui/MyAuctionsGui.class new file mode 100644 index 0000000..d93c075 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/MyAuctionsGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/MyListingsGui.class b/target/classes/pt/henrique/communityMarket/gui/MyListingsGui.class new file mode 100644 index 0000000..3f69f61 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/MyListingsGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/NumberInputGui$NumberInputCallback.class b/target/classes/pt/henrique/communityMarket/gui/NumberInputGui$NumberInputCallback.class new file mode 100644 index 0000000..c4fe151 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/NumberInputGui$NumberInputCallback.class differ diff --git a/target/classes/pt/henrique/communityMarket/gui/NumberInputGui.class b/target/classes/pt/henrique/communityMarket/gui/NumberInputGui.class new file mode 100644 index 0000000..7f453cf Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/gui/NumberInputGui.class differ diff --git a/target/classes/pt/henrique/communityMarket/listener/GuiListener$1.class b/target/classes/pt/henrique/communityMarket/listener/GuiListener$1.class new file mode 100644 index 0000000..d3d3890 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/listener/GuiListener$1.class differ diff --git a/target/classes/pt/henrique/communityMarket/listener/GuiListener.class b/target/classes/pt/henrique/communityMarket/listener/GuiListener.class new file mode 100644 index 0000000..b6d82cc Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/listener/GuiListener.class differ diff --git a/target/classes/pt/henrique/communityMarket/listener/PlayerListener.class b/target/classes/pt/henrique/communityMarket/listener/PlayerListener.class new file mode 100644 index 0000000..0d9b212 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/listener/PlayerListener.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/Auction$AuctionStatus.class b/target/classes/pt/henrique/communityMarket/model/Auction$AuctionStatus.class new file mode 100644 index 0000000..2723f9d Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/Auction$AuctionStatus.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/Auction.class b/target/classes/pt/henrique/communityMarket/model/Auction.class new file mode 100644 index 0000000..456743c Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/Auction.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/Bid.class b/target/classes/pt/henrique/communityMarket/model/Bid.class new file mode 100644 index 0000000..128fea0 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/Bid.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/ClaimItem$ClaimReason.class b/target/classes/pt/henrique/communityMarket/model/ClaimItem$ClaimReason.class new file mode 100644 index 0000000..8f871f0 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/ClaimItem$ClaimReason.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/ClaimItem.class b/target/classes/pt/henrique/communityMarket/model/ClaimItem.class new file mode 100644 index 0000000..16dc042 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/ClaimItem.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/Listing$ListingStatus.class b/target/classes/pt/henrique/communityMarket/model/Listing$ListingStatus.class new file mode 100644 index 0000000..f1e58f4 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/Listing$ListingStatus.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/Listing.class b/target/classes/pt/henrique/communityMarket/model/Listing.class new file mode 100644 index 0000000..35634db Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/Listing.class differ diff --git a/target/classes/pt/henrique/communityMarket/model/PendingEarnings.class b/target/classes/pt/henrique/communityMarket/model/PendingEarnings.class new file mode 100644 index 0000000..b5e85ed Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/model/PendingEarnings.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/AuctionService$BidResult.class b/target/classes/pt/henrique/communityMarket/service/AuctionService$BidResult.class new file mode 100644 index 0000000..94da621 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/AuctionService$BidResult.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/AuctionService$CancelResult.class b/target/classes/pt/henrique/communityMarket/service/AuctionService$CancelResult.class new file mode 100644 index 0000000..33e6fb7 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/AuctionService$CancelResult.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/AuctionService.class b/target/classes/pt/henrique/communityMarket/service/AuctionService.class new file mode 100644 index 0000000..dbbc037 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/AuctionService.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/ClaimService$ClaimResult.class b/target/classes/pt/henrique/communityMarket/service/ClaimService$ClaimResult.class new file mode 100644 index 0000000..082b7eb Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/ClaimService$ClaimResult.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/ClaimService.class b/target/classes/pt/henrique/communityMarket/service/ClaimService.class new file mode 100644 index 0000000..a9f67e0 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/ClaimService.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/EarningsService$WithdrawResult.class b/target/classes/pt/henrique/communityMarket/service/EarningsService$WithdrawResult.class new file mode 100644 index 0000000..caeda88 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/EarningsService$WithdrawResult.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/EarningsService.class b/target/classes/pt/henrique/communityMarket/service/EarningsService.class new file mode 100644 index 0000000..159cc3a Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/EarningsService.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/ListingService$PurchaseResult.class b/target/classes/pt/henrique/communityMarket/service/ListingService$PurchaseResult.class new file mode 100644 index 0000000..a13f586 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/ListingService$PurchaseResult.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/ListingService.class b/target/classes/pt/henrique/communityMarket/service/ListingService.class new file mode 100644 index 0000000..a7dbca2 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/ListingService.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/TransactionService$TransactionResult.class b/target/classes/pt/henrique/communityMarket/service/TransactionService$TransactionResult.class new file mode 100644 index 0000000..4ea8986 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/TransactionService$TransactionResult.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/TransactionService$ValidationResult.class b/target/classes/pt/henrique/communityMarket/service/TransactionService$ValidationResult.class new file mode 100644 index 0000000..9c7c550 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/TransactionService$ValidationResult.class differ diff --git a/target/classes/pt/henrique/communityMarket/service/TransactionService.class b/target/classes/pt/henrique/communityMarket/service/TransactionService.class new file mode 100644 index 0000000..e8e46c5 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/service/TransactionService.class differ diff --git a/target/classes/pt/henrique/communityMarket/task/AuctionTask.class b/target/classes/pt/henrique/communityMarket/task/AuctionTask.class new file mode 100644 index 0000000..cbb6099 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/task/AuctionTask.class differ diff --git a/target/classes/pt/henrique/communityMarket/task/ExpiredListingTask.class b/target/classes/pt/henrique/communityMarket/task/ExpiredListingTask.class new file mode 100644 index 0000000..b92cf5e Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/task/ExpiredListingTask.class differ diff --git a/target/classes/pt/henrique/communityMarket/util/InventoryUtil$ItemCategory.class b/target/classes/pt/henrique/communityMarket/util/InventoryUtil$ItemCategory.class new file mode 100644 index 0000000..769010c Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/util/InventoryUtil$ItemCategory.class differ diff --git a/target/classes/pt/henrique/communityMarket/util/InventoryUtil.class b/target/classes/pt/henrique/communityMarket/util/InventoryUtil.class new file mode 100644 index 0000000..f17a1cd Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/util/InventoryUtil.class differ diff --git a/target/classes/pt/henrique/communityMarket/util/ItemBuilder.class b/target/classes/pt/henrique/communityMarket/util/ItemBuilder.class new file mode 100644 index 0000000..37508ac Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/util/ItemBuilder.class differ diff --git a/target/classes/pt/henrique/communityMarket/util/ItemSerializer.class b/target/classes/pt/henrique/communityMarket/util/ItemSerializer.class new file mode 100644 index 0000000..a03f75b Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/util/ItemSerializer.class differ diff --git a/target/classes/pt/henrique/communityMarket/util/SoundUtil.class b/target/classes/pt/henrique/communityMarket/util/SoundUtil.class new file mode 100644 index 0000000..3880429 Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/util/SoundUtil.class differ diff --git a/target/classes/pt/henrique/communityMarket/util/TextUtil.class b/target/classes/pt/henrique/communityMarket/util/TextUtil.class new file mode 100644 index 0000000..f88cdea Binary files /dev/null and b/target/classes/pt/henrique/communityMarket/util/TextUtil.class differ diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..6ff5736 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=CommunityMarket +groupId=pt.henrique +version=1.0.0 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..0f91240 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,63 @@ +pt/henrique/communityMarket/economy/EconomyManager.class +pt/henrique/communityMarket/util/TextUtil.class +pt/henrique/communityMarket/config/MessageManager.class +pt/henrique/communityMarket/model/PendingEarnings.class +pt/henrique/communityMarket/gui/AdminGui$AdminListingsGui.class +pt/henrique/communityMarket/model/Auction.class +pt/henrique/communityMarket/listener/PlayerListener.class +pt/henrique/communityMarket/gui/MarketGui.class +pt/henrique/communityMarket/service/AuctionService.class +pt/henrique/communityMarket/util/ItemSerializer.class +pt/henrique/communityMarket/gui/MainMenuGui.class +pt/henrique/communityMarket/service/EarningsService$WithdrawResult.class +pt/henrique/communityMarket/gui/ConfirmationGui.class +pt/henrique/communityMarket/service/ListingService.class +pt/henrique/communityMarket/gui/HelpGui.class +pt/henrique/communityMarket/db/DatabaseManager.class +pt/henrique/communityMarket/service/TransactionService.class +pt/henrique/communityMarket/task/AuctionTask.class +pt/henrique/communityMarket/model/Bid.class +pt/henrique/communityMarket/service/ClaimService.class +pt/henrique/communityMarket/task/ExpiredListingTask.class +pt/henrique/communityMarket/gui/NumberInputGui$NumberInputCallback.class +pt/henrique/communityMarket/gui/MyAuctionsGui.class +pt/henrique/communityMarket/service/ClaimService$ClaimResult.class +pt/henrique/communityMarket/gui/ConfirmationGui$ConfirmCallback.class +pt/henrique/communityMarket/CommunityMarket.class +pt/henrique/communityMarket/gui/BrowseMarketGui.class +pt/henrique/communityMarket/model/ClaimItem$ClaimReason.class +pt/henrique/communityMarket/model/Auction$AuctionStatus.class +pt/henrique/communityMarket/util/SoundUtil.class +pt/henrique/communityMarket/gui/BrowseMarketGui$1.class +pt/henrique/communityMarket/gui/CreateAuctionGui.class +pt/henrique/communityMarket/util/InventoryUtil.class +pt/henrique/communityMarket/gui/AdminGui$AdminAuctionsGui.class +pt/henrique/communityMarket/command/MarketCommand.class +pt/henrique/communityMarket/listener/GuiListener$1.class +pt/henrique/communityMarket/service/AuctionService$BidResult.class +pt/henrique/communityMarket/gui/CreateListingGui.class +pt/henrique/communityMarket/gui/ClaimGui$1.class +pt/henrique/communityMarket/model/ClaimItem.class +pt/henrique/communityMarket/util/ItemBuilder.class +pt/henrique/communityMarket/gui/NumberInputGui.class +pt/henrique/communityMarket/gui/AdminGui.class +pt/henrique/communityMarket/gui/ItemSelectionGui$SelectionMode.class +pt/henrique/communityMarket/gui/BrowseAuctionsGui.class +pt/henrique/communityMarket/service/TransactionService$TransactionResult.class +pt/henrique/communityMarket/service/EarningsService.class +pt/henrique/communityMarket/economy/EconomyManager$EconomyProvider.class +pt/henrique/communityMarket/service/TransactionService$ValidationResult.class +pt/henrique/communityMarket/gui/BrowseAuctionsGui$1.class +pt/henrique/communityMarket/model/Listing.class +pt/henrique/communityMarket/service/AuctionService$CancelResult.class +pt/henrique/communityMarket/gui/GuiManager.class +pt/henrique/communityMarket/gui/ClaimGui.class +pt/henrique/communityMarket/gui/EarningsGui.class +pt/henrique/communityMarket/service/ListingService$PurchaseResult.class +pt/henrique/communityMarket/config/ConfigManager.class +pt/henrique/communityMarket/gui/MarketGui$GuiType.class +pt/henrique/communityMarket/util/InventoryUtil$ItemCategory.class +pt/henrique/communityMarket/gui/MyListingsGui.class +pt/henrique/communityMarket/gui/ItemSelectionGui.class +pt/henrique/communityMarket/model/Listing$ListingStatus.class +pt/henrique/communityMarket/listener/GuiListener.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..b20c0c7 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,41 @@ +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/CommunityMarket.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/command/MarketCommand.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/config/ConfigManager.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/config/MessageManager.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/db/DatabaseManager.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/economy/EconomyManager.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/AdminGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/BrowseAuctionsGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/BrowseMarketGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/ClaimGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/ConfirmationGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/CreateAuctionGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/CreateListingGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/EarningsGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/GuiManager.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/HelpGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/ItemSelectionGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/MainMenuGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/MarketGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/MyAuctionsGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/MyListingsGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/gui/NumberInputGui.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/listener/GuiListener.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/listener/PlayerListener.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/model/Auction.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/model/Bid.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/model/ClaimItem.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/model/Listing.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/model/PendingEarnings.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/service/AuctionService.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/service/ClaimService.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/service/EarningsService.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/service/ListingService.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/service/TransactionService.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/task/AuctionTask.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/task/ExpiredListingTask.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/util/InventoryUtil.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/util/ItemBuilder.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/util/ItemSerializer.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/util/SoundUtil.java +/Users/henrique/IdeaProjects/CommunityMarket/src/main/java/pt/henrique/communityMarket/util/TextUtil.java diff --git a/target/original-CommunityMarket-1.0.0.jar b/target/original-CommunityMarket-1.0.0.jar new file mode 100644 index 0000000..31fb45a Binary files /dev/null and b/target/original-CommunityMarket-1.0.0.jar differ