Initial Realease
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
package pt.henrique.communityMarket;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import pt.henrique.communityMarket.command.MarketCommand;
|
||||
import pt.henrique.communityMarket.config.ConfigManager;
|
||||
import pt.henrique.communityMarket.config.MessageManager;
|
||||
import pt.henrique.communityMarket.db.DatabaseManager;
|
||||
import pt.henrique.communityMarket.economy.EconomyManager;
|
||||
import pt.henrique.communityMarket.gui.GuiManager;
|
||||
import pt.henrique.communityMarket.listener.GuiListener;
|
||||
import pt.henrique.communityMarket.listener.PlayerListener;
|
||||
import pt.henrique.communityMarket.service.*;
|
||||
import pt.henrique.communityMarket.task.AuctionTask;
|
||||
import pt.henrique.communityMarket.task.ExpiredListingTask;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class CommunityMarket extends JavaPlugin {
|
||||
|
||||
private static CommunityMarket instance;
|
||||
|
||||
private ConfigManager configManager;
|
||||
private MessageManager messageManager;
|
||||
private DatabaseManager databaseManager;
|
||||
private EconomyManager economyManager;
|
||||
private GuiManager guiManager;
|
||||
|
||||
private ListingService listingService;
|
||||
private AuctionService auctionService;
|
||||
private ClaimService claimService;
|
||||
private EarningsService earningsService;
|
||||
private TransactionService transactionService;
|
||||
|
||||
private AuctionTask auctionTask;
|
||||
private ExpiredListingTask expiredListingTask;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
|
||||
// Load configurations
|
||||
configManager = new ConfigManager(this);
|
||||
messageManager = new MessageManager(this);
|
||||
|
||||
// Initialize economy
|
||||
economyManager = new EconomyManager(this);
|
||||
if (!economyManager.setupEconomy()) {
|
||||
getLogger().severe("No economy plugin found! Please install Vault or EssentialsX.");
|
||||
getLogger().severe("Disabling CommunityMarket...");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
getLogger().info("Economy provider: " + economyManager.getProviderName());
|
||||
|
||||
// Initialize database
|
||||
databaseManager = new DatabaseManager(this);
|
||||
if (!databaseManager.initialize()) {
|
||||
getLogger().severe("Failed to initialize database!");
|
||||
getLogger().severe("Disabling CommunityMarket...");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
getLogger().info("Database initialized successfully.");
|
||||
|
||||
// Initialize services
|
||||
claimService = new ClaimService(this);
|
||||
earningsService = new EarningsService(this);
|
||||
listingService = new ListingService(this);
|
||||
auctionService = new AuctionService(this);
|
||||
transactionService = new TransactionService(this);
|
||||
|
||||
// Initialize GUI manager
|
||||
guiManager = new GuiManager(this);
|
||||
|
||||
// Register command
|
||||
MarketCommand marketCommand = new MarketCommand(this);
|
||||
var command = getCommand("market");
|
||||
if (command != null) {
|
||||
command.setExecutor(marketCommand);
|
||||
command.setTabCompleter(marketCommand);
|
||||
}
|
||||
|
||||
// Register listeners
|
||||
getServer().getPluginManager().registerEvents(new GuiListener(this), this);
|
||||
getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
|
||||
|
||||
// Start tasks
|
||||
startTasks();
|
||||
|
||||
getLogger().info("CommunityMarket has been enabled!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Stop tasks
|
||||
if (auctionTask != null) {
|
||||
auctionTask.cancel();
|
||||
}
|
||||
if (expiredListingTask != null) {
|
||||
expiredListingTask.cancel();
|
||||
}
|
||||
|
||||
// Close GUI manager
|
||||
if (guiManager != null) {
|
||||
guiManager.closeAllGuis();
|
||||
}
|
||||
|
||||
// Close database
|
||||
if (databaseManager != null) {
|
||||
databaseManager.shutdown();
|
||||
}
|
||||
|
||||
getLogger().info("CommunityMarket has been disabled!");
|
||||
}
|
||||
|
||||
private void startTasks() {
|
||||
// Auction check task
|
||||
int auctionInterval = configManager.getAuctionCheckInterval() * 20; // Convert to ticks
|
||||
auctionTask = new AuctionTask(this);
|
||||
auctionTask.runTaskTimerAsynchronously(this, auctionInterval, auctionInterval);
|
||||
|
||||
// Expired listing check task
|
||||
int expiredInterval = configManager.getExpiredCheckInterval() * 60 * 20; // Convert minutes to ticks
|
||||
expiredListingTask = new ExpiredListingTask(this);
|
||||
expiredListingTask.runTaskTimerAsynchronously(this, expiredInterval, expiredInterval);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
configManager.reload();
|
||||
messageManager.reload();
|
||||
getLogger().info("Configuration reloaded.");
|
||||
}
|
||||
|
||||
public static CommunityMarket getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ConfigManager getConfigManager() {
|
||||
return configManager;
|
||||
}
|
||||
|
||||
public MessageManager getMessageManager() {
|
||||
return messageManager;
|
||||
}
|
||||
|
||||
public DatabaseManager getDatabaseManager() {
|
||||
return databaseManager;
|
||||
}
|
||||
|
||||
public EconomyManager getEconomyManager() {
|
||||
return economyManager;
|
||||
}
|
||||
|
||||
public GuiManager getGuiManager() {
|
||||
return guiManager;
|
||||
}
|
||||
|
||||
public ListingService getListingService() {
|
||||
return listingService;
|
||||
}
|
||||
|
||||
public AuctionService getAuctionService() {
|
||||
return auctionService;
|
||||
}
|
||||
|
||||
public ClaimService getClaimService() {
|
||||
return claimService;
|
||||
}
|
||||
|
||||
public EarningsService getEarningsService() {
|
||||
return earningsService;
|
||||
}
|
||||
|
||||
public TransactionService getTransactionService() {
|
||||
return transactionService;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package pt.henrique.communityMarket.command;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The only command in the plugin: /market (alias: /cmarket)
|
||||
* Opens the main market GUI. All other actions are done through GUIs.
|
||||
*/
|
||||
public class MarketCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public MarketCommand(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
// Only players can use this command
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(plugin.getMessageManager().get("messages.player-only"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check basic permission
|
||||
if (!player.hasPermission("communitymarket.use")) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Open the main market GUI
|
||||
plugin.getGuiManager().openMainMenu(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
// No tab completions - everything is GUI-based
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
package pt.henrique.communityMarket.config;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Manages the main plugin configuration
|
||||
*/
|
||||
public class ConfigManager {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private FileConfiguration config;
|
||||
|
||||
// Database settings
|
||||
private String databaseType;
|
||||
private String sqliteFile;
|
||||
private String mysqlHost;
|
||||
private int mysqlPort;
|
||||
private String mysqlDatabase;
|
||||
private String mysqlUsername;
|
||||
private String mysqlPassword;
|
||||
private int poolMaxSize;
|
||||
private int poolMinIdle;
|
||||
private long poolConnectionTimeout;
|
||||
private long poolIdleTimeout;
|
||||
private long poolMaxLifetime;
|
||||
|
||||
// Economy settings
|
||||
private String currencyFormat;
|
||||
private String currencySymbol;
|
||||
private double marketTax;
|
||||
private double auctionTax;
|
||||
|
||||
// Market settings
|
||||
private int maxListingsPerPlayer;
|
||||
private int listingCooldown;
|
||||
private int defaultDurationHours;
|
||||
private List<Integer> availableDurations;
|
||||
private double minPrice;
|
||||
private double maxPrice;
|
||||
|
||||
// Auction settings
|
||||
private int maxAuctionsPerPlayer;
|
||||
private int minDurationHours;
|
||||
private int maxDurationHours;
|
||||
private int defaultAuctionDurationHours;
|
||||
private List<Integer> availableAuctionDurations;
|
||||
private double minStartPrice;
|
||||
private double maxStartPrice;
|
||||
private double minBidIncrementPercent;
|
||||
private double minBidIncrementAbsolute;
|
||||
private boolean antiSnipeEnabled;
|
||||
private int antiSnipeTriggerSeconds;
|
||||
private int antiSnipeExtensionSeconds;
|
||||
private int antiSnipeMaxExtensions;
|
||||
|
||||
// Blacklist
|
||||
private Set<Material> blacklistedMaterials;
|
||||
private List<String> blacklistedKeywords;
|
||||
|
||||
// GUI settings
|
||||
private String mainMenuTitle;
|
||||
private String browseMarketTitle;
|
||||
private String browseAuctionsTitle;
|
||||
private String createListingTitle;
|
||||
private String createAuctionTitle;
|
||||
private String myListingsTitle;
|
||||
private String myAuctionsTitle;
|
||||
private String claimTitle;
|
||||
private String earningsTitle;
|
||||
private String confirmTitle;
|
||||
private String numberInputTitle;
|
||||
private String adminTitle;
|
||||
private int itemsPerPage;
|
||||
private boolean helpButtonEnabled;
|
||||
private String clickSound;
|
||||
private String successSound;
|
||||
private String errorSound;
|
||||
private String purchaseSound;
|
||||
|
||||
// Notifications
|
||||
private boolean notifyOnSale;
|
||||
private boolean notifyOnOutbid;
|
||||
private boolean notifyOnWin;
|
||||
private boolean notifyOnExpire;
|
||||
|
||||
// Performance
|
||||
private int cacheDuration;
|
||||
private int auctionCheckInterval;
|
||||
private int expiredCheckInterval;
|
||||
|
||||
public ConfigManager(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
reload();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
plugin.saveDefaultConfig();
|
||||
plugin.reloadConfig();
|
||||
config = plugin.getConfig();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
// Database settings
|
||||
databaseType = config.getString("database.type", "sqlite");
|
||||
sqliteFile = config.getString("database.sqlite.file", "database.db");
|
||||
mysqlHost = config.getString("database.mysql.host", "localhost");
|
||||
mysqlPort = config.getInt("database.mysql.port", 3306);
|
||||
mysqlDatabase = config.getString("database.mysql.database", "communitymarket");
|
||||
mysqlUsername = config.getString("database.mysql.username", "root");
|
||||
mysqlPassword = config.getString("database.mysql.password", "");
|
||||
poolMaxSize = config.getInt("database.mysql.pool.maximum-pool-size", 10);
|
||||
poolMinIdle = config.getInt("database.mysql.pool.minimum-idle", 2);
|
||||
poolConnectionTimeout = config.getLong("database.mysql.pool.connection-timeout", 30000);
|
||||
poolIdleTimeout = config.getLong("database.mysql.pool.idle-timeout", 600000);
|
||||
poolMaxLifetime = config.getLong("database.mysql.pool.max-lifetime", 1800000);
|
||||
|
||||
// Economy settings
|
||||
currencyFormat = config.getString("economy.currency-format", "$#,##0.00");
|
||||
currencySymbol = config.getString("economy.currency-symbol", "$");
|
||||
marketTax = config.getDouble("economy.taxes.market-tax", 5.0);
|
||||
auctionTax = config.getDouble("economy.taxes.auction-tax", 7.5);
|
||||
|
||||
// Market settings
|
||||
maxListingsPerPlayer = config.getInt("market.max-listings-per-player", 20);
|
||||
listingCooldown = config.getInt("market.listing-cooldown", 0);
|
||||
defaultDurationHours = config.getInt("market.default-duration-hours", 168);
|
||||
availableDurations = config.getIntegerList("market.available-durations");
|
||||
minPrice = config.getDouble("market.min-price", 1.0);
|
||||
maxPrice = config.getDouble("market.max-price", 1000000000.0);
|
||||
|
||||
// Auction settings
|
||||
maxAuctionsPerPlayer = config.getInt("auction.max-auctions-per-player", 10);
|
||||
minDurationHours = config.getInt("auction.min-duration-hours", 1);
|
||||
maxDurationHours = config.getInt("auction.max-duration-hours", 168);
|
||||
defaultAuctionDurationHours = config.getInt("auction.default-duration-hours", 24);
|
||||
availableAuctionDurations = config.getIntegerList("auction.available-durations");
|
||||
minStartPrice = config.getDouble("auction.min-start-price", 1.0);
|
||||
maxStartPrice = config.getDouble("auction.max-start-price", 1000000000.0);
|
||||
minBidIncrementPercent = config.getDouble("auction.min-bid-increment-percent", 5.0);
|
||||
minBidIncrementAbsolute = config.getDouble("auction.min-bid-increment-absolute", 1.0);
|
||||
antiSnipeEnabled = config.getBoolean("auction.anti-snipe.enabled", true);
|
||||
antiSnipeTriggerSeconds = config.getInt("auction.anti-snipe.trigger-seconds", 30);
|
||||
antiSnipeExtensionSeconds = config.getInt("auction.anti-snipe.extension-seconds", 30);
|
||||
antiSnipeMaxExtensions = config.getInt("auction.anti-snipe.max-extensions", 10);
|
||||
|
||||
// Blacklist
|
||||
blacklistedMaterials = new HashSet<>();
|
||||
for (String materialName : config.getStringList("blacklist.materials")) {
|
||||
try {
|
||||
Material material = Material.valueOf(materialName.toUpperCase());
|
||||
blacklistedMaterials.add(material);
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLogger().warning("Invalid material in blacklist: " + materialName);
|
||||
}
|
||||
}
|
||||
blacklistedKeywords = config.getStringList("blacklist.keywords");
|
||||
|
||||
// GUI settings
|
||||
mainMenuTitle = config.getString("gui.main-menu-title", "&8&lCommunity Market");
|
||||
browseMarketTitle = config.getString("gui.browse-market-title", "&8&lBrowse Market");
|
||||
browseAuctionsTitle = config.getString("gui.browse-auctions-title", "&8&lBrowse Auctions");
|
||||
createListingTitle = config.getString("gui.create-listing-title", "&8&lCreate Listing");
|
||||
createAuctionTitle = config.getString("gui.create-auction-title", "&8&lCreate Auction");
|
||||
myListingsTitle = config.getString("gui.my-listings-title", "&8&lMy Listings");
|
||||
myAuctionsTitle = config.getString("gui.my-auctions-title", "&8&lMy Auctions");
|
||||
claimTitle = config.getString("gui.claim-title", "&8&lClaim Items");
|
||||
earningsTitle = config.getString("gui.earnings-title", "&8&lEarnings");
|
||||
confirmTitle = config.getString("gui.confirm-title", "&8&lConfirm Action");
|
||||
numberInputTitle = config.getString("gui.number-input-title", "&8&lEnter Amount");
|
||||
adminTitle = config.getString("gui.admin-title", "&8&lAdmin Panel");
|
||||
itemsPerPage = config.getInt("gui.items-per-page", 45);
|
||||
helpButtonEnabled = config.getBoolean("gui.show-help-button", true);
|
||||
clickSound = config.getString("gui.sounds.click", "UI_BUTTON_CLICK");
|
||||
successSound = config.getString("gui.sounds.success", "ENTITY_PLAYER_LEVELUP");
|
||||
errorSound = config.getString("gui.sounds.error", "ENTITY_VILLAGER_NO");
|
||||
purchaseSound = config.getString("gui.sounds.purchase", "ENTITY_EXPERIENCE_ORB_PICKUP");
|
||||
|
||||
// Notifications
|
||||
notifyOnSale = config.getBoolean("notifications.notify-on-sale", true);
|
||||
notifyOnOutbid = config.getBoolean("notifications.notify-on-outbid", true);
|
||||
notifyOnWin = config.getBoolean("notifications.notify-on-win", true);
|
||||
notifyOnExpire = config.getBoolean("notifications.notify-on-expire", true);
|
||||
|
||||
// Performance
|
||||
cacheDuration = config.getInt("performance.cache-duration", 30);
|
||||
auctionCheckInterval = config.getInt("performance.auction-check-interval", 5);
|
||||
expiredCheckInterval = config.getInt("performance.expired-check-interval", 5);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getDatabaseType() { return databaseType; }
|
||||
public String getSqliteFile() { return sqliteFile; }
|
||||
public String getMysqlHost() { return mysqlHost; }
|
||||
public int getMysqlPort() { return mysqlPort; }
|
||||
public String getMysqlDatabase() { return mysqlDatabase; }
|
||||
public String getMysqlUsername() { return mysqlUsername; }
|
||||
public String getMysqlPassword() { return mysqlPassword; }
|
||||
public int getPoolMaxSize() { return poolMaxSize; }
|
||||
public int getPoolMinIdle() { return poolMinIdle; }
|
||||
public long getPoolConnectionTimeout() { return poolConnectionTimeout; }
|
||||
public long getPoolIdleTimeout() { return poolIdleTimeout; }
|
||||
public long getPoolMaxLifetime() { return poolMaxLifetime; }
|
||||
|
||||
public String getCurrencyFormat() { return currencyFormat; }
|
||||
public String getCurrencySymbol() { return currencySymbol; }
|
||||
public double getMarketTax() { return marketTax; }
|
||||
public double getAuctionTax() { return auctionTax; }
|
||||
|
||||
public int getMaxListingsPerPlayer() { return maxListingsPerPlayer; }
|
||||
public int getListingCooldown() { return listingCooldown; }
|
||||
public int getDefaultDurationHours() { return defaultDurationHours; }
|
||||
public List<Integer> getAvailableDurations() { return availableDurations; }
|
||||
public double getMinPrice() { return minPrice; }
|
||||
public double getMaxPrice() { return maxPrice; }
|
||||
|
||||
public int getMaxAuctionsPerPlayer() { return maxAuctionsPerPlayer; }
|
||||
public int getMinDurationHours() { return minDurationHours; }
|
||||
public int getMaxDurationHours() { return maxDurationHours; }
|
||||
public int getDefaultAuctionDurationHours() { return defaultAuctionDurationHours; }
|
||||
public List<Integer> getAvailableAuctionDurations() { return availableAuctionDurations; }
|
||||
public double getMinStartPrice() { return minStartPrice; }
|
||||
public double getMaxStartPrice() { return maxStartPrice; }
|
||||
public double getMinBidIncrementPercent() { return minBidIncrementPercent; }
|
||||
public double getMinBidIncrementAbsolute() { return minBidIncrementAbsolute; }
|
||||
public boolean isAntiSnipeEnabled() { return antiSnipeEnabled; }
|
||||
public int getAntiSnipeTriggerSeconds() { return antiSnipeTriggerSeconds; }
|
||||
public int getAntiSnipeExtensionSeconds() { return antiSnipeExtensionSeconds; }
|
||||
public int getAntiSnipeMaxExtensions() { return antiSnipeMaxExtensions; }
|
||||
|
||||
public Set<Material> getBlacklistedMaterials() { return blacklistedMaterials; }
|
||||
public List<String> getBlacklistedKeywords() { return blacklistedKeywords; }
|
||||
|
||||
public String getMainMenuTitle() { return mainMenuTitle; }
|
||||
public String getBrowseMarketTitle() { return browseMarketTitle; }
|
||||
public String getBrowseAuctionsTitle() { return browseAuctionsTitle; }
|
||||
public String getCreateListingTitle() { return createListingTitle; }
|
||||
public String getCreateAuctionTitle() { return createAuctionTitle; }
|
||||
public String getMyListingsTitle() { return myListingsTitle; }
|
||||
public String getMyAuctionsTitle() { return myAuctionsTitle; }
|
||||
public String getClaimTitle() { return claimTitle; }
|
||||
public String getEarningsTitle() { return earningsTitle; }
|
||||
public String getConfirmTitle() { return confirmTitle; }
|
||||
public String getNumberInputTitle() { return numberInputTitle; }
|
||||
public String getAdminTitle() { return adminTitle; }
|
||||
public int getItemsPerPage() { return itemsPerPage; }
|
||||
public boolean isHelpButtonEnabled() { return helpButtonEnabled; }
|
||||
public String getClickSound() { return clickSound; }
|
||||
public String getSuccessSound() { return successSound; }
|
||||
public String getErrorSound() { return errorSound; }
|
||||
public String getPurchaseSound() { return purchaseSound; }
|
||||
|
||||
public boolean isNotifyOnSale() { return notifyOnSale; }
|
||||
public boolean isNotifyOnOutbid() { return notifyOnOutbid; }
|
||||
public boolean isNotifyOnWin() { return notifyOnWin; }
|
||||
public boolean isNotifyOnExpire() { return notifyOnExpire; }
|
||||
|
||||
public int getCacheDuration() { return cacheDuration; }
|
||||
public int getAuctionCheckInterval() { return auctionCheckInterval; }
|
||||
public int getExpiredCheckInterval() { return expiredCheckInterval; }
|
||||
|
||||
/**
|
||||
* Checks if a material is blacklisted
|
||||
*/
|
||||
public boolean isMaterialBlacklisted(Material material) {
|
||||
return blacklistedMaterials.contains(material);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text contains blacklisted keywords
|
||||
*/
|
||||
public boolean containsBlacklistedKeyword(String text) {
|
||||
if (text == null) return false;
|
||||
String lowerText = text.toLowerCase();
|
||||
for (String keyword : blacklistedKeywords) {
|
||||
if (lowerText.contains(keyword.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package pt.henrique.communityMarket.config;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Manages plugin messages and localization.
|
||||
* Supports multiple languages loaded from lang/ folder.
|
||||
*/
|
||||
public class MessageManager {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private FileConfiguration messagesConfig;
|
||||
private final Map<String, String> messageCache;
|
||||
private DecimalFormat currencyFormatter;
|
||||
private String currentLanguage;
|
||||
|
||||
public MessageManager(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
this.messageCache = new HashMap<>();
|
||||
reload();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
messageCache.clear();
|
||||
|
||||
// Get language from config
|
||||
currentLanguage = plugin.getConfig().getString("language", "en_US");
|
||||
|
||||
// Save default language files
|
||||
saveDefaultLanguageFiles();
|
||||
|
||||
// Load the selected language file
|
||||
File langFolder = new File(plugin.getDataFolder(), "lang");
|
||||
File langFile = new File(langFolder, currentLanguage + ".yml");
|
||||
|
||||
if (!langFile.exists()) {
|
||||
plugin.getLogger().warning("Language file not found: " + currentLanguage + ".yml, falling back to en_US");
|
||||
langFile = new File(langFolder, "en_US.yml");
|
||||
}
|
||||
|
||||
messagesConfig = YamlConfiguration.loadConfiguration(langFile);
|
||||
|
||||
// Load defaults from jar as fallback
|
||||
InputStream defaultStream = plugin.getResource("lang/en_US.yml");
|
||||
if (defaultStream != null) {
|
||||
YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(
|
||||
new InputStreamReader(defaultStream, StandardCharsets.UTF_8));
|
||||
messagesConfig.setDefaults(defaultConfig);
|
||||
}
|
||||
|
||||
// Setup currency formatter
|
||||
String format = plugin.getConfigManager().getCurrencyFormat();
|
||||
try {
|
||||
currencyFormatter = new DecimalFormat(format.replace("$", ""));
|
||||
} catch (Exception e) {
|
||||
currencyFormatter = new DecimalFormat("#,##0.00");
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Loaded language: " + currentLanguage);
|
||||
}
|
||||
|
||||
private void saveDefaultLanguageFiles() {
|
||||
File langFolder = new File(plugin.getDataFolder(), "lang");
|
||||
if (!langFolder.exists()) {
|
||||
langFolder.mkdirs();
|
||||
}
|
||||
|
||||
// Save default language files if they don't exist
|
||||
String[] languages = {"en_US.yml", "pt_PT.yml"};
|
||||
for (String lang : languages) {
|
||||
File langFile = new File(langFolder, lang);
|
||||
if (!langFile.exists()) {
|
||||
plugin.saveResource("lang/" + lang, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current language code
|
||||
*/
|
||||
public String getCurrentLanguage() {
|
||||
return currentLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a raw message string from the config
|
||||
*/
|
||||
public String getRaw(String path) {
|
||||
if (messageCache.containsKey(path)) {
|
||||
return messageCache.get(path);
|
||||
}
|
||||
|
||||
String message = messagesConfig.getString(path, "&cMissing message: " + path);
|
||||
messageCache.put(path, message);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message as a Component
|
||||
*/
|
||||
public Component get(String path) {
|
||||
return TextUtil.colorize(getRaw(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message with placeholders replaced
|
||||
*/
|
||||
public Component get(String path, Map<String, String> placeholders) {
|
||||
String message = getRaw(path);
|
||||
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||
message = message.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||
}
|
||||
return TextUtil.colorize(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a message with a single placeholder replaced
|
||||
*/
|
||||
public Component get(String path, String placeholder, String value) {
|
||||
String message = getRaw(path).replace("{" + placeholder + "}", value);
|
||||
return TextUtil.colorize(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a prefixed message
|
||||
*/
|
||||
public Component getPrefixed(String path) {
|
||||
return TextUtil.colorize(getRaw("prefix") + getRaw(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a prefixed message with placeholders
|
||||
*/
|
||||
public Component getPrefixed(String path, Map<String, String> placeholders) {
|
||||
String message = getRaw(path);
|
||||
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||
message = message.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||
}
|
||||
return TextUtil.colorize(getRaw("prefix") + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a prefixed message with a single placeholder
|
||||
*/
|
||||
public Component getPrefixed(String path, String placeholder, String value) {
|
||||
String message = getRaw(path).replace("{" + placeholder + "}", value);
|
||||
return TextUtil.colorize(getRaw("prefix") + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of messages from the config
|
||||
*/
|
||||
public List<String> getList(String path) {
|
||||
return messagesConfig.getStringList(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of messages as Components
|
||||
*/
|
||||
public List<Component> getComponentList(String path) {
|
||||
return getList(path).stream()
|
||||
.map(TextUtil::colorize)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a currency amount
|
||||
*/
|
||||
public String formatCurrency(double amount) {
|
||||
String symbol = plugin.getConfigManager().getCurrencySymbol();
|
||||
return symbol + currencyFormatter.format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a button name from config
|
||||
*/
|
||||
public String getButton(String buttonKey) {
|
||||
return getRaw("buttons." + buttonKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets button lore list from config
|
||||
*/
|
||||
public List<String> getLore(String loreKey) {
|
||||
return getList("lore." + loreKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets lore with placeholders replaced
|
||||
*/
|
||||
public List<String> getLore(String loreKey, Map<String, String> placeholders) {
|
||||
List<String> lore = getList("lore." + loreKey);
|
||||
return lore.stream()
|
||||
.map(line -> {
|
||||
String result = line;
|
||||
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||
result = result.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets filter display name
|
||||
*/
|
||||
public String getFilter(String filterKey) {
|
||||
return getRaw("filters." + filterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets sort display name
|
||||
*/
|
||||
public String getSort(String sortKey) {
|
||||
return getRaw("sort." + sortKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,987 @@
|
||||
package pt.henrique.communityMarket.db;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.*;
|
||||
import pt.henrique.communityMarket.util.ItemSerializer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Manages database connections and operations.
|
||||
* Supports SQLite (default) and MySQL.
|
||||
* All operations are asynchronous to prevent blocking the main thread.
|
||||
*/
|
||||
public class DatabaseManager {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private HikariDataSource dataSource;
|
||||
private boolean isMySQL;
|
||||
|
||||
// Schema version for migrations
|
||||
private static final int SCHEMA_VERSION = 1;
|
||||
|
||||
public DatabaseManager(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the database connection pool and creates tables.
|
||||
*
|
||||
* @return true if successful
|
||||
*/
|
||||
public boolean initialize() {
|
||||
try {
|
||||
var config = plugin.getConfigManager();
|
||||
isMySQL = "mysql".equalsIgnoreCase(config.getDatabaseType());
|
||||
|
||||
HikariConfig hikariConfig = new HikariConfig();
|
||||
|
||||
if (isMySQL) {
|
||||
// MySQL configuration
|
||||
hikariConfig.setJdbcUrl("jdbc:mysql://" + config.getMysqlHost() + ":" +
|
||||
config.getMysqlPort() + "/" + config.getMysqlDatabase() +
|
||||
"?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8");
|
||||
hikariConfig.setUsername(config.getMysqlUsername());
|
||||
hikariConfig.setPassword(config.getMysqlPassword());
|
||||
hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
|
||||
} else {
|
||||
// SQLite configuration
|
||||
File dataFolder = plugin.getDataFolder();
|
||||
if (!dataFolder.exists()) {
|
||||
dataFolder.mkdirs();
|
||||
}
|
||||
File dbFile = new File(dataFolder, config.getSqliteFile());
|
||||
hikariConfig.setJdbcUrl("jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
hikariConfig.setDriverClassName("org.sqlite.JDBC");
|
||||
}
|
||||
|
||||
// Connection pool settings
|
||||
hikariConfig.setMaximumPoolSize(config.getPoolMaxSize());
|
||||
hikariConfig.setMinimumIdle(config.getPoolMinIdle());
|
||||
hikariConfig.setConnectionTimeout(config.getPoolConnectionTimeout());
|
||||
hikariConfig.setIdleTimeout(config.getPoolIdleTimeout());
|
||||
hikariConfig.setMaxLifetime(config.getPoolMaxLifetime());
|
||||
hikariConfig.setPoolName("CommunityMarket-Pool");
|
||||
|
||||
dataSource = new HikariDataSource(hikariConfig);
|
||||
|
||||
// Create tables
|
||||
createTables();
|
||||
|
||||
plugin.getLogger().info("Database connection established (" +
|
||||
(isMySQL ? "MySQL" : "SQLite") + ")");
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to initialize database", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the database connection pool.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
plugin.getLogger().info("Database connection closed.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a connection from the pool.
|
||||
*/
|
||||
private Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all database tables.
|
||||
*/
|
||||
private void createTables() throws SQLException {
|
||||
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
|
||||
|
||||
// Listings table
|
||||
stmt.execute("""
|
||||
CREATE TABLE IF NOT EXISTS listings (
|
||||
id INTEGER PRIMARY KEY %s,
|
||||
seller_uuid VARCHAR(36) NOT NULL,
|
||||
seller_name VARCHAR(16) NOT NULL,
|
||||
item_data TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
price DOUBLE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at BIGINT NOT NULL,
|
||||
expires_at BIGINT,
|
||||
buyer_uuid VARCHAR(36),
|
||||
buyer_name VARCHAR(16),
|
||||
sold_at BIGINT
|
||||
)
|
||||
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||
|
||||
// Auctions table
|
||||
stmt.execute("""
|
||||
CREATE TABLE IF NOT EXISTS auctions (
|
||||
id INTEGER PRIMARY KEY %s,
|
||||
seller_uuid VARCHAR(36) NOT NULL,
|
||||
seller_name VARCHAR(16) NOT NULL,
|
||||
item_data TEXT NOT NULL,
|
||||
start_price DOUBLE NOT NULL,
|
||||
current_bid DOUBLE NOT NULL DEFAULT 0,
|
||||
highest_bidder_uuid VARCHAR(36),
|
||||
highest_bidder_name VARCHAR(16),
|
||||
bid_count INTEGER NOT NULL DEFAULT 0,
|
||||
buyout_price DOUBLE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at BIGINT NOT NULL,
|
||||
ends_at BIGINT NOT NULL,
|
||||
extension_count INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||
|
||||
// Bids table (bid history)
|
||||
stmt.execute("""
|
||||
CREATE TABLE IF NOT EXISTS bids (
|
||||
id INTEGER PRIMARY KEY %s,
|
||||
auction_id INTEGER NOT NULL,
|
||||
bidder_uuid VARCHAR(36) NOT NULL,
|
||||
bidder_name VARCHAR(16) NOT NULL,
|
||||
amount DOUBLE NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
FOREIGN KEY (auction_id) REFERENCES auctions(id)
|
||||
)
|
||||
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||
|
||||
// Claim storage table
|
||||
stmt.execute("""
|
||||
CREATE TABLE IF NOT EXISTS claim_storage (
|
||||
id INTEGER PRIMARY KEY %s,
|
||||
player_uuid VARCHAR(36) NOT NULL,
|
||||
item_data TEXT NOT NULL,
|
||||
reason VARCHAR(50) NOT NULL,
|
||||
source_info VARCHAR(100),
|
||||
created_at BIGINT NOT NULL
|
||||
)
|
||||
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||
|
||||
// Pending earnings table
|
||||
stmt.execute("""
|
||||
CREATE TABLE IF NOT EXISTS pending_earnings (
|
||||
id INTEGER PRIMARY KEY %s,
|
||||
player_uuid VARCHAR(36) NOT NULL,
|
||||
amount DOUBLE NOT NULL,
|
||||
source VARCHAR(100),
|
||||
created_at BIGINT NOT NULL,
|
||||
withdrawn BOOLEAN NOT NULL DEFAULT FALSE
|
||||
)
|
||||
""".formatted(isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"));
|
||||
|
||||
// Player data table (for cooldowns, etc.)
|
||||
stmt.execute("""
|
||||
CREATE TABLE IF NOT EXISTS player_data (
|
||||
player_uuid VARCHAR(36) PRIMARY KEY,
|
||||
last_listing_time BIGINT,
|
||||
preferred_language VARCHAR(10)
|
||||
)
|
||||
""");
|
||||
|
||||
// Create indexes for performance
|
||||
try {
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS idx_listings_seller ON listings(seller_uuid)");
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS idx_listings_status ON listings(status)");
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS idx_auctions_seller ON auctions(seller_uuid)");
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS idx_auctions_status ON auctions(status)");
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS idx_claim_player ON claim_storage(player_uuid)");
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS idx_earnings_player ON pending_earnings(player_uuid)");
|
||||
} catch (SQLException e) {
|
||||
// Indexes might already exist, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== LISTING OPERATIONS ====================
|
||||
|
||||
/**
|
||||
* Creates a new listing and returns its ID.
|
||||
*/
|
||||
public CompletableFuture<Integer> createListing(Listing listing) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = """
|
||||
INSERT INTO listings (seller_uuid, seller_name, item_data, amount, price, status, created_at, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
stmt.setString(1, listing.getSellerUuid().toString());
|
||||
stmt.setString(2, listing.getSellerName());
|
||||
stmt.setString(3, ItemSerializer.serialize(listing.getItem()));
|
||||
stmt.setInt(4, listing.getAmount());
|
||||
stmt.setDouble(5, listing.getPrice());
|
||||
stmt.setString(6, listing.getStatus().name());
|
||||
stmt.setLong(7, listing.getCreatedAt().toEpochMilli());
|
||||
stmt.setLong(8, listing.getExpiresAt() != null ? listing.getExpiresAt().toEpochMilli() : 0);
|
||||
|
||||
stmt.executeUpdate();
|
||||
|
||||
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to create listing", e);
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all active listings.
|
||||
*/
|
||||
public CompletableFuture<List<Listing>> getActiveListings() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Listing> listings = new ArrayList<>();
|
||||
String sql = "SELECT * FROM listings WHERE status = 'ACTIVE' ORDER BY created_at DESC";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
|
||||
while (rs.next()) {
|
||||
Listing listing = mapListing(rs);
|
||||
if (listing != null) {
|
||||
listings.add(listing);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get active listings", e);
|
||||
}
|
||||
return listings;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a listing by ID.
|
||||
*/
|
||||
public CompletableFuture<Optional<Listing>> getListing(int id) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "SELECT * FROM listings WHERE id = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setInt(1, id);
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.ofNullable(mapListing(rs));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get listing", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all listings for a player.
|
||||
*/
|
||||
public CompletableFuture<List<Listing>> getPlayerListings(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Listing> listings = new ArrayList<>();
|
||||
String sql = "SELECT * FROM listings WHERE seller_uuid = ? AND status = 'ACTIVE' ORDER BY created_at DESC";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
Listing listing = mapListing(rs);
|
||||
if (listing != null) {
|
||||
listings.add(listing);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get player listings", e);
|
||||
}
|
||||
return listings;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts active listings for a player.
|
||||
*/
|
||||
public CompletableFuture<Integer> countPlayerListings(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "SELECT COUNT(*) FROM listings WHERE seller_uuid = ? AND status = 'ACTIVE'";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to count player listings", e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically purchases a listing.
|
||||
* Returns true if successful (listing was still available).
|
||||
*/
|
||||
public CompletableFuture<Boolean> purchaseListing(int listingId, UUID buyerUuid, String buyerName) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "UPDATE listings SET status = 'SOLD', buyer_uuid = ?, buyer_name = ?, sold_at = ? " +
|
||||
"WHERE id = ? AND status = 'ACTIVE'";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, buyerUuid.toString());
|
||||
stmt.setString(2, buyerName);
|
||||
stmt.setLong(3, Instant.now().toEpochMilli());
|
||||
stmt.setInt(4, listingId);
|
||||
|
||||
int updated = stmt.executeUpdate();
|
||||
return updated > 0;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to purchase listing", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a listing's status.
|
||||
*/
|
||||
public CompletableFuture<Boolean> updateListingStatus(int listingId, Listing.ListingStatus status) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "UPDATE listings SET status = ? WHERE id = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, status.name());
|
||||
stmt.setInt(2, listingId);
|
||||
|
||||
return stmt.executeUpdate() > 0;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to update listing status", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets expired active listings.
|
||||
*/
|
||||
public CompletableFuture<List<Listing>> getExpiredListings() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Listing> listings = new ArrayList<>();
|
||||
String sql = "SELECT * FROM listings WHERE status = 'ACTIVE' AND expires_at > 0 AND expires_at < ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setLong(1, Instant.now().toEpochMilli());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
Listing listing = mapListing(rs);
|
||||
if (listing != null) {
|
||||
listings.add(listing);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get expired listings", e);
|
||||
}
|
||||
return listings;
|
||||
});
|
||||
}
|
||||
|
||||
private Listing mapListing(ResultSet rs) throws SQLException {
|
||||
try {
|
||||
Listing listing = new Listing();
|
||||
listing.setId(rs.getInt("id"));
|
||||
listing.setSellerUuid(UUID.fromString(rs.getString("seller_uuid")));
|
||||
listing.setSellerName(rs.getString("seller_name"));
|
||||
listing.setItem(ItemSerializer.deserialize(rs.getString("item_data")));
|
||||
listing.setAmount(rs.getInt("amount"));
|
||||
listing.setPrice(rs.getDouble("price"));
|
||||
listing.setStatus(Listing.ListingStatus.valueOf(rs.getString("status")));
|
||||
listing.setCreatedAt(Instant.ofEpochMilli(rs.getLong("created_at")));
|
||||
long expiresAt = rs.getLong("expires_at");
|
||||
if (expiresAt > 0) {
|
||||
listing.setExpiresAt(Instant.ofEpochMilli(expiresAt));
|
||||
}
|
||||
return listing;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to map listing", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AUCTION OPERATIONS ====================
|
||||
|
||||
/**
|
||||
* Creates a new auction and returns its ID.
|
||||
*/
|
||||
public CompletableFuture<Integer> createAuction(Auction auction) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = """
|
||||
INSERT INTO auctions (seller_uuid, seller_name, item_data, start_price, current_bid,
|
||||
buyout_price, status, created_at, ends_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
stmt.setString(1, auction.getSellerUuid().toString());
|
||||
stmt.setString(2, auction.getSellerName());
|
||||
stmt.setString(3, ItemSerializer.serialize(auction.getItem()));
|
||||
stmt.setDouble(4, auction.getStartPrice());
|
||||
stmt.setDouble(5, 0);
|
||||
if (auction.getBuyoutPrice() != null) {
|
||||
stmt.setDouble(6, auction.getBuyoutPrice());
|
||||
} else {
|
||||
stmt.setNull(6, Types.DOUBLE);
|
||||
}
|
||||
stmt.setString(7, auction.getStatus().name());
|
||||
stmt.setLong(8, auction.getCreatedAt().toEpochMilli());
|
||||
stmt.setLong(9, auction.getEndsAt().toEpochMilli());
|
||||
|
||||
stmt.executeUpdate();
|
||||
|
||||
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to create auction", e);
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all active auctions.
|
||||
*/
|
||||
public CompletableFuture<List<Auction>> getActiveAuctions() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Auction> auctions = new ArrayList<>();
|
||||
String sql = "SELECT * FROM auctions WHERE status = 'ACTIVE' ORDER BY ends_at ASC";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
|
||||
while (rs.next()) {
|
||||
Auction auction = mapAuction(rs);
|
||||
if (auction != null) {
|
||||
auctions.add(auction);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get active auctions", e);
|
||||
}
|
||||
return auctions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an auction by ID.
|
||||
*/
|
||||
public CompletableFuture<Optional<Auction>> getAuction(int id) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "SELECT * FROM auctions WHERE id = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setInt(1, id);
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return Optional.ofNullable(mapAuction(rs));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get auction", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all auctions for a player.
|
||||
*/
|
||||
public CompletableFuture<List<Auction>> getPlayerAuctions(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Auction> auctions = new ArrayList<>();
|
||||
String sql = "SELECT * FROM auctions WHERE seller_uuid = ? AND status = 'ACTIVE' ORDER BY ends_at ASC";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
Auction auction = mapAuction(rs);
|
||||
if (auction != null) {
|
||||
auctions.add(auction);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get player auctions", e);
|
||||
}
|
||||
return auctions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts active auctions for a player.
|
||||
*/
|
||||
public CompletableFuture<Integer> countPlayerAuctions(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "SELECT COUNT(*) FROM auctions WHERE seller_uuid = ? AND status = 'ACTIVE'";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to count player auctions", e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a bid on an auction.
|
||||
*/
|
||||
public CompletableFuture<Boolean> placeBid(int auctionId, UUID bidderUuid, String bidderName, double amount) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String updateSql = """
|
||||
UPDATE auctions SET current_bid = ?, highest_bidder_uuid = ?, highest_bidder_name = ?,
|
||||
bid_count = bid_count + 1
|
||||
WHERE id = ? AND status = 'ACTIVE' AND current_bid < ?
|
||||
""";
|
||||
|
||||
String insertBidSql = """
|
||||
INSERT INTO bids (auction_id, bidder_uuid, bidder_name, amount, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (Connection conn = getConnection()) {
|
||||
conn.setAutoCommit(false);
|
||||
|
||||
try (PreparedStatement updateStmt = conn.prepareStatement(updateSql);
|
||||
PreparedStatement insertStmt = conn.prepareStatement(insertBidSql)) {
|
||||
|
||||
updateStmt.setDouble(1, amount);
|
||||
updateStmt.setString(2, bidderUuid.toString());
|
||||
updateStmt.setString(3, bidderName);
|
||||
updateStmt.setInt(4, auctionId);
|
||||
updateStmt.setDouble(5, amount);
|
||||
|
||||
int updated = updateStmt.executeUpdate();
|
||||
|
||||
if (updated > 0) {
|
||||
// Insert bid history
|
||||
insertStmt.setInt(1, auctionId);
|
||||
insertStmt.setString(2, bidderUuid.toString());
|
||||
insertStmt.setString(3, bidderName);
|
||||
insertStmt.setDouble(4, amount);
|
||||
insertStmt.setLong(5, Instant.now().toEpochMilli());
|
||||
insertStmt.executeUpdate();
|
||||
|
||||
conn.commit();
|
||||
return true;
|
||||
} else {
|
||||
conn.rollback();
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
conn.rollback();
|
||||
throw e;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to place bid", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an auction's status.
|
||||
*/
|
||||
public CompletableFuture<Boolean> updateAuctionStatus(int auctionId, Auction.AuctionStatus status) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "UPDATE auctions SET status = ? WHERE id = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, status.name());
|
||||
stmt.setInt(2, auctionId);
|
||||
|
||||
return stmt.executeUpdate() > 0;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to update auction status", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets auctions that have ended but are still active.
|
||||
*/
|
||||
public CompletableFuture<List<Auction>> getEndedAuctions() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Auction> auctions = new ArrayList<>();
|
||||
String sql = "SELECT * FROM auctions WHERE status = 'ACTIVE' AND ends_at < ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setLong(1, Instant.now().toEpochMilli());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
Auction auction = mapAuction(rs);
|
||||
if (auction != null) {
|
||||
auctions.add(auction);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get ended auctions", e);
|
||||
}
|
||||
return auctions;
|
||||
});
|
||||
}
|
||||
|
||||
private Auction mapAuction(ResultSet rs) throws SQLException {
|
||||
try {
|
||||
Auction auction = new Auction();
|
||||
auction.setId(rs.getInt("id"));
|
||||
auction.setSellerUuid(UUID.fromString(rs.getString("seller_uuid")));
|
||||
auction.setSellerName(rs.getString("seller_name"));
|
||||
auction.setItem(ItemSerializer.deserialize(rs.getString("item_data")));
|
||||
auction.setStartPrice(rs.getDouble("start_price"));
|
||||
auction.setCurrentBid(rs.getDouble("current_bid"));
|
||||
|
||||
String highestBidder = rs.getString("highest_bidder_uuid");
|
||||
if (highestBidder != null) {
|
||||
auction.setHighestBidderUuid(UUID.fromString(highestBidder));
|
||||
auction.setHighestBidderName(rs.getString("highest_bidder_name"));
|
||||
}
|
||||
|
||||
auction.setBidCount(rs.getInt("bid_count"));
|
||||
|
||||
double buyout = rs.getDouble("buyout_price");
|
||||
if (!rs.wasNull()) {
|
||||
auction.setBuyoutPrice(buyout);
|
||||
}
|
||||
|
||||
auction.setStatus(Auction.AuctionStatus.valueOf(rs.getString("status")));
|
||||
auction.setCreatedAt(Instant.ofEpochMilli(rs.getLong("created_at")));
|
||||
auction.setEndsAt(Instant.ofEpochMilli(rs.getLong("ends_at")));
|
||||
auction.setExtensionCount(rs.getInt("extension_count"));
|
||||
|
||||
return auction;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to map auction", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CLAIM STORAGE OPERATIONS ====================
|
||||
|
||||
/**
|
||||
* Adds an item to claim storage.
|
||||
*/
|
||||
public CompletableFuture<Integer> addClaimItem(ClaimItem claimItem) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = """
|
||||
INSERT INTO claim_storage (player_uuid, item_data, reason, source_info, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
stmt.setString(1, claimItem.getPlayerUuid().toString());
|
||||
stmt.setString(2, ItemSerializer.serialize(claimItem.getItem()));
|
||||
stmt.setString(3, claimItem.getReason().name());
|
||||
stmt.setString(4, claimItem.getSourceInfo());
|
||||
stmt.setLong(5, claimItem.getCreatedAt().toEpochMilli());
|
||||
|
||||
stmt.executeUpdate();
|
||||
|
||||
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to add claim item", e);
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all claim items for a player.
|
||||
*/
|
||||
public CompletableFuture<List<ClaimItem>> getPlayerClaimItems(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<ClaimItem> items = new ArrayList<>();
|
||||
String sql = "SELECT * FROM claim_storage WHERE player_uuid = ? ORDER BY created_at DESC";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
ClaimItem item = mapClaimItem(rs);
|
||||
if (item != null) {
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get claim items", e);
|
||||
}
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts claim items for a player.
|
||||
*/
|
||||
public CompletableFuture<Integer> countPlayerClaimItems(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "SELECT COUNT(*) FROM claim_storage WHERE player_uuid = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to count claim items", e);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a claim item.
|
||||
*/
|
||||
public CompletableFuture<Boolean> removeClaimItem(int id) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "DELETE FROM claim_storage WHERE id = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setInt(1, id);
|
||||
return stmt.executeUpdate() > 0;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to remove claim item", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private ClaimItem mapClaimItem(ResultSet rs) throws SQLException {
|
||||
try {
|
||||
ClaimItem item = new ClaimItem();
|
||||
item.setId(rs.getInt("id"));
|
||||
item.setPlayerUuid(UUID.fromString(rs.getString("player_uuid")));
|
||||
item.setItem(ItemSerializer.deserialize(rs.getString("item_data")));
|
||||
item.setReason(ClaimItem.ClaimReason.valueOf(rs.getString("reason")));
|
||||
item.setSourceInfo(rs.getString("source_info"));
|
||||
item.setCreatedAt(Instant.ofEpochMilli(rs.getLong("created_at")));
|
||||
return item;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to map claim item", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== EARNINGS OPERATIONS ====================
|
||||
|
||||
/**
|
||||
* Adds pending earnings.
|
||||
*/
|
||||
public CompletableFuture<Integer> addPendingEarnings(PendingEarnings earnings) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = """
|
||||
INSERT INTO pending_earnings (player_uuid, amount, source, created_at, withdrawn)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
stmt.setString(1, earnings.getPlayerUuid().toString());
|
||||
stmt.setDouble(2, earnings.getAmount());
|
||||
stmt.setString(3, earnings.getSource());
|
||||
stmt.setLong(4, earnings.getCreatedAt().toEpochMilli());
|
||||
stmt.setBoolean(5, false);
|
||||
|
||||
stmt.executeUpdate();
|
||||
|
||||
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to add pending earnings", e);
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets total pending earnings for a player.
|
||||
*/
|
||||
public CompletableFuture<Double> getPlayerPendingEarnings(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "SELECT SUM(amount) FROM pending_earnings WHERE player_uuid = ? AND withdrawn = FALSE";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return rs.getDouble(1);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get pending earnings", e);
|
||||
}
|
||||
return 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all earnings as withdrawn for a player.
|
||||
*/
|
||||
public CompletableFuture<Boolean> withdrawAllEarnings(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "UPDATE pending_earnings SET withdrawn = TRUE WHERE player_uuid = ? AND withdrawn = FALSE";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
return stmt.executeUpdate() > 0;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to withdraw earnings", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== PLAYER DATA OPERATIONS ====================
|
||||
|
||||
/**
|
||||
* Gets the last listing time for a player.
|
||||
*/
|
||||
public CompletableFuture<Optional<Instant>> getLastListingTime(UUID playerUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
String sql = "SELECT last_listing_time FROM player_data WHERE player_uuid = ?";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
long time = rs.getLong("last_listing_time");
|
||||
if (!rs.wasNull() && time > 0) {
|
||||
return Optional.of(Instant.ofEpochMilli(time));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get last listing time", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last listing time for a player.
|
||||
*/
|
||||
public void updateLastListingTime(UUID playerUuid) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
String sql = isMySQL
|
||||
? "INSERT INTO player_data (player_uuid, last_listing_time) VALUES (?, ?) ON DUPLICATE KEY UPDATE last_listing_time = ?"
|
||||
: "INSERT OR REPLACE INTO player_data (player_uuid, last_listing_time) VALUES (?, ?)";
|
||||
|
||||
try (Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
long now = Instant.now().toEpochMilli();
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
stmt.setLong(2, now);
|
||||
if (isMySQL) {
|
||||
stmt.setLong(3, now);
|
||||
}
|
||||
|
||||
stmt.executeUpdate();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to update last listing time", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package pt.henrique.communityMarket.economy;
|
||||
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Manages economy operations with support for Vault and EssentialsX fallback.
|
||||
* <p>
|
||||
* Priority: Vault > EssentialsX
|
||||
* If neither is available, the plugin will disable itself.
|
||||
*/
|
||||
public class EconomyManager {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private Economy vaultEconomy;
|
||||
private com.earth2me.essentials.Essentials essentials;
|
||||
private EconomyProvider provider = EconomyProvider.NONE;
|
||||
|
||||
public enum EconomyProvider {
|
||||
VAULT,
|
||||
ESSENTIALS,
|
||||
NONE
|
||||
}
|
||||
|
||||
public EconomyManager(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set up an economy provider.
|
||||
* Tries Vault first, then EssentialsX.
|
||||
*
|
||||
* @return true if an economy provider was found
|
||||
*/
|
||||
public boolean setupEconomy() {
|
||||
// Try Vault first
|
||||
if (setupVault()) {
|
||||
provider = EconomyProvider.VAULT;
|
||||
plugin.getLogger().info("Using Vault as economy provider.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to EssentialsX
|
||||
if (setupEssentials()) {
|
||||
provider = EconomyProvider.ESSENTIALS;
|
||||
plugin.getLogger().info("Using EssentialsX as economy provider.");
|
||||
return true;
|
||||
}
|
||||
|
||||
plugin.getLogger().severe("No economy provider found!");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to hook into Vault economy
|
||||
*/
|
||||
private boolean setupVault() {
|
||||
if (Bukkit.getPluginManager().getPlugin("Vault") == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
RegisteredServiceProvider<Economy> rsp = Bukkit.getServicesManager().getRegistration(Economy.class);
|
||||
if (rsp == null) {
|
||||
plugin.getLogger().warning("Vault found but no economy provider registered.");
|
||||
return false;
|
||||
}
|
||||
|
||||
vaultEconomy = rsp.getProvider();
|
||||
return vaultEconomy != null;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to hook into Vault", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to hook into EssentialsX economy
|
||||
*/
|
||||
private boolean setupEssentials() {
|
||||
if (Bukkit.getPluginManager().getPlugin("Essentials") == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
essentials = (com.earth2me.essentials.Essentials) Bukkit.getPluginManager().getPlugin("Essentials");
|
||||
return essentials != null && essentials.isEnabled();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to hook into EssentialsX", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the active economy provider
|
||||
*/
|
||||
public String getProviderName() {
|
||||
return switch (provider) {
|
||||
case VAULT -> "Vault (" + (vaultEconomy != null ? vaultEconomy.getName() : "Unknown") + ")";
|
||||
case ESSENTIALS -> "EssentialsX";
|
||||
case NONE -> "None";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a player's current balance
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @return The player's balance
|
||||
*/
|
||||
public double getBalance(UUID playerUuid) {
|
||||
OfflinePlayer player = Bukkit.getOfflinePlayer(playerUuid);
|
||||
|
||||
return switch (provider) {
|
||||
case VAULT -> vaultEconomy.getBalance(player);
|
||||
case ESSENTIALS -> {
|
||||
try {
|
||||
yield essentials.getUser(playerUuid).getMoney().doubleValue();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to get balance from EssentialsX", e);
|
||||
yield 0.0;
|
||||
}
|
||||
}
|
||||
case NONE -> 0.0;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player has at least the specified amount
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param amount The amount to check
|
||||
* @return true if the player has enough money
|
||||
*/
|
||||
public boolean has(UUID playerUuid, double amount) {
|
||||
return getBalance(playerUuid) >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraws money from a player's account
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param amount The amount to withdraw
|
||||
* @return true if successful
|
||||
*/
|
||||
public boolean withdraw(UUID playerUuid, double amount) {
|
||||
if (amount <= 0) return true;
|
||||
|
||||
OfflinePlayer player = Bukkit.getOfflinePlayer(playerUuid);
|
||||
|
||||
return switch (provider) {
|
||||
case VAULT -> {
|
||||
if (!vaultEconomy.has(player, amount)) {
|
||||
yield false;
|
||||
}
|
||||
yield vaultEconomy.withdrawPlayer(player, amount).transactionSuccess();
|
||||
}
|
||||
case ESSENTIALS -> {
|
||||
try {
|
||||
var user = essentials.getUser(playerUuid);
|
||||
if (user.getMoney().doubleValue() < amount) {
|
||||
yield false;
|
||||
}
|
||||
user.setMoney(user.getMoney().subtract(java.math.BigDecimal.valueOf(amount)));
|
||||
yield true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to withdraw from EssentialsX", e);
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
case NONE -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposits money into a player's account
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param amount The amount to deposit
|
||||
* @return true if successful
|
||||
*/
|
||||
public boolean deposit(UUID playerUuid, double amount) {
|
||||
if (amount <= 0) return true;
|
||||
|
||||
OfflinePlayer player = Bukkit.getOfflinePlayer(playerUuid);
|
||||
|
||||
return switch (provider) {
|
||||
case VAULT -> vaultEconomy.depositPlayer(player, amount).transactionSuccess();
|
||||
case ESSENTIALS -> {
|
||||
try {
|
||||
var user = essentials.getUser(playerUuid);
|
||||
user.setMoney(user.getMoney().add(java.math.BigDecimal.valueOf(amount)));
|
||||
yield true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to deposit to EssentialsX", e);
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
case NONE -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers money between two players
|
||||
*
|
||||
* @param fromUuid The UUID of the payer
|
||||
* @param toUuid The UUID of the receiver
|
||||
* @param amount The amount to transfer
|
||||
* @return true if successful
|
||||
*/
|
||||
public boolean transfer(UUID fromUuid, UUID toUuid, double amount) {
|
||||
if (amount <= 0) return true;
|
||||
|
||||
// Withdraw first
|
||||
if (!withdraw(fromUuid, amount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then deposit - if this fails, refund the withdrawal
|
||||
if (!deposit(toUuid, amount)) {
|
||||
deposit(fromUuid, amount); // Attempt refund
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an amount according to the economy's formatting
|
||||
*
|
||||
* @param amount The amount to format
|
||||
* @return Formatted currency string
|
||||
*/
|
||||
public String format(double amount) {
|
||||
if (provider == EconomyProvider.VAULT && vaultEconomy != null) {
|
||||
return vaultEconomy.format(amount);
|
||||
}
|
||||
return plugin.getMessageManager().formatCurrency(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the economy provider type
|
||||
*/
|
||||
public EconomyProvider getProvider() {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
/**
|
||||
* Admin panel GUI for moderating the marketplace.
|
||||
* Provides access to view all listings/auctions, remove items, and reload config.
|
||||
*/
|
||||
public class AdminGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
|
||||
private static final int VIEW_LISTINGS_SLOT = 20;
|
||||
private static final int VIEW_AUCTIONS_SLOT = 24;
|
||||
private static final int RELOAD_SLOT = 40;
|
||||
private static final int BACK_SLOT = 49;
|
||||
|
||||
public AdminGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
if (!player.hasPermission("communitymarket.admin")) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.admin-panel"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Fill with glass
|
||||
ItemStack filler = new ItemBuilder(Material.RED_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Admin panel header
|
||||
inventory.setItem(4, new ItemBuilder(Material.COMMAND_BLOCK)
|
||||
.name("&c&lAdmin Panel")
|
||||
.lore(
|
||||
"&7Manage the marketplace.",
|
||||
"&7Remove listings, cancel auctions,",
|
||||
"&7and reload configuration."
|
||||
)
|
||||
.build());
|
||||
|
||||
// View all listings
|
||||
inventory.setItem(VIEW_LISTINGS_SLOT, new ItemBuilder(Material.CHEST)
|
||||
.name(msgManager.getButton("admin-view-listings"))
|
||||
.lore(
|
||||
"&7View all active listings",
|
||||
"&7from all players.",
|
||||
"",
|
||||
"&cClick on items to remove them."
|
||||
)
|
||||
.build());
|
||||
|
||||
// View all auctions
|
||||
inventory.setItem(VIEW_AUCTIONS_SLOT, new ItemBuilder(Material.GOLD_BLOCK)
|
||||
.name(msgManager.getButton("admin-view-auctions"))
|
||||
.lore(
|
||||
"&7View all active auctions",
|
||||
"&7from all players.",
|
||||
"",
|
||||
"&cClick on items to force-end them."
|
||||
)
|
||||
.build());
|
||||
|
||||
// Reload config
|
||||
inventory.setItem(RELOAD_SLOT, new ItemBuilder(Material.REPEATING_COMMAND_BLOCK)
|
||||
.name(msgManager.getButton("admin-reload"))
|
||||
.lore(
|
||||
"&7Reload plugin configuration",
|
||||
"&7and language files."
|
||||
)
|
||||
.build());
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
switch (slot) {
|
||||
case VIEW_LISTINGS_SLOT -> openAdminListings(player);
|
||||
case VIEW_AUCTIONS_SLOT -> openAdminAuctions(player);
|
||||
case RELOAD_SLOT -> {
|
||||
plugin.reload();
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.admin-reload"));
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
}
|
||||
case BACK_SLOT -> guiManager.openMainMenu(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void openAdminListings(Player player) {
|
||||
// Opens browse market but with admin remove capability
|
||||
new AdminListingsGui(plugin, guiManager).open(player, 0);
|
||||
}
|
||||
|
||||
private void openAdminAuctions(Player player) {
|
||||
new AdminAuctionsGui(plugin, guiManager).open(player, 0);
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.ADMIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
|
||||
// ==================== Inner Admin GUIs ====================
|
||||
|
||||
/**
|
||||
* Admin view of all listings with remove capability
|
||||
*/
|
||||
private static class AdminListingsGui implements MarketGui {
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private int page;
|
||||
private java.util.List<pt.henrique.communityMarket.model.Listing> listings;
|
||||
|
||||
public AdminListingsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player, int page) {
|
||||
this.player = player;
|
||||
this.page = page;
|
||||
|
||||
plugin.getListingService().getActiveListings().thenAccept(loaded -> {
|
||||
this.listings = loaded;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.admin-listings"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
ItemStack filler = new ItemBuilder(Material.RED_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
int start = page * 45;
|
||||
int end = Math.min(start + 45, listings.size());
|
||||
for (int i = start; i < end; i++) {
|
||||
var listing = listings.get(i);
|
||||
ItemStack display = listing.getItem().clone();
|
||||
inventory.setItem(i - start, new ItemBuilder(display)
|
||||
.addLore(java.util.List.of(
|
||||
"",
|
||||
"&7Seller: &f" + listing.getSellerName(),
|
||||
"&7Price: &a" + msgManager.formatCurrency(listing.getPrice()),
|
||||
"&7ID: &f#" + listing.getId(),
|
||||
"",
|
||||
"&cClick to remove"
|
||||
))
|
||||
.build());
|
||||
}
|
||||
|
||||
inventory.setItem(49, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == 49) {
|
||||
new AdminGui(plugin, guiManager).open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot >= 0 && slot < 45 && slot + page * 45 < listings.size()) {
|
||||
var listing = listings.get(slot + page * 45);
|
||||
plugin.getListingService().cancelListing(listing.getId(), player.getUniqueId(), true)
|
||||
.thenAccept(success -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (success) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||
"messages.admin-listing-removed", "id", String.valueOf(listing.getId())));
|
||||
}
|
||||
open(player, page);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.ADMIN_LISTINGS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin view of all auctions with cancel capability
|
||||
*/
|
||||
private static class AdminAuctionsGui implements MarketGui {
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private int page;
|
||||
private java.util.List<pt.henrique.communityMarket.model.Auction> auctions;
|
||||
|
||||
public AdminAuctionsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player, int page) {
|
||||
this.player = player;
|
||||
this.page = page;
|
||||
|
||||
plugin.getAuctionService().getActiveAuctions().thenAccept(loaded -> {
|
||||
this.auctions = loaded;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.admin-auctions"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
ItemStack filler = new ItemBuilder(Material.RED_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
int start = page * 45;
|
||||
int end = Math.min(start + 45, auctions.size());
|
||||
for (int i = start; i < end; i++) {
|
||||
var auction = auctions.get(i);
|
||||
ItemStack display = auction.getItem().clone();
|
||||
String bidder = auction.getHighestBidderName() != null ? auction.getHighestBidderName() : "None";
|
||||
inventory.setItem(i - start, new ItemBuilder(display)
|
||||
.addLore(java.util.List.of(
|
||||
"",
|
||||
"&7Seller: &f" + auction.getSellerName(),
|
||||
"&7Current Bid: &a" + msgManager.formatCurrency(auction.getCurrentBid()),
|
||||
"&7Bidder: &f" + bidder,
|
||||
"&7Bids: &f" + auction.getBidCount(),
|
||||
"&7ID: &f#" + auction.getId(),
|
||||
"",
|
||||
"&cClick to force-end"
|
||||
))
|
||||
.build());
|
||||
}
|
||||
|
||||
inventory.setItem(49, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == 49) {
|
||||
new AdminGui(plugin, guiManager).open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot >= 0 && slot < 45 && slot + page * 45 < auctions.size()) {
|
||||
var auction = auctions.get(slot + page * 45);
|
||||
plugin.getAuctionService().cancelAuction(auction.getId(), player.getUniqueId(), true)
|
||||
.thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||
"messages.admin-auction-cancelled", "id", String.valueOf(auction.getId())));
|
||||
open(player, page);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.ADMIN_AUCTIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.Auction;
|
||||
import pt.henrique.communityMarket.service.AuctionService;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GUI for browsing active auctions.
|
||||
* Left-click to bid, right-click to buyout (if available).
|
||||
*/
|
||||
public class BrowseAuctionsGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private int currentPage;
|
||||
private List<Auction> auctions;
|
||||
|
||||
// Layout constants
|
||||
private static final int ITEMS_PER_PAGE = 45;
|
||||
private static final int PREV_PAGE_SLOT = 45;
|
||||
private static final int INFO_SLOT = 49;
|
||||
private static final int NEXT_PAGE_SLOT = 53;
|
||||
private static final int BACK_SLOT = 48;
|
||||
|
||||
public BrowseAuctionsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player, int page) {
|
||||
this.player = player;
|
||||
this.currentPage = page;
|
||||
|
||||
// Load auctions asynchronously
|
||||
plugin.getAuctionService().getActiveAuctions().thenAccept(loadedAuctions -> {
|
||||
this.auctions = loadedAuctions;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
playSound(player, plugin.getConfigManager().getClickSound());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String title = msgManager.getRaw("gui-titles.browse-auctions")
|
||||
.replace("{page}", String.valueOf(currentPage + 1));
|
||||
inventory = Bukkit.createInventory(this, 54, TextUtil.colorizeToString(title));
|
||||
|
||||
// Fill bottom row
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Add auctions
|
||||
int startIndex = currentPage * ITEMS_PER_PAGE;
|
||||
int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, auctions.size());
|
||||
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
Auction auction = auctions.get(i);
|
||||
int slot = i - startIndex;
|
||||
inventory.setItem(slot, createAuctionItem(auction));
|
||||
}
|
||||
|
||||
// Navigation
|
||||
if (currentPage > 0) {
|
||||
inventory.setItem(PREV_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||
.name(msgManager.getButton("previous-page"))
|
||||
.build());
|
||||
}
|
||||
|
||||
int totalPages = (int) Math.ceil((double) auctions.size() / ITEMS_PER_PAGE);
|
||||
if (currentPage < totalPages - 1) {
|
||||
inventory.setItem(NEXT_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||
.name(msgManager.getButton("next-page"))
|
||||
.build());
|
||||
}
|
||||
|
||||
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||
.name("&ePage " + (currentPage + 1) + "/" + Math.max(1, totalPages))
|
||||
.lore("&7Total auctions: &f" + auctions.size())
|
||||
.build());
|
||||
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
private ItemStack createAuctionItem(Auction auction) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
ItemStack display = auction.getItem().clone();
|
||||
|
||||
// Time remaining
|
||||
Duration remaining = Duration.between(Instant.now(), auction.getEndsAt());
|
||||
String ends = TextUtil.formatDuration(remaining);
|
||||
|
||||
// Current bidder
|
||||
String bidder = auction.getHighestBidderName() != null ? auction.getHighestBidderName() : "&7None";
|
||||
String currentBid = auction.getBidCount() > 0
|
||||
? msgManager.formatCurrency(auction.getCurrentBid())
|
||||
: msgManager.formatCurrency(auction.getStartPrice());
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("");
|
||||
for (String line : msgManager.getLore("auction-info", Map.of(
|
||||
"seller", auction.getSellerName(),
|
||||
"start_price", msgManager.formatCurrency(auction.getStartPrice()),
|
||||
"current_bid", currentBid,
|
||||
"bidder", bidder,
|
||||
"bid_count", String.valueOf(auction.getBidCount()),
|
||||
"ends", ends
|
||||
))) {
|
||||
lore.add(line);
|
||||
}
|
||||
|
||||
// Add buyout info if available
|
||||
if (auction.hasBuyout()) {
|
||||
lore.add("&7Buyout: &a" + msgManager.formatCurrency(auction.getBuyoutPrice()));
|
||||
}
|
||||
|
||||
return new ItemBuilder(display)
|
||||
.addLore(lore)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot == PREV_PAGE_SLOT && currentPage > 0) {
|
||||
open(player, currentPage - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
int totalPages = (int) Math.ceil((double) auctions.size() / ITEMS_PER_PAGE);
|
||||
if (slot == NEXT_PAGE_SLOT && currentPage < totalPages - 1) {
|
||||
open(player, currentPage + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on auction
|
||||
if (slot >= 0 && slot < ITEMS_PER_PAGE) {
|
||||
int auctionIndex = currentPage * ITEMS_PER_PAGE + slot;
|
||||
if (auctionIndex < auctions.size()) {
|
||||
Auction auction = auctions.get(auctionIndex);
|
||||
|
||||
// Right-click for buyout
|
||||
if (event.getClick() == ClickType.RIGHT && auction.hasBuyout()) {
|
||||
handleBuyout(player, auction);
|
||||
} else {
|
||||
// Left-click for bid
|
||||
handleBid(player, auction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBid(Player player, Auction auction) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Can't bid on own auction
|
||||
if (auction.getSellerUuid().equals(player.getUniqueId())) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-own-item"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate minimum bid
|
||||
double minBid = plugin.getAuctionService().calculateMinBid(auction);
|
||||
|
||||
// Open number input for bid amount
|
||||
guiManager.openNumberInput(player, bidAmount -> {
|
||||
if (bidAmount <= 0) {
|
||||
guiManager.openBrowseAuctions(player, currentPage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Place bid
|
||||
plugin.getAuctionService().placeBid(auction.getId(), player, bidAmount)
|
||||
.thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
switch (result) {
|
||||
case SUCCESS -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-bid-placed", Map.of(
|
||||
"amount", msgManager.formatCurrency(bidAmount),
|
||||
"item", auction.getItem().getType().name()
|
||||
)));
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
guiManager.openBrowseAuctions(player, currentPage);
|
||||
}
|
||||
case BID_TOO_LOW -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-bid-too-low",
|
||||
"min", msgManager.formatCurrency(minBid)));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
case INSUFFICIENT_FUNDS -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-insufficient-funds",
|
||||
"price", msgManager.formatCurrency(bidAmount)));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
default -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
guiManager.openBrowseAuctions(player, currentPage);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, minBid, minBid, plugin.getConfigManager().getMaxPrice(),
|
||||
msgManager.getRaw("gui-titles.number-input"));
|
||||
}
|
||||
|
||||
private void handleBuyout(Player player, Auction auction) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
if (auction.getSellerUuid().equals(player.getUniqueId())) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-own-item"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
String[] info = {
|
||||
"&7Item: &f" + auction.getItem().getType().name(),
|
||||
"&7Buyout Price: &a" + msgManager.formatCurrency(auction.getBuyoutPrice()),
|
||||
"",
|
||||
"&eClick to confirm buyout!"
|
||||
};
|
||||
|
||||
guiManager.openConfirmation(player, confirmed -> {
|
||||
if (confirmed) {
|
||||
plugin.getAuctionService().buyout(auction.getId(), player)
|
||||
.thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (result == AuctionService.BidResult.BUYOUT_SUCCESS) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-buyout", Map.of(
|
||||
"item", auction.getItem().getType().name(),
|
||||
"price", msgManager.formatCurrency(auction.getBuyoutPrice())
|
||||
)));
|
||||
playSound(player, plugin.getConfigManager().getPurchaseSound());
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
guiManager.openBrowseAuctions(player, currentPage);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
guiManager.openBrowseAuctions(player, currentPage);
|
||||
}
|
||||
}, msgManager.getRaw("gui-titles.confirm-purchase"), info);
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.BROWSE_AUCTIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.Listing;
|
||||
import pt.henrique.communityMarket.service.ListingService;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GUI for browsing the market's fixed-price listings.
|
||||
* Features pagination, and click-to-buy functionality.
|
||||
*/
|
||||
public class BrowseMarketGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private int currentPage;
|
||||
private List<Listing> listings;
|
||||
|
||||
// Layout constants
|
||||
private static final int ITEMS_PER_PAGE = 45;
|
||||
private static final int PREV_PAGE_SLOT = 45;
|
||||
private static final int INFO_SLOT = 49;
|
||||
private static final int NEXT_PAGE_SLOT = 53;
|
||||
private static final int BACK_SLOT = 48;
|
||||
private static final int FILTER_SLOT = 47;
|
||||
private static final int SORT_SLOT = 51;
|
||||
|
||||
public BrowseMarketGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the browse market GUI for a player
|
||||
*/
|
||||
public void open(Player player, int page) {
|
||||
this.player = player;
|
||||
this.currentPage = page;
|
||||
|
||||
// Load listings asynchronously
|
||||
plugin.getListingService().getActiveListings().thenAccept(loadedListings -> {
|
||||
this.listings = loadedListings;
|
||||
|
||||
// Build GUI on main thread
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
playSound(player, plugin.getConfigManager().getClickSound());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String title = msgManager.getRaw("gui-titles.browse-market")
|
||||
.replace("{page}", String.valueOf(currentPage + 1));
|
||||
inventory = Bukkit.createInventory(this, 54, TextUtil.colorizeToString(title));
|
||||
|
||||
// Fill bottom row with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Add listings to slots 0-44
|
||||
int startIndex = currentPage * ITEMS_PER_PAGE;
|
||||
int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, listings.size());
|
||||
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
Listing listing = listings.get(i);
|
||||
int slot = i - startIndex;
|
||||
inventory.setItem(slot, createListingItem(listing));
|
||||
}
|
||||
|
||||
// Navigation buttons
|
||||
if (currentPage > 0) {
|
||||
inventory.setItem(PREV_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||
.name(msgManager.getButton("previous-page"))
|
||||
.build());
|
||||
}
|
||||
|
||||
int totalPages = (int) Math.ceil((double) listings.size() / ITEMS_PER_PAGE);
|
||||
if (currentPage < totalPages - 1) {
|
||||
inventory.setItem(NEXT_PAGE_SLOT, new ItemBuilder(Material.ARROW)
|
||||
.name(msgManager.getButton("next-page"))
|
||||
.build());
|
||||
}
|
||||
|
||||
// Info/page indicator
|
||||
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||
.name("&ePage " + (currentPage + 1) + "/" + Math.max(1, totalPages))
|
||||
.lore("&7Total listings: &f" + listings.size())
|
||||
.build());
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
private ItemStack createListingItem(Listing listing) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
ItemStack display = listing.getItem().clone();
|
||||
display.setAmount(listing.getAmount());
|
||||
|
||||
// Calculate time remaining
|
||||
String expires;
|
||||
if (listing.getExpiresAt() != null) {
|
||||
Duration remaining = Duration.between(Instant.now(), listing.getExpiresAt());
|
||||
expires = TextUtil.formatDuration(remaining);
|
||||
} else {
|
||||
expires = "Never";
|
||||
}
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add(""); // Empty line separator
|
||||
for (String line : msgManager.getLore("listing-info", Map.of(
|
||||
"seller", listing.getSellerName(),
|
||||
"price", msgManager.formatCurrency(listing.getPrice()),
|
||||
"amount", String.valueOf(listing.getAmount()),
|
||||
"expires", expires
|
||||
))) {
|
||||
lore.add(line);
|
||||
}
|
||||
|
||||
return new ItemBuilder(display)
|
||||
.addLore(lore)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
// Bottom row navigation
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot == PREV_PAGE_SLOT && currentPage > 0) {
|
||||
open(player, currentPage - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
int totalPages = (int) Math.ceil((double) listings.size() / ITEMS_PER_PAGE);
|
||||
if (slot == NEXT_PAGE_SLOT && currentPage < totalPages - 1) {
|
||||
open(player, currentPage + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on a listing (slots 0-44)
|
||||
if (slot >= 0 && slot < ITEMS_PER_PAGE) {
|
||||
int listingIndex = currentPage * ITEMS_PER_PAGE + slot;
|
||||
if (listingIndex < listings.size()) {
|
||||
Listing listing = listings.get(listingIndex);
|
||||
handleListingClick(player, listing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleListingClick(Player player, Listing listing) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Can't buy own listing
|
||||
if (listing.getSellerUuid().equals(player.getUniqueId())) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-own-item"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation
|
||||
double tax = plugin.getTransactionService().calculateListingTax(listing.getPrice());
|
||||
String[] info = {
|
||||
"&7Item: &f" + listing.getItem().getType().name() + " x" + listing.getAmount(),
|
||||
"&7Seller: &f" + listing.getSellerName(),
|
||||
"&7Price: &a" + msgManager.formatCurrency(listing.getPrice()),
|
||||
"",
|
||||
"&eClick to confirm purchase!"
|
||||
};
|
||||
|
||||
guiManager.openConfirmation(player, confirmed -> {
|
||||
if (confirmed) {
|
||||
// Attempt purchase
|
||||
plugin.getListingService().purchaseListing(listing.getId(), player)
|
||||
.thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
switch (result) {
|
||||
case SUCCESS -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-purchased", Map.of(
|
||||
"item", listing.getItem().getType().name(),
|
||||
"amount", String.valueOf(listing.getAmount()),
|
||||
"price", msgManager.formatCurrency(listing.getPrice())
|
||||
)));
|
||||
playSound(player, plugin.getConfigManager().getPurchaseSound());
|
||||
guiManager.openBrowseMarket(player, currentPage);
|
||||
}
|
||||
case INSUFFICIENT_FUNDS -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-insufficient-funds",
|
||||
"price", msgManager.formatCurrency(listing.getPrice())));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
case ALREADY_SOLD, NOT_FOUND -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
guiManager.openBrowseMarket(player, currentPage);
|
||||
}
|
||||
default -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
guiManager.openBrowseMarket(player, currentPage);
|
||||
}
|
||||
}, msgManager.getRaw("gui-titles.confirm-purchase"), info);
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.BROWSE_MARKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.ClaimItem;
|
||||
import pt.henrique.communityMarket.service.ClaimService;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GUI for claiming items from expired listings, won auctions, etc.
|
||||
* Click on an item to claim it, or use "Claim All" button.
|
||||
*/
|
||||
public class ClaimGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private List<ClaimItem> claimItems;
|
||||
|
||||
private static final int BACK_SLOT = 49;
|
||||
private static final int CLAIM_ALL_SLOT = 45;
|
||||
|
||||
public ClaimGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
plugin.getClaimService().getPlayerClaimItems(player.getUniqueId())
|
||||
.thenAccept(items -> {
|
||||
this.claimItems = items;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.claim-items"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
// Fill bottom row
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Add claim items
|
||||
for (int i = 0; i < Math.min(claimItems.size(), 45); i++) {
|
||||
ClaimItem item = claimItems.get(i);
|
||||
inventory.setItem(i, createClaimItemDisplay(item));
|
||||
}
|
||||
|
||||
// Claim All button
|
||||
if (!claimItems.isEmpty()) {
|
||||
inventory.setItem(CLAIM_ALL_SLOT, new ItemBuilder(Material.HOPPER)
|
||||
.name(msgManager.getButton("claim-all"))
|
||||
.lore("&7Claim all items at once")
|
||||
.glow()
|
||||
.build());
|
||||
}
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
|
||||
// Show empty message if no items
|
||||
if (claimItems.isEmpty()) {
|
||||
inventory.setItem(22, new ItemBuilder(Material.BARRIER)
|
||||
.name("&cNo items to claim")
|
||||
.lore("&7Items from expired listings,",
|
||||
"&7won auctions, etc. appear here.")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack createClaimItemDisplay(ClaimItem claimItem) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
ItemStack display = claimItem.getItem().clone();
|
||||
|
||||
String age = TextUtil.formatDuration(
|
||||
Duration.between(claimItem.getCreatedAt(), Instant.now())) + " ago";
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("");
|
||||
lore.addAll(msgManager.getLore("claim-item-info", Map.of(
|
||||
"reason", claimItem.getReason().getDisplayName(),
|
||||
"source", claimItem.getSourceInfo() != null ? claimItem.getSourceInfo() : "Unknown",
|
||||
"date", age
|
||||
)));
|
||||
|
||||
return new ItemBuilder(display)
|
||||
.addLore(lore)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot == CLAIM_ALL_SLOT && !claimItems.isEmpty()) {
|
||||
claimAll(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on item to claim
|
||||
if (slot >= 0 && slot < 45 && slot < claimItems.size()) {
|
||||
ClaimItem item = claimItems.get(slot);
|
||||
claimSingle(player, item);
|
||||
}
|
||||
}
|
||||
|
||||
private void claimSingle(Player player, ClaimItem item) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
plugin.getClaimService().claimItem(item.getId(), player)
|
||||
.thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
switch (result) {
|
||||
case SUCCESS -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.claim-success"));
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
open(player); // Refresh
|
||||
}
|
||||
case INVENTORY_FULL -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.claim-inventory-full"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
default -> {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.claim-empty"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
open(player);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void claimAll(Player player) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
plugin.getClaimService().claimAll(player)
|
||||
.thenAccept(count -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (count > 0) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.claim-all-success",
|
||||
"count", String.valueOf(count)));
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.claim-empty"));
|
||||
}
|
||||
open(player); // Refresh
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
try {
|
||||
Sound sound = Sound.valueOf(soundName);
|
||||
player.playSound(player.getLocation(), sound, 0.5f, 1.0f);
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.CLAIM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Confirmation dialog GUI for important actions like purchases and cancellations.
|
||||
*/
|
||||
public class ConfirmationGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private final ConfirmCallback callback;
|
||||
private final String title;
|
||||
private final String[] infoLines;
|
||||
|
||||
private Inventory inventory;
|
||||
|
||||
private static final int INFO_SLOT = 13;
|
||||
private static final int CONFIRM_SLOT = 29;
|
||||
private static final int CANCEL_SLOT = 33;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConfirmCallback {
|
||||
void onComplete(boolean confirmed);
|
||||
}
|
||||
|
||||
public ConfirmationGui(CommunityMarket plugin, GuiManager guiManager,
|
||||
ConfirmCallback callback, String title, String... infoLines) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
this.callback = callback;
|
||||
this.title = title;
|
||||
this.infoLines = infoLines;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
inventory = Bukkit.createInventory(this, 45, TextUtil.colorizeToString(title));
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Fill with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 45; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Info display
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (String line : infoLines) {
|
||||
lore.add(line);
|
||||
}
|
||||
|
||||
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||
.name("&e&lConfirm Action")
|
||||
.lore(lore)
|
||||
.build());
|
||||
|
||||
// Confirm button
|
||||
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||
.name(msgManager.getButton("confirm"))
|
||||
.lore("&aClick to confirm")
|
||||
.glow()
|
||||
.build());
|
||||
|
||||
// Cancel button
|
||||
inventory.setItem(CANCEL_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||
.name(msgManager.getButton("cancel"))
|
||||
.lore("&cClick to cancel")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == CONFIRM_SLOT) {
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
player.closeInventory();
|
||||
callback.onComplete(true);
|
||||
} else if (slot == CANCEL_SLOT) {
|
||||
player.closeInventory();
|
||||
callback.onComplete(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.CONFIRMATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||
import pt.henrique.communityMarket.util.SoundUtil;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* GUI for setting up a new auction.
|
||||
* This GUI is opened AFTER the player has selected an item and quantity.
|
||||
* All elements are merged: start price, buyout, and duration are single clickable items.
|
||||
*
|
||||
* Layout (54-slot chest):
|
||||
* ┌─────────────────────────────────────────────────────┐
|
||||
* │ . . . . INFO . . . . │ Row 0 │
|
||||
* │ . . . . ITEM . . . . │ Row 1: Item │
|
||||
* │ . . . . . . . . . │ Row 2 │
|
||||
* │ . START . BUYOUT . DURATION . │ Row 3: Setup │
|
||||
* │ . . . . . . . . . │ Row 4 │
|
||||
* │ BACK . . . CONFIRM . . . .│ Row 5: Action│
|
||||
* └─────────────────────────────────────────────────────┘
|
||||
*/
|
||||
public class CreateAuctionGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
|
||||
// Selected item info (from ItemSelectionGui -> QuantitySelectGui)
|
||||
private int sourceInventorySlot = -1;
|
||||
private ItemStack selectedItem = null;
|
||||
|
||||
// Auction settings
|
||||
private double startPrice;
|
||||
private Double buyoutPrice = null;
|
||||
private int durationHours;
|
||||
|
||||
// ==================== LAYOUT CONSTANTS ====================
|
||||
private static final int INFO_SLOT = 4; // Top center
|
||||
private static final int ITEM_DISPLAY_SLOT = 13; // Center
|
||||
|
||||
// Row 3: Merged elements (single slot each)
|
||||
private static final int START_PRICE_SLOT = 28; // Start price (display + click)
|
||||
private static final int BUYOUT_SLOT = 31; // Buyout (display + click)
|
||||
private static final int DURATION_SLOT = 34; // Duration (display + click)
|
||||
|
||||
private static final int BACK_SLOT = 45; // Bottom-left
|
||||
private static final int CONFIRM_SLOT = 49; // Bottom-center
|
||||
// ===========================================================
|
||||
|
||||
public CreateAuctionGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the auction creation GUI with a pre-selected item and quantity.
|
||||
* Called from QuantitySelectGui or ItemSelectionGui (for unstackable items).
|
||||
*
|
||||
* @param player The player
|
||||
* @param inventorySlot The slot in the player's inventory where the item is
|
||||
* @param item A clone of the selected item with the desired quantity
|
||||
*/
|
||||
public void openWithItem(Player player, int inventorySlot, ItemStack item) {
|
||||
this.player = player;
|
||||
this.sourceInventorySlot = inventorySlot;
|
||||
this.selectedItem = item;
|
||||
this.durationHours = plugin.getConfigManager().getDefaultAuctionDurationHours();
|
||||
this.startPrice = plugin.getConfigManager().getMinStartPrice();
|
||||
this.buyoutPrice = null;
|
||||
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-auction"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use openWithItem instead. This opens item selection first.
|
||||
*/
|
||||
public void open(Player player) {
|
||||
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Fill with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Info panel
|
||||
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.OAK_SIGN)
|
||||
.name("&6&lCreate Auction")
|
||||
.lore(
|
||||
"&7Set starting price, optional buyout,",
|
||||
"&7and duration for your auction.",
|
||||
"",
|
||||
"&7Tax on sale: &f" + plugin.getConfigManager().getAuctionTax() + "%"
|
||||
)
|
||||
.build());
|
||||
|
||||
// Selected item display
|
||||
if (selectedItem != null) {
|
||||
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(selectedItem.clone())
|
||||
.addLore(List.of(
|
||||
"",
|
||||
"&7Quantity: &f" + selectedItem.getAmount(),
|
||||
"&eThis item will be auctioned"
|
||||
))
|
||||
.build());
|
||||
}
|
||||
|
||||
// Merged start price element
|
||||
updateStartPriceElement();
|
||||
|
||||
// Merged buyout element
|
||||
updateBuyoutElement();
|
||||
|
||||
// Merged duration element
|
||||
updateDurationElement();
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||
.name(msgManager.getButton("back"))
|
||||
.lore("&7Return to item selection")
|
||||
.build());
|
||||
|
||||
// Confirm button
|
||||
updateConfirmButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the merged start price element (displays + clickable)
|
||||
*/
|
||||
private void updateStartPriceElement() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
inventory.setItem(START_PRICE_SLOT, new ItemBuilder(Material.GOLD_INGOT)
|
||||
.name("&6Starting Price: " + msgManager.formatCurrency(startPrice))
|
||||
.lore(
|
||||
"",
|
||||
"&7Minimum bid to start",
|
||||
"&7the auction.",
|
||||
"",
|
||||
"&eClick to change"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the merged buyout element (displays + clickable)
|
||||
*/
|
||||
private void updateBuyoutElement() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
if (buyoutPrice != null) {
|
||||
inventory.setItem(BUYOUT_SLOT, new ItemBuilder(Material.DIAMOND)
|
||||
.name("&bBuyout: " + msgManager.formatCurrency(buyoutPrice))
|
||||
.lore(
|
||||
"",
|
||||
"&7Instant purchase price.",
|
||||
"",
|
||||
"&eLeft-click to change",
|
||||
"&cRight-click to remove"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
} else {
|
||||
inventory.setItem(BUYOUT_SLOT, new ItemBuilder(Material.DIAMOND)
|
||||
.name("&bBuyout: &7Not set")
|
||||
.lore(
|
||||
"",
|
||||
"&7Optional instant purchase",
|
||||
"&7price for your auction.",
|
||||
"",
|
||||
"&eClick to set buyout price"
|
||||
)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the merged duration element (displays + clickable)
|
||||
*/
|
||||
private void updateDurationElement() {
|
||||
String durationText = formatDuration(durationHours);
|
||||
|
||||
inventory.setItem(DURATION_SLOT, new ItemBuilder(Material.CLOCK)
|
||||
.name("&eDuration: " + durationText)
|
||||
.lore(
|
||||
"",
|
||||
"&7Auction ends after this time.",
|
||||
"",
|
||||
"&eClick to change duration"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
}
|
||||
|
||||
private String formatDuration(int hours) {
|
||||
if (hours >= 24) {
|
||||
int days = hours / 24;
|
||||
return days + " day" + (days > 1 ? "s" : "");
|
||||
} else {
|
||||
return hours + " hour" + (hours > 1 ? "s" : "");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateConfirmButton() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||
.name(msgManager.getButton("confirm"))
|
||||
.lore(
|
||||
"&7Item: &f" + selectedItem.getType().name() + " x" + selectedItem.getAmount(),
|
||||
"&7Start: &a" + msgManager.formatCurrency(startPrice),
|
||||
buyoutPrice != null ? "&7Buyout: &b" + msgManager.formatCurrency(buyoutPrice) : "&7Buyout: &7None",
|
||||
"&7Duration: &e" + formatDuration(durationHours),
|
||||
"",
|
||||
"&aClick to create auction!"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
switch (slot) {
|
||||
case START_PRICE_SLOT -> {
|
||||
guiManager.openNumberInput(player, newPrice -> {
|
||||
if (newPrice > 0) {
|
||||
this.startPrice = newPrice;
|
||||
// Ensure buyout is higher than start
|
||||
if (buyoutPrice != null && buyoutPrice <= startPrice) {
|
||||
buyoutPrice = null;
|
||||
}
|
||||
}
|
||||
reopenGui();
|
||||
}, startPrice, plugin.getConfigManager().getMinStartPrice(),
|
||||
plugin.getConfigManager().getMaxStartPrice(),
|
||||
msgManager.getRaw("gui-titles.number-input"));
|
||||
}
|
||||
case BUYOUT_SLOT -> {
|
||||
if (event.isRightClick() && buyoutPrice != null) {
|
||||
// Remove buyout
|
||||
buyoutPrice = null;
|
||||
updateBuyoutElement();
|
||||
updateConfirmButton();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
} else {
|
||||
// Set buyout
|
||||
double defaultBuyout = buyoutPrice != null ? buyoutPrice : startPrice * 2;
|
||||
guiManager.openNumberInput(player, newPrice -> {
|
||||
if (newPrice > startPrice) {
|
||||
this.buyoutPrice = newPrice;
|
||||
}
|
||||
reopenGui();
|
||||
}, defaultBuyout, startPrice + 1, plugin.getConfigManager().getMaxPrice(),
|
||||
msgManager.getRaw("gui-titles.number-input"));
|
||||
}
|
||||
}
|
||||
case DURATION_SLOT -> {
|
||||
List<Integer> durations = plugin.getConfigManager().getAvailableAuctionDurations();
|
||||
if (!durations.isEmpty()) {
|
||||
int currentIndex = durations.indexOf(durationHours);
|
||||
int nextIndex = (currentIndex + 1) % durations.size();
|
||||
durationHours = durations.get(nextIndex);
|
||||
updateDurationElement();
|
||||
updateConfirmButton();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
}
|
||||
}
|
||||
case CONFIRM_SLOT -> {
|
||||
confirmAuction(player);
|
||||
}
|
||||
case BACK_SLOT -> {
|
||||
// Go back to item selection
|
||||
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmAuction(Player player) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Verify item still exists in player's inventory with sufficient quantity
|
||||
int available = InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||
|
||||
if (available < selectedItem.getAmount()) {
|
||||
if (available < 1) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.item-no-longer-available"));
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.quantity-changed"));
|
||||
}
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate item again
|
||||
var validation = plugin.getTransactionService().validateItem(selectedItem);
|
||||
if (!validation.isValid()) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages." + validation.getErrorKey()));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the auction
|
||||
ItemStack auctionItem = selectedItem.clone();
|
||||
|
||||
plugin.getTransactionService().createAuctionTransaction(
|
||||
player, auctionItem, startPrice, buyoutPrice, durationHours
|
||||
).thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (result.isSuccess()) {
|
||||
// Remove items from inventory AFTER successful creation
|
||||
InventoryUtil.removeItems(player, auctionItem, auctionItem.getAmount());
|
||||
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-created",
|
||||
"id", String.valueOf(result.getId())));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
player.closeInventory();
|
||||
guiManager.openMainMenu(player);
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages." + result.getErrorKey()));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void reopenGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-auction"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowsItemMovement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.CREATE_AUCTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||
import pt.henrique.communityMarket.util.SoundUtil;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* GUI for setting up a new fixed-price listing.
|
||||
* This GUI is opened AFTER the player has selected an item and quantity.
|
||||
* All elements are merged: price and duration are single clickable items.
|
||||
*
|
||||
* Layout (54-slot chest):
|
||||
* ┌─────────────────────────────────────────────────────┐
|
||||
* │ . . . . INFO . . . . │ Row 0 │
|
||||
* │ . . . . ITEM . . . . │ Row 1: Item │
|
||||
* │ . . . . . . . . . │ Row 2 │
|
||||
* │ . . PRICE . . . DURATION │ Row 3: Setup │
|
||||
* │ . . . . . . . . . │ Row 4 │
|
||||
* │ BACK . . . CONFIRM . . . .│ Row 5: Action│
|
||||
* └─────────────────────────────────────────────────────┘
|
||||
*/
|
||||
public class CreateListingGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
|
||||
// Selected item info (from ItemSelectionGui -> QuantitySelectGui)
|
||||
private int sourceInventorySlot = -1;
|
||||
private ItemStack selectedItem = null;
|
||||
|
||||
// Listing settings
|
||||
private double price;
|
||||
private int durationHours;
|
||||
|
||||
// ==================== LAYOUT CONSTANTS ====================
|
||||
private static final int INFO_SLOT = 4; // Top center
|
||||
private static final int ITEM_DISPLAY_SLOT = 13; // Center row 1
|
||||
|
||||
// Row 3: Merged elements (single slot each)
|
||||
private static final int PRICE_SLOT = 29; // Price (display + click to adjust)
|
||||
private static final int DURATION_SLOT = 33; // Duration (display + click to cycle)
|
||||
|
||||
private static final int BACK_SLOT = 45; // Bottom-left
|
||||
private static final int CONFIRM_SLOT = 49; // Bottom-center
|
||||
// ===========================================================
|
||||
|
||||
public CreateListingGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the listing creation GUI with a pre-selected item and quantity.
|
||||
* Called from QuantitySelectGui or ItemSelectionGui (for unstackable items).
|
||||
*
|
||||
* @param player The player
|
||||
* @param inventorySlot The slot in the player's inventory where the item is
|
||||
* @param item A clone of the selected item with the desired quantity
|
||||
*/
|
||||
public void openWithItem(Player player, int inventorySlot, ItemStack item) {
|
||||
this.player = player;
|
||||
this.sourceInventorySlot = inventorySlot;
|
||||
this.selectedItem = item;
|
||||
this.durationHours = plugin.getConfigManager().getDefaultDurationHours();
|
||||
this.price = plugin.getConfigManager().getMinPrice();
|
||||
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-listing"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use openWithItem instead. This opens item selection first.
|
||||
*/
|
||||
public void open(Player player) {
|
||||
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Fill with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Info panel
|
||||
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.OAK_SIGN)
|
||||
.name("&6&lCreate Listing")
|
||||
.lore(
|
||||
"&7Set a price and duration",
|
||||
"&7for your listing.",
|
||||
"",
|
||||
"&7Tax: &f" + plugin.getConfigManager().getMarketTax() + "%"
|
||||
)
|
||||
.build());
|
||||
|
||||
// Selected item display
|
||||
if (selectedItem != null) {
|
||||
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(selectedItem.clone())
|
||||
.addLore(List.of(
|
||||
"",
|
||||
"&7Quantity: &f" + selectedItem.getAmount(),
|
||||
"&eThis item will be listed"
|
||||
))
|
||||
.build());
|
||||
}
|
||||
|
||||
// Merged price element
|
||||
updatePriceElement();
|
||||
|
||||
// Merged duration element
|
||||
updateDurationElement();
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||
.name(msgManager.getButton("back"))
|
||||
.lore("&7Return to item selection")
|
||||
.build());
|
||||
|
||||
// Confirm button
|
||||
updateConfirmButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the merged price element (displays current price + clickable to change)
|
||||
*/
|
||||
private void updatePriceElement() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
double tax = plugin.getTransactionService().calculateListingTax(price);
|
||||
double earnings = price - tax;
|
||||
|
||||
inventory.setItem(PRICE_SLOT, new ItemBuilder(Material.GOLD_INGOT)
|
||||
.name("&6Price: " + msgManager.formatCurrency(price))
|
||||
.lore(
|
||||
"",
|
||||
"&7Tax (" + plugin.getConfigManager().getMarketTax() + "%): &c" + msgManager.formatCurrency(tax),
|
||||
"&7You receive: &a" + msgManager.formatCurrency(earnings),
|
||||
"",
|
||||
"&eClick to change price"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the merged duration element (displays current duration + clickable to cycle)
|
||||
*/
|
||||
private void updateDurationElement() {
|
||||
String durationText = formatDuration(durationHours);
|
||||
|
||||
inventory.setItem(DURATION_SLOT, new ItemBuilder(Material.CLOCK)
|
||||
.name("&eDuration: " + durationText)
|
||||
.lore(
|
||||
"",
|
||||
"&7Listing expires after this time",
|
||||
"",
|
||||
"&eClick to change duration"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
}
|
||||
|
||||
private String formatDuration(int hours) {
|
||||
if (hours >= 24) {
|
||||
int days = hours / 24;
|
||||
return days + " day" + (days > 1 ? "s" : "");
|
||||
} else {
|
||||
return hours + " hour" + (hours > 1 ? "s" : "");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateConfirmButton() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
double tax = plugin.getTransactionService().calculateListingTax(price);
|
||||
double earnings = price - tax;
|
||||
|
||||
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||
.name(msgManager.getButton("confirm"))
|
||||
.lore(
|
||||
"&7Item: &f" + selectedItem.getType().name() + " x" + selectedItem.getAmount(),
|
||||
"&7Price: &a" + msgManager.formatCurrency(price),
|
||||
"&7You receive: &a" + msgManager.formatCurrency(earnings),
|
||||
"&7Duration: &e" + formatDuration(durationHours),
|
||||
"",
|
||||
"&aClick to create listing!"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
switch (slot) {
|
||||
case PRICE_SLOT -> {
|
||||
// Open number input for price
|
||||
guiManager.openNumberInput(player, newPrice -> {
|
||||
if (newPrice > 0) {
|
||||
this.price = newPrice;
|
||||
}
|
||||
reopenGui();
|
||||
}, price, plugin.getConfigManager().getMinPrice(),
|
||||
plugin.getConfigManager().getMaxPrice(),
|
||||
msgManager.getRaw("gui-titles.number-input"));
|
||||
}
|
||||
case DURATION_SLOT -> {
|
||||
// Cycle through available durations
|
||||
List<Integer> durations = plugin.getConfigManager().getAvailableDurations();
|
||||
if (!durations.isEmpty()) {
|
||||
int currentIndex = durations.indexOf(durationHours);
|
||||
int nextIndex = (currentIndex + 1) % durations.size();
|
||||
durationHours = durations.get(nextIndex);
|
||||
updateDurationElement();
|
||||
updateConfirmButton();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
}
|
||||
}
|
||||
case CONFIRM_SLOT -> {
|
||||
confirmListing(player);
|
||||
}
|
||||
case BACK_SLOT -> {
|
||||
// Go back to item selection
|
||||
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmListing(Player player) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Verify item still exists in player's inventory with sufficient quantity
|
||||
int available = InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||
|
||||
if (available < selectedItem.getAmount()) {
|
||||
if (available < 1) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.item-no-longer-available"));
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.quantity-changed"));
|
||||
}
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
new ItemSelectionGui(plugin, guiManager, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate item again
|
||||
var validation = plugin.getTransactionService().validateItem(selectedItem);
|
||||
if (!validation.isValid()) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages." + validation.getErrorKey()));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the listing
|
||||
int amount = selectedItem.getAmount();
|
||||
ItemStack listItem = selectedItem.clone();
|
||||
|
||||
plugin.getTransactionService().createListingTransaction(
|
||||
player, listItem, amount, price, durationHours
|
||||
).thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (result.isSuccess()) {
|
||||
// Remove items from inventory AFTER successful creation
|
||||
InventoryUtil.removeItems(player, listItem, amount);
|
||||
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-created",
|
||||
"id", String.valueOf(result.getId())));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
player.closeInventory();
|
||||
guiManager.openMainMenu(player);
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages." + result.getErrorKey()));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void reopenGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.create-listing"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowsItemMovement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.CREATE_LISTING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
/**
|
||||
* GUI for viewing and withdrawing pending earnings from sales.
|
||||
*/
|
||||
public class EarningsGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private double pendingAmount;
|
||||
|
||||
private static final int EARNINGS_DISPLAY_SLOT = 13;
|
||||
private static final int WITHDRAW_SLOT = 31;
|
||||
private static final int BACK_SLOT = 49;
|
||||
|
||||
public EarningsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
plugin.getEarningsService().getPendingEarnings(player.getUniqueId())
|
||||
.thenAccept(amount -> {
|
||||
this.pendingAmount = amount;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.earnings"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
// Fill with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Earnings display
|
||||
inventory.setItem(EARNINGS_DISPLAY_SLOT, new ItemBuilder(Material.EMERALD_BLOCK)
|
||||
.name("&a&lPending Earnings")
|
||||
.lore(
|
||||
"",
|
||||
"&7Total: &a" + msgManager.formatCurrency(pendingAmount),
|
||||
"",
|
||||
"&7This is money from your sales",
|
||||
"&7waiting to be withdrawn."
|
||||
)
|
||||
.glow(pendingAmount > 0)
|
||||
.build());
|
||||
|
||||
// Withdraw button
|
||||
if (pendingAmount > 0) {
|
||||
inventory.setItem(WITHDRAW_SLOT, new ItemBuilder(Material.GOLD_BLOCK)
|
||||
.name(msgManager.getButton("withdraw"))
|
||||
.lore(
|
||||
"&7Click to withdraw all earnings",
|
||||
"&7Amount: &a" + msgManager.formatCurrency(pendingAmount)
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
} else {
|
||||
inventory.setItem(WITHDRAW_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name("&cNo Earnings")
|
||||
.lore("&7You have no pending earnings to withdraw.")
|
||||
.build());
|
||||
}
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot == WITHDRAW_SLOT && pendingAmount > 0) {
|
||||
withdrawEarnings(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void withdrawEarnings(Player player) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
plugin.getEarningsService().withdrawAll(player)
|
||||
.thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (result.isSuccess()) {
|
||||
double newBalance = plugin.getEconomyManager().getBalance(player.getUniqueId());
|
||||
player.sendMessage(msgManager.getPrefixed("messages.earnings-withdrawn",
|
||||
java.util.Map.of(
|
||||
"amount", msgManager.formatCurrency(result.getAmount()),
|
||||
"balance", msgManager.formatCurrency(newBalance)
|
||||
)));
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
open(player); // Refresh
|
||||
} else {
|
||||
if ("no_earnings".equals(result.getError())) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.earnings-empty"));
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.earnings-empty"));
|
||||
}
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.EARNINGS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages all GUI instances and player GUI states.
|
||||
* Central hub for opening, closing, and tracking GUI interactions.
|
||||
*/
|
||||
public class GuiManager {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
// Track open GUIs per player
|
||||
private final Map<UUID, MarketGui> openGuis = new HashMap<>();
|
||||
|
||||
// GUI builders/templates
|
||||
private final MainMenuGui mainMenuGui;
|
||||
private final BrowseMarketGui browseMarketGui;
|
||||
private final BrowseAuctionsGui browseAuctionsGui;
|
||||
private final CreateListingGui createListingGui;
|
||||
private final CreateAuctionGui createAuctionGui;
|
||||
private final MyListingsGui myListingsGui;
|
||||
private final MyAuctionsGui myAuctionsGui;
|
||||
private final ClaimGui claimGui;
|
||||
private final EarningsGui earningsGui;
|
||||
private final HelpGui helpGui;
|
||||
private final AdminGui adminGui;
|
||||
|
||||
public GuiManager(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
|
||||
// Initialize GUI handlers
|
||||
this.mainMenuGui = new MainMenuGui(plugin, this);
|
||||
this.browseMarketGui = new BrowseMarketGui(plugin, this);
|
||||
this.browseAuctionsGui = new BrowseAuctionsGui(plugin, this);
|
||||
this.createListingGui = new CreateListingGui(plugin, this);
|
||||
this.createAuctionGui = new CreateAuctionGui(plugin, this);
|
||||
this.myListingsGui = new MyListingsGui(plugin, this);
|
||||
this.myAuctionsGui = new MyAuctionsGui(plugin, this);
|
||||
this.claimGui = new ClaimGui(plugin, this);
|
||||
this.earningsGui = new EarningsGui(plugin, this);
|
||||
this.helpGui = new HelpGui(plugin, this);
|
||||
this.adminGui = new AdminGui(plugin, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the main menu for a player
|
||||
*/
|
||||
public void openMainMenu(Player player) {
|
||||
mainMenuGui.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the browse market GUI
|
||||
*/
|
||||
public void openBrowseMarket(Player player, int page) {
|
||||
browseMarketGui.open(player, page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the browse auctions GUI
|
||||
*/
|
||||
public void openBrowseAuctions(Player player, int page) {
|
||||
browseAuctionsGui.open(player, page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the create listing flow (starts with item selection)
|
||||
*/
|
||||
public void openCreateListing(Player player) {
|
||||
// Open item selection first, which will then open CreateListingGui
|
||||
new ItemSelectionGui(plugin, this, ItemSelectionGui.SelectionMode.LISTING).open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the create auction flow (starts with item selection)
|
||||
*/
|
||||
public void openCreateAuction(Player player) {
|
||||
// Open item selection first, which will then open CreateAuctionGui
|
||||
new ItemSelectionGui(plugin, this, ItemSelectionGui.SelectionMode.AUCTION).open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the player's listings GUI
|
||||
*/
|
||||
public void openMyListings(Player player) {
|
||||
myListingsGui.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the player's auctions GUI
|
||||
*/
|
||||
public void openMyAuctions(Player player) {
|
||||
myAuctionsGui.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the claim GUI
|
||||
*/
|
||||
public void openClaim(Player player) {
|
||||
claimGui.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the earnings GUI
|
||||
*/
|
||||
public void openEarnings(Player player) {
|
||||
earningsGui.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the help GUI
|
||||
*/
|
||||
public void openHelp(Player player) {
|
||||
helpGui.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the admin panel
|
||||
*/
|
||||
public void openAdmin(Player player) {
|
||||
adminGui.open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a number input GUI
|
||||
*/
|
||||
public void openNumberInput(Player player, NumberInputGui.NumberInputCallback callback,
|
||||
double currentValue, double minValue, double maxValue, String title) {
|
||||
new NumberInputGui(plugin, this, callback, currentValue, minValue, maxValue, title).open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a confirmation GUI
|
||||
*/
|
||||
public void openConfirmation(Player player, ConfirmationGui.ConfirmCallback callback,
|
||||
String title, String... infoLines) {
|
||||
new ConfirmationGui(plugin, this, callback, title, infoLines).open(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an open GUI for a player
|
||||
*/
|
||||
public void registerGui(UUID playerUuid, MarketGui gui) {
|
||||
openGuis.put(playerUuid, gui);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a GUI when closed
|
||||
*/
|
||||
public void unregisterGui(UUID playerUuid) {
|
||||
openGuis.remove(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently open GUI for a player
|
||||
*/
|
||||
public MarketGui getOpenGui(UUID playerUuid) {
|
||||
return openGuis.get(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player has a market GUI open
|
||||
*/
|
||||
public boolean hasGuiOpen(UUID playerUuid) {
|
||||
return openGuis.containsKey(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all open market GUIs (used on plugin disable)
|
||||
*/
|
||||
public void closeAllGuis() {
|
||||
for (UUID uuid : openGuis.keySet()) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if (player != null && player.isOnline()) {
|
||||
player.closeInventory();
|
||||
}
|
||||
}
|
||||
openGuis.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin instance
|
||||
*/
|
||||
public CommunityMarket getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
// Getters for GUI handlers
|
||||
public MainMenuGui getMainMenuGui() { return mainMenuGui; }
|
||||
public BrowseMarketGui getBrowseMarketGui() { return browseMarketGui; }
|
||||
public BrowseAuctionsGui getBrowseAuctionsGui() { return browseAuctionsGui; }
|
||||
public CreateListingGui getCreateListingGui() { return createListingGui; }
|
||||
public CreateAuctionGui getCreateAuctionGui() { return createAuctionGui; }
|
||||
public MyListingsGui getMyListingsGui() { return myListingsGui; }
|
||||
public MyAuctionsGui getMyAuctionsGui() { return myAuctionsGui; }
|
||||
public ClaimGui getClaimGui() { return claimGui; }
|
||||
public EarningsGui getEarningsGui() { return earningsGui; }
|
||||
public HelpGui getHelpGui() { return helpGui; }
|
||||
public AdminGui getAdminGui() { return adminGui; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Help GUI showing how to use the marketplace.
|
||||
*/
|
||||
public class HelpGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
|
||||
private static final int BACK_SLOT = 49;
|
||||
|
||||
public HelpGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.help"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Fill with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Help content
|
||||
List<String> helpContent = msgManager.getList("help.content");
|
||||
|
||||
// Main help book
|
||||
inventory.setItem(4, new ItemBuilder(Material.WRITTEN_BOOK)
|
||||
.name(msgManager.getRaw("help.title"))
|
||||
.build());
|
||||
|
||||
// Feature explanations
|
||||
inventory.setItem(19, new ItemBuilder(Material.CHEST)
|
||||
.name("&aBrowse Market")
|
||||
.lore(
|
||||
"&7View all fixed-price listings",
|
||||
"&7from other players.",
|
||||
"",
|
||||
"&7Click on items to purchase them."
|
||||
)
|
||||
.build());
|
||||
|
||||
inventory.setItem(20, new ItemBuilder(Material.GOLD_INGOT)
|
||||
.name("&6Browse Auctions")
|
||||
.lore(
|
||||
"&7View all active auctions.",
|
||||
"",
|
||||
"&eLeft-click &7to place a bid",
|
||||
"&eRight-click &7to buyout (if available)"
|
||||
)
|
||||
.build());
|
||||
|
||||
inventory.setItem(21, new ItemBuilder(Material.WRITABLE_BOOK)
|
||||
.name("&eCreate Listing")
|
||||
.lore(
|
||||
"&7Sell items at a fixed price.",
|
||||
"",
|
||||
"&71. Place your item in the slot",
|
||||
"&72. Set the price",
|
||||
"&73. Choose duration",
|
||||
"&74. Click confirm"
|
||||
)
|
||||
.build());
|
||||
|
||||
inventory.setItem(22, new ItemBuilder(Material.GOLDEN_HELMET)
|
||||
.name("&eCreate Auction")
|
||||
.lore(
|
||||
"&7Auction items to the highest bidder.",
|
||||
"",
|
||||
"&71. Place your item in the slot",
|
||||
"&72. Set starting price",
|
||||
"&73. Optionally set buyout",
|
||||
"&74. Choose duration",
|
||||
"&75. Click confirm"
|
||||
)
|
||||
.build());
|
||||
|
||||
inventory.setItem(23, new ItemBuilder(Material.BOOK)
|
||||
.name("&bMy Listings")
|
||||
.lore(
|
||||
"&7View your active listings.",
|
||||
"",
|
||||
"&7Click on a listing to cancel it.",
|
||||
"&7Cancelled items go to claim storage."
|
||||
)
|
||||
.build());
|
||||
|
||||
inventory.setItem(24, new ItemBuilder(Material.CLOCK)
|
||||
.name("&bMy Auctions")
|
||||
.lore(
|
||||
"&7View your active auctions.",
|
||||
"",
|
||||
"&7You can only cancel auctions",
|
||||
"&7that have no bids yet."
|
||||
)
|
||||
.build());
|
||||
|
||||
inventory.setItem(25, new ItemBuilder(Material.ENDER_CHEST)
|
||||
.name("&dClaim Items")
|
||||
.lore(
|
||||
"&7Collect items waiting for you:",
|
||||
"",
|
||||
"&7- Expired listings",
|
||||
"&7- Cancelled listings",
|
||||
"&7- Won auctions",
|
||||
"&7- Auction refunds"
|
||||
)
|
||||
.build());
|
||||
|
||||
inventory.setItem(31, new ItemBuilder(Material.EMERALD)
|
||||
.name("&aEarnings")
|
||||
.lore(
|
||||
"&7Withdraw money from sales.",
|
||||
"",
|
||||
"&7When you sell something, the money",
|
||||
"&7goes to pending earnings first.",
|
||||
"&7Withdraw it here."
|
||||
)
|
||||
.build());
|
||||
|
||||
// Tax info
|
||||
inventory.setItem(40, new ItemBuilder(Material.GOLD_NUGGET)
|
||||
.name("&6Tax Information")
|
||||
.lore(
|
||||
"&7Market Tax: &f" + plugin.getConfigManager().getMarketTax() + "%",
|
||||
"&7Auction Tax: &f" + plugin.getConfigManager().getAuctionTax() + "%",
|
||||
"",
|
||||
"&7Taxes are deducted from seller earnings."
|
||||
)
|
||||
.build());
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.HELP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||
import pt.henrique.communityMarket.util.SoundUtil;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GUI for selecting an item from the player's inventory to list or auction.
|
||||
* Displays a mirror view of the player's inventory as clickable icons.
|
||||
* This replaces the "drag item into slot" workflow for better UX.
|
||||
*/
|
||||
public class ItemSelectionGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private final SelectionMode mode;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
|
||||
// Maps GUI slot -> player inventory slot for tracking selections
|
||||
private final Map<Integer, Integer> slotMapping = new HashMap<>();
|
||||
|
||||
// Layout: 54-slot chest
|
||||
// Rows 0-3 (slots 0-35): Player inventory mirror
|
||||
// Row 4 (slots 36-44): Hotbar mirror
|
||||
// Row 5 (slots 45-53): Navigation/info bar
|
||||
|
||||
private static final int INFO_SLOT = 49;
|
||||
private static final int BACK_SLOT = 45;
|
||||
|
||||
public enum SelectionMode {
|
||||
LISTING,
|
||||
AUCTION
|
||||
}
|
||||
|
||||
public ItemSelectionGui(CommunityMarket plugin, GuiManager guiManager, SelectionMode mode) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the item selection GUI for a player
|
||||
*/
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String titleKey = mode == SelectionMode.LISTING
|
||||
? "gui-titles.select-item-listing"
|
||||
: "gui-titles.select-item-auction";
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw(titleKey));
|
||||
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
slotMapping.clear();
|
||||
|
||||
// Fill bottom row with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
PlayerInventory playerInv = player.getInventory();
|
||||
|
||||
// Mirror player's main inventory (slots 9-35 in player inv -> slots 0-26 in GUI)
|
||||
for (int i = 9; i < 36; i++) {
|
||||
ItemStack item = playerInv.getItem(i);
|
||||
int guiSlot = i - 9; // Maps 9-35 to 0-26
|
||||
|
||||
if (item != null && !item.getType().isAir()) {
|
||||
// Check if item can be listed
|
||||
if (canBeSelected(item)) {
|
||||
inventory.setItem(guiSlot, createSelectableItem(item));
|
||||
slotMapping.put(guiSlot, i);
|
||||
} else {
|
||||
// Show as blocked/unavailable
|
||||
inventory.setItem(guiSlot, createBlockedItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror hotbar (slots 0-8 in player inv -> slots 27-35 in GUI)
|
||||
for (int i = 0; i < 9; i++) {
|
||||
ItemStack item = playerInv.getItem(i);
|
||||
int guiSlot = 27 + i; // Maps 0-8 to 27-35
|
||||
|
||||
if (item != null && !item.getType().isAir()) {
|
||||
if (canBeSelected(item)) {
|
||||
inventory.setItem(guiSlot, createSelectableItem(item));
|
||||
slotMapping.put(guiSlot, i);
|
||||
} else {
|
||||
inventory.setItem(guiSlot, createBlockedItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info display
|
||||
String modeText = mode == SelectionMode.LISTING ? "&eListing" : "&6Auction";
|
||||
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.PAPER)
|
||||
.name("&fSelect an Item")
|
||||
.lore(
|
||||
"&7Click on an item from your",
|
||||
"&7inventory to create a " + modeText + "&7.",
|
||||
"",
|
||||
"&7Blacklisted items are shown in red."
|
||||
)
|
||||
.build());
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.lore("&7Return to main menu")
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an item can be selected for listing/auction
|
||||
*/
|
||||
private boolean canBeSelected(ItemStack item) {
|
||||
if (item == null || item.getType().isAir()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check material blacklist
|
||||
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check keyword blacklist in name/lore
|
||||
var validation = plugin.getTransactionService().validateItem(item);
|
||||
return validation.isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a display item with selection lore
|
||||
*/
|
||||
private ItemStack createSelectableItem(ItemStack original) {
|
||||
return new ItemBuilder(original.clone())
|
||||
.addLore(java.util.List.of(
|
||||
"",
|
||||
"&a► Click to select"
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blocked/unavailable item display
|
||||
*/
|
||||
private ItemStack createBlockedItem(ItemStack original) {
|
||||
return new ItemBuilder(Material.BARRIER)
|
||||
.name("&c" + original.getType().name())
|
||||
.lore(
|
||||
"&7This item cannot be listed.",
|
||||
"&cBlacklisted or invalid."
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
// Back button
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicked slot contains a selectable item
|
||||
if (slotMapping.containsKey(slot)) {
|
||||
int playerInvSlot = slotMapping.get(slot);
|
||||
ItemStack selectedItem = player.getInventory().getItem(playerInvSlot);
|
||||
|
||||
// Verify item still exists
|
||||
if (selectedItem == null || selectedItem.getType().isAir()) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.invalid-item"));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
open(player); // Refresh
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify item is still valid
|
||||
if (!canBeSelected(selectedItem)) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.blacklisted-item"));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
|
||||
// Check if item is stackable (max stack size > 1)
|
||||
int maxStackSize = selectedItem.getMaxStackSize();
|
||||
int availableQuantity = InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||
|
||||
if (maxStackSize > 1 && availableQuantity > 1) {
|
||||
// Stackable item with more than 1 available - show quantity selector
|
||||
new QuantitySelectGui(plugin, guiManager, quantity -> {
|
||||
if (quantity > 0) {
|
||||
// Proceed with the selected quantity
|
||||
ItemStack itemWithQuantity = selectedItem.clone();
|
||||
itemWithQuantity.setAmount(quantity);
|
||||
|
||||
if (mode == SelectionMode.LISTING) {
|
||||
guiManager.getCreateListingGui().openWithItem(player, playerInvSlot, itemWithQuantity);
|
||||
} else {
|
||||
guiManager.getCreateAuctionGui().openWithItem(player, playerInvSlot, itemWithQuantity);
|
||||
}
|
||||
} else {
|
||||
// User cancelled - go back to item selection
|
||||
open(player);
|
||||
}
|
||||
}, selectedItem, availableQuantity).open(player);
|
||||
} else {
|
||||
// Unstackable item or only 1 available - skip quantity selection
|
||||
ItemStack singleItem = selectedItem.clone();
|
||||
singleItem.setAmount(1);
|
||||
|
||||
if (mode == SelectionMode.LISTING) {
|
||||
guiManager.getCreateListingGui().openWithItem(player, playerInvSlot, singleItem);
|
||||
} else {
|
||||
guiManager.getCreateAuctionGui().openWithItem(player, playerInvSlot, singleItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.ITEM_SELECTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.SoundUtil;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The main menu GUI - the hub of the marketplace.
|
||||
* Contains buttons for all major features.
|
||||
*/
|
||||
public class MainMenuGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
|
||||
// Slot positions for buttons
|
||||
private static final int SLOT_BROWSE_MARKET = 10;
|
||||
private static final int SLOT_BROWSE_AUCTIONS = 12;
|
||||
private static final int SLOT_CREATE_LISTING = 14;
|
||||
private static final int SLOT_CREATE_AUCTION = 16;
|
||||
private static final int SLOT_MY_LISTINGS = 28;
|
||||
private static final int SLOT_MY_AUCTIONS = 30;
|
||||
private static final int SLOT_CLAIM = 32;
|
||||
private static final int SLOT_EARNINGS = 34;
|
||||
private static final int SLOT_HELP = 40;
|
||||
private static final int SLOT_ADMIN = 44;
|
||||
|
||||
public MainMenuGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the main menu for a player
|
||||
*/
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
String title = TextUtil.colorizeToString(
|
||||
plugin.getMessageManager().getRaw("gui-titles.main-menu"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
// Fill background with glass panes
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE)
|
||||
.name(" ")
|
||||
.build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Build menu items asynchronously (to get counts from DB)
|
||||
buildMenuAsync();
|
||||
}
|
||||
|
||||
private void buildMenuAsync() {
|
||||
// Get player stats for lore
|
||||
var listingCountFuture = plugin.getListingService().countPlayerListings(player.getUniqueId());
|
||||
var auctionCountFuture = plugin.getAuctionService().countPlayerAuctions(player.getUniqueId());
|
||||
var claimCountFuture = plugin.getClaimService().countPlayerClaimItems(player.getUniqueId());
|
||||
var earningsFuture = plugin.getEarningsService().getPendingEarnings(player.getUniqueId());
|
||||
|
||||
// When all futures complete, build the GUI
|
||||
listingCountFuture.thenCombine(auctionCountFuture, (listingCount, auctionCount) ->
|
||||
claimCountFuture.thenCombine(earningsFuture, (claimCount, earnings) -> {
|
||||
// Run on main thread
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildMenu(listingCount, auctionCount, claimCount, earnings);
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
playSound(player, plugin.getConfigManager().getClickSound());
|
||||
});
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void buildMenu(int listingCount, int auctionCount, int claimCount, double earnings) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
var configManager = plugin.getConfigManager();
|
||||
|
||||
// Browse Market
|
||||
inventory.setItem(SLOT_BROWSE_MARKET, new ItemBuilder(Material.CHEST)
|
||||
.name(msgManager.getButton("browse-market"))
|
||||
.lore(msgManager.getLore("browse-market"))
|
||||
.build());
|
||||
|
||||
// Browse Auctions
|
||||
inventory.setItem(SLOT_BROWSE_AUCTIONS, new ItemBuilder(Material.GOLD_INGOT)
|
||||
.name(msgManager.getButton("browse-auctions"))
|
||||
.lore(msgManager.getLore("browse-auctions"))
|
||||
.build());
|
||||
|
||||
// Create Listing
|
||||
inventory.setItem(SLOT_CREATE_LISTING, new ItemBuilder(Material.WRITABLE_BOOK)
|
||||
.name(msgManager.getButton("create-listing"))
|
||||
.lore(msgManager.getLore("create-listing", Map.of(
|
||||
"tax", String.valueOf(configManager.getMarketTax())
|
||||
)))
|
||||
.build());
|
||||
|
||||
// Create Auction
|
||||
inventory.setItem(SLOT_CREATE_AUCTION, new ItemBuilder(Material.GOLDEN_HELMET)
|
||||
.name(msgManager.getButton("create-auction"))
|
||||
.lore(msgManager.getLore("create-auction", Map.of(
|
||||
"tax", String.valueOf(configManager.getAuctionTax())
|
||||
)))
|
||||
.build());
|
||||
|
||||
// My Listings
|
||||
inventory.setItem(SLOT_MY_LISTINGS, new ItemBuilder(Material.BOOK)
|
||||
.name(msgManager.getButton("my-listings"))
|
||||
.lore(msgManager.getLore("my-listings", Map.of(
|
||||
"count", String.valueOf(listingCount),
|
||||
"max", String.valueOf(configManager.getMaxListingsPerPlayer())
|
||||
)))
|
||||
.build());
|
||||
|
||||
// My Auctions
|
||||
inventory.setItem(SLOT_MY_AUCTIONS, new ItemBuilder(Material.CLOCK)
|
||||
.name(msgManager.getButton("my-auctions"))
|
||||
.lore(msgManager.getLore("my-auctions", Map.of(
|
||||
"count", String.valueOf(auctionCount),
|
||||
"max", String.valueOf(configManager.getMaxAuctionsPerPlayer())
|
||||
)))
|
||||
.build());
|
||||
|
||||
// Claim Items
|
||||
inventory.setItem(SLOT_CLAIM, new ItemBuilder(Material.ENDER_CHEST)
|
||||
.name(msgManager.getButton("claim-items"))
|
||||
.lore(msgManager.getLore("claim-items", Map.of(
|
||||
"count", String.valueOf(claimCount)
|
||||
)))
|
||||
.glow(claimCount > 0) // Glow if there are items to claim
|
||||
.build());
|
||||
|
||||
// Earnings
|
||||
inventory.setItem(SLOT_EARNINGS, new ItemBuilder(Material.EMERALD)
|
||||
.name(msgManager.getButton("earnings"))
|
||||
.lore(msgManager.getLore("earnings", Map.of(
|
||||
"amount", msgManager.formatCurrency(earnings)
|
||||
)))
|
||||
.glow(earnings > 0) // Glow if there are earnings
|
||||
.build());
|
||||
|
||||
// Help (only if enabled in config)
|
||||
if (configManager.isHelpButtonEnabled()) {
|
||||
inventory.setItem(SLOT_HELP, new ItemBuilder(Material.OAK_SIGN)
|
||||
.name(msgManager.getButton("help"))
|
||||
.lore(msgManager.getLore("help"))
|
||||
.build());
|
||||
}
|
||||
// If help is disabled, the slot stays as glass pane (already filled)
|
||||
|
||||
// Admin button (only if player has permission)
|
||||
if (player.hasPermission("communitymarket.admin")) {
|
||||
inventory.setItem(SLOT_ADMIN, new ItemBuilder(Material.COMMAND_BLOCK)
|
||||
.name(msgManager.getButton("admin"))
|
||||
.lore(msgManager.getLore("admin"))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
if (event.getCurrentItem() == null) return;
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
switch (slot) {
|
||||
case SLOT_BROWSE_MARKET -> {
|
||||
if (player.hasPermission("communitymarket.buy")) {
|
||||
guiManager.openBrowseMarket(player, 0);
|
||||
} else {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
}
|
||||
case SLOT_BROWSE_AUCTIONS -> {
|
||||
if (player.hasPermission("communitymarket.bid")) {
|
||||
guiManager.openBrowseAuctions(player, 0);
|
||||
} else {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
}
|
||||
case SLOT_CREATE_LISTING -> {
|
||||
if (player.hasPermission("communitymarket.sell")) {
|
||||
guiManager.openCreateListing(player);
|
||||
} else {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
}
|
||||
case SLOT_CREATE_AUCTION -> {
|
||||
if (player.hasPermission("communitymarket.auction")) {
|
||||
guiManager.openCreateAuction(player);
|
||||
} else {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
}
|
||||
case SLOT_MY_LISTINGS -> guiManager.openMyListings(player);
|
||||
case SLOT_MY_AUCTIONS -> guiManager.openMyAuctions(player);
|
||||
case SLOT_CLAIM -> {
|
||||
if (player.hasPermission("communitymarket.claim")) {
|
||||
guiManager.openClaim(player);
|
||||
} else {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
}
|
||||
case SLOT_EARNINGS -> {
|
||||
if (player.hasPermission("communitymarket.withdraw")) {
|
||||
guiManager.openEarnings(player);
|
||||
} else {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.no-permission"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
}
|
||||
case SLOT_HELP -> {
|
||||
if (plugin.getConfigManager().isHelpButtonEnabled()) {
|
||||
guiManager.openHelp(player);
|
||||
}
|
||||
}
|
||||
case SLOT_ADMIN -> {
|
||||
if (player.hasPermission("communitymarket.admin")) {
|
||||
guiManager.openAdmin(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.MAIN_MENU;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @org.jetbrains.annotations.NotNull Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
/**
|
||||
* Base interface for all market GUI screens.
|
||||
* Implements InventoryHolder to allow identification of market inventories.
|
||||
*/
|
||||
public interface MarketGui extends InventoryHolder {
|
||||
|
||||
/**
|
||||
* Gets the GUI type identifier
|
||||
*/
|
||||
GuiType getType();
|
||||
|
||||
/**
|
||||
* Handles a click event in this GUI
|
||||
*
|
||||
* @param event The click event
|
||||
*/
|
||||
void handleClick(InventoryClickEvent event);
|
||||
|
||||
/**
|
||||
* Handles a drag event in this GUI
|
||||
*
|
||||
* @param event The drag event
|
||||
*/
|
||||
default void handleDrag(InventoryDragEvent event) {
|
||||
// By default, cancel all drags to prevent item manipulation
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the inventory being closed
|
||||
*
|
||||
* @param event The close event
|
||||
*/
|
||||
default void handleClose(InventoryCloseEvent event) {
|
||||
// Default: do nothing special
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if items can be moved in this GUI
|
||||
* Most GUIs should return false, but create listing/auction GUIs need true
|
||||
*/
|
||||
default boolean allowsItemMovement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum of all GUI types for identification
|
||||
*/
|
||||
enum GuiType {
|
||||
MAIN_MENU,
|
||||
BROWSE_MARKET,
|
||||
BROWSE_AUCTIONS,
|
||||
CREATE_LISTING,
|
||||
CREATE_AUCTION,
|
||||
ITEM_SELECTION,
|
||||
QUANTITY_SELECT,
|
||||
MY_LISTINGS,
|
||||
MY_AUCTIONS,
|
||||
CLAIM,
|
||||
EARNINGS,
|
||||
HELP,
|
||||
ADMIN,
|
||||
ADMIN_LISTINGS,
|
||||
ADMIN_AUCTIONS,
|
||||
NUMBER_INPUT,
|
||||
CONFIRMATION,
|
||||
LISTING_DETAILS,
|
||||
AUCTION_DETAILS,
|
||||
DURATION_SELECT,
|
||||
FILTER_MENU,
|
||||
SORT_MENU
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.Auction;
|
||||
import pt.henrique.communityMarket.service.AuctionService;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GUI for viewing and managing the player's own auctions.
|
||||
* Click to cancel (only if no bids).
|
||||
*/
|
||||
public class MyAuctionsGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private List<Auction> auctions;
|
||||
|
||||
private static final int BACK_SLOT = 49;
|
||||
|
||||
public MyAuctionsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
plugin.getAuctionService().getPlayerAuctions(player.getUniqueId())
|
||||
.thenAccept(loadedAuctions -> {
|
||||
this.auctions = loadedAuctions;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.my-auctions"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
// Fill bottom
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Add auctions
|
||||
for (int i = 0; i < Math.min(auctions.size(), 45); i++) {
|
||||
Auction auction = auctions.get(i);
|
||||
inventory.setItem(i, createAuctionItem(auction));
|
||||
}
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
private ItemStack createAuctionItem(Auction auction) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
ItemStack display = auction.getItem().clone();
|
||||
|
||||
Duration remaining = Duration.between(Instant.now(), auction.getEndsAt());
|
||||
String ends = TextUtil.formatDuration(remaining);
|
||||
|
||||
String bidder = auction.getHighestBidderName() != null ? auction.getHighestBidderName() : "&7None";
|
||||
String currentBid = auction.getBidCount() > 0
|
||||
? msgManager.formatCurrency(auction.getCurrentBid())
|
||||
: "&7No bids";
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("");
|
||||
lore.addAll(msgManager.getLore("my-auction-info", Map.of(
|
||||
"start_price", msgManager.formatCurrency(auction.getStartPrice()),
|
||||
"current_bid", currentBid,
|
||||
"bidder", bidder,
|
||||
"bid_count", String.valueOf(auction.getBidCount()),
|
||||
"ends", ends
|
||||
)));
|
||||
|
||||
// Show if cancellable
|
||||
if (auction.getBidCount() == 0) {
|
||||
lore.add("");
|
||||
lore.add("&aClick to cancel");
|
||||
} else {
|
||||
lore.add("");
|
||||
lore.add("&cCannot cancel - has bids");
|
||||
}
|
||||
|
||||
return new ItemBuilder(display)
|
||||
.addLore(lore)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on auction to cancel
|
||||
if (slot >= 0 && slot < 45 && slot < auctions.size()) {
|
||||
Auction auction = auctions.get(slot);
|
||||
|
||||
if (auction.getBidCount() > 0) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.auction-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
confirmCancel(player, auction);
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmCancel(Player player, Auction auction) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String[] info = {
|
||||
"&7Item: &f" + auction.getItem().getType().name(),
|
||||
"&7Starting Price: &a" + msgManager.formatCurrency(auction.getStartPrice()),
|
||||
"",
|
||||
"&cThis will cancel your auction.",
|
||||
"&cThe item will be moved to claim storage."
|
||||
};
|
||||
|
||||
guiManager.openConfirmation(player, confirmed -> {
|
||||
if (confirmed) {
|
||||
plugin.getAuctionService().cancelAuction(auction.getId(), player.getUniqueId(), false)
|
||||
.thenAccept(result -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (result == AuctionService.CancelResult.SUCCESS) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-cancelled"));
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
} else if (result == AuctionService.CancelResult.HAS_BIDS) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.auction-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
open(player); // Refresh
|
||||
});
|
||||
});
|
||||
} else {
|
||||
open(player);
|
||||
}
|
||||
}, msgManager.getRaw("gui-titles.confirm-cancel"), info);
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.MY_AUCTIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.Listing;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GUI for viewing and managing the player's own listings.
|
||||
* Click on a listing to cancel it.
|
||||
*/
|
||||
public class MyListingsGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private List<Listing> listings;
|
||||
|
||||
private static final int BACK_SLOT = 49;
|
||||
|
||||
public MyListingsGui(CommunityMarket plugin, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
plugin.getListingService().getPlayerListings(player.getUniqueId())
|
||||
.thenAccept(loadedListings -> {
|
||||
this.listings = loadedListings;
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.my-listings"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
// Fill bottom
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 45; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Add listings
|
||||
for (int i = 0; i < Math.min(listings.size(), 45); i++) {
|
||||
Listing listing = listings.get(i);
|
||||
inventory.setItem(i, createListingItem(listing));
|
||||
}
|
||||
|
||||
// Back button
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.BARRIER)
|
||||
.name(msgManager.getButton("back"))
|
||||
.build());
|
||||
}
|
||||
|
||||
private ItemStack createListingItem(Listing listing) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
ItemStack display = listing.getItem().clone();
|
||||
display.setAmount(listing.getAmount());
|
||||
|
||||
String expires;
|
||||
if (listing.getExpiresAt() != null) {
|
||||
Duration remaining = Duration.between(Instant.now(), listing.getExpiresAt());
|
||||
expires = TextUtil.formatDuration(remaining);
|
||||
} else {
|
||||
expires = "Never";
|
||||
}
|
||||
|
||||
String created = TextUtil.formatDuration(
|
||||
Duration.between(listing.getCreatedAt(), Instant.now())) + " ago";
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("");
|
||||
lore.addAll(msgManager.getLore("my-listing-info", Map.of(
|
||||
"price", msgManager.formatCurrency(listing.getPrice()),
|
||||
"amount", String.valueOf(listing.getAmount()),
|
||||
"created", created,
|
||||
"expires", expires
|
||||
)));
|
||||
|
||||
return new ItemBuilder(display)
|
||||
.addLore(lore)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
if (slot == BACK_SLOT) {
|
||||
guiManager.openMainMenu(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on listing to cancel
|
||||
if (slot >= 0 && slot < 45 && slot < listings.size()) {
|
||||
Listing listing = listings.get(slot);
|
||||
confirmCancel(player, listing);
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmCancel(Player player, Listing listing) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
String[] info = {
|
||||
"&7Item: &f" + listing.getItem().getType().name() + " x" + listing.getAmount(),
|
||||
"&7Price: &a" + msgManager.formatCurrency(listing.getPrice()),
|
||||
"",
|
||||
"&cThis will cancel your listing.",
|
||||
"&cThe item will be moved to claim storage."
|
||||
};
|
||||
|
||||
guiManager.openConfirmation(player, confirmed -> {
|
||||
if (confirmed) {
|
||||
plugin.getListingService().cancelListing(listing.getId(), player.getUniqueId(), false)
|
||||
.thenAccept(success -> {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (success) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-cancelled"));
|
||||
playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
} else {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.listing-not-found"));
|
||||
playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
}
|
||||
open(player); // Refresh
|
||||
});
|
||||
});
|
||||
} else {
|
||||
open(player);
|
||||
}
|
||||
}, msgManager.getRaw("gui-titles.confirm-cancel"), info);
|
||||
}
|
||||
|
||||
private void playSound(Player player, String soundName) {
|
||||
pt.henrique.communityMarket.util.SoundUtil.playSound(player, soundName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.MY_LISTINGS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.SoundUtil;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
/**
|
||||
* GUI-based numeric input to replace chat input.
|
||||
* Players can increment/decrement values using buttons.
|
||||
*
|
||||
* Layout (54-slot chest):
|
||||
* ┌─────────────────────────────────────────────────────┐
|
||||
* │ . . . . . . . . . │ Row 0: Empty │
|
||||
* │ . . . . [DISPLAY] . . . │ Row 1: Value │
|
||||
* │ . . . . . . . . . │ Row 2: Empty │
|
||||
* │ -1K -100 -10 -1 . +1 +10 +100 +1K│ Row 3: Adjust │
|
||||
* │ . MIN . . . . . MAX . │ Row 4: Presets│
|
||||
* │ BACK . . . CONFIRM . . . │ Row 5: Actions│
|
||||
* └─────────────────────────────────────────────────────┘
|
||||
*/
|
||||
public class NumberInputGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private final NumberInputCallback callback;
|
||||
private final double minValue;
|
||||
private final double maxValue;
|
||||
private final String title;
|
||||
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private double currentValue;
|
||||
|
||||
// ==================== LAYOUT CONSTANTS ====================
|
||||
// Row 1: Current value display (center)
|
||||
private static final int DISPLAY_SLOT = 13;
|
||||
|
||||
// Row 3: Decrease buttons (LEFT side) - slots 27-30
|
||||
private static final int SUB_1000_SLOT = 27; // -1,000
|
||||
private static final int SUB_100_SLOT = 28; // -100
|
||||
private static final int SUB_10_SLOT = 29; // -10
|
||||
private static final int SUB_1_SLOT = 30; // -1
|
||||
|
||||
// Row 3: Increase buttons (RIGHT side) - slots 32-35
|
||||
private static final int ADD_1_SLOT = 32; // +1
|
||||
private static final int ADD_10_SLOT = 33; // +10
|
||||
private static final int ADD_100_SLOT = 34; // +100
|
||||
private static final int ADD_1000_SLOT = 35; // +1,000
|
||||
|
||||
// Row 4: Preset buttons
|
||||
private static final int SET_MIN_SLOT = 37; // Set to minimum
|
||||
private static final int SET_MAX_SLOT = 43; // Set to maximum
|
||||
|
||||
// Row 5: Action buttons
|
||||
private static final int BACK_SLOT = 45; // Cancel/Back (bottom-left)
|
||||
private static final int CONFIRM_SLOT = 49; // Confirm (bottom-center)
|
||||
// ===========================================================
|
||||
|
||||
@FunctionalInterface
|
||||
public interface NumberInputCallback {
|
||||
void onComplete(double value);
|
||||
}
|
||||
|
||||
public NumberInputGui(CommunityMarket plugin, GuiManager guiManager,
|
||||
NumberInputCallback callback, double currentValue,
|
||||
double minValue, double maxValue, String title) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
this.callback = callback;
|
||||
this.currentValue = currentValue;
|
||||
this.minValue = minValue;
|
||||
this.maxValue = maxValue;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
inventory = Bukkit.createInventory(this, 54, TextUtil.colorizeToString(title));
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Fill all slots with glass panes
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Current value display (center, row 1)
|
||||
updateDisplay();
|
||||
|
||||
// === DECREASE BUTTONS (LEFT SIDE) ===
|
||||
inventory.setItem(SUB_1000_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-1,000")
|
||||
.lore("&7Click: &c-1,000", "&7Shift-click: &c-10,000")
|
||||
.build());
|
||||
|
||||
inventory.setItem(SUB_100_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-100")
|
||||
.lore("&7Click: &c-100", "&7Shift-click: &c-1,000")
|
||||
.build());
|
||||
|
||||
inventory.setItem(SUB_10_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-10")
|
||||
.lore("&7Click: &c-10", "&7Shift-click: &c-100")
|
||||
.build());
|
||||
|
||||
inventory.setItem(SUB_1_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-1")
|
||||
.lore("&7Click: &c-1", "&7Shift-click: &c-10")
|
||||
.build());
|
||||
|
||||
// === INCREASE BUTTONS (RIGHT SIDE) ===
|
||||
inventory.setItem(ADD_1_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+1")
|
||||
.lore("&7Click: &a+1", "&7Shift-click: &a+10")
|
||||
.build());
|
||||
|
||||
inventory.setItem(ADD_10_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+10")
|
||||
.lore("&7Click: &a+10", "&7Shift-click: &a+100")
|
||||
.build());
|
||||
|
||||
inventory.setItem(ADD_100_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+100")
|
||||
.lore("&7Click: &a+100", "&7Shift-click: &a+1,000")
|
||||
.build());
|
||||
|
||||
inventory.setItem(ADD_1000_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+1,000")
|
||||
.lore("&7Click: &a+1,000", "&7Shift-click: &a+10,000")
|
||||
.build());
|
||||
|
||||
// === PRESET BUTTONS ===
|
||||
inventory.setItem(SET_MIN_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||
.name("&6Set Minimum")
|
||||
.lore("&7Set to: &f" + msgManager.formatCurrency(minValue))
|
||||
.build());
|
||||
|
||||
inventory.setItem(SET_MAX_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||
.name("&6Set Maximum")
|
||||
.lore("&7Set to: &f" + msgManager.formatCurrency(maxValue))
|
||||
.build());
|
||||
|
||||
// === ACTION BUTTONS ===
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||
.name(msgManager.getButton("cancel"))
|
||||
.lore("&7Cancel and go back")
|
||||
.build());
|
||||
|
||||
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||
.name(msgManager.getButton("confirm"))
|
||||
.lore("&7Confirm: &a" + msgManager.formatCurrency(currentValue))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void updateDisplay() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
inventory.setItem(DISPLAY_SLOT, new ItemBuilder(Material.GOLD_INGOT)
|
||||
.name("&6&l" + msgManager.formatCurrency(currentValue))
|
||||
.lore(
|
||||
"",
|
||||
"&7Minimum: &f" + msgManager.formatCurrency(minValue),
|
||||
"&7Maximum: &f" + msgManager.formatCurrency(maxValue),
|
||||
"",
|
||||
"&eUse buttons to adjust"
|
||||
)
|
||||
.glow()
|
||||
.build());
|
||||
|
||||
// Also update confirm button lore
|
||||
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||
.name(plugin.getMessageManager().getButton("confirm"))
|
||||
.lore("&7Confirm: &a" + msgManager.formatCurrency(currentValue))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
boolean shift = event.isShiftClick();
|
||||
|
||||
// Multiplier for shift-click (10x)
|
||||
double multiplier = shift ? 10 : 1;
|
||||
|
||||
switch (slot) {
|
||||
// Decrease buttons
|
||||
case SUB_1000_SLOT -> adjustValue(-1000 * multiplier);
|
||||
case SUB_100_SLOT -> adjustValue(-100 * multiplier);
|
||||
case SUB_10_SLOT -> adjustValue(-10 * multiplier);
|
||||
case SUB_1_SLOT -> adjustValue(-1 * multiplier);
|
||||
|
||||
// Increase buttons
|
||||
case ADD_1_SLOT -> adjustValue(1 * multiplier);
|
||||
case ADD_10_SLOT -> adjustValue(10 * multiplier);
|
||||
case ADD_100_SLOT -> adjustValue(100 * multiplier);
|
||||
case ADD_1000_SLOT -> adjustValue(1000 * multiplier);
|
||||
|
||||
// Preset buttons
|
||||
case SET_MIN_SLOT -> {
|
||||
currentValue = minValue;
|
||||
updateDisplay();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
}
|
||||
case SET_MAX_SLOT -> {
|
||||
currentValue = maxValue;
|
||||
updateDisplay();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
case CONFIRM_SLOT -> {
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
player.closeInventory();
|
||||
callback.onComplete(currentValue);
|
||||
}
|
||||
case BACK_SLOT -> {
|
||||
player.closeInventory();
|
||||
callback.onComplete(-1); // Signal cancellation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustValue(double delta) {
|
||||
double newValue = currentValue + delta;
|
||||
|
||||
// Clamp to min/max
|
||||
newValue = Math.max(minValue, Math.min(maxValue, newValue));
|
||||
|
||||
// Round to 2 decimal places
|
||||
currentValue = Math.round(newValue * 100.0) / 100.0;
|
||||
|
||||
updateDisplay();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound(), 0.3f, 1.2f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.NUMBER_INPUT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package pt.henrique.communityMarket.gui;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.util.ItemBuilder;
|
||||
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||
import pt.henrique.communityMarket.util.SoundUtil;
|
||||
import pt.henrique.communityMarket.util.TextUtil;
|
||||
|
||||
/**
|
||||
* GUI for selecting quantity of an item to list or auction.
|
||||
* Only shown for stackable items; unstackable items skip this step.
|
||||
*
|
||||
* Layout (54-slot chest):
|
||||
* ┌─────────────────────────────────────────────────────┐
|
||||
* │ . . . . INFO . . . . │ Row 0 │
|
||||
* │ . . . . ITEM . . . . │ Row 1: Item │
|
||||
* │ . . . . DISPLAY . . . . │ Row 2: Qty │
|
||||
* │ -64 -32 -16 -1 . +1 +16 +32 +64 │ Row 3: Adjust│
|
||||
* │ . MIN . . . . . MAX . │ Row 4: Preset│
|
||||
* │ BACK . . . CONFIRM . . . .│ Row 5: Action│
|
||||
* └─────────────────────────────────────────────────────┘
|
||||
*/
|
||||
public class QuantitySelectGui implements MarketGui {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
private final GuiManager guiManager;
|
||||
private final QuantityCallback callback;
|
||||
private final ItemStack selectedItem;
|
||||
private final int maxQuantity;
|
||||
|
||||
private Inventory inventory;
|
||||
private Player player;
|
||||
private int currentQuantity;
|
||||
|
||||
// ==================== LAYOUT CONSTANTS ====================
|
||||
private static final int INFO_SLOT = 4; // Top center info
|
||||
private static final int ITEM_DISPLAY_SLOT = 13; // Item preview
|
||||
private static final int QUANTITY_DISPLAY_SLOT = 22; // Current quantity display
|
||||
|
||||
// Row 3: Decrease buttons (LEFT side) - slots 27-30
|
||||
private static final int SUB_64_SLOT = 27; // -64
|
||||
private static final int SUB_32_SLOT = 28; // -32
|
||||
private static final int SUB_16_SLOT = 29; // -16
|
||||
private static final int SUB_1_SLOT = 30; // -1
|
||||
|
||||
// Row 3: Increase buttons (RIGHT side) - slots 32-35
|
||||
private static final int ADD_1_SLOT = 32; // +1
|
||||
private static final int ADD_16_SLOT = 33; // +16
|
||||
private static final int ADD_32_SLOT = 34; // +32
|
||||
private static final int ADD_64_SLOT = 35; // +64
|
||||
|
||||
// Row 4: Preset buttons
|
||||
private static final int SET_MIN_SLOT = 37; // Set to 1
|
||||
private static final int SET_MAX_SLOT = 43; // Set to max
|
||||
|
||||
// Row 5: Action buttons
|
||||
private static final int BACK_SLOT = 45; // Cancel/Back
|
||||
private static final int CONFIRM_SLOT = 49; // Confirm
|
||||
// ===========================================================
|
||||
|
||||
@FunctionalInterface
|
||||
public interface QuantityCallback {
|
||||
void onComplete(int quantity);
|
||||
}
|
||||
|
||||
public QuantitySelectGui(CommunityMarket plugin, GuiManager guiManager,
|
||||
QuantityCallback callback, ItemStack selectedItem,
|
||||
int maxQuantity) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
this.callback = callback;
|
||||
this.selectedItem = selectedItem;
|
||||
this.maxQuantity = maxQuantity;
|
||||
this.currentQuantity = Math.min(selectedItem.getAmount(), maxQuantity);
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
|
||||
var msgManager = plugin.getMessageManager();
|
||||
String title = TextUtil.colorizeToString(msgManager.getRaw("gui-titles.quantity-select"));
|
||||
inventory = Bukkit.createInventory(this, 54, title);
|
||||
|
||||
buildGui();
|
||||
player.openInventory(inventory);
|
||||
guiManager.registerGui(player.getUniqueId(), this);
|
||||
}
|
||||
|
||||
private void buildGui() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Fill with glass
|
||||
ItemStack filler = new ItemBuilder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
|
||||
for (int i = 0; i < 54; i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
|
||||
// Info panel
|
||||
inventory.setItem(INFO_SLOT, new ItemBuilder(Material.OAK_SIGN)
|
||||
.name("&6&lSelect Quantity")
|
||||
.lore(
|
||||
"&7Choose how many items",
|
||||
"&7you want to sell.",
|
||||
"",
|
||||
"&7Available: &f" + maxQuantity
|
||||
)
|
||||
.build());
|
||||
|
||||
// Item preview
|
||||
ItemStack displayItem = selectedItem.clone();
|
||||
displayItem.setAmount(currentQuantity);
|
||||
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(displayItem)
|
||||
.addLore(java.util.List.of(
|
||||
"",
|
||||
"&7Selected: &f" + currentQuantity
|
||||
))
|
||||
.build());
|
||||
|
||||
// Quantity display
|
||||
updateQuantityDisplay();
|
||||
|
||||
// === DECREASE BUTTONS (LEFT SIDE) ===
|
||||
inventory.setItem(SUB_64_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-64")
|
||||
.lore("&7Click: &c-64")
|
||||
.amount(64)
|
||||
.build());
|
||||
|
||||
inventory.setItem(SUB_32_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-32")
|
||||
.lore("&7Click: &c-32")
|
||||
.amount(32)
|
||||
.build());
|
||||
|
||||
inventory.setItem(SUB_16_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-16")
|
||||
.lore("&7Click: &c-16")
|
||||
.amount(16)
|
||||
.build());
|
||||
|
||||
inventory.setItem(SUB_1_SLOT, new ItemBuilder(Material.RED_STAINED_GLASS_PANE)
|
||||
.name("&c-1")
|
||||
.lore("&7Click: &c-1")
|
||||
.build());
|
||||
|
||||
// === INCREASE BUTTONS (RIGHT SIDE) ===
|
||||
inventory.setItem(ADD_1_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+1")
|
||||
.lore("&7Click: &a+1")
|
||||
.build());
|
||||
|
||||
inventory.setItem(ADD_16_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+16")
|
||||
.lore("&7Click: &a+16")
|
||||
.amount(16)
|
||||
.build());
|
||||
|
||||
inventory.setItem(ADD_32_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+32")
|
||||
.lore("&7Click: &a+32")
|
||||
.amount(32)
|
||||
.build());
|
||||
|
||||
inventory.setItem(ADD_64_SLOT, new ItemBuilder(Material.LIME_STAINED_GLASS_PANE)
|
||||
.name("&a+64")
|
||||
.lore("&7Click: &a+64")
|
||||
.amount(64)
|
||||
.build());
|
||||
|
||||
// === PRESET BUTTONS ===
|
||||
inventory.setItem(SET_MIN_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||
.name("&6Set Minimum")
|
||||
.lore("&7Set to: &f1")
|
||||
.build());
|
||||
|
||||
inventory.setItem(SET_MAX_SLOT, new ItemBuilder(Material.ORANGE_STAINED_GLASS_PANE)
|
||||
.name("&6Set Maximum")
|
||||
.lore("&7Set to: &f" + maxQuantity)
|
||||
.build());
|
||||
|
||||
// === ACTION BUTTONS ===
|
||||
inventory.setItem(BACK_SLOT, new ItemBuilder(Material.RED_WOOL)
|
||||
.name(msgManager.getButton("back"))
|
||||
.lore("&7Return to item selection")
|
||||
.build());
|
||||
|
||||
updateConfirmButton();
|
||||
}
|
||||
|
||||
private void updateQuantityDisplay() {
|
||||
inventory.setItem(QUANTITY_DISPLAY_SLOT, new ItemBuilder(Material.PAPER)
|
||||
.name("&6&lQuantity: " + currentQuantity)
|
||||
.lore(
|
||||
"",
|
||||
"&7Minimum: &f1",
|
||||
"&7Maximum: &f" + maxQuantity,
|
||||
"",
|
||||
"&eUse buttons to adjust"
|
||||
)
|
||||
.amount(Math.min(currentQuantity, 64))
|
||||
.glow()
|
||||
.build());
|
||||
|
||||
// Update item display amount
|
||||
ItemStack displayItem = selectedItem.clone();
|
||||
displayItem.setAmount(Math.min(currentQuantity, 64));
|
||||
inventory.setItem(ITEM_DISPLAY_SLOT, new ItemBuilder(displayItem)
|
||||
.addLore(java.util.List.of(
|
||||
"",
|
||||
"&7Selected: &f" + currentQuantity
|
||||
))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void updateConfirmButton() {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
inventory.setItem(CONFIRM_SLOT, new ItemBuilder(Material.LIME_WOOL)
|
||||
.name(msgManager.getButton("confirm"))
|
||||
.lore("&7Quantity: &a" + currentQuantity)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
switch (slot) {
|
||||
// Decrease buttons
|
||||
case SUB_64_SLOT -> adjustQuantity(-64);
|
||||
case SUB_32_SLOT -> adjustQuantity(-32);
|
||||
case SUB_16_SLOT -> adjustQuantity(-16);
|
||||
case SUB_1_SLOT -> adjustQuantity(-1);
|
||||
|
||||
// Increase buttons
|
||||
case ADD_1_SLOT -> adjustQuantity(1);
|
||||
case ADD_16_SLOT -> adjustQuantity(16);
|
||||
case ADD_32_SLOT -> adjustQuantity(32);
|
||||
case ADD_64_SLOT -> adjustQuantity(64);
|
||||
|
||||
// Preset buttons
|
||||
case SET_MIN_SLOT -> {
|
||||
currentQuantity = 1;
|
||||
updateQuantityDisplay();
|
||||
updateConfirmButton();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
}
|
||||
case SET_MAX_SLOT -> {
|
||||
currentQuantity = maxQuantity;
|
||||
updateQuantityDisplay();
|
||||
updateConfirmButton();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound());
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
case CONFIRM_SLOT -> {
|
||||
// Validate quantity is still valid
|
||||
int currentAvailable = countAvailableItems(player);
|
||||
if (currentQuantity > currentAvailable) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.quantity-changed"));
|
||||
currentQuantity = Math.min(currentQuantity, currentAvailable);
|
||||
if (currentQuantity < 1) {
|
||||
player.sendMessage(plugin.getMessageManager().getPrefixed("messages.item-no-longer-available"));
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
player.closeInventory();
|
||||
return;
|
||||
}
|
||||
updateQuantityDisplay();
|
||||
updateConfirmButton();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getErrorSound());
|
||||
return;
|
||||
}
|
||||
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getSuccessSound());
|
||||
player.closeInventory();
|
||||
callback.onComplete(currentQuantity);
|
||||
}
|
||||
case BACK_SLOT -> {
|
||||
player.closeInventory();
|
||||
callback.onComplete(-1); // Signal cancellation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustQuantity(int delta) {
|
||||
int newQuantity = currentQuantity + delta;
|
||||
|
||||
// Clamp to 1..maxQuantity
|
||||
newQuantity = Math.max(1, Math.min(maxQuantity, newQuantity));
|
||||
|
||||
currentQuantity = newQuantity;
|
||||
updateQuantityDisplay();
|
||||
updateConfirmButton();
|
||||
SoundUtil.playSound(player, plugin.getConfigManager().getClickSound(), 0.3f, 1.2f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how many of the selected item the player currently has.
|
||||
* Uses strict item comparison including all metadata.
|
||||
*/
|
||||
private int countAvailableItems(Player player) {
|
||||
return InventoryUtil.countSimilarItems(player.getInventory(), selectedItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiType getType() {
|
||||
return GuiType.QUANTITY_SELECT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package pt.henrique.communityMarket.listener;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.*;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.gui.MarketGui;
|
||||
|
||||
/**
|
||||
* Listener for all GUI-related inventory events.
|
||||
* Handles clicks, drags, and closes for market GUIs.
|
||||
* Implements security measures to prevent item duplication exploits.
|
||||
*/
|
||||
public class GuiListener implements Listener {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public GuiListener(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all click events in market GUIs.
|
||||
* Security: Validates all click actions and prevents item manipulation.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)) return;
|
||||
|
||||
Inventory topInventory = event.getView().getTopInventory();
|
||||
InventoryHolder holder = topInventory.getHolder();
|
||||
|
||||
// Check if this is a market GUI
|
||||
if (!(holder instanceof MarketGui marketGui)) return;
|
||||
|
||||
// Get the clicked inventory
|
||||
Inventory clickedInventory = event.getClickedInventory();
|
||||
|
||||
// Handle different GUI types
|
||||
if (marketGui.allowsItemMovement()) {
|
||||
// GUIs like CreateListing allow item placement in specific slots
|
||||
handleItemMovementGui(event, marketGui);
|
||||
} else {
|
||||
// Standard GUIs don't allow any item movement
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
// Always delegate to the GUI's click handler
|
||||
marketGui.handleClick(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles GUIs that allow item movement (like create listing/auction).
|
||||
* Only allows items in designated slots.
|
||||
*/
|
||||
private void handleItemMovementGui(InventoryClickEvent event, MarketGui marketGui) {
|
||||
// Cancel by default - the GUI handler will un-cancel for specific slots
|
||||
ClickType clickType = event.getClick();
|
||||
|
||||
// Block potentially exploitative click types
|
||||
switch (clickType) {
|
||||
case DOUBLE_CLICK -> {
|
||||
// Prevent collecting items from market GUI via double-click
|
||||
event.setCancelled(true);
|
||||
}
|
||||
case NUMBER_KEY -> {
|
||||
// Block number key swaps to prevent inventory tricks
|
||||
if (event.getRawSlot() < event.getView().getTopInventory().getSize()) {
|
||||
// Allow in player inventory, cancel in market GUI
|
||||
// The specific GUI handler will manage this
|
||||
}
|
||||
}
|
||||
case SWAP_OFFHAND -> {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
default -> {
|
||||
// Let the GUI handler decide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles drag events - prevents dragging items across market GUIs.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player)) return;
|
||||
|
||||
Inventory topInventory = event.getView().getTopInventory();
|
||||
InventoryHolder holder = topInventory.getHolder();
|
||||
|
||||
if (!(holder instanceof MarketGui marketGui)) return;
|
||||
|
||||
// Check if any dragged slots are in the top inventory
|
||||
int topSize = topInventory.getSize();
|
||||
boolean affectsTop = event.getRawSlots().stream()
|
||||
.anyMatch(slot -> slot < topSize);
|
||||
|
||||
if (affectsTop) {
|
||||
// Delegate to GUI handler (most will cancel)
|
||||
marketGui.handleDrag(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles inventory close events.
|
||||
* Ensures items are returned and GUI state is cleaned up.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (!(event.getPlayer() instanceof Player player)) return;
|
||||
|
||||
Inventory inventory = event.getInventory();
|
||||
InventoryHolder holder = inventory.getHolder();
|
||||
|
||||
if (!(holder instanceof MarketGui marketGui)) return;
|
||||
|
||||
// Notify the GUI of the close
|
||||
marketGui.handleClose(event);
|
||||
|
||||
// Unregister from GUI manager
|
||||
plugin.getGuiManager().unregisterGui(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents moving items out of market GUIs via shift-click from player inventory.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onInventoryMoveItem(InventoryMoveItemEvent event) {
|
||||
// This handles hopper/dropper interactions - cancel if involves market GUI
|
||||
if (event.getSource().getHolder() instanceof MarketGui ||
|
||||
event.getDestination().getHolder() instanceof MarketGui) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package pt.henrique.communityMarket.listener;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
/**
|
||||
* Listener for player join/quit events.
|
||||
* Notifies players of pending items and earnings on join.
|
||||
*/
|
||||
public class PlayerListener implements Listener {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public PlayerListener(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies players of pending claims and earnings when they join.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// Delay notification slightly to ensure player is fully loaded
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
|
||||
notifyPendingItems(player);
|
||||
}, 40L); // 2 second delay
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up when a player quits.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
// Close any open market GUI to prevent issues
|
||||
if (plugin.getGuiManager().hasGuiOpen(player.getUniqueId())) {
|
||||
player.closeInventory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies a player about pending claims and earnings.
|
||||
*/
|
||||
private void notifyPendingItems(Player player) {
|
||||
var msgManager = plugin.getMessageManager();
|
||||
|
||||
// Check for pending claim items
|
||||
plugin.getClaimService().countPlayerClaimItems(player.getUniqueId())
|
||||
.thenAccept(claimCount -> {
|
||||
if (claimCount > 0) {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.claim-items",
|
||||
java.util.Map.of("count", String.valueOf(claimCount))));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check for pending earnings
|
||||
plugin.getEarningsService().getPendingEarnings(player.getUniqueId())
|
||||
.thenAccept(earnings -> {
|
||||
if (earnings > 0) {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
player.sendMessage(msgManager.getPrefixed("messages.earnings-balance",
|
||||
"amount", msgManager.formatCurrency(earnings)));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package pt.henrique.communityMarket.model;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents an auction listing
|
||||
*/
|
||||
public class Auction {
|
||||
|
||||
private int id;
|
||||
private UUID sellerUuid;
|
||||
private String sellerName;
|
||||
private ItemStack item;
|
||||
private double startPrice;
|
||||
private double currentBid;
|
||||
private UUID highestBidderUuid;
|
||||
private String highestBidderName;
|
||||
private int bidCount;
|
||||
private Double buyoutPrice; // nullable
|
||||
private Instant createdAt;
|
||||
private Instant endsAt;
|
||||
private int extensionCount;
|
||||
private AuctionStatus status;
|
||||
|
||||
public Auction() {
|
||||
this.status = AuctionStatus.ACTIVE;
|
||||
this.createdAt = Instant.now();
|
||||
this.bidCount = 0;
|
||||
this.extensionCount = 0;
|
||||
this.currentBid = 0;
|
||||
}
|
||||
|
||||
public Auction(UUID sellerUuid, String sellerName, ItemStack item, double startPrice, Double buyoutPrice, Instant endsAt) {
|
||||
this();
|
||||
this.sellerUuid = sellerUuid;
|
||||
this.sellerName = sellerName;
|
||||
this.item = item;
|
||||
this.startPrice = startPrice;
|
||||
this.buyoutPrice = buyoutPrice;
|
||||
this.endsAt = endsAt;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UUID getSellerUuid() {
|
||||
return sellerUuid;
|
||||
}
|
||||
|
||||
public void setSellerUuid(UUID sellerUuid) {
|
||||
this.sellerUuid = sellerUuid;
|
||||
}
|
||||
|
||||
public String getSellerName() {
|
||||
return sellerName;
|
||||
}
|
||||
|
||||
public void setSellerName(String sellerName) {
|
||||
this.sellerName = sellerName;
|
||||
}
|
||||
|
||||
public ItemStack getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public void setItem(ItemStack item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public double getStartPrice() {
|
||||
return startPrice;
|
||||
}
|
||||
|
||||
public void setStartPrice(double startPrice) {
|
||||
this.startPrice = startPrice;
|
||||
}
|
||||
|
||||
public double getCurrentBid() {
|
||||
return currentBid;
|
||||
}
|
||||
|
||||
public void setCurrentBid(double currentBid) {
|
||||
this.currentBid = currentBid;
|
||||
}
|
||||
|
||||
public UUID getHighestBidderUuid() {
|
||||
return highestBidderUuid;
|
||||
}
|
||||
|
||||
public void setHighestBidderUuid(UUID highestBidderUuid) {
|
||||
this.highestBidderUuid = highestBidderUuid;
|
||||
}
|
||||
|
||||
public String getHighestBidderName() {
|
||||
return highestBidderName;
|
||||
}
|
||||
|
||||
public void setHighestBidderName(String highestBidderName) {
|
||||
this.highestBidderName = highestBidderName;
|
||||
}
|
||||
|
||||
public int getBidCount() {
|
||||
return bidCount;
|
||||
}
|
||||
|
||||
public void setBidCount(int bidCount) {
|
||||
this.bidCount = bidCount;
|
||||
}
|
||||
|
||||
public Double getBuyoutPrice() {
|
||||
return buyoutPrice;
|
||||
}
|
||||
|
||||
public void setBuyoutPrice(Double buyoutPrice) {
|
||||
this.buyoutPrice = buyoutPrice;
|
||||
}
|
||||
|
||||
public boolean hasBuyout() {
|
||||
return buyoutPrice != null && buyoutPrice > 0;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Instant getEndsAt() {
|
||||
return endsAt;
|
||||
}
|
||||
|
||||
public void setEndsAt(Instant endsAt) {
|
||||
this.endsAt = endsAt;
|
||||
}
|
||||
|
||||
public int getExtensionCount() {
|
||||
return extensionCount;
|
||||
}
|
||||
|
||||
public void setExtensionCount(int extensionCount) {
|
||||
this.extensionCount = extensionCount;
|
||||
}
|
||||
|
||||
public void incrementExtensionCount() {
|
||||
this.extensionCount++;
|
||||
}
|
||||
|
||||
public AuctionStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(AuctionStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean isEnded() {
|
||||
return Instant.now().isAfter(endsAt);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return status == AuctionStatus.ACTIVE && !isEnded();
|
||||
}
|
||||
|
||||
public boolean hasBids() {
|
||||
return bidCount > 0;
|
||||
}
|
||||
|
||||
public double getEffectivePrice() {
|
||||
return hasBids() ? currentBid : startPrice;
|
||||
}
|
||||
|
||||
public double getMinimumBid(double minIncrementPercent, double minIncrementAbsolute) {
|
||||
if (!hasBids()) {
|
||||
return startPrice;
|
||||
}
|
||||
double percentIncrement = currentBid * (minIncrementPercent / 100.0);
|
||||
return currentBid + Math.max(percentIncrement, minIncrementAbsolute);
|
||||
}
|
||||
|
||||
public enum AuctionStatus {
|
||||
ACTIVE,
|
||||
ENDED,
|
||||
SOLD,
|
||||
CANCELLED,
|
||||
EXPIRED,
|
||||
NO_BIDS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package pt.henrique.communityMarket.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a bid on an auction
|
||||
*/
|
||||
public class Bid {
|
||||
|
||||
private int id;
|
||||
private int auctionId;
|
||||
private UUID bidderUuid;
|
||||
private String bidderName;
|
||||
private double amount;
|
||||
private Instant createdAt;
|
||||
|
||||
public Bid() {
|
||||
this.createdAt = Instant.now();
|
||||
}
|
||||
|
||||
public Bid(int auctionId, UUID bidderUuid, String bidderName, double amount) {
|
||||
this();
|
||||
this.auctionId = auctionId;
|
||||
this.bidderUuid = bidderUuid;
|
||||
this.bidderName = bidderName;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getAuctionId() {
|
||||
return auctionId;
|
||||
}
|
||||
|
||||
public void setAuctionId(int auctionId) {
|
||||
this.auctionId = auctionId;
|
||||
}
|
||||
|
||||
public UUID getBidderUuid() {
|
||||
return bidderUuid;
|
||||
}
|
||||
|
||||
public void setBidderUuid(UUID bidderUuid) {
|
||||
this.bidderUuid = bidderUuid;
|
||||
}
|
||||
|
||||
public String getBidderName() {
|
||||
return bidderName;
|
||||
}
|
||||
|
||||
public void setBidderName(String bidderName) {
|
||||
this.bidderName = bidderName;
|
||||
}
|
||||
|
||||
public double getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(double amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package pt.henrique.communityMarket.model;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents an item in claim storage waiting to be claimed by a player
|
||||
*/
|
||||
public class ClaimItem {
|
||||
|
||||
private int id;
|
||||
private UUID playerUuid;
|
||||
private ItemStack item;
|
||||
private ClaimReason reason;
|
||||
private String sourceInfo; // Additional info like listing ID, auction ID, etc.
|
||||
private Instant createdAt;
|
||||
|
||||
public ClaimItem() {
|
||||
this.createdAt = Instant.now();
|
||||
}
|
||||
|
||||
public ClaimItem(UUID playerUuid, ItemStack item, ClaimReason reason, String sourceInfo) {
|
||||
this();
|
||||
this.playerUuid = playerUuid;
|
||||
this.item = item;
|
||||
this.reason = reason;
|
||||
this.sourceInfo = sourceInfo;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UUID getPlayerUuid() {
|
||||
return playerUuid;
|
||||
}
|
||||
|
||||
public void setPlayerUuid(UUID playerUuid) {
|
||||
this.playerUuid = playerUuid;
|
||||
}
|
||||
|
||||
public ItemStack getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public void setItem(ItemStack item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public ClaimReason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(ClaimReason reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public String getSourceInfo() {
|
||||
return sourceInfo;
|
||||
}
|
||||
|
||||
public void setSourceInfo(String sourceInfo) {
|
||||
this.sourceInfo = sourceInfo;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public enum ClaimReason {
|
||||
EXPIRED_LISTING("Expired Listing"),
|
||||
CANCELLED_LISTING("Cancelled Listing"),
|
||||
WON_AUCTION("Won Auction"),
|
||||
AUCTION_NO_BIDS("Auction Ended (No Bids)"),
|
||||
CANCELLED_AUCTION("Cancelled Auction"),
|
||||
PURCHASE_FULL_INVENTORY("Purchase (Inventory Full)"),
|
||||
OUTBID_REFUND("Outbid Refund"),
|
||||
ADMIN_RETURN("Admin Return");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
ClaimReason(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package pt.henrique.communityMarket.model;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a fixed-price market listing
|
||||
*/
|
||||
public class Listing {
|
||||
|
||||
private int id;
|
||||
private UUID sellerUuid;
|
||||
private String sellerName;
|
||||
private ItemStack item;
|
||||
private int amount;
|
||||
private double price;
|
||||
private Instant createdAt;
|
||||
private Instant expiresAt;
|
||||
private ListingStatus status;
|
||||
|
||||
public Listing() {
|
||||
this.status = ListingStatus.ACTIVE;
|
||||
this.createdAt = Instant.now();
|
||||
}
|
||||
|
||||
public Listing(UUID sellerUuid, String sellerName, ItemStack item, int amount, double price, Instant expiresAt) {
|
||||
this();
|
||||
this.sellerUuid = sellerUuid;
|
||||
this.sellerName = sellerName;
|
||||
this.item = item;
|
||||
this.amount = amount;
|
||||
this.price = price;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UUID getSellerUuid() {
|
||||
return sellerUuid;
|
||||
}
|
||||
|
||||
public void setSellerUuid(UUID sellerUuid) {
|
||||
this.sellerUuid = sellerUuid;
|
||||
}
|
||||
|
||||
public String getSellerName() {
|
||||
return sellerName;
|
||||
}
|
||||
|
||||
public void setSellerName(String sellerName) {
|
||||
this.sellerName = sellerName;
|
||||
}
|
||||
|
||||
public ItemStack getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public void setItem(ItemStack item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(int amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public double getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(double price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Instant expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public ListingStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(ListingStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return expiresAt != null && Instant.now().isAfter(expiresAt);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return status == ListingStatus.ACTIVE && !isExpired();
|
||||
}
|
||||
|
||||
public enum ListingStatus {
|
||||
ACTIVE,
|
||||
SOLD,
|
||||
EXPIRED,
|
||||
CANCELLED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package pt.henrique.communityMarket.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents pending earnings that a player can withdraw
|
||||
*/
|
||||
public class PendingEarnings {
|
||||
|
||||
private int id;
|
||||
private UUID playerUuid;
|
||||
private double amount;
|
||||
private String source; // e.g., "Listing #123", "Auction #456"
|
||||
private Instant createdAt;
|
||||
private boolean withdrawn;
|
||||
|
||||
public PendingEarnings() {
|
||||
this.createdAt = Instant.now();
|
||||
this.withdrawn = false;
|
||||
}
|
||||
|
||||
public PendingEarnings(UUID playerUuid, double amount, String source) {
|
||||
this();
|
||||
this.playerUuid = playerUuid;
|
||||
this.amount = amount;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UUID getPlayerUuid() {
|
||||
return playerUuid;
|
||||
}
|
||||
|
||||
public void setPlayerUuid(UUID playerUuid) {
|
||||
this.playerUuid = playerUuid;
|
||||
}
|
||||
|
||||
public double getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(double amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public boolean isWithdrawn() {
|
||||
return withdrawn;
|
||||
}
|
||||
|
||||
public void setWithdrawn(boolean withdrawn) {
|
||||
this.withdrawn = withdrawn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
package pt.henrique.communityMarket.service;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.Auction;
|
||||
import pt.henrique.communityMarket.model.Bid;
|
||||
import pt.henrique.communityMarket.model.ClaimItem;
|
||||
import pt.henrique.communityMarket.model.PendingEarnings;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Service layer for managing auctions.
|
||||
* Handles creation, bidding, buyout, and auction end processing.
|
||||
*/
|
||||
public class AuctionService {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
// Simple cache for active auctions
|
||||
private List<Auction> cachedAuctions;
|
||||
private long cacheExpiry = 0;
|
||||
|
||||
// Track pending operations to prevent race conditions
|
||||
private final Map<Integer, Boolean> pendingBids = new ConcurrentHashMap<>();
|
||||
|
||||
public AuctionService(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new auction
|
||||
*
|
||||
* @param player The seller
|
||||
* @param item The item to auction
|
||||
* @param startPrice Starting bid price
|
||||
* @param buyoutPrice Optional buyout price (null for no buyout)
|
||||
* @param durationHours Duration in hours
|
||||
* @return CompletableFuture with the auction ID or -1 if failed
|
||||
*/
|
||||
public CompletableFuture<Integer> createAuction(Player player, ItemStack item, double startPrice,
|
||||
Double buyoutPrice, int durationHours) {
|
||||
// Validate
|
||||
if (item == null || item.getType().isAir()) {
|
||||
return CompletableFuture.completedFuture(-1);
|
||||
}
|
||||
|
||||
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||
return CompletableFuture.completedFuture(-1);
|
||||
}
|
||||
|
||||
// Create auction object
|
||||
Auction auction = new Auction(
|
||||
player.getUniqueId(),
|
||||
player.getName(),
|
||||
item.clone(),
|
||||
startPrice,
|
||||
buyoutPrice,
|
||||
Instant.now().plusSeconds(durationHours * 3600L)
|
||||
);
|
||||
|
||||
// Save to database
|
||||
return plugin.getDatabaseManager().createAuction(auction)
|
||||
.thenApply(id -> {
|
||||
if (id > 0) {
|
||||
invalidateCache();
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all active auctions with caching
|
||||
*/
|
||||
public CompletableFuture<List<Auction>> getActiveAuctions() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (cachedAuctions != null && now < cacheExpiry) {
|
||||
return CompletableFuture.completedFuture(cachedAuctions);
|
||||
}
|
||||
|
||||
return plugin.getDatabaseManager().getActiveAuctions()
|
||||
.thenApply(auctions -> {
|
||||
cachedAuctions = auctions;
|
||||
cacheExpiry = System.currentTimeMillis() +
|
||||
(plugin.getConfigManager().getCacheDuration() * 1000L);
|
||||
return auctions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific auction by ID
|
||||
*/
|
||||
public CompletableFuture<Optional<Auction>> getAuction(int id) {
|
||||
return plugin.getDatabaseManager().getAuction(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all auctions for a specific player
|
||||
*/
|
||||
public CompletableFuture<List<Auction>> getPlayerAuctions(UUID playerUuid) {
|
||||
return plugin.getDatabaseManager().getPlayerAuctions(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts active auctions for a player
|
||||
*/
|
||||
public CompletableFuture<Integer> countPlayerAuctions(UUID playerUuid) {
|
||||
return plugin.getDatabaseManager().countPlayerAuctions(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player can create a new auction (not at limit)
|
||||
*/
|
||||
public CompletableFuture<Boolean> canCreateAuction(UUID playerUuid) {
|
||||
int maxAuctions = plugin.getConfigManager().getMaxAuctionsPerPlayer();
|
||||
|
||||
return countPlayerAuctions(playerUuid)
|
||||
.thenApply(count -> count < maxAuctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum bid for an auction
|
||||
*
|
||||
* @param auction The auction
|
||||
* @return Minimum bid amount
|
||||
*/
|
||||
public double calculateMinBid(Auction auction) {
|
||||
return auction.getMinimumBid(
|
||||
plugin.getConfigManager().getMinBidIncrementPercent(),
|
||||
plugin.getConfigManager().getMinBidIncrementAbsolute()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a bid on an auction
|
||||
*
|
||||
* @param auctionId The auction to bid on
|
||||
* @param bidder The bidder
|
||||
* @param amount The bid amount
|
||||
* @return CompletableFuture with bid result
|
||||
*/
|
||||
public CompletableFuture<BidResult> placeBid(int auctionId, Player bidder, double amount) {
|
||||
// Check if already processing this auction
|
||||
if (pendingBids.putIfAbsent(auctionId, true) != null) {
|
||||
return CompletableFuture.completedFuture(BidResult.ALREADY_PROCESSING);
|
||||
}
|
||||
|
||||
try {
|
||||
return getAuction(auctionId)
|
||||
.thenCompose(optAuction -> {
|
||||
if (optAuction.isEmpty()) {
|
||||
pendingBids.remove(auctionId);
|
||||
return CompletableFuture.completedFuture(BidResult.NOT_FOUND);
|
||||
}
|
||||
|
||||
Auction auction = optAuction.get();
|
||||
|
||||
// Can't bid on own auction
|
||||
if (auction.getSellerUuid().equals(bidder.getUniqueId())) {
|
||||
pendingBids.remove(auctionId);
|
||||
return CompletableFuture.completedFuture(BidResult.OWN_AUCTION);
|
||||
}
|
||||
|
||||
// Check bid amount
|
||||
double minBid = calculateMinBid(auction);
|
||||
if (amount < minBid) {
|
||||
pendingBids.remove(auctionId);
|
||||
return CompletableFuture.completedFuture(BidResult.BID_TOO_LOW);
|
||||
}
|
||||
|
||||
// Check bidder funds
|
||||
if (!plugin.getEconomyManager().has(bidder.getUniqueId(), amount)) {
|
||||
pendingBids.remove(auctionId);
|
||||
return CompletableFuture.completedFuture(BidResult.INSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
// Store previous bidder info for refund
|
||||
UUID previousBidder = auction.getHighestBidderUuid();
|
||||
double previousBid = auction.getCurrentBid();
|
||||
|
||||
// Withdraw from bidder first
|
||||
if (!plugin.getEconomyManager().withdraw(bidder.getUniqueId(), amount)) {
|
||||
pendingBids.remove(auctionId);
|
||||
return CompletableFuture.completedFuture(BidResult.ECONOMY_ERROR);
|
||||
}
|
||||
|
||||
// Place bid in database
|
||||
return plugin.getDatabaseManager().placeBid(auctionId, bidder.getUniqueId(), bidder.getName(), amount)
|
||||
.thenApply(success -> {
|
||||
if (!success) {
|
||||
// Refund bidder
|
||||
plugin.getEconomyManager().deposit(bidder.getUniqueId(), amount);
|
||||
pendingBids.remove(auctionId);
|
||||
return BidResult.AUCTION_ENDED;
|
||||
}
|
||||
|
||||
// Refund previous bidder
|
||||
if (previousBidder != null && previousBid > 0) {
|
||||
plugin.getEconomyManager().deposit(previousBidder, previousBid);
|
||||
|
||||
// Notify previous bidder if online
|
||||
if (plugin.getConfigManager().isNotifyOnOutbid()) {
|
||||
Player prevBidderPlayer = Bukkit.getPlayer(previousBidder);
|
||||
if (prevBidderPlayer != null && prevBidderPlayer.isOnline()) {
|
||||
Map<String, String> placeholders = Map.of(
|
||||
"item", auction.getItem().getType().name(),
|
||||
"amount", plugin.getMessageManager().formatCurrency(amount),
|
||||
"bidder", bidder.getName()
|
||||
);
|
||||
prevBidderPlayer.sendMessage(plugin.getMessageManager()
|
||||
.getPrefixed("messages.auction-outbid", placeholders));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidateCache();
|
||||
pendingBids.remove(auctionId);
|
||||
return BidResult.SUCCESS;
|
||||
});
|
||||
});
|
||||
} catch (Exception e) {
|
||||
pendingBids.remove(auctionId);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buys out an auction immediately
|
||||
*
|
||||
* @param auctionId The auction to buyout
|
||||
* @param buyer The buyer
|
||||
* @return CompletableFuture with buyout result
|
||||
*/
|
||||
public CompletableFuture<BidResult> buyout(int auctionId, Player buyer) {
|
||||
return getAuction(auctionId)
|
||||
.thenCompose(optAuction -> {
|
||||
if (optAuction.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(BidResult.NOT_FOUND);
|
||||
}
|
||||
|
||||
Auction auction = optAuction.get();
|
||||
|
||||
// Check if buyout is available
|
||||
if (!auction.hasBuyout()) {
|
||||
return CompletableFuture.completedFuture(BidResult.NO_BUYOUT);
|
||||
}
|
||||
|
||||
// Can't buyout own auction
|
||||
if (auction.getSellerUuid().equals(buyer.getUniqueId())) {
|
||||
return CompletableFuture.completedFuture(BidResult.OWN_AUCTION);
|
||||
}
|
||||
|
||||
// Check buyer funds
|
||||
double buyoutPrice = auction.getBuyoutPrice();
|
||||
if (!plugin.getEconomyManager().has(buyer.getUniqueId(), buyoutPrice)) {
|
||||
return CompletableFuture.completedFuture(BidResult.INSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
// Place bid at buyout price (this will end the auction)
|
||||
return placeBid(auctionId, buyer, buyoutPrice)
|
||||
.thenCompose(result -> {
|
||||
if (result == BidResult.SUCCESS) {
|
||||
// Immediately end the auction
|
||||
return processAuctionEnd(auctionId)
|
||||
.thenApply(v -> BidResult.BUYOUT_SUCCESS);
|
||||
}
|
||||
return CompletableFuture.completedFuture(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an auction (only if no bids)
|
||||
*
|
||||
* @param auctionId The auction to cancel
|
||||
* @param playerUuid The player attempting to cancel
|
||||
* @param isAdmin Whether this is an admin action
|
||||
* @return CompletableFuture with success status
|
||||
*/
|
||||
public CompletableFuture<CancelResult> cancelAuction(int auctionId, UUID playerUuid, boolean isAdmin) {
|
||||
return getAuction(auctionId)
|
||||
.thenCompose(optAuction -> {
|
||||
if (optAuction.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(CancelResult.NOT_FOUND);
|
||||
}
|
||||
|
||||
Auction auction = optAuction.get();
|
||||
|
||||
// Check permission
|
||||
if (!isAdmin && !auction.getSellerUuid().equals(playerUuid)) {
|
||||
return CompletableFuture.completedFuture(CancelResult.NOT_OWNER);
|
||||
}
|
||||
|
||||
// Can only cancel if no bids (unless admin)
|
||||
if (!isAdmin && auction.getBidCount() > 0) {
|
||||
return CompletableFuture.completedFuture(CancelResult.HAS_BIDS);
|
||||
}
|
||||
|
||||
// If admin cancelling with bids, refund highest bidder
|
||||
if (isAdmin && auction.getBidCount() > 0 && auction.getHighestBidderUuid() != null) {
|
||||
plugin.getEconomyManager().deposit(auction.getHighestBidderUuid(), auction.getCurrentBid());
|
||||
}
|
||||
|
||||
// Update status
|
||||
return plugin.getDatabaseManager().updateAuctionStatus(auctionId, Auction.AuctionStatus.CANCELLED)
|
||||
.thenApply(success -> {
|
||||
if (success) {
|
||||
// Return item to claim storage
|
||||
ClaimItem claimItem = new ClaimItem(
|
||||
auction.getSellerUuid(),
|
||||
auction.getItem().clone(),
|
||||
ClaimItem.ClaimReason.CANCELLED_AUCTION,
|
||||
"Auction #" + auctionId
|
||||
);
|
||||
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||
invalidateCache();
|
||||
return CancelResult.SUCCESS;
|
||||
}
|
||||
return CancelResult.FAILED;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes ended auctions - delivers items and handles payments
|
||||
*/
|
||||
public CompletableFuture<Void> processEndedAuctions() {
|
||||
return plugin.getDatabaseManager().getEndedAuctions()
|
||||
.thenAccept(auctions -> {
|
||||
for (Auction auction : auctions) {
|
||||
processAuctionEnd(auction.getId());
|
||||
}
|
||||
if (!auctions.isEmpty()) {
|
||||
invalidateCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a single auction end
|
||||
*/
|
||||
private CompletableFuture<Void> processAuctionEnd(int auctionId) {
|
||||
return getAuction(auctionId)
|
||||
.thenAccept(optAuction -> {
|
||||
if (optAuction.isEmpty()) return;
|
||||
|
||||
Auction auction = optAuction.get();
|
||||
|
||||
// Update status first
|
||||
Auction.AuctionStatus newStatus = auction.getBidCount() > 0
|
||||
? Auction.AuctionStatus.SOLD
|
||||
: Auction.AuctionStatus.EXPIRED;
|
||||
|
||||
plugin.getDatabaseManager().updateAuctionStatus(auctionId, newStatus);
|
||||
|
||||
if (auction.getBidCount() > 0 && auction.getHighestBidderUuid() != null) {
|
||||
// Auction has a winner
|
||||
double winningBid = auction.getCurrentBid();
|
||||
double tax = plugin.getConfigManager().getAuctionTax();
|
||||
double sellerEarnings = winningBid * (1 - tax / 100);
|
||||
|
||||
// Add pending earnings for seller
|
||||
PendingEarnings earnings = new PendingEarnings(
|
||||
auction.getSellerUuid(),
|
||||
sellerEarnings,
|
||||
"Auction #" + auctionId
|
||||
);
|
||||
plugin.getDatabaseManager().addPendingEarnings(earnings);
|
||||
|
||||
// Add item to winner's claim storage
|
||||
ClaimItem claimItem = new ClaimItem(
|
||||
auction.getHighestBidderUuid(),
|
||||
auction.getItem().clone(),
|
||||
ClaimItem.ClaimReason.WON_AUCTION,
|
||||
"Auction #" + auctionId
|
||||
);
|
||||
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||
|
||||
// Notify winner if online
|
||||
if (plugin.getConfigManager().isNotifyOnWin()) {
|
||||
Player winner = Bukkit.getPlayer(auction.getHighestBidderUuid());
|
||||
if (winner != null && winner.isOnline()) {
|
||||
Map<String, String> placeholders = Map.of(
|
||||
"item", auction.getItem().getType().name(),
|
||||
"price", plugin.getMessageManager().formatCurrency(winningBid)
|
||||
);
|
||||
winner.sendMessage(plugin.getMessageManager()
|
||||
.getPrefixed("messages.auction-ended-winner", placeholders));
|
||||
}
|
||||
}
|
||||
|
||||
// Notify seller if online
|
||||
if (plugin.getConfigManager().isNotifyOnSale()) {
|
||||
Player seller = Bukkit.getPlayer(auction.getSellerUuid());
|
||||
if (seller != null && seller.isOnline()) {
|
||||
Map<String, String> placeholders = Map.of(
|
||||
"item", auction.getItem().getType().name(),
|
||||
"winner", auction.getHighestBidderName(),
|
||||
"price", plugin.getMessageManager().formatCurrency(sellerEarnings)
|
||||
);
|
||||
seller.sendMessage(plugin.getMessageManager()
|
||||
.getPrefixed("messages.auction-ended-seller", placeholders));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No bids - return item to seller
|
||||
ClaimItem claimItem = new ClaimItem(
|
||||
auction.getSellerUuid(),
|
||||
auction.getItem().clone(),
|
||||
ClaimItem.ClaimReason.AUCTION_NO_BIDS,
|
||||
"Auction #" + auctionId
|
||||
);
|
||||
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||
|
||||
// Notify seller if online
|
||||
if (plugin.getConfigManager().isNotifyOnExpire()) {
|
||||
Player seller = Bukkit.getPlayer(auction.getSellerUuid());
|
||||
if (seller != null && seller.isOnline()) {
|
||||
seller.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||
"messages.auction-ended-no-bids",
|
||||
"item", auction.getItem().getType().name()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the auction cache
|
||||
*/
|
||||
public void invalidateCache() {
|
||||
cachedAuctions = null;
|
||||
cacheExpiry = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a bid attempt
|
||||
*/
|
||||
public enum BidResult {
|
||||
SUCCESS,
|
||||
BUYOUT_SUCCESS,
|
||||
NOT_FOUND,
|
||||
AUCTION_ENDED,
|
||||
OWN_AUCTION,
|
||||
BID_TOO_LOW,
|
||||
INSUFFICIENT_FUNDS,
|
||||
ECONOMY_ERROR,
|
||||
ALREADY_PROCESSING,
|
||||
NO_BUYOUT
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a cancel attempt
|
||||
*/
|
||||
public enum CancelResult {
|
||||
SUCCESS,
|
||||
NOT_FOUND,
|
||||
NOT_OWNER,
|
||||
HAS_BIDS,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package pt.henrique.communityMarket.service;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.ClaimItem;
|
||||
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Service layer for managing claim storage.
|
||||
* Players claim items from expired listings, won auctions, etc.
|
||||
*/
|
||||
public class ClaimService {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public ClaimService(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all pending claim items for a player
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @return CompletableFuture with list of claim items
|
||||
*/
|
||||
public CompletableFuture<List<ClaimItem>> getPlayerClaimItems(UUID playerUuid) {
|
||||
return plugin.getDatabaseManager().getPlayerClaimItems(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts pending claim items for a player
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @return CompletableFuture with count
|
||||
*/
|
||||
public CompletableFuture<Integer> countPlayerClaimItems(UUID playerUuid) {
|
||||
return plugin.getDatabaseManager().countPlayerClaimItems(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims a single item
|
||||
*
|
||||
* @param claimId The claim item ID
|
||||
* @param player The player claiming
|
||||
* @return CompletableFuture with claim result
|
||||
*/
|
||||
public CompletableFuture<ClaimResult> claimItem(int claimId, Player player) {
|
||||
return plugin.getDatabaseManager().getPlayerClaimItems(player.getUniqueId())
|
||||
.thenCompose(items -> {
|
||||
// Find the specific item
|
||||
ClaimItem claimItem = items.stream()
|
||||
.filter(i -> i.getId() == claimId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (claimItem == null) {
|
||||
return CompletableFuture.completedFuture(ClaimResult.NOT_FOUND);
|
||||
}
|
||||
|
||||
// Check inventory space
|
||||
if (!InventoryUtil.hasSpace(player, claimItem.getItem())) {
|
||||
return CompletableFuture.completedFuture(ClaimResult.INVENTORY_FULL);
|
||||
}
|
||||
|
||||
// Remove from database first
|
||||
return plugin.getDatabaseManager().removeClaimItem(claimId)
|
||||
.thenApply(success -> {
|
||||
if (!success) {
|
||||
return ClaimResult.FAILED;
|
||||
}
|
||||
|
||||
// Give item to player on main thread
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
ItemStack leftover = InventoryUtil.giveItem(player, claimItem.getItem());
|
||||
if (leftover != null) {
|
||||
// This shouldn't happen since we checked space, but just in case
|
||||
plugin.getDatabaseManager().addClaimItem(new ClaimItem(
|
||||
player.getUniqueId(),
|
||||
leftover,
|
||||
claimItem.getReason(),
|
||||
claimItem.getSourceInfo()
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
return ClaimResult.SUCCESS;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims all items for a player
|
||||
*
|
||||
* @param player The player claiming
|
||||
* @return CompletableFuture with number of items claimed
|
||||
*/
|
||||
public CompletableFuture<Integer> claimAll(Player player) {
|
||||
return plugin.getDatabaseManager().getPlayerClaimItems(player.getUniqueId())
|
||||
.thenApply(items -> {
|
||||
int claimed = 0;
|
||||
|
||||
for (ClaimItem item : items) {
|
||||
// Check if player has space
|
||||
if (!InventoryUtil.hasSpace(player, item.getItem())) {
|
||||
break; // Stop claiming if inventory is full
|
||||
}
|
||||
|
||||
// Remove from database
|
||||
boolean removed = plugin.getDatabaseManager().removeClaimItem(item.getId()).join();
|
||||
if (removed) {
|
||||
// Give item on main thread
|
||||
ItemStack itemToGive = item.getItem();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
InventoryUtil.giveItem(player, itemToGive);
|
||||
});
|
||||
claimed++;
|
||||
}
|
||||
}
|
||||
|
||||
return claimed;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to a player's claim storage
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param item The item to add
|
||||
* @param reason The reason for the claim
|
||||
* @param sourceInfo Additional info about the source
|
||||
* @return CompletableFuture with the claim ID
|
||||
*/
|
||||
public CompletableFuture<Integer> addClaimItem(UUID playerUuid, ItemStack item, ClaimItem.ClaimReason reason, String sourceInfo) {
|
||||
ClaimItem claimItem = new ClaimItem(playerUuid, item, reason, sourceInfo);
|
||||
return plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a claim attempt
|
||||
*/
|
||||
public enum ClaimResult {
|
||||
SUCCESS,
|
||||
NOT_FOUND,
|
||||
INVENTORY_FULL,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package pt.henrique.communityMarket.service;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Service layer for managing player earnings.
|
||||
* Handles pending earnings from sales and withdrawals.
|
||||
*/
|
||||
public class EarningsService {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public EarningsService(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total pending earnings for a player
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @return CompletableFuture with the total pending amount
|
||||
*/
|
||||
public CompletableFuture<Double> getPendingEarnings(UUID playerUuid) {
|
||||
return plugin.getDatabaseManager().getPlayerPendingEarnings(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraws all pending earnings for a player
|
||||
*
|
||||
* @param player The player withdrawing
|
||||
* @return CompletableFuture with the withdrawal result
|
||||
*/
|
||||
public CompletableFuture<WithdrawResult> withdrawAll(Player player) {
|
||||
return getPendingEarnings(player.getUniqueId())
|
||||
.thenCompose(amount -> {
|
||||
if (amount <= 0) {
|
||||
return CompletableFuture.completedFuture(WithdrawResult.NO_EARNINGS);
|
||||
}
|
||||
|
||||
// Mark earnings as withdrawn in database first
|
||||
return plugin.getDatabaseManager().withdrawAllEarnings(player.getUniqueId())
|
||||
.thenApply(success -> {
|
||||
if (!success) {
|
||||
return WithdrawResult.FAILED;
|
||||
}
|
||||
|
||||
// Deposit to player's economy account
|
||||
if (!plugin.getEconomyManager().deposit(player.getUniqueId(), amount)) {
|
||||
// Economy failed - this is a problem
|
||||
// The earnings are marked as withdrawn but money wasn't given
|
||||
plugin.getLogger().severe("Failed to deposit " + amount + " to " + player.getName() + " after marking earnings as withdrawn!");
|
||||
return WithdrawResult.ECONOMY_ERROR;
|
||||
}
|
||||
|
||||
return WithdrawResult.success(amount);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds pending earnings for a player
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param amount The amount to add
|
||||
* @param source Description of the source (e.g., "Listing #123")
|
||||
* @return CompletableFuture with the earnings ID
|
||||
*/
|
||||
public CompletableFuture<Integer> addEarnings(UUID playerUuid, double amount, String source) {
|
||||
var earnings = new pt.henrique.communityMarket.model.PendingEarnings(playerUuid, amount, source);
|
||||
return plugin.getDatabaseManager().addPendingEarnings(earnings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a withdrawal attempt
|
||||
*/
|
||||
public static class WithdrawResult {
|
||||
private final boolean success;
|
||||
private final double amount;
|
||||
private final String error;
|
||||
|
||||
private WithdrawResult(boolean success, double amount, String error) {
|
||||
this.success = success;
|
||||
this.amount = amount;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public static WithdrawResult success(double amount) {
|
||||
return new WithdrawResult(true, amount, null);
|
||||
}
|
||||
|
||||
public static final WithdrawResult NO_EARNINGS = new WithdrawResult(false, 0, "no_earnings");
|
||||
public static final WithdrawResult FAILED = new WithdrawResult(false, 0, "failed");
|
||||
public static final WithdrawResult ECONOMY_ERROR = new WithdrawResult(false, 0, "economy_error");
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public double getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
package pt.henrique.communityMarket.service;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
import pt.henrique.communityMarket.model.ClaimItem;
|
||||
import pt.henrique.communityMarket.model.Listing;
|
||||
import pt.henrique.communityMarket.model.PendingEarnings;
|
||||
import pt.henrique.communityMarket.util.InventoryUtil;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Service layer for managing fixed-price market listings.
|
||||
* Handles creation, purchase, cancellation, and expiration of listings.
|
||||
*/
|
||||
public class ListingService {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
// Simple cache for active listings
|
||||
private List<Listing> cachedListings;
|
||||
private long cacheExpiry = 0;
|
||||
|
||||
// Track pending operations to prevent double-purchases
|
||||
private final Map<Integer, Boolean> pendingPurchases = new ConcurrentHashMap<>();
|
||||
|
||||
public ListingService(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new listing for a player
|
||||
*
|
||||
* @param player The seller
|
||||
* @param item The item to sell
|
||||
* @param amount Amount of items
|
||||
* @param price Total price for all items
|
||||
* @param durationHours Duration in hours
|
||||
* @return CompletableFuture with the listing ID or -1 if failed
|
||||
*/
|
||||
public CompletableFuture<Integer> createListing(Player player, ItemStack item, int amount, double price, int durationHours) {
|
||||
// Validate
|
||||
if (item == null || item.getType().isAir()) {
|
||||
return CompletableFuture.completedFuture(-1);
|
||||
}
|
||||
|
||||
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||
return CompletableFuture.completedFuture(-1);
|
||||
}
|
||||
|
||||
// Create listing object
|
||||
Listing listing = new Listing(
|
||||
player.getUniqueId(),
|
||||
player.getName(),
|
||||
item.clone(),
|
||||
amount,
|
||||
price,
|
||||
Instant.now().plusSeconds(durationHours * 3600L)
|
||||
);
|
||||
|
||||
// Save to database
|
||||
return plugin.getDatabaseManager().createListing(listing)
|
||||
.thenApply(id -> {
|
||||
if (id > 0) {
|
||||
invalidateCache();
|
||||
// Update cooldown
|
||||
plugin.getDatabaseManager().updateLastListingTime(player.getUniqueId());
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all active listings with caching
|
||||
*/
|
||||
public CompletableFuture<List<Listing>> getActiveListings() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// Return cached if still valid
|
||||
if (cachedListings != null && now < cacheExpiry) {
|
||||
return CompletableFuture.completedFuture(cachedListings);
|
||||
}
|
||||
|
||||
return plugin.getDatabaseManager().getActiveListings()
|
||||
.thenApply(listings -> {
|
||||
cachedListings = listings;
|
||||
cacheExpiry = System.currentTimeMillis() +
|
||||
(plugin.getConfigManager().getCacheDuration() * 1000L);
|
||||
return listings;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific listing by ID
|
||||
*/
|
||||
public CompletableFuture<Optional<Listing>> getListing(int id) {
|
||||
return plugin.getDatabaseManager().getListing(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all listings for a specific player
|
||||
*/
|
||||
public CompletableFuture<List<Listing>> getPlayerListings(UUID playerUuid) {
|
||||
return plugin.getDatabaseManager().getPlayerListings(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts active listings for a player
|
||||
*/
|
||||
public CompletableFuture<Integer> countPlayerListings(UUID playerUuid) {
|
||||
return plugin.getDatabaseManager().countPlayerListings(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player can create a new listing (not at limit and no cooldown)
|
||||
*/
|
||||
public CompletableFuture<Boolean> canCreateListing(UUID playerUuid) {
|
||||
int maxListings = plugin.getConfigManager().getMaxListingsPerPlayer();
|
||||
|
||||
return countPlayerListings(playerUuid)
|
||||
.thenCompose(count -> {
|
||||
if (count >= maxListings) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
int cooldown = plugin.getConfigManager().getListingCooldown();
|
||||
if (cooldown <= 0) {
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
|
||||
return plugin.getDatabaseManager().getLastListingTime(playerUuid)
|
||||
.thenApply(lastTime -> {
|
||||
if (lastTime.isEmpty()) return true;
|
||||
return Instant.now().isAfter(lastTime.get().plusSeconds(cooldown));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the remaining cooldown time in seconds
|
||||
*/
|
||||
public CompletableFuture<Long> getRemainingCooldown(UUID playerUuid) {
|
||||
int cooldown = plugin.getConfigManager().getListingCooldown();
|
||||
if (cooldown <= 0) {
|
||||
return CompletableFuture.completedFuture(0L);
|
||||
}
|
||||
|
||||
return plugin.getDatabaseManager().getLastListingTime(playerUuid)
|
||||
.thenApply(lastTime -> {
|
||||
if (lastTime.isEmpty()) return 0L;
|
||||
long remaining = lastTime.get().plusSeconds(cooldown).getEpochSecond() - Instant.now().getEpochSecond();
|
||||
return Math.max(0, remaining);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to purchase a listing.
|
||||
* This is an atomic operation that prevents double-purchases.
|
||||
*
|
||||
* @param listingId The listing to purchase
|
||||
* @param buyer The buyer
|
||||
* @return CompletableFuture with success status
|
||||
*/
|
||||
public CompletableFuture<PurchaseResult> purchaseListing(int listingId, Player buyer) {
|
||||
// Check if already processing this listing
|
||||
if (pendingPurchases.putIfAbsent(listingId, true) != null) {
|
||||
return CompletableFuture.completedFuture(PurchaseResult.ALREADY_PROCESSING);
|
||||
}
|
||||
|
||||
try {
|
||||
return getListing(listingId)
|
||||
.thenCompose(optListing -> {
|
||||
if (optListing.isEmpty()) {
|
||||
pendingPurchases.remove(listingId);
|
||||
return CompletableFuture.completedFuture(PurchaseResult.NOT_FOUND);
|
||||
}
|
||||
|
||||
Listing listing = optListing.get();
|
||||
|
||||
// Can't buy own listing
|
||||
if (listing.getSellerUuid().equals(buyer.getUniqueId())) {
|
||||
pendingPurchases.remove(listingId);
|
||||
return CompletableFuture.completedFuture(PurchaseResult.OWN_LISTING);
|
||||
}
|
||||
|
||||
// Check buyer funds
|
||||
if (!plugin.getEconomyManager().has(buyer.getUniqueId(), listing.getPrice())) {
|
||||
pendingPurchases.remove(listingId);
|
||||
return CompletableFuture.completedFuture(PurchaseResult.INSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
// Attempt atomic purchase in DB
|
||||
return plugin.getDatabaseManager().purchaseListing(listingId, buyer.getUniqueId(), buyer.getName())
|
||||
.thenApply(success -> {
|
||||
if (!success) {
|
||||
pendingPurchases.remove(listingId);
|
||||
return PurchaseResult.ALREADY_SOLD;
|
||||
}
|
||||
|
||||
// Withdraw from buyer
|
||||
if (!plugin.getEconomyManager().withdraw(buyer.getUniqueId(), listing.getPrice())) {
|
||||
// Rollback DB change
|
||||
plugin.getDatabaseManager().updateListingStatus(listingId, Listing.ListingStatus.ACTIVE);
|
||||
pendingPurchases.remove(listingId);
|
||||
return PurchaseResult.ECONOMY_ERROR;
|
||||
}
|
||||
|
||||
// Calculate seller earnings after tax
|
||||
double tax = plugin.getConfigManager().getMarketTax();
|
||||
double sellerEarnings = listing.getPrice() * (1 - tax / 100);
|
||||
|
||||
// Add pending earnings for seller
|
||||
PendingEarnings earnings = new PendingEarnings(
|
||||
listing.getSellerUuid(),
|
||||
sellerEarnings,
|
||||
"Listing #" + listingId
|
||||
);
|
||||
plugin.getDatabaseManager().addPendingEarnings(earnings);
|
||||
|
||||
// Give item to buyer or add to claim storage
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
ItemStack item = listing.getItem().clone();
|
||||
item.setAmount(listing.getAmount());
|
||||
ItemStack leftover = InventoryUtil.giveItem(buyer, item);
|
||||
|
||||
if (leftover != null) {
|
||||
// Couldn't fit in inventory, add to claim storage
|
||||
ClaimItem claimItem = new ClaimItem(
|
||||
buyer.getUniqueId(),
|
||||
leftover,
|
||||
ClaimItem.ClaimReason.PURCHASE_FULL_INVENTORY,
|
||||
"Listing #" + listingId
|
||||
);
|
||||
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||
buyer.sendMessage(plugin.getMessageManager().getPrefixed("messages.claim-inventory-full"));
|
||||
}
|
||||
});
|
||||
|
||||
// Notify seller if online
|
||||
if (plugin.getConfigManager().isNotifyOnSale()) {
|
||||
Player seller = Bukkit.getPlayer(listing.getSellerUuid());
|
||||
if (seller != null && seller.isOnline()) {
|
||||
Map<String, String> placeholders = Map.of(
|
||||
"item", listing.getItem().getType().name(),
|
||||
"amount", String.valueOf(listing.getAmount()),
|
||||
"buyer", buyer.getName(),
|
||||
"price", plugin.getMessageManager().formatCurrency(listing.getPrice())
|
||||
);
|
||||
seller.sendMessage(plugin.getMessageManager().getPrefixed("messages.listing-sold", placeholders));
|
||||
}
|
||||
}
|
||||
|
||||
invalidateCache();
|
||||
pendingPurchases.remove(listingId);
|
||||
return PurchaseResult.SUCCESS;
|
||||
});
|
||||
});
|
||||
} catch (Exception e) {
|
||||
pendingPurchases.remove(listingId);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a listing and returns the item to the seller's claim storage
|
||||
*
|
||||
* @param listingId The listing to cancel
|
||||
* @param playerUuid The player attempting to cancel (must be seller or admin)
|
||||
* @param isAdmin Whether this is an admin action
|
||||
* @return CompletableFuture with success status
|
||||
*/
|
||||
public CompletableFuture<Boolean> cancelListing(int listingId, UUID playerUuid, boolean isAdmin) {
|
||||
return getListing(listingId)
|
||||
.thenCompose(optListing -> {
|
||||
if (optListing.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
Listing listing = optListing.get();
|
||||
|
||||
// Check permission
|
||||
if (!isAdmin && !listing.getSellerUuid().equals(playerUuid)) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
// Update status
|
||||
return plugin.getDatabaseManager().updateListingStatus(listingId, Listing.ListingStatus.CANCELLED)
|
||||
.thenApply(success -> {
|
||||
if (success) {
|
||||
// Return item to claim storage
|
||||
ItemStack item = listing.getItem().clone();
|
||||
item.setAmount(listing.getAmount());
|
||||
ClaimItem claimItem = new ClaimItem(
|
||||
listing.getSellerUuid(),
|
||||
item,
|
||||
ClaimItem.ClaimReason.CANCELLED_LISTING,
|
||||
"Listing #" + listingId
|
||||
);
|
||||
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||
invalidateCache();
|
||||
}
|
||||
return success;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes expired listings - moves items to claim storage
|
||||
*/
|
||||
public CompletableFuture<Void> processExpiredListings() {
|
||||
return plugin.getDatabaseManager().getExpiredListings()
|
||||
.thenAccept(listings -> {
|
||||
for (Listing listing : listings) {
|
||||
// Update status to expired
|
||||
plugin.getDatabaseManager().updateListingStatus(listing.getId(), Listing.ListingStatus.EXPIRED)
|
||||
.thenAccept(success -> {
|
||||
if (success) {
|
||||
// Return item to claim storage
|
||||
ItemStack item = listing.getItem().clone();
|
||||
item.setAmount(listing.getAmount());
|
||||
ClaimItem claimItem = new ClaimItem(
|
||||
listing.getSellerUuid(),
|
||||
item,
|
||||
ClaimItem.ClaimReason.EXPIRED_LISTING,
|
||||
"Listing #" + listing.getId()
|
||||
);
|
||||
plugin.getDatabaseManager().addClaimItem(claimItem);
|
||||
|
||||
// Notify seller if online
|
||||
if (plugin.getConfigManager().isNotifyOnExpire()) {
|
||||
Player seller = Bukkit.getPlayer(listing.getSellerUuid());
|
||||
if (seller != null && seller.isOnline()) {
|
||||
seller.sendMessage(plugin.getMessageManager().getPrefixed(
|
||||
"messages.listing-expired",
|
||||
"id", String.valueOf(listing.getId())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!listings.isEmpty()) {
|
||||
invalidateCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the listing cache
|
||||
*/
|
||||
public void invalidateCache() {
|
||||
cachedListings = null;
|
||||
cacheExpiry = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a purchase attempt
|
||||
*/
|
||||
public enum PurchaseResult {
|
||||
SUCCESS,
|
||||
NOT_FOUND,
|
||||
ALREADY_SOLD,
|
||||
OWN_LISTING,
|
||||
INSUFFICIENT_FUNDS,
|
||||
ECONOMY_ERROR,
|
||||
ALREADY_PROCESSING
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package pt.henrique.communityMarket.service;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Service layer for handling marketplace transactions.
|
||||
* Provides a unified interface for complex operations that
|
||||
* involve multiple services (purchases, bids, etc.)
|
||||
*/
|
||||
public class TransactionService {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public TransactionService(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an item before it can be listed or auctioned.
|
||||
* Checks material blacklist, keywords in name/lore, and other restrictions.
|
||||
*
|
||||
* @param item The item to validate
|
||||
* @return ValidationResult with success status and error message if applicable
|
||||
*/
|
||||
public ValidationResult validateItem(ItemStack item) {
|
||||
if (item == null || item.getType().isAir()) {
|
||||
return new ValidationResult(false, "invalid-item");
|
||||
}
|
||||
|
||||
// Check material blacklist
|
||||
if (plugin.getConfigManager().isMaterialBlacklisted(item.getType())) {
|
||||
return new ValidationResult(false, "blacklisted-item");
|
||||
}
|
||||
|
||||
// Check for blacklisted keywords in item name/lore
|
||||
if (item.hasItemMeta()) {
|
||||
var meta = item.getItemMeta();
|
||||
|
||||
// Check display name
|
||||
if (meta.hasDisplayName()) {
|
||||
String displayName = meta.getDisplayName();
|
||||
if (plugin.getConfigManager().containsBlacklistedKeyword(displayName)) {
|
||||
return new ValidationResult(false, "blacklisted-content");
|
||||
}
|
||||
}
|
||||
|
||||
// Check lore
|
||||
if (meta.hasLore()) {
|
||||
for (String loreLine : meta.getLore()) {
|
||||
if (plugin.getConfigManager().containsBlacklistedKeyword(loreLine)) {
|
||||
return new ValidationResult(false, "blacklisted-content");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ValidationResult(true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a price for a listing
|
||||
*
|
||||
* @param price The price to validate
|
||||
* @return true if the price is within allowed range
|
||||
*/
|
||||
public boolean validateListingPrice(double price) {
|
||||
double min = plugin.getConfigManager().getMinPrice();
|
||||
double max = plugin.getConfigManager().getMaxPrice();
|
||||
return price >= min && price <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a starting price for an auction
|
||||
*
|
||||
* @param price The price to validate
|
||||
* @return true if the price is within allowed range
|
||||
*/
|
||||
public boolean validateAuctionStartPrice(double price) {
|
||||
double min = plugin.getConfigManager().getMinStartPrice();
|
||||
double max = plugin.getConfigManager().getMaxStartPrice();
|
||||
return price >= min && price <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an auction duration
|
||||
*
|
||||
* @param hours The duration in hours
|
||||
* @return true if the duration is within allowed range
|
||||
*/
|
||||
public boolean validateAuctionDuration(int hours) {
|
||||
int min = plugin.getConfigManager().getMinDurationHours();
|
||||
int max = plugin.getConfigManager().getMaxDurationHours();
|
||||
return hours >= min && hours <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tax amount for a listing sale
|
||||
*
|
||||
* @param salePrice The sale price
|
||||
* @return The tax amount
|
||||
*/
|
||||
public double calculateListingTax(double salePrice) {
|
||||
double taxPercent = plugin.getConfigManager().getMarketTax();
|
||||
return salePrice * (taxPercent / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the seller's earnings after tax for a listing
|
||||
*
|
||||
* @param salePrice The sale price
|
||||
* @return The amount the seller receives
|
||||
*/
|
||||
public double calculateListingEarnings(double salePrice) {
|
||||
return salePrice - calculateListingTax(salePrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tax amount for an auction sale
|
||||
*
|
||||
* @param salePrice The final sale price
|
||||
* @return The tax amount
|
||||
*/
|
||||
public double calculateAuctionTax(double salePrice) {
|
||||
double taxPercent = plugin.getConfigManager().getAuctionTax();
|
||||
return salePrice * (taxPercent / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the seller's earnings after tax for an auction
|
||||
*
|
||||
* @param salePrice The final sale price
|
||||
* @return The amount the seller receives
|
||||
*/
|
||||
public double calculateAuctionEarnings(double salePrice) {
|
||||
return salePrice - calculateAuctionTax(salePrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a listing with full validation and item removal
|
||||
*
|
||||
* @param player The seller
|
||||
* @param item The item to list
|
||||
* @param amount Amount to list
|
||||
* @param price Total price
|
||||
* @param durationHours Duration in hours
|
||||
* @return CompletableFuture with transaction result
|
||||
*/
|
||||
public CompletableFuture<TransactionResult> createListingTransaction(
|
||||
Player player, ItemStack item, int amount, double price, int durationHours) {
|
||||
|
||||
// Validate item
|
||||
ValidationResult validation = validateItem(item);
|
||||
if (!validation.isValid()) {
|
||||
return CompletableFuture.completedFuture(
|
||||
TransactionResult.failed(validation.getErrorKey()));
|
||||
}
|
||||
|
||||
// Validate price
|
||||
if (!validateListingPrice(price)) {
|
||||
return CompletableFuture.completedFuture(
|
||||
TransactionResult.failed("invalid-price"));
|
||||
}
|
||||
|
||||
// Check listing limit
|
||||
return plugin.getListingService().canCreateListing(player.getUniqueId())
|
||||
.thenCompose(canCreate -> {
|
||||
if (!canCreate) {
|
||||
// Check if it's cooldown or limit
|
||||
return plugin.getListingService().getRemainingCooldown(player.getUniqueId())
|
||||
.thenApply(cooldown -> {
|
||||
if (cooldown > 0) {
|
||||
return TransactionResult.failed("listing-cooldown");
|
||||
}
|
||||
return TransactionResult.failed("listing-limit-reached");
|
||||
});
|
||||
}
|
||||
|
||||
// Create the listing
|
||||
ItemStack listItem = item.clone();
|
||||
listItem.setAmount(amount);
|
||||
|
||||
return plugin.getListingService().createListing(player, listItem, amount, price, durationHours)
|
||||
.thenApply(id -> {
|
||||
if (id > 0) {
|
||||
return TransactionResult.success(id);
|
||||
}
|
||||
return TransactionResult.failed("failed");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an auction with full validation and item removal
|
||||
*
|
||||
* @param player The seller
|
||||
* @param item The item to auction
|
||||
* @param startPrice Starting bid
|
||||
* @param buyoutPrice Optional buyout price
|
||||
* @param durationHours Duration in hours
|
||||
* @return CompletableFuture with transaction result
|
||||
*/
|
||||
public CompletableFuture<TransactionResult> createAuctionTransaction(
|
||||
Player player, ItemStack item, double startPrice, Double buyoutPrice, int durationHours) {
|
||||
|
||||
// Validate item
|
||||
ValidationResult validation = validateItem(item);
|
||||
if (!validation.isValid()) {
|
||||
return CompletableFuture.completedFuture(
|
||||
TransactionResult.failed(validation.getErrorKey()));
|
||||
}
|
||||
|
||||
// Validate prices
|
||||
if (!validateAuctionStartPrice(startPrice)) {
|
||||
return CompletableFuture.completedFuture(
|
||||
TransactionResult.failed("invalid-price"));
|
||||
}
|
||||
|
||||
if (buyoutPrice != null && buyoutPrice <= startPrice) {
|
||||
return CompletableFuture.completedFuture(
|
||||
TransactionResult.failed("invalid-price"));
|
||||
}
|
||||
|
||||
// Validate duration
|
||||
if (!validateAuctionDuration(durationHours)) {
|
||||
return CompletableFuture.completedFuture(
|
||||
TransactionResult.failed("invalid-duration"));
|
||||
}
|
||||
|
||||
// Check auction limit
|
||||
return plugin.getAuctionService().canCreateAuction(player.getUniqueId())
|
||||
.thenCompose(canCreate -> {
|
||||
if (!canCreate) {
|
||||
return CompletableFuture.completedFuture(
|
||||
TransactionResult.failed("auction-limit-reached"));
|
||||
}
|
||||
|
||||
// Create the auction
|
||||
return plugin.getAuctionService().createAuction(
|
||||
player, item.clone(), startPrice, buyoutPrice, durationHours)
|
||||
.thenApply(id -> {
|
||||
if (id > 0) {
|
||||
return TransactionResult.success(id);
|
||||
}
|
||||
return TransactionResult.failed("failed");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of item validation
|
||||
*/
|
||||
public record ValidationResult(boolean isValid, String errorKey) {
|
||||
public String getErrorKey() {
|
||||
return errorKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a transaction
|
||||
*/
|
||||
public static class TransactionResult {
|
||||
private final boolean success;
|
||||
private final int id;
|
||||
private final String errorKey;
|
||||
|
||||
private TransactionResult(boolean success, int id, String errorKey) {
|
||||
this.success = success;
|
||||
this.id = id;
|
||||
this.errorKey = errorKey;
|
||||
}
|
||||
|
||||
public static TransactionResult success(int id) {
|
||||
return new TransactionResult(true, id, null);
|
||||
}
|
||||
|
||||
public static TransactionResult failed(String errorKey) {
|
||||
return new TransactionResult(false, -1, errorKey);
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getErrorKey() {
|
||||
return errorKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package pt.henrique.communityMarket.task;
|
||||
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
/**
|
||||
* Periodic task that checks for ended auctions and processes them.
|
||||
* Runs asynchronously to avoid blocking the main thread.
|
||||
*/
|
||||
public class AuctionTask extends BukkitRunnable {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public AuctionTask(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Process ended auctions
|
||||
plugin.getAuctionService().processEndedAuctions()
|
||||
.exceptionally(ex -> {
|
||||
plugin.getLogger().warning("Error processing ended auctions: " + ex.getMessage());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package pt.henrique.communityMarket.task;
|
||||
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
/**
|
||||
* Periodic task that checks for expired listings and moves items to claim storage.
|
||||
* Runs asynchronously to avoid blocking the main thread.
|
||||
*/
|
||||
public class ExpiredListingTask extends BukkitRunnable {
|
||||
|
||||
private final CommunityMarket plugin;
|
||||
|
||||
public ExpiredListingTask(CommunityMarket plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Process expired listings
|
||||
plugin.getListingService().processExpiredListings()
|
||||
.exceptionally(ex -> {
|
||||
plugin.getLogger().warning("Error processing expired listings: " + ex.getMessage());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package pt.henrique.communityMarket.util;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import pt.henrique.communityMarket.CommunityMarket;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility class for inventory operations
|
||||
*/
|
||||
public class InventoryUtil {
|
||||
|
||||
/**
|
||||
* Checks if a player has space for an item in their inventory
|
||||
*
|
||||
* @param player The player
|
||||
* @param item The item to check
|
||||
* @return True if the player has space
|
||||
*/
|
||||
public static boolean hasSpace(Player player, ItemStack item) {
|
||||
if (item == null || item.getType().isAir()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to add a clone and see if anything remains
|
||||
ItemStack clone = item.clone();
|
||||
Map<Integer, ItemStack> leftover = new HashMap<>();
|
||||
|
||||
// Check each slot
|
||||
for (int i = 0; i < 36; i++) {
|
||||
ItemStack slot = player.getInventory().getItem(i);
|
||||
|
||||
if (slot == null || slot.getType().isAir()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (slot.isSimilar(clone) && slot.getAmount() < slot.getMaxStackSize()) {
|
||||
int canAdd = slot.getMaxStackSize() - slot.getAmount();
|
||||
if (canAdd >= clone.getAmount()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an item to a player, returning any items that couldn't fit
|
||||
*
|
||||
* @param player The player
|
||||
* @param item The item to give
|
||||
* @return Items that couldn't fit, or null if all items were added
|
||||
*/
|
||||
public static ItemStack giveItem(Player player, ItemStack item) {
|
||||
if (item == null || item.getType().isAir()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HashMap<Integer, ItemStack> leftover = player.getInventory().addItem(item.clone());
|
||||
|
||||
if (leftover.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return leftover.values().iterator().next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specific amount of an item from a player's inventory
|
||||
*
|
||||
* @param player The player
|
||||
* @param item The item type to remove
|
||||
* @param amount The amount to remove
|
||||
* @return True if the full amount was removed
|
||||
*/
|
||||
public static boolean removeItem(Player player, ItemStack item, int amount) {
|
||||
if (item == null || amount <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ItemStack toRemove = item.clone();
|
||||
toRemove.setAmount(amount);
|
||||
|
||||
HashMap<Integer, ItemStack> notRemoved = player.getInventory().removeItem(toRemove);
|
||||
|
||||
return notRemoved.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how many of a specific item a player has
|
||||
*
|
||||
* @param player The player
|
||||
* @param item The item type to count
|
||||
* @return Total count
|
||||
*/
|
||||
public static int countItem(Player player, ItemStack item) {
|
||||
if (item == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (ItemStack slot : player.getInventory().getContents()) {
|
||||
if (slot != null && slot.isSimilar(item)) {
|
||||
count += slot.getAmount();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how many similar items exist in an inventory.
|
||||
* Uses strict comparison including all metadata (material, name, lore,
|
||||
* enchantments, custom model data, etc.).
|
||||
*
|
||||
* @param inventory The inventory to search
|
||||
* @param item The item to match against
|
||||
* @return Total count of matching items
|
||||
*/
|
||||
public static int countSimilarItems(org.bukkit.inventory.Inventory inventory, ItemStack item) {
|
||||
if (item == null || inventory == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (ItemStack slot : inventory.getContents()) {
|
||||
if (slot != null && slot.isSimilar(item)) {
|
||||
count += slot.getAmount();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a display name for an item, using material name if custom name is not set
|
||||
*
|
||||
* @param item The item
|
||||
* @return Display name
|
||||
*/
|
||||
public static String getDisplayName(ItemStack item) {
|
||||
if (item == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null && meta.hasDisplayName()) {
|
||||
return TextUtil.stripColor(meta.getDisplayName());
|
||||
}
|
||||
|
||||
return formatMaterialName(item.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a material name into a readable string
|
||||
*
|
||||
* @param material The material
|
||||
* @return Formatted name (e.g., "Diamond Sword")
|
||||
*/
|
||||
public static String formatMaterialName(Material material) {
|
||||
String name = material.name().replace("_", " ").toLowerCase();
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean capitalizeNext = true;
|
||||
|
||||
for (char c : name.toCharArray()) {
|
||||
if (c == ' ') {
|
||||
result.append(c);
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
result.append(Character.toUpperCase(c));
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the category of an item for filtering purposes
|
||||
*
|
||||
* @param item The item
|
||||
* @return Category name
|
||||
*/
|
||||
public static ItemCategory getCategory(ItemStack item) {
|
||||
if (item == null) {
|
||||
return ItemCategory.MISC;
|
||||
}
|
||||
|
||||
Material material = item.getType();
|
||||
String name = material.name();
|
||||
|
||||
// Check for enchantments first
|
||||
if (item.hasItemMeta() && item.getItemMeta().hasEnchants()) {
|
||||
return ItemCategory.ENCHANTED;
|
||||
}
|
||||
|
||||
// Weapons
|
||||
if (name.endsWith("_SWORD") || name.endsWith("_AXE") || name.equals("BOW") ||
|
||||
name.equals("CROSSBOW") || name.equals("TRIDENT") || name.equals("MACE")) {
|
||||
return ItemCategory.WEAPONS;
|
||||
}
|
||||
|
||||
// Armor
|
||||
if (name.endsWith("_HELMET") || name.endsWith("_CHESTPLATE") ||
|
||||
name.endsWith("_LEGGINGS") || name.endsWith("_BOOTS") ||
|
||||
name.equals("SHIELD") || name.equals("ELYTRA")) {
|
||||
return ItemCategory.ARMOR;
|
||||
}
|
||||
|
||||
// Tools
|
||||
if (name.endsWith("_PICKAXE") || name.endsWith("_SHOVEL") ||
|
||||
name.endsWith("_HOE") || name.equals("FISHING_ROD") ||
|
||||
name.equals("FLINT_AND_STEEL") || name.equals("SHEARS")) {
|
||||
return ItemCategory.TOOLS;
|
||||
}
|
||||
|
||||
// Blocks
|
||||
if (material.isBlock()) {
|
||||
return ItemCategory.BLOCKS;
|
||||
}
|
||||
|
||||
// Food
|
||||
if (material.isEdible()) {
|
||||
return ItemCategory.FOOD;
|
||||
}
|
||||
|
||||
// Potions
|
||||
if (name.contains("POTION") || name.equals("DRAGON_BREATH")) {
|
||||
return ItemCategory.POTIONS;
|
||||
}
|
||||
|
||||
return ItemCategory.MISC;
|
||||
}
|
||||
|
||||
public enum ItemCategory {
|
||||
ALL("All Items", Material.CHEST),
|
||||
WEAPONS("Weapons", Material.DIAMOND_SWORD),
|
||||
ARMOR("Armor", Material.DIAMOND_CHESTPLATE),
|
||||
TOOLS("Tools", Material.DIAMOND_PICKAXE),
|
||||
BLOCKS("Blocks", Material.GRASS_BLOCK),
|
||||
FOOD("Food", Material.GOLDEN_APPLE),
|
||||
POTIONS("Potions", Material.POTION),
|
||||
ENCHANTED("Enchanted", Material.ENCHANTED_BOOK),
|
||||
MISC("Miscellaneous", Material.ENDER_PEARL);
|
||||
|
||||
private final String displayName;
|
||||
private final Material icon;
|
||||
|
||||
ItemCategory(String displayName, Material icon) {
|
||||
this.displayName = displayName;
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public Material getIcon() {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package pt.henrique.communityMarket.util;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Fluent builder for creating ItemStacks with custom properties.
|
||||
* Used throughout the GUI system to create menu items.
|
||||
*/
|
||||
public class ItemBuilder {
|
||||
|
||||
private final ItemStack item;
|
||||
private final ItemMeta meta;
|
||||
|
||||
/**
|
||||
* Creates a new ItemBuilder with the specified material
|
||||
*/
|
||||
public ItemBuilder(Material material) {
|
||||
this(material, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ItemBuilder with the specified material and amount
|
||||
*/
|
||||
public ItemBuilder(Material material, int amount) {
|
||||
this.item = new ItemStack(material, amount);
|
||||
this.meta = item.getItemMeta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ItemBuilder from an existing ItemStack
|
||||
*/
|
||||
public ItemBuilder(ItemStack item) {
|
||||
this.item = item.clone();
|
||||
this.meta = this.item.getItemMeta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display name using legacy color codes
|
||||
*/
|
||||
public ItemBuilder name(String name) {
|
||||
if (meta != null) {
|
||||
meta.displayName(TextUtil.colorize(name));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display name using a Component
|
||||
*/
|
||||
public ItemBuilder name(Component name) {
|
||||
if (meta != null) {
|
||||
meta.displayName(name);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lore using a list of strings with legacy color codes
|
||||
*/
|
||||
public ItemBuilder lore(List<String> lore) {
|
||||
if (meta != null && lore != null) {
|
||||
List<Component> components = lore.stream()
|
||||
.map(TextUtil::colorize)
|
||||
.collect(Collectors.toList());
|
||||
meta.lore(components);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lore using varargs strings
|
||||
*/
|
||||
public ItemBuilder lore(String... lore) {
|
||||
return lore(List.of(lore));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a line to the existing lore
|
||||
*/
|
||||
public ItemBuilder addLore(String line) {
|
||||
if (meta != null) {
|
||||
List<Component> existingLore = meta.lore();
|
||||
List<Component> newLore = existingLore != null ? new ArrayList<>(existingLore) : new ArrayList<>();
|
||||
newLore.add(TextUtil.colorize(line));
|
||||
meta.lore(newLore);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple lines to the existing lore
|
||||
*/
|
||||
public ItemBuilder addLore(List<String> lines) {
|
||||
if (meta != null && lines != null) {
|
||||
List<Component> existingLore = meta.lore();
|
||||
List<Component> newLore = existingLore != null ? new ArrayList<>(existingLore) : new ArrayList<>();
|
||||
for (String line : lines) {
|
||||
newLore.add(TextUtil.colorize(line));
|
||||
}
|
||||
meta.lore(newLore);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item amount
|
||||
*/
|
||||
public ItemBuilder amount(int amount) {
|
||||
item.setAmount(Math.min(64, Math.max(1, amount)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an enchantment glow effect (uses LUCK with HIDE_ENCHANTS flag)
|
||||
*/
|
||||
public ItemBuilder glow(boolean glow) {
|
||||
if (glow && meta != null) {
|
||||
meta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always adds glow effect
|
||||
*/
|
||||
public ItemBuilder glow() {
|
||||
return glow(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an enchantment
|
||||
*/
|
||||
public ItemBuilder enchant(Enchantment enchantment, int level) {
|
||||
if (meta != null) {
|
||||
meta.addEnchant(enchantment, level, true);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides all item flags (enchants, attributes, etc.)
|
||||
*/
|
||||
public ItemBuilder hideFlags() {
|
||||
if (meta != null) {
|
||||
meta.addItemFlags(ItemFlag.values());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds specific item flags
|
||||
*/
|
||||
public ItemBuilder addFlags(ItemFlag... flags) {
|
||||
if (meta != null) {
|
||||
meta.addItemFlags(flags);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item as unbreakable
|
||||
*/
|
||||
public ItemBuilder unbreakable(boolean unbreakable) {
|
||||
if (meta != null) {
|
||||
meta.setUnbreakable(unbreakable);
|
||||
if (unbreakable) {
|
||||
meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets custom model data
|
||||
*/
|
||||
public ItemBuilder customModelData(int data) {
|
||||
if (meta != null) {
|
||||
meta.setCustomModelData(data);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the final ItemStack
|
||||
*/
|
||||
public ItemStack build() {
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of an ItemStack with modified lore
|
||||
*/
|
||||
public static ItemStack withLore(ItemStack original, List<String> lore) {
|
||||
return new ItemBuilder(original).lore(lore).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of an ItemStack with additional lore appended
|
||||
*/
|
||||
public static ItemStack appendLore(ItemStack original, List<String> additionalLore) {
|
||||
return new ItemBuilder(original).addLore(additionalLore).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package pt.henrique.communityMarket.util;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Utility class for serializing and deserializing ItemStacks to/from Base64 strings
|
||||
*/
|
||||
public class ItemSerializer {
|
||||
|
||||
/**
|
||||
* Serializes an ItemStack to a Base64 encoded string
|
||||
*
|
||||
* @param item The ItemStack to serialize
|
||||
* @return Base64 encoded string representation
|
||||
* @throws IOException If serialization fails
|
||||
*/
|
||||
public static String serialize(ItemStack item) throws IOException {
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
|
||||
|
||||
dataOutput.writeObject(item);
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a Base64 encoded string to an ItemStack
|
||||
*
|
||||
* @param data The Base64 encoded string
|
||||
* @return The deserialized ItemStack
|
||||
* @throws IOException If deserialization fails
|
||||
* @throws ClassNotFoundException If the class is not found
|
||||
*/
|
||||
public static ItemStack deserialize(String data) throws IOException, ClassNotFoundException {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] bytes = Base64.getDecoder().decode(data);
|
||||
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) {
|
||||
|
||||
return (ItemStack) dataInput.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely deserializes an ItemStack, returning null if any error occurs
|
||||
*
|
||||
* @param data The Base64 encoded string
|
||||
* @return The deserialized ItemStack or null if failed
|
||||
*/
|
||||
public static ItemStack deserializeSafe(String data) {
|
||||
try {
|
||||
return deserialize(data);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package pt.henrique.communityMarket.util;
|
||||
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Utility class for playing sounds.
|
||||
* Handles Sound API changes gracefully for Paper 1.21+.
|
||||
*/
|
||||
public class SoundUtil {
|
||||
|
||||
/**
|
||||
* Plays a sound to a player by name.
|
||||
*
|
||||
* @param player The player
|
||||
* @param soundName The sound name (e.g., "UI_BUTTON_CLICK" or "ui.button.click")
|
||||
* @param volume Volume (0.0 - 1.0)
|
||||
* @param pitch Pitch (0.5 - 2.0)
|
||||
*/
|
||||
public static void playSound(Player player, String soundName, float volume, float pitch) {
|
||||
if (player == null || soundName == null || soundName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Sound sound = findSound(soundName);
|
||||
if (sound != null) {
|
||||
player.playSound(player.getLocation(), sound, volume, pitch);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Sound not found or error playing, ignore silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a sound with default volume and pitch.
|
||||
*/
|
||||
public static void playSound(Player player, String soundName) {
|
||||
playSound(player, soundName, 0.5f, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a Sound by name using the Registry API (Paper 1.21+).
|
||||
* Supports both formats: "UI_BUTTON_CLICK" and "ui.button.click"
|
||||
*/
|
||||
private static Sound findSound(String name) {
|
||||
if (name == null) return null;
|
||||
|
||||
// Convert legacy format (UI_BUTTON_CLICK) to namespaced format (ui.button.click)
|
||||
String key = name.toLowerCase().replace("_", ".");
|
||||
|
||||
// Try direct lookup with the converted key
|
||||
Sound sound = Registry.SOUNDS.get(NamespacedKey.minecraft(key));
|
||||
if (sound != null) {
|
||||
return sound;
|
||||
}
|
||||
|
||||
// Try some common mappings for legacy sound names
|
||||
String mappedKey = mapLegacySoundName(name);
|
||||
if (mappedKey != null) {
|
||||
sound = Registry.SOUNDS.get(NamespacedKey.minecraft(mappedKey));
|
||||
if (sound != null) {
|
||||
return sound;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try the original name as-is (lowercase)
|
||||
return Registry.SOUNDS.get(NamespacedKey.minecraft(name.toLowerCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps legacy enum-style sound names to their registry keys.
|
||||
*/
|
||||
private static String mapLegacySoundName(String legacyName) {
|
||||
if (legacyName == null) return null;
|
||||
|
||||
String upper = legacyName.toUpperCase();
|
||||
|
||||
return switch (upper) {
|
||||
case "UI_BUTTON_CLICK" -> "ui.button.click";
|
||||
case "ENTITY_PLAYER_LEVELUP" -> "entity.player.levelup";
|
||||
case "ENTITY_VILLAGER_NO" -> "entity.villager.no";
|
||||
case "ENTITY_EXPERIENCE_ORB_PICKUP" -> "entity.experience_orb.pickup";
|
||||
case "BLOCK_NOTE_BLOCK_PLING" -> "block.note_block.pling";
|
||||
case "ENTITY_ITEM_PICKUP" -> "entity.item.pickup";
|
||||
case "BLOCK_CHEST_OPEN" -> "block.chest.open";
|
||||
case "BLOCK_CHEST_CLOSE" -> "block.chest.close";
|
||||
case "ENTITY_ARROW_HIT_PLAYER" -> "entity.arrow.hit_player";
|
||||
case "BLOCK_ANVIL_USE" -> "block.anvil.use";
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Common sounds for easy access
|
||||
*/
|
||||
public static void playClickSound(Player player) {
|
||||
playSound(player, "ui.button.click", 0.5f, 1.0f);
|
||||
}
|
||||
|
||||
public static void playSuccessSound(Player player) {
|
||||
playSound(player, "entity.player.levelup", 0.5f, 1.5f);
|
||||
}
|
||||
|
||||
public static void playErrorSound(Player player) {
|
||||
playSound(player, "entity.villager.no", 0.5f, 1.0f);
|
||||
}
|
||||
|
||||
public static void playPurchaseSound(Player player) {
|
||||
playSound(player, "entity.experience_orb.pickup", 0.5f, 1.0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package pt.henrique.communityMarket.util;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility class for text formatting and color code handling
|
||||
*/
|
||||
public class TextUtil {
|
||||
|
||||
private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})");
|
||||
private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacyAmpersand();
|
||||
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
|
||||
|
||||
/**
|
||||
* Colorizes a string using legacy & color codes
|
||||
*
|
||||
* @param text The text to colorize
|
||||
* @return Colorized Component
|
||||
*/
|
||||
public static Component colorize(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
// Convert hex codes like &#FFFFFF to adventure format
|
||||
text = HEX_PATTERN.matcher(text).replaceAll("<#$1>");
|
||||
|
||||
// First try legacy format, then MiniMessage
|
||||
try {
|
||||
Component component = LEGACY_SERIALIZER.deserialize(text);
|
||||
// Remove italic decoration that gets added by default
|
||||
return component.decoration(TextDecoration.ITALIC, false);
|
||||
} catch (Exception e) {
|
||||
return MINI_MESSAGE.deserialize(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorizes a string and returns the legacy string representation
|
||||
*
|
||||
* @param text The text to colorize
|
||||
* @return Colorized string with § codes
|
||||
*/
|
||||
public static String colorizeToString(String text) {
|
||||
return LegacyComponentSerializer.legacySection().serialize(colorize(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips color codes from a string
|
||||
*
|
||||
* @param text The text to strip
|
||||
* @return Text without color codes
|
||||
*/
|
||||
public static String stripColor(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return text.replaceAll("(?i)[&§][0-9a-fk-or]", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration into a human-readable string
|
||||
*
|
||||
* @param duration The duration to format
|
||||
* @return Formatted string (e.g., "2d 5h 30m")
|
||||
*/
|
||||
public static String formatDuration(Duration duration) {
|
||||
if (duration.isNegative() || duration.isZero()) {
|
||||
return "Expired";
|
||||
}
|
||||
|
||||
long days = duration.toDays();
|
||||
long hours = duration.toHoursPart();
|
||||
long minutes = duration.toMinutesPart();
|
||||
long seconds = duration.toSecondsPart();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (days > 0) {
|
||||
sb.append(days).append("d ");
|
||||
}
|
||||
if (hours > 0) {
|
||||
sb.append(hours).append("h ");
|
||||
}
|
||||
if (minutes > 0 && days == 0) {
|
||||
sb.append(minutes).append("m ");
|
||||
}
|
||||
if (seconds > 0 && days == 0 && hours == 0) {
|
||||
sb.append(seconds).append("s");
|
||||
}
|
||||
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration from now until a future instant
|
||||
*
|
||||
* @param future The future instant
|
||||
* @return Formatted duration string
|
||||
*/
|
||||
public static String formatTimeUntil(Instant future) {
|
||||
if (future == null) {
|
||||
return "Never";
|
||||
}
|
||||
|
||||
Duration duration = Duration.between(Instant.now(), future);
|
||||
return formatDuration(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a duration is considered "ending soon" (less than 5 minutes)
|
||||
*
|
||||
* @param endsAt The end time
|
||||
* @return True if ending soon
|
||||
*/
|
||||
public static boolean isEndingSoon(Instant endsAt) {
|
||||
if (endsAt == null) {
|
||||
return false;
|
||||
}
|
||||
Duration remaining = Duration.between(Instant.now(), endsAt);
|
||||
return !remaining.isNegative() && remaining.toMinutes() < 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a string to a maximum length
|
||||
*
|
||||
* @param text The text to truncate
|
||||
* @param maxLength Maximum length
|
||||
* @return Truncated string with "..." if needed
|
||||
*/
|
||||
public static String truncate(String text, int maxLength) {
|
||||
if (text == null || text.length() <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# ============================================
|
||||
# CommunityMarket Configuration
|
||||
# A GUI-only marketplace plugin
|
||||
# ============================================
|
||||
|
||||
# Language setting (available: en_US, pt_PT)
|
||||
language: en_US
|
||||
|
||||
# Database Configuration
|
||||
database:
|
||||
# Type: sqlite or mysql
|
||||
type: sqlite
|
||||
|
||||
sqlite:
|
||||
file: database.db
|
||||
|
||||
mysql:
|
||||
host: localhost
|
||||
port: 3306
|
||||
database: communitymarket
|
||||
username: root
|
||||
password: ""
|
||||
pool:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
# Economy Settings
|
||||
economy:
|
||||
# Currency display format (uses Java DecimalFormat)
|
||||
currency-format: "$#,##0.00"
|
||||
currency-symbol: "$"
|
||||
|
||||
taxes:
|
||||
# Tax percentage for fixed-price market sales (seller pays)
|
||||
market-tax: 5.0
|
||||
# Tax percentage for auction sales (seller pays)
|
||||
auction-tax: 7.5
|
||||
|
||||
# Fixed-Price Market Settings
|
||||
market:
|
||||
# Maximum active listings per player
|
||||
max-listings-per-player: 20
|
||||
# Cooldown between creating listings (seconds, 0 = disabled)
|
||||
listing-cooldown: 0
|
||||
# Default listing duration in hours
|
||||
default-duration-hours: 168
|
||||
# Available durations for players to choose (in hours)
|
||||
available-durations:
|
||||
- 24
|
||||
- 72
|
||||
- 168
|
||||
- 336
|
||||
# Price limits
|
||||
min-price: 1.0
|
||||
max-price: 1000000000.0
|
||||
|
||||
# Auction Settings
|
||||
auction:
|
||||
# Maximum active auctions per player
|
||||
max-auctions-per-player: 10
|
||||
# Duration limits (hours)
|
||||
min-duration-hours: 1
|
||||
max-duration-hours: 168
|
||||
default-duration-hours: 24
|
||||
# Available durations for players to choose (in hours)
|
||||
available-durations:
|
||||
- 1
|
||||
- 6
|
||||
- 12
|
||||
- 24
|
||||
- 48
|
||||
- 72
|
||||
- 168
|
||||
# Price limits for starting bid
|
||||
min-start-price: 1.0
|
||||
max-start-price: 1000000000.0
|
||||
# Minimum bid increment (percentage of current bid)
|
||||
min-bid-increment-percent: 5.0
|
||||
# Minimum absolute bid increment
|
||||
min-bid-increment-absolute: 1.0
|
||||
|
||||
# Anti-snipe protection
|
||||
anti-snipe:
|
||||
enabled: true
|
||||
# If bid placed within this many seconds of end, extend auction
|
||||
trigger-seconds: 30
|
||||
# How many seconds to extend
|
||||
extension-seconds: 30
|
||||
# Maximum number of extensions (0 = unlimited)
|
||||
max-extensions: 10
|
||||
|
||||
# Item Blacklist
|
||||
blacklist:
|
||||
# Materials that cannot be listed/auctioned
|
||||
materials:
|
||||
- BARRIER
|
||||
- COMMAND_BLOCK
|
||||
- CHAIN_COMMAND_BLOCK
|
||||
- REPEATING_COMMAND_BLOCK
|
||||
- COMMAND_BLOCK_MINECART
|
||||
- STRUCTURE_BLOCK
|
||||
- STRUCTURE_VOID
|
||||
- JIGSAW
|
||||
- DEBUG_STICK
|
||||
- KNOWLEDGE_BOOK
|
||||
- SPAWNER
|
||||
- BEDROCK
|
||||
# Items with these keywords in their name/lore are blocked
|
||||
keywords:
|
||||
- "admin"
|
||||
- "illegal"
|
||||
- "exploit"
|
||||
|
||||
# GUI Settings
|
||||
gui:
|
||||
# Items displayed per page in browse views
|
||||
items-per-page: 45
|
||||
|
||||
# Whether to show the Help button in the main menu
|
||||
# Set to false to hide it (slot will be filled with glass)
|
||||
show-help-button: true
|
||||
|
||||
# Sound effects (use Bukkit Sound enum names)
|
||||
sounds:
|
||||
click: UI_BUTTON_CLICK
|
||||
success: ENTITY_PLAYER_LEVELUP
|
||||
error: ENTITY_VILLAGER_NO
|
||||
purchase: ENTITY_EXPERIENCE_ORB_PICKUP
|
||||
|
||||
# Notification Settings
|
||||
notifications:
|
||||
# Notify seller when their item sells
|
||||
notify-on-sale: true
|
||||
# Notify bidder when they are outbid
|
||||
notify-on-outbid: true
|
||||
# Notify winner when they win an auction
|
||||
notify-on-win: true
|
||||
# Notify seller when their listing expires
|
||||
notify-on-expire: true
|
||||
|
||||
# Performance Settings
|
||||
performance:
|
||||
# How often to cache listings/auctions (seconds)
|
||||
cache-duration: 30
|
||||
# How often to check for ended auctions (seconds)
|
||||
auction-check-interval: 5
|
||||
# How often to check for expired listings (minutes)
|
||||
expired-check-interval: 5
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
# ============================================
|
||||
# CommunityMarket Language File - English (US)
|
||||
# ============================================
|
||||
|
||||
# General
|
||||
prefix: "&8[&6Market&8] &r"
|
||||
|
||||
# General Messages
|
||||
messages:
|
||||
no-permission: "&cYou don't have permission to do that."
|
||||
player-only: "&cThis command can only be used by players."
|
||||
reload-success: "&aConfiguration reloaded successfully!"
|
||||
economy-not-found: "&cNo economy plugin found! Market disabled."
|
||||
|
||||
# Listing Messages
|
||||
listing-created: "&aListing created successfully! ID: #{id}"
|
||||
listing-cancelled: "&aListing cancelled. Item returned to claim storage."
|
||||
listing-expired: "&eYour listing #{id} has expired. Item moved to claim storage."
|
||||
listing-purchased: "&aYou purchased {item} x{amount} for {price}!"
|
||||
listing-sold: "&aYour {item} x{amount} was sold to {buyer} for {price}!"
|
||||
listing-limit-reached: "&cYou've reached the maximum number of listings ({max})."
|
||||
listing-cooldown: "&cPlease wait {time} before creating another listing."
|
||||
listing-not-found: "&cListing not found or no longer available."
|
||||
listing-own-item: "&cYou cannot buy your own listing."
|
||||
listing-insufficient-funds: "&cYou don't have enough money. Required: {price}"
|
||||
|
||||
# Auction Messages
|
||||
auction-created: "&aAuction created successfully! ID: #{id}"
|
||||
auction-cancelled: "&aAuction cancelled. Item returned to claim storage."
|
||||
auction-ended-winner: "&aCongratulations! You won the auction for {item}! Paid: {price}"
|
||||
auction-ended-seller: "&aYour auction for {item} ended! Winner: {winner}, Earned: {price}"
|
||||
auction-ended-no-bids: "&eYour auction for {item} ended with no bids. Item moved to claim storage."
|
||||
auction-limit-reached: "&cYou've reached the maximum number of auctions ({max})."
|
||||
auction-not-found: "&cAuction not found or no longer available."
|
||||
auction-own-item: "&cYou cannot bid on your own auction."
|
||||
auction-bid-placed: "&aYou placed a bid of {amount} on {item}!"
|
||||
auction-outbid: "&eYou've been outbid on {item}! New bid: {amount} by {bidder}"
|
||||
auction-bid-too-low: "&cBid too low! Minimum bid: {min}"
|
||||
auction-insufficient-funds: "&cYou don't have enough money. Required: {price}"
|
||||
auction-buyout: "&aYou bought out the auction for {item} for {price}!"
|
||||
auction-extended: "&eAuction extended by {seconds}s due to anti-snipe protection."
|
||||
|
||||
# Claim Messages
|
||||
claim-success: "&aItem claimed successfully!"
|
||||
claim-empty: "&eYou have no items to claim."
|
||||
claim-inventory-full: "&cYour inventory is full! Please make space."
|
||||
claim-all-success: "&aClaimed {count} items!"
|
||||
|
||||
# Earnings Messages
|
||||
earnings-withdrawn: "&aWithdrew {amount}! New balance: {balance}"
|
||||
earnings-empty: "&eYou have no pending earnings."
|
||||
earnings-balance: "&aYour pending earnings: {amount}"
|
||||
|
||||
# Item Validation
|
||||
invalid-item: "&cPlease select a valid item."
|
||||
item-no-longer-available: "&cThe selected item is no longer in your inventory."
|
||||
item-changed: "&cThe selected item has changed. Please select again."
|
||||
quantity-changed: "&cThe available quantity has changed. Please verify and try again."
|
||||
blacklisted-item: "&cThis item type is not allowed on the market."
|
||||
blacklisted-content: "&cThis item contains blacklisted content."
|
||||
invalid-price: "&cInvalid price. Range: {min} - {max}"
|
||||
invalid-amount: "&cInvalid amount."
|
||||
invalid-duration: "&cInvalid duration."
|
||||
|
||||
# Admin Messages
|
||||
admin-listing-removed: "&aListing #{id} removed by admin."
|
||||
admin-auction-cancelled: "&aAuction #{id} cancelled by admin."
|
||||
admin-reload: "&aConfiguration reloaded."
|
||||
|
||||
# GUI Titles (support color codes)
|
||||
gui-titles:
|
||||
main-menu: "&8&lCommunity Market"
|
||||
browse-market: "&8&lBrowse Market &7(Page {page})"
|
||||
browse-auctions: "&8&lBrowse Auctions &7(Page {page})"
|
||||
create-listing: "&8&lCreate Listing"
|
||||
create-auction: "&8&lCreate Auction"
|
||||
select-item-listing: "&8&lSelect Item to Sell"
|
||||
select-item-auction: "&8&lSelect Item to Auction"
|
||||
quantity-select: "&8&lSelect Quantity"
|
||||
my-listings: "&8&lMy Listings"
|
||||
my-auctions: "&8&lMy Auctions"
|
||||
claim-items: "&8&lClaim Items"
|
||||
earnings: "&8&lEarnings"
|
||||
confirm-purchase: "&8&lConfirm Purchase"
|
||||
confirm-bid: "&8&lConfirm Bid"
|
||||
confirm-cancel: "&8&lConfirm Cancellation"
|
||||
number-input: "&8&lEnter Amount"
|
||||
admin-panel: "&8&lAdmin Panel"
|
||||
admin-listings: "&8&lAll Listings"
|
||||
admin-auctions: "&8&lAll Auctions"
|
||||
listing-details: "&8&lListing Details"
|
||||
auction-details: "&8&lAuction Details"
|
||||
filter-menu: "&8&lFilter Options"
|
||||
sort-menu: "&8&lSort Options"
|
||||
duration-select: "&8&lSelect Duration"
|
||||
help: "&8&lHelp"
|
||||
|
||||
# Button Names
|
||||
buttons:
|
||||
# Main Menu
|
||||
browse-market: "&aBrowse Market"
|
||||
browse-auctions: "&6Browse Auctions"
|
||||
create-listing: "&eCreate Listing"
|
||||
create-auction: "&eCreate Auction"
|
||||
my-listings: "&bMy Listings"
|
||||
my-auctions: "&bMy Auctions"
|
||||
claim-items: "&dClaim Items"
|
||||
earnings: "&aEarnings"
|
||||
help: "&fHelp"
|
||||
admin: "&cAdmin Panel"
|
||||
|
||||
# Navigation
|
||||
next-page: "&aNext Page →"
|
||||
previous-page: "&a← Previous Page"
|
||||
back: "&cBack"
|
||||
close: "&cClose"
|
||||
|
||||
# Actions
|
||||
confirm: "&aConfirm"
|
||||
cancel: "&cCancel"
|
||||
buy: "&aBuy Now"
|
||||
bid: "&6Place Bid"
|
||||
buyout: "&eBuyout"
|
||||
claim: "&aClaim"
|
||||
claim-all: "&aClaim All"
|
||||
withdraw: "&aWithdraw All"
|
||||
remove: "&cRemove Listing"
|
||||
cancel-auction: "&cCancel Auction"
|
||||
|
||||
# Number Input
|
||||
add-1: "&a+1"
|
||||
add-10: "&a+10"
|
||||
add-100: "&a+100"
|
||||
add-1000: "&a+1,000"
|
||||
subtract-1: "&c-1"
|
||||
subtract-10: "&c-10"
|
||||
subtract-100: "&c-100"
|
||||
subtract-1000: "&c-1,000"
|
||||
set-min: "&eSet Min"
|
||||
set-max: "&eSet Max"
|
||||
custom-amount: "&bCustom Amount"
|
||||
|
||||
# Filters & Sort
|
||||
filter: "&eFilter"
|
||||
sort: "&eSort"
|
||||
search: "&eSearch"
|
||||
clear-filter: "&cClear Filters"
|
||||
|
||||
# Duration
|
||||
duration-1h: "&e1 Hour"
|
||||
duration-6h: "&e6 Hours"
|
||||
duration-12h: "&e12 Hours"
|
||||
duration-24h: "&e24 Hours"
|
||||
duration-48h: "&e48 Hours"
|
||||
duration-72h: "&e3 Days"
|
||||
duration-168h: "&e7 Days"
|
||||
duration-336h: "&e14 Days"
|
||||
|
||||
# Admin
|
||||
admin-view-listings: "&aView All Listings"
|
||||
admin-view-auctions: "&6View All Auctions"
|
||||
admin-reload: "&eReload Config"
|
||||
|
||||
# Button Lore (descriptions)
|
||||
lore:
|
||||
browse-market:
|
||||
- "&7Browse all fixed-price"
|
||||
- "&7listings from players."
|
||||
- ""
|
||||
- "&eClick to browse!"
|
||||
browse-auctions:
|
||||
- "&7Browse all active auctions"
|
||||
- "&7and place bids."
|
||||
- ""
|
||||
- "&eClick to browse!"
|
||||
create-listing:
|
||||
- "&7Sell items at a fixed price."
|
||||
- "&7Tax: &f{tax}%"
|
||||
- ""
|
||||
- "&eClick to create!"
|
||||
create-auction:
|
||||
- "&7Auction items to the"
|
||||
- "&7highest bidder."
|
||||
- "&7Tax: &f{tax}%"
|
||||
- ""
|
||||
- "&eClick to create!"
|
||||
my-listings:
|
||||
- "&7View and manage your"
|
||||
- "&7active listings."
|
||||
- ""
|
||||
- "&7Active: &f{count}/{max}"
|
||||
- ""
|
||||
- "&eClick to view!"
|
||||
my-auctions:
|
||||
- "&7View and manage your"
|
||||
- "&7active auctions."
|
||||
- ""
|
||||
- "&7Active: &f{count}/{max}"
|
||||
- ""
|
||||
- "&eClick to view!"
|
||||
claim-items:
|
||||
- "&7Claim items from expired"
|
||||
- "&7listings or won auctions."
|
||||
- ""
|
||||
- "&7Pending: &f{count}"
|
||||
- ""
|
||||
- "&eClick to claim!"
|
||||
earnings:
|
||||
- "&7View and withdraw your"
|
||||
- "&7pending earnings from sales."
|
||||
- ""
|
||||
- "&7Pending: &a{amount}"
|
||||
- ""
|
||||
- "&eClick to view!"
|
||||
help:
|
||||
- "&7Learn how to use the"
|
||||
- "&7Community Market."
|
||||
- ""
|
||||
- "&eClick for help!"
|
||||
admin:
|
||||
- "&cAdmin Panel"
|
||||
- "&7Manage listings and auctions."
|
||||
- ""
|
||||
- "&eClick to open!"
|
||||
|
||||
# Listing Info
|
||||
listing-info:
|
||||
- "&7Seller: &f{seller}"
|
||||
- "&7Price: &a{price}"
|
||||
- "&7Amount: &f{amount}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- ""
|
||||
- "&eLeft-click to buy!"
|
||||
|
||||
# Auction Info
|
||||
auction-info:
|
||||
- "&7Seller: &f{seller}"
|
||||
- "&7Starting bid: &a{start_price}"
|
||||
- "&7Current bid: &a{current_bid}"
|
||||
- "&7Bidder: &f{bidder}"
|
||||
- "&7Bids: &f{bid_count}"
|
||||
- "&7Ends: &f{ends}"
|
||||
- ""
|
||||
- "&eLeft-click to bid!"
|
||||
- "&eRight-click to buyout!"
|
||||
|
||||
# My Listing Info
|
||||
my-listing-info:
|
||||
- "&7Price: &a{price}"
|
||||
- "&7Amount: &f{amount}"
|
||||
- "&7Created: &f{created}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- ""
|
||||
- "&cClick to cancel"
|
||||
|
||||
# My Auction Info
|
||||
my-auction-info:
|
||||
- "&7Starting bid: &a{start_price}"
|
||||
- "&7Current bid: &a{current_bid}"
|
||||
- "&7Bidder: &f{bidder}"
|
||||
- "&7Bids: &f{bid_count}"
|
||||
- "&7Ends: &f{ends}"
|
||||
- ""
|
||||
- "&cClick to cancel (if no bids)"
|
||||
|
||||
# Confirm Purchase
|
||||
confirm-purchase-info:
|
||||
- "&7You are purchasing:"
|
||||
- "&f{item} x{amount}"
|
||||
- ""
|
||||
- "&7Price: &a{price}"
|
||||
- "&7Tax: &e{tax}"
|
||||
- "&7Total: &a{total}"
|
||||
- ""
|
||||
- "&aClick to confirm!"
|
||||
|
||||
# Confirm Bid
|
||||
confirm-bid-info:
|
||||
- "&7You are bidding on:"
|
||||
- "&f{item}"
|
||||
- ""
|
||||
- "&7Your bid: &a{bid}"
|
||||
- "&7Current high: &e{current}"
|
||||
- ""
|
||||
- "&aClick to confirm!"
|
||||
|
||||
# Claim Item
|
||||
claim-item-info:
|
||||
- "&7Reason: &f{reason}"
|
||||
- "&7From: &f{source}"
|
||||
- "&7Date: &f{date}"
|
||||
- ""
|
||||
- "&eClick to claim!"
|
||||
|
||||
# Earnings Info
|
||||
earnings-info:
|
||||
- "&7Your pending earnings"
|
||||
- "&7from market sales."
|
||||
- ""
|
||||
- "&7Total: &a{amount}"
|
||||
- ""
|
||||
- "&aClick to withdraw!"
|
||||
|
||||
# Number Input Info
|
||||
current-value:
|
||||
- "&7Current: &a{value}"
|
||||
|
||||
# Filter Options
|
||||
filters:
|
||||
all: "&fAll Items"
|
||||
weapons: "&cWeapons"
|
||||
armor: "&bArmor"
|
||||
tools: "&eTools"
|
||||
blocks: "&7Blocks"
|
||||
food: "&6Food"
|
||||
potions: "&dPotions"
|
||||
materials: "&aMaterials"
|
||||
enchanted: "&5Enchanted Items"
|
||||
misc: "&8Miscellaneous"
|
||||
|
||||
# Sort Options
|
||||
sort:
|
||||
newest: "&aNewest First"
|
||||
oldest: "&eOldest First"
|
||||
price-low: "&aPrice: Low to High"
|
||||
price-high: "&cPrice: High to Low"
|
||||
ending-soon: "&6Ending Soon"
|
||||
most-bids: "&bMost Bids"
|
||||
|
||||
# Time Formats
|
||||
time:
|
||||
expired: "&cExpired"
|
||||
days: "{d}d"
|
||||
hours: "{h}h"
|
||||
minutes: "{m}m"
|
||||
seconds: "{s}s"
|
||||
|
||||
# Help Content
|
||||
help:
|
||||
title: "&6&lCommunity Market Help"
|
||||
content:
|
||||
- "&eBrowse Market &7- View and buy fixed-price listings"
|
||||
- "&eBrowse Auctions &7- View and bid on auctions"
|
||||
- "&eCreate Listing &7- Sell items at a fixed price"
|
||||
- "&eCreate Auction &7- Auction items to highest bidder"
|
||||
- "&eMy Listings &7- Manage your active listings"
|
||||
- "&eMy Auctions &7- Manage your active auctions"
|
||||
- "&eClaim Items &7- Collect unsold/won items"
|
||||
- "&eEarnings &7- Withdraw money from sales"
|
||||
- ""
|
||||
- "&7&oTip: All actions are done through GUIs!"
|
||||
- "&7&oJust click on buttons to navigate."
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
# ============================================
|
||||
# CommunityMarket Ficheiro de Idioma - Português (Portugal)
|
||||
# ============================================
|
||||
|
||||
# Geral
|
||||
prefix: "&8[&6Mercado&8] &r"
|
||||
|
||||
# Mensagens Gerais
|
||||
messages:
|
||||
no-permission: "&cNão tens permissão para fazer isso."
|
||||
player-only: "&cEste comando só pode ser usado por jogadores."
|
||||
reload-success: "&aConfiguração recarregada com sucesso!"
|
||||
economy-not-found: "&cNenhum plugin de economia encontrado! Mercado desativado."
|
||||
|
||||
# Mensagens de Anúncios
|
||||
listing-created: "&aAnúncio criado com sucesso! ID: #{id}"
|
||||
listing-cancelled: "&aAnúncio cancelado. Item devolvido ao armazém."
|
||||
listing-expired: "&eO teu anúncio #{id} expirou. Item movido para o armazém."
|
||||
listing-purchased: "&aCompraste {item} x{amount} por {price}!"
|
||||
listing-sold: "&aO teu {item} x{amount} foi vendido a {buyer} por {price}!"
|
||||
listing-limit-reached: "&cAtingiste o número máximo de anúncios ({max})."
|
||||
listing-cooldown: "&cPor favor aguarda {time} antes de criar outro anúncio."
|
||||
listing-not-found: "&cAnúncio não encontrado ou já não está disponível."
|
||||
listing-own-item: "&cNão podes comprar o teu próprio anúncio."
|
||||
listing-insufficient-funds: "&cNão tens dinheiro suficiente. Necessário: {price}"
|
||||
|
||||
# Mensagens de Leilões
|
||||
auction-created: "&aLeilão criado com sucesso! ID: #{id}"
|
||||
auction-cancelled: "&aLeilão cancelado. Item devolvido ao armazém."
|
||||
auction-ended-winner: "&aParabéns! Ganhaste o leilão por {item}! Pagaste: {price}"
|
||||
auction-ended-seller: "&aO teu leilão por {item} terminou! Vencedor: {winner}, Ganhaste: {price}"
|
||||
auction-ended-no-bids: "&eO teu leilão por {item} terminou sem licitações. Item movido para o armazém."
|
||||
auction-limit-reached: "&cAtingiste o número máximo de leilões ({max})."
|
||||
auction-not-found: "&cLeilão não encontrado ou já não está disponível."
|
||||
auction-own-item: "&cNão podes licitar no teu próprio leilão."
|
||||
auction-bid-placed: "&aFizeste uma licitação de {amount} em {item}!"
|
||||
auction-outbid: "&eForam feitas licitações superiores à tua em {item}! Nova licitação: {amount} por {bidder}"
|
||||
auction-bid-too-low: "&cLicitação muito baixa! Mínimo: {min}"
|
||||
auction-insufficient-funds: "&cNão tens dinheiro suficiente. Necessário: {price}"
|
||||
auction-buyout: "&aCompraste o leilão de {item} por {price}!"
|
||||
auction-extended: "&eLeilão prolongado por {seconds}s devido à proteção anti-snipe."
|
||||
|
||||
# Mensagens de Reclamação
|
||||
claim-success: "&aItem reclamado com sucesso!"
|
||||
claim-empty: "&eNão tens itens para reclamar."
|
||||
claim-inventory-full: "&cO teu inventário está cheio! Por favor liberta espaço."
|
||||
claim-all-success: "&aReclamaste {count} itens!"
|
||||
|
||||
# Mensagens de Ganhos
|
||||
earnings-withdrawn: "&aLevantaste {amount}! Novo saldo: {balance}"
|
||||
earnings-empty: "&eNão tens ganhos pendentes."
|
||||
earnings-balance: "&aOs teus ganhos pendentes: {amount}"
|
||||
|
||||
# Validação de Itens
|
||||
invalid-item: "&cPor favor seleciona um item válido."
|
||||
item-no-longer-available: "&cO item selecionado já não está no teu inventário."
|
||||
item-changed: "&cO item selecionado foi alterado. Por favor seleciona novamente."
|
||||
quantity-changed: "&cA quantidade disponível foi alterada. Por favor verifica e tenta novamente."
|
||||
blacklisted-item: "&cEste tipo de item não é permitido no mercado."
|
||||
blacklisted-content: "&cEste item contém conteúdo bloqueado."
|
||||
invalid-price: "&cPreço inválido. Intervalo: {min} - {max}"
|
||||
invalid-amount: "&cQuantidade inválida."
|
||||
invalid-duration: "&cDuração inválida."
|
||||
|
||||
# Mensagens de Admin
|
||||
admin-listing-removed: "&aAnúncio #{id} removido pelo admin."
|
||||
admin-auction-cancelled: "&aLeilão #{id} cancelado pelo admin."
|
||||
admin-reload: "&aConfiguração recarregada."
|
||||
|
||||
# Títulos GUI (suportam códigos de cor)
|
||||
gui-titles:
|
||||
main-menu: "&8&lMercado Comunitário"
|
||||
browse-market: "&8&lExplorar Mercado &7(Página {page})"
|
||||
browse-auctions: "&8&lExplorar Leilões &7(Página {page})"
|
||||
create-listing: "&8&lCriar Anúncio"
|
||||
create-auction: "&8&lCriar Leilão"
|
||||
select-item-listing: "&8&lSelecionar Item para Vender"
|
||||
select-item-auction: "&8&lSelecionar Item para Leilão"
|
||||
quantity-select: "&8&lSelecionar Quantidade"
|
||||
my-listings: "&8&lOs Meus Anúncios"
|
||||
my-auctions: "&8&lOs Meus Leilões"
|
||||
claim-items: "&8&lReclamar Itens"
|
||||
earnings: "&8&lGanhos"
|
||||
confirm-purchase: "&8&lConfirmar Compra"
|
||||
confirm-bid: "&8&lConfirmar Licitação"
|
||||
confirm-cancel: "&8&lConfirmar Cancelamento"
|
||||
number-input: "&8&lIntroduzir Valor"
|
||||
admin-panel: "&8&lPainel de Admin"
|
||||
admin-listings: "&8&lTodos os Anúncios"
|
||||
admin-auctions: "&8&lTodos os Leilões"
|
||||
listing-details: "&8&lDetalhes do Anúncio"
|
||||
auction-details: "&8&lDetalhes do Leilão"
|
||||
filter-menu: "&8&lOpções de Filtro"
|
||||
sort-menu: "&8&lOpções de Ordenação"
|
||||
duration-select: "&8&lSelecionar Duração"
|
||||
help: "&8&lAjuda"
|
||||
|
||||
# Nomes dos Botões
|
||||
buttons:
|
||||
# Menu Principal
|
||||
browse-market: "&aExplorar Mercado"
|
||||
browse-auctions: "&6Explorar Leilões"
|
||||
create-listing: "&eCriar Anúncio"
|
||||
create-auction: "&eCriar Leilão"
|
||||
my-listings: "&bOs Meus Anúncios"
|
||||
my-auctions: "&bOs Meus Leilões"
|
||||
claim-items: "&dReclamar Itens"
|
||||
earnings: "&aGanhos"
|
||||
help: "&fAjuda"
|
||||
admin: "&cPainel de Admin"
|
||||
|
||||
# Navegação
|
||||
next-page: "&aPágina Seguinte →"
|
||||
previous-page: "&a← Página Anterior"
|
||||
back: "&cVoltar"
|
||||
close: "&cFechar"
|
||||
|
||||
# Ações
|
||||
confirm: "&aConfirmar"
|
||||
cancel: "&cCancelar"
|
||||
buy: "&aComprar Agora"
|
||||
bid: "&6Fazer Licitação"
|
||||
buyout: "&eCompra Imediata"
|
||||
claim: "&aReclamar"
|
||||
claim-all: "&aReclamar Tudo"
|
||||
withdraw: "&aLevantar Tudo"
|
||||
remove: "&cRemover Anúncio"
|
||||
cancel-auction: "&cCancelar Leilão"
|
||||
|
||||
# Entrada Numérica
|
||||
add-1: "&a+1"
|
||||
add-10: "&a+10"
|
||||
add-100: "&a+100"
|
||||
add-1000: "&a+1.000"
|
||||
subtract-1: "&c-1"
|
||||
subtract-10: "&c-10"
|
||||
subtract-100: "&c-100"
|
||||
subtract-1000: "&c-1.000"
|
||||
set-min: "&eDefinir Mín"
|
||||
set-max: "&eDefinir Máx"
|
||||
custom-amount: "&bValor Personalizado"
|
||||
|
||||
# Filtros e Ordenação
|
||||
filter: "&eFiltro"
|
||||
sort: "&eOrdenar"
|
||||
search: "&ePesquisar"
|
||||
clear-filter: "&cLimpar Filtros"
|
||||
|
||||
# Duração
|
||||
duration-1h: "&e1 Hora"
|
||||
duration-6h: "&e6 Horas"
|
||||
duration-12h: "&e12 Horas"
|
||||
duration-24h: "&e24 Horas"
|
||||
duration-48h: "&e48 Horas"
|
||||
duration-72h: "&e3 Dias"
|
||||
duration-168h: "&e7 Dias"
|
||||
duration-336h: "&e14 Dias"
|
||||
|
||||
# Admin
|
||||
admin-view-listings: "&aVer Todos os Anúncios"
|
||||
admin-view-auctions: "&6Ver Todos os Leilões"
|
||||
admin-reload: "&eRecarregar Config"
|
||||
|
||||
# Lore dos Botões (descrições)
|
||||
lore:
|
||||
browse-market:
|
||||
- "&7Explora todos os anúncios"
|
||||
- "&7de preço fixo dos jogadores."
|
||||
- ""
|
||||
- "&eClica para explorar!"
|
||||
browse-auctions:
|
||||
- "&7Explora todos os leilões ativos"
|
||||
- "&7e faz licitações."
|
||||
- ""
|
||||
- "&eClica para explorar!"
|
||||
create-listing:
|
||||
- "&7Vende itens a um preço fixo."
|
||||
- "&7Taxa: &f{tax}%"
|
||||
- ""
|
||||
- "&eClica para criar!"
|
||||
create-auction:
|
||||
- "&7Leiloa itens ao"
|
||||
- "&7maior licitador."
|
||||
- "&7Taxa: &f{tax}%"
|
||||
- ""
|
||||
- "&eClica para criar!"
|
||||
my-listings:
|
||||
- "&7Vê e gere os teus"
|
||||
- "&7anúncios ativos."
|
||||
- ""
|
||||
- "&7Ativos: &f{count}/{max}"
|
||||
- ""
|
||||
- "&eClica para ver!"
|
||||
my-auctions:
|
||||
- "&7Vê e gere os teus"
|
||||
- "&7leilões ativos."
|
||||
- ""
|
||||
- "&7Ativos: &f{count}/{max}"
|
||||
- ""
|
||||
- "&eClica para ver!"
|
||||
claim-items:
|
||||
- "&7Reclama itens de anúncios"
|
||||
- "&7expirados ou leilões ganhos."
|
||||
- ""
|
||||
- "&7Pendentes: &f{count}"
|
||||
- ""
|
||||
- "&eClica para reclamar!"
|
||||
earnings:
|
||||
- "&7Vê e levanta os teus"
|
||||
- "&7ganhos pendentes das vendas."
|
||||
- ""
|
||||
- "&7Pendente: &a{amount}"
|
||||
- ""
|
||||
- "&eClica para ver!"
|
||||
help:
|
||||
- "&7Aprende a usar o"
|
||||
- "&7Mercado Comunitário."
|
||||
- ""
|
||||
- "&eClica para ajuda!"
|
||||
admin:
|
||||
- "&cPainel de Admin"
|
||||
- "&7Gere anúncios e leilões."
|
||||
- ""
|
||||
- "&eClica para abrir!"
|
||||
|
||||
# Info do Anúncio
|
||||
listing-info:
|
||||
- "&7Vendedor: &f{seller}"
|
||||
- "&7Preço: &a{price}"
|
||||
- "&7Quantidade: &f{amount}"
|
||||
- "&7Expira: &f{expires}"
|
||||
- ""
|
||||
- "&eClique esquerdo para comprar!"
|
||||
|
||||
# Info do Leilão
|
||||
auction-info:
|
||||
- "&7Vendedor: &f{seller}"
|
||||
- "&7Licitação inicial: &a{start_price}"
|
||||
- "&7Licitação atual: &a{current_bid}"
|
||||
- "&7Licitador: &f{bidder}"
|
||||
- "&7Licitações: &f{bid_count}"
|
||||
- "&7Termina: &f{ends}"
|
||||
- ""
|
||||
- "&eClique esquerdo para licitar!"
|
||||
- "&eClique direito para compra imediata!"
|
||||
|
||||
# Info do Meu Anúncio
|
||||
my-listing-info:
|
||||
- "&7Preço: &a{price}"
|
||||
- "&7Quantidade: &f{amount}"
|
||||
- "&7Criado: &f{created}"
|
||||
- "&7Expira: &f{expires}"
|
||||
- ""
|
||||
- "&cClica para cancelar"
|
||||
|
||||
# Info do Meu Leilão
|
||||
my-auction-info:
|
||||
- "&7Licitação inicial: &a{start_price}"
|
||||
- "&7Licitação atual: &a{current_bid}"
|
||||
- "&7Licitador: &f{bidder}"
|
||||
- "&7Licitações: &f{bid_count}"
|
||||
- "&7Termina: &f{ends}"
|
||||
- ""
|
||||
- "&cClica para cancelar (sem licitações)"
|
||||
|
||||
# Confirmar Compra
|
||||
confirm-purchase-info:
|
||||
- "&7Estás a comprar:"
|
||||
- "&f{item} x{amount}"
|
||||
- ""
|
||||
- "&7Preço: &a{price}"
|
||||
- "&7Taxa: &e{tax}"
|
||||
- "&7Total: &a{total}"
|
||||
- ""
|
||||
- "&aClica para confirmar!"
|
||||
|
||||
# Confirmar Licitação
|
||||
confirm-bid-info:
|
||||
- "&7Estás a licitar em:"
|
||||
- "&f{item}"
|
||||
- ""
|
||||
- "&7A tua licitação: &a{bid}"
|
||||
- "&7Atual mais alta: &e{current}"
|
||||
- ""
|
||||
- "&aClica para confirmar!"
|
||||
|
||||
# Reclamar Item
|
||||
claim-item-info:
|
||||
- "&7Razão: &f{reason}"
|
||||
- "&7De: &f{source}"
|
||||
- "&7Data: &f{date}"
|
||||
- ""
|
||||
- "&eClica para reclamar!"
|
||||
|
||||
# Info de Ganhos
|
||||
earnings-info:
|
||||
- "&7Os teus ganhos pendentes"
|
||||
- "&7das vendas no mercado."
|
||||
- ""
|
||||
- "&7Total: &a{amount}"
|
||||
- ""
|
||||
- "&aClica para levantar!"
|
||||
|
||||
# Info de Entrada Numérica
|
||||
current-value:
|
||||
- "&7Atual: &a{value}"
|
||||
|
||||
# Opções de Filtro
|
||||
filters:
|
||||
all: "&fTodos os Itens"
|
||||
weapons: "&cArmas"
|
||||
armor: "&bArmadura"
|
||||
tools: "&eFerramentas"
|
||||
blocks: "&7Blocos"
|
||||
food: "&6Comida"
|
||||
potions: "&dPoções"
|
||||
materials: "&aMateriais"
|
||||
enchanted: "&5Itens Encantados"
|
||||
misc: "&8Diversos"
|
||||
|
||||
# Opções de Ordenação
|
||||
sort:
|
||||
newest: "&aMais Recente"
|
||||
oldest: "&eMais Antigo"
|
||||
price-low: "&aPreço: Menor para Maior"
|
||||
price-high: "&cPreço: Maior para Menor"
|
||||
ending-soon: "&6A Terminar Em Breve"
|
||||
most-bids: "&bMais Licitações"
|
||||
|
||||
# Formatos de Tempo
|
||||
time:
|
||||
expired: "&cExpirado"
|
||||
days: "{d}d"
|
||||
hours: "{h}h"
|
||||
minutes: "{m}m"
|
||||
seconds: "{s}s"
|
||||
|
||||
# Conteúdo de Ajuda
|
||||
help:
|
||||
title: "&6&lAjuda do Mercado Comunitário"
|
||||
content:
|
||||
- "&eExplorar Mercado &7- Ver e comprar anúncios de preço fixo"
|
||||
- "&eExplorar Leilões &7- Ver e licitar em leilões"
|
||||
- "&eCriar Anúncio &7- Vender itens a um preço fixo"
|
||||
- "&eCriar Leilão &7- Leiloar itens ao maior licitador"
|
||||
- "&eOs Meus Anúncios &7- Gerir os teus anúncios ativos"
|
||||
- "&eOs Meus Leilões &7- Gerir os teus leilões ativos"
|
||||
- "&eReclamar Itens &7- Recolher itens não vendidos/ganhos"
|
||||
- "&eGanhos &7- Levantar dinheiro das vendas"
|
||||
- ""
|
||||
- "&7&oDica: Todas as ações são feitas através de GUIs!"
|
||||
- "&7&oBasta clicar nos botões para navegar."
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
name: CommunityMarket
|
||||
version: '${project.version}'
|
||||
main: pt.henrique.communityMarket.CommunityMarket
|
||||
api-version: '1.21'
|
||||
description: A GUI-only marketplace plugin for fixed-price listings and auctions
|
||||
author: Henrique
|
||||
website: https://github.com/henrique/CommunityMarket
|
||||
|
||||
# Soft dependencies - plugin will detect and use these if available
|
||||
softdepend:
|
||||
- Vault
|
||||
- Essentials
|
||||
|
||||
load: POSTWORLD
|
||||
|
||||
commands:
|
||||
market:
|
||||
description: Opens the Community Market main menu
|
||||
usage: /<command>
|
||||
aliases: [cmarket]
|
||||
permission: communitymarket.use
|
||||
|
||||
permissions:
|
||||
communitymarket.*:
|
||||
description: Grants all CommunityMarket permissions
|
||||
default: op
|
||||
children:
|
||||
communitymarket.use: true
|
||||
communitymarket.sell: true
|
||||
communitymarket.auction: true
|
||||
communitymarket.buy: true
|
||||
communitymarket.bid: true
|
||||
communitymarket.claim: true
|
||||
communitymarket.withdraw: true
|
||||
communitymarket.admin: true
|
||||
|
||||
communitymarket.use:
|
||||
description: Allows access to the market GUI
|
||||
default: true
|
||||
|
||||
communitymarket.sell:
|
||||
description: Allows creating fixed-price listings
|
||||
default: true
|
||||
|
||||
communitymarket.auction:
|
||||
description: Allows creating auctions
|
||||
default: true
|
||||
|
||||
communitymarket.buy:
|
||||
description: Allows purchasing from the market
|
||||
default: true
|
||||
|
||||
communitymarket.bid:
|
||||
description: Allows bidding on auctions
|
||||
default: true
|
||||
|
||||
communitymarket.claim:
|
||||
description: Allows claiming items from storage
|
||||
default: true
|
||||
|
||||
communitymarket.withdraw:
|
||||
description: Allows withdrawing earnings
|
||||
default: true
|
||||
|
||||
communitymarket.admin:
|
||||
description: Allows access to admin functions
|
||||
default: op
|
||||
children:
|
||||
communitymarket.admin.viewall: true
|
||||
communitymarket.admin.remove: true
|
||||
communitymarket.admin.reload: true
|
||||
|
||||
communitymarket.admin.viewall:
|
||||
description: Allows viewing all listings/auctions
|
||||
default: op
|
||||
|
||||
communitymarket.admin.remove:
|
||||
description: Allows removing any listing or auction
|
||||
default: op
|
||||
|
||||
communitymarket.admin.reload:
|
||||
description: Allows reloading configuration
|
||||
default: op
|
||||
Reference in New Issue
Block a user