commit c71200350962f1dc96d6728050710d22aa59e082 Author: Xelara Networks Date: Tue Jun 23 18:06:43 2026 -0400 E diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3125a8e --- /dev/null +++ b/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + com.yourname + dirt-auctions + 1.0.0 + jar + + Dirt Auctions + Premium GUI-first auction house plugin for modern Paper servers. + + + 21 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + jitpack.io + https://jitpack.io + + + + + + io.papermc.paper + paper-api + 1.21.8-R0.1-SNAPSHOT + provided + + + com.github.MilkBowl + VaultAPI + 1.7.1 + provided + + + org.xerial + sqlite-jdbc + 3.46.1.0 + + + + + DirtAuctions-${project.version} + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/src/main/java/com/yourname/premiumah/PremiumAHPlugin.java b/src/main/java/com/yourname/premiumah/PremiumAHPlugin.java new file mode 100644 index 0000000..058beb9 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/PremiumAHPlugin.java @@ -0,0 +1,164 @@ +package com.yourname.premiumah; + +import com.yourname.premiumah.command.AhAdminCommand; +import com.yourname.premiumah.command.AhCommand; +import com.yourname.premiumah.config.ConfigManager; +import com.yourname.premiumah.config.MessageManager; +import com.yourname.premiumah.economy.EconomyService; +import com.yourname.premiumah.economy.NoopEconomyService; +import com.yourname.premiumah.economy.VaultEconomyService; +import com.yourname.premiumah.gui.GuiManager; +import com.yourname.premiumah.listener.ChatInputListener; +import com.yourname.premiumah.listener.InventoryGuiListener; +import com.yourname.premiumah.listener.PlayerSessionListener; +import com.yourname.premiumah.manager.AuctionHouseManager; +import com.yourname.premiumah.manager.ClaimManager; +import com.yourname.premiumah.manager.ListingManager; +import com.yourname.premiumah.storage.StorageManager; +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.event.HandlerList; +import org.bukkit.plugin.java.JavaPlugin; + +public final class PremiumAHPlugin extends JavaPlugin { + private ConfigManager configManager; + private MessageManager messageManager; + private StorageManager storageManager; + private ListingManager listingManager; + private ClaimManager claimManager; + private EconomyService economyService; + private AuctionHouseManager auctionHouseManager; + private GuiManager guiManager; + + @Override + public void onEnable() { + this.configManager = new ConfigManager(this); + configManager.reload(); + this.messageManager = new MessageManager(this, configManager); + messageManager.reload(); + + this.storageManager = new StorageManager(this); + storageManager.load(); + this.listingManager = new ListingManager(storageManager); + this.claimManager = new ClaimManager(storageManager); + listingManager.loadFromStorage(); + claimManager.loadFromStorage(); + + this.economyService = createEconomyService(); + economyService.reload(); + + this.auctionHouseManager = new AuctionHouseManager(this, configManager, messageManager, storageManager, listingManager, claimManager, economyService); + this.guiManager = new GuiManager(this, configManager, messageManager, listingManager, claimManager, auctionHouseManager); + + registerCommands(); + registerListeners(); + auctionHouseManager.startTasks(); + + if (!auctionHouseManager.marketplaceReady() && configManager.requireEconomy()) { + getLogger().warning("No usable economy provider was found. Players can open GUIs and claims, but listing and buying are disabled."); + } + getLogger().info("Dirt Auctions enabled."); + } + + @Override + public void onDisable() { + if (auctionHouseManager != null) { + auctionHouseManager.stopTasks(); + } + Bukkit.getScheduler().cancelTasks(this); + if (guiManager != null) { + guiManager.shutdown(); + } + if (auctionHouseManager != null) { + auctionHouseManager.saveAll(); + } + if (storageManager != null) { + storageManager.close(); + } + if (listingManager != null) { + listingManager.clear(); + } + if (claimManager != null) { + claimManager.clear(); + } + HandlerList.unregisterAll(this); + getLogger().info("Dirt Auctions disabled cleanly."); + } + + public void reloadPluginState() { + if (auctionHouseManager != null) { + auctionHouseManager.stopTasks(); + auctionHouseManager.saveAll(); + } + if (guiManager != null) { + guiManager.shutdown(); + } + + configManager.reload(); + messageManager.reload(); + storageManager.load(); + listingManager.loadFromStorage(); + claimManager.loadFromStorage(); + economyService.reload(); + auctionHouseManager.startTasks(); + } + + public ConfigManager configManager() { + return configManager; + } + + public MessageManager messageManager() { + return messageManager; + } + + public ListingManager listingManager() { + return listingManager; + } + + public ClaimManager claimManager() { + return claimManager; + } + + public AuctionHouseManager auctionHouseManager() { + return auctionHouseManager; + } + + public GuiManager guiManager() { + return guiManager; + } + + private EconomyService createEconomyService() { + if (!configManager.economyEnabled()) { + getLogger().warning("Economy is disabled in config.yml. Marketplace listing and buying are disabled."); + return new NoopEconomyService(); + } + try { + return new VaultEconomyService(this); + } catch (NoClassDefFoundError error) { + getLogger().warning("Vault API is not available. Install Vault or a compatible injector for CMI economy support."); + return new NoopEconomyService(); + } + } + + private void registerCommands() { + AhCommand ahCommand = new AhCommand(guiManager, auctionHouseManager, messageManager); + PluginCommand ah = getCommand("ah"); + if (ah != null) { + ah.setExecutor(ahCommand); + ah.setTabCompleter(ahCommand); + } + + AhAdminCommand ahAdminCommand = new AhAdminCommand(this, guiManager, auctionHouseManager, listingManager, messageManager); + PluginCommand ahAdmin = getCommand("ahadmin"); + if (ahAdmin != null) { + ahAdmin.setExecutor(ahAdminCommand); + ahAdmin.setTabCompleter(ahAdminCommand); + } + } + + private void registerListeners() { + Bukkit.getPluginManager().registerEvents(new InventoryGuiListener(guiManager), this); + Bukkit.getPluginManager().registerEvents(new ChatInputListener(guiManager), this); + Bukkit.getPluginManager().registerEvents(new PlayerSessionListener(guiManager), this); + } +} diff --git a/src/main/java/com/yourname/premiumah/command/AhAdminCommand.java b/src/main/java/com/yourname/premiumah/command/AhAdminCommand.java new file mode 100644 index 0000000..5fbb39f --- /dev/null +++ b/src/main/java/com/yourname/premiumah/command/AhAdminCommand.java @@ -0,0 +1,131 @@ +package com.yourname.premiumah.command; + +import com.yourname.premiumah.PremiumAHPlugin; +import com.yourname.premiumah.config.MessageManager; +import com.yourname.premiumah.gui.GuiManager; +import com.yourname.premiumah.manager.AuctionHouseManager; +import com.yourname.premiumah.manager.ListingManager; +import com.yourname.premiumah.model.ActionResult; +import com.yourname.premiumah.model.Listing; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public final class AhAdminCommand implements TabExecutor { + private final PremiumAHPlugin plugin; + private final GuiManager guiManager; + private final AuctionHouseManager auctionHouse; + private final ListingManager listingManager; + private final MessageManager messages; + + public AhAdminCommand(PremiumAHPlugin plugin, + GuiManager guiManager, + AuctionHouseManager auctionHouse, + ListingManager listingManager, + MessageManager messages) { + this.plugin = plugin; + this.guiManager = guiManager; + this.auctionHouse = auctionHouse; + this.listingManager = listingManager; + this.messages = messages; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + messages.send(sender, "admin-help"); + return true; + } + String sub = args[0].toLowerCase(Locale.ROOT); + switch (sub) { + case "reload" -> reload(sender); + case "remove" -> remove(sender, args); + case "forceexpire" -> forceExpire(sender, args); + case "view" -> view(sender, args); + default -> messages.send(sender, "admin-help"); + } + return true; + } + + private void reload(CommandSender sender) { + if (!sender.hasPermission("premiumah.admin.reload")) { + messages.send(sender, "no-permission"); + return; + } + plugin.reloadPluginState(); + messages.send(sender, "reload-complete"); + } + + private void remove(CommandSender sender, String[] args) { + if (!sender.hasPermission("premiumah.admin.remove")) { + messages.send(sender, "no-permission"); + return; + } + if (args.length < 2) { + messages.send(sender, "admin-help"); + return; + } + ActionResult result = auctionHouse.adminRemoveListing(args[1]); + messages.send(sender, result.messageKey(), result.placeholders()); + } + + private void forceExpire(CommandSender sender, String[] args) { + if (!sender.hasPermission("premiumah.admin.forceexpire")) { + messages.send(sender, "no-permission"); + return; + } + if (args.length < 2) { + messages.send(sender, "admin-help"); + return; + } + ActionResult result = auctionHouse.forceExpireListing(args[1]); + messages.send(sender, result.messageKey(), result.placeholders()); + } + + private void view(CommandSender sender, String[] args) { + if (!sender.hasPermission("premiumah.admin.view")) { + messages.send(sender, "no-permission"); + return; + } + if (!(sender instanceof Player player)) { + messages.send(sender, "player-only"); + return; + } + if (args.length < 2) { + guiManager.openAdmin(player, 0); + return; + } + guiManager.openAdminForSeller(player, args[1], 0); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + return filter(List.of("reload", "remove", "view", "forceexpire", "help"), args[0]); + } + if (args.length == 2 && (args[0].equalsIgnoreCase("remove") || args[0].equalsIgnoreCase("forceexpire"))) { + return filter(listingManager.all().stream().map(Listing::id).limit(50).toList(), args[1]); + } + if (args.length == 2 && args[0].equalsIgnoreCase("view")) { + return filter(Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(), args[1]); + } + return List.of(); + } + + private List filter(List values, String prefix) { + String normalized = prefix.toLowerCase(Locale.ROOT); + List result = new ArrayList<>(); + for (String value : values) { + if (value.toLowerCase(Locale.ROOT).startsWith(normalized)) { + result.add(value); + } + } + return result; + } +} diff --git a/src/main/java/com/yourname/premiumah/command/AhCommand.java b/src/main/java/com/yourname/premiumah/command/AhCommand.java new file mode 100644 index 0000000..4b45a87 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/command/AhCommand.java @@ -0,0 +1,133 @@ +package com.yourname.premiumah.command; + +import com.yourname.premiumah.config.MessageManager; +import com.yourname.premiumah.gui.GuiManager; +import com.yourname.premiumah.manager.AuctionHouseManager; +import com.yourname.premiumah.model.ListingCreationResult; +import com.yourname.premiumah.model.SortMode; +import com.yourname.premiumah.util.InventoryUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public final class AhCommand implements TabExecutor { + private final GuiManager guiManager; + private final AuctionHouseManager auctionHouse; + private final MessageManager messages; + + public AhCommand(GuiManager guiManager, AuctionHouseManager auctionHouse, MessageManager messages) { + this.guiManager = guiManager; + this.auctionHouse = auctionHouse; + this.messages = messages; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + messages.send(sender, "player-only"); + return true; + } + if (!player.hasPermission("premiumah.use")) { + messages.send(player, "no-permission"); + return true; + } + if (args.length == 0) { + guiManager.openMain(player); + return true; + } + + String sub = args[0].toLowerCase(Locale.ROOT); + switch (sub) { + case "browse" -> guiManager.openBrowse(player, 0); + case "listings", "my", "mine" -> { + if (!player.hasPermission("premiumah.listings")) { + messages.send(player, "no-permission"); + return true; + } + guiManager.openMyListings(player, 0); + } + case "expired", "claims", "claim" -> { + if (!player.hasPermission("premiumah.expired")) { + messages.send(player, "no-permission"); + return true; + } + guiManager.openClaims(player, 0); + } + case "sell" -> handleSell(player, args); + case "sort" -> handleSort(player, args); + default -> guiManager.openMain(player); + } + return true; + } + + private void handleSell(Player player, String[] args) { + if (!player.hasPermission("premiumah.sell")) { + messages.send(player, "no-permission"); + return; + } + if (args.length == 1) { + guiManager.openSell(player, true); + return; + } + double price; + try { + price = Double.parseDouble(args[1].replace(",", "")); + } catch (NumberFormatException exception) { + messages.send(player, "usage-sell"); + return; + } + ItemStack item = player.getInventory().getItemInMainHand(); + if (InventoryUtil.isAir(item)) { + messages.send(player, "no-item-in-hand"); + return; + } + ListingCreationResult result = auctionHouse.createListing(player, item, price); + if (result.success()) { + player.getInventory().setItemInMainHand(null); + } + messages.send(player, result.messageKey(), result.placeholders()); + } + + private void handleSort(Player player, String[] args) { + if (args.length < 2) { + messages.send(player, "invalid-sort", Map.of()); + return; + } + SortMode mode = SortMode.fromString(args[1], null); + if (mode == null) { + messages.send(player, "invalid-sort", Map.of()); + return; + } + guiManager.setSort(player, mode); + guiManager.openBrowse(player, 0); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + return filter(List.of("browse", "sell", "listings", "expired", "claims", "sort"), args[0]); + } + if (args.length == 2 && args[0].equalsIgnoreCase("sort")) { + return filter(List.of("newest", "oldest", "lowest_price", "highest_price"), args[1]); + } + return List.of(); + } + + private List filter(List values, String prefix) { + String normalized = prefix.toLowerCase(Locale.ROOT); + List result = new ArrayList<>(); + for (String value : values) { + if (value.toLowerCase(Locale.ROOT).startsWith(normalized)) { + result.add(value); + } + } + return result; + } +} diff --git a/src/main/java/com/yourname/premiumah/config/ButtonConfig.java b/src/main/java/com/yourname/premiumah/config/ButtonConfig.java new file mode 100644 index 0000000..9c14a47 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/config/ButtonConfig.java @@ -0,0 +1,8 @@ +package com.yourname.premiumah.config; + +import org.bukkit.Material; + +import java.util.List; + +public record ButtonConfig(Material material, String name, List lore) { +} diff --git a/src/main/java/com/yourname/premiumah/config/ConfigManager.java b/src/main/java/com/yourname/premiumah/config/ConfigManager.java new file mode 100644 index 0000000..bdc71d6 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/config/ConfigManager.java @@ -0,0 +1,238 @@ +package com.yourname.premiumah.config; + +import com.yourname.premiumah.model.SortMode; +import com.yourname.premiumah.util.MaterialUtil; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public final class ConfigManager { + private final JavaPlugin plugin; + private FileConfiguration config; + private Set restrictedMaterials = Set.of(); + private List limitPermissions = List.of(); + private List listingSlots = List.of(); + + public ConfigManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + public void reload() { + plugin.saveDefaultConfig(); + plugin.reloadConfig(); + this.config = plugin.getConfig(); + this.restrictedMaterials = loadRestrictedMaterials(); + this.limitPermissions = loadLimitPermissions(); + this.listingSlots = loadListingSlots(); + } + + public FileConfiguration raw() { + return config; + } + + public boolean debug() { + return config.getBoolean("settings.debug", false); + } + + public String prefix() { + return config.getString("settings.command-prefix", "<#c79542>Dirt Auctions <#6b7280>»"); + } + + public boolean economyEnabled() { + return config.getBoolean("economy.enabled", true); + } + + public boolean requireEconomy() { + return config.getBoolean("economy.require-economy", true); + } + + public boolean allowSellerSelfPurchase() { + return config.getBoolean("settings.allow-seller-self-purchase", false); + } + + public boolean requireInventorySpaceToBuy() { + return config.getBoolean("settings.require-inventory-space-to-buy", false); + } + + public boolean claimFullInventoryPurchases() { + return config.getString("claims.buyer-full-inventory-action", "CLAIM").equalsIgnoreCase("CLAIM"); + } + + public boolean instantSellerPayment() { + String action = config.getString("claims.seller-payment-action", "INSTANT"); + return config.getBoolean("economy.instant-seller-payment", true) && !"CLAIM".equalsIgnoreCase(action); + } + + public int chatPriceTimeoutSeconds() { + return Math.max(5, config.getInt("settings.chat-price-timeout-seconds", 45)); + } + + public long clickDebounceMillis() { + return Math.max(0L, config.getLong("settings.click-debounce-millis", 350L)); + } + + public long listingExpireCheckTicks() { + long seconds = Math.max(10L, config.getLong("settings.listing-expire-check-seconds", 60L)); + return seconds * 20L; + } + + public SortMode defaultSort() { + return SortMode.fromString(config.getString("settings.default-sort", "NEWEST"), SortMode.NEWEST); + } + + public long defaultListingDurationMillis() { + long seconds = Math.max(60L, config.getLong("listings.default-duration-seconds", 604800L)); + return seconds * 1000L; + } + + public boolean allowCancelActiveListings() { + return config.getBoolean("listings.allow-cancel-active-listings", true); + } + + public boolean reclaimAdminRemovedItems() { + return config.getBoolean("listings.reclaim-admin-removed-items", true); + } + + public double minPrice() { + return Math.max(0.0D, config.getDouble("economy.price.min", 1.0D)); + } + + public double maxPrice() { + return Math.max(minPrice(), config.getDouble("economy.price.max", 1_000_000_000.0D)); + } + + public boolean listingFeeEnabled() { + return config.getBoolean("economy.listing-fee.enabled", false); + } + + public double listingFee() { + return Math.max(0.0D, config.getDouble("economy.listing-fee.amount", 0.0D)); + } + + public boolean salesTaxEnabled() { + return config.getBoolean("economy.sales-tax.enabled", false); + } + + public double salesTaxPercent() { + return Math.max(0.0D, Math.min(100.0D, config.getDouble("economy.sales-tax.percent", 0.0D))); + } + + public boolean soundsEnabled() { + return config.getBoolean("sounds.enabled", true); + } + + public String soundName(String key) { + return config.getString("sounds." + key, ""); + } + + public int guiSize(String key) { + int requested = config.getInt("gui.size." + key, 54); + int clamped = Math.max(9, Math.min(54, requested)); + return clamped - (clamped % 9); + } + + public String guiTitle(String key) { + return config.getString("gui.titles." + key, key); + } + + public boolean fillerEnabled() { + return config.getBoolean("gui.filler.enabled", true); + } + + public Material fillerMaterial() { + return MaterialUtil.parse(config.getString("gui.filler.material", "BLACK_STAINED_GLASS_PANE")) + .orElse(Material.BLACK_STAINED_GLASS_PANE); + } + + public String fillerName() { + return config.getString("gui.filler.name", " "); + } + + public List listingSlots() { + return listingSlots; + } + + public ButtonConfig button(String key) { + String path = "gui.buttons." + key; + Material material = MaterialUtil.parse(config.getString(path + ".material", "STONE")).orElse(Material.STONE); + String name = config.getString(path + ".name", key); + List lore = config.getStringList(path + ".lore"); + return new ButtonConfig(material, name, lore); + } + + public List guiLore(String key) { + return config.getStringList("gui-lore." + key); + } + + public int listingLimit(Player player) { + int limit = Math.max(0, config.getInt("listing-limits.default", 5)); + for (LimitPermission permission : limitPermissions) { + if (player.hasPermission(permission.permission())) { + limit = Math.max(limit, permission.amount()); + } + } + return limit; + } + + public boolean isItemAllowed(Player player, Material material) { + if (player.hasPermission("premiumah.bypass.restrictions")) { + return true; + } + String mode = config.getString("item-restrictions.mode", "BLACKLIST").toUpperCase(Locale.ROOT); + if ("WHITELIST".equals(mode)) { + return restrictedMaterials.contains(material); + } + return !restrictedMaterials.contains(material); + } + + private Set loadRestrictedMaterials() { + Set materials = new HashSet<>(); + for (String entry : config.getStringList("item-restrictions.materials")) { + MaterialUtil.parse(entry).ifPresent(materials::add); + } + return Collections.unmodifiableSet(materials); + } + + private List loadLimitPermissions() { + List permissions = new ArrayList<>(); + for (var map : config.getMapList("listing-limits.permissions")) { + Object permission = map.get("permission"); + Object amount = map.get("amount"); + if (permission instanceof String permissionString && amount instanceof Number number) { + permissions.add(new LimitPermission(permissionString, Math.max(0, number.intValue()))); + } + } + return List.copyOf(permissions); + } + + private List loadListingSlots() { + List slots = new ArrayList<>(); + for (int slot : config.getIntegerList("gui.listing-slots")) { + if (slot >= 0 && slot < 54) { + slots.add(slot); + } + } + if (slots.isEmpty()) { + for (int slot = 10; slot <= 34; slot++) { + int column = slot % 9; + if (column > 0 && column < 8) { + slots.add(slot); + } + } + } + return List.copyOf(slots); + } + + public ConfigurationSection section(String path) { + return config.getConfigurationSection(path); + } +} diff --git a/src/main/java/com/yourname/premiumah/config/LimitPermission.java b/src/main/java/com/yourname/premiumah/config/LimitPermission.java new file mode 100644 index 0000000..60b8207 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/config/LimitPermission.java @@ -0,0 +1,4 @@ +package com.yourname.premiumah.config; + +public record LimitPermission(String permission, int amount) { +} diff --git a/src/main/java/com/yourname/premiumah/config/MessageManager.java b/src/main/java/com/yourname/premiumah/config/MessageManager.java new file mode 100644 index 0000000..8795290 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/config/MessageManager.java @@ -0,0 +1,90 @@ +package com.yourname.premiumah.config; + +import com.yourname.premiumah.util.TextUtil; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class MessageManager { + private final JavaPlugin plugin; + private final ConfigManager configManager; + private File file; + private YamlConfiguration messages; + + public MessageManager(JavaPlugin plugin, ConfigManager configManager) { + this.plugin = plugin; + this.configManager = configManager; + } + + public void reload() { + this.file = new File(plugin.getDataFolder(), "messages.yml"); + if (!file.exists()) { + plugin.saveResource("messages.yml", false); + } + this.messages = YamlConfiguration.loadConfiguration(file); + } + + public void send(CommandSender sender, String key) { + send(sender, key, Collections.emptyMap()); + } + + public void send(CommandSender sender, String key, Map placeholders) { + sender.sendMessage(message(key, placeholders)); + } + + public void action(Player player, String key, Map placeholders) { + player.sendActionBar(message(key, placeholders)); + } + + public Component message(String key, Map placeholders) { + String raw = messages.getString("messages." + key, "<#ef4444>Missing message: " + key); + return component(apply(raw, placeholders)); + } + + public List guiLore(String key, Map placeholders) { + return messages.getStringList("gui-lore." + key).stream() + .map(line -> component(apply(line, placeholders))) + .toList(); + } + + public List rawGuiLore(String key) { + return messages.getStringList("gui-lore." + key); + } + + public Component component(String raw) { + return TextUtil.component(apply(raw, Collections.emptyMap())); + } + + public String legacy(String raw) { + return TextUtil.legacy(apply(raw, Collections.emptyMap())); + } + + public String legacyWithPlaceholders(String raw, Map placeholders) { + return TextUtil.legacy(apply(raw, placeholders)); + } + + public Map placeholders(String... pairs) { + Map placeholders = new HashMap<>(); + for (int i = 0; i + 1 < pairs.length; i += 2) { + placeholders.put(pairs[i], pairs[i + 1]); + } + return placeholders; + } + + public String apply(String raw, Map placeholders) { + String result = raw == null ? "" : raw; + result = result.replace("{prefix}", configManager.prefix()); + for (Map.Entry entry : placeholders.entrySet()) { + result = result.replace("{" + entry.getKey() + "}", entry.getValue()); + } + return result; + } +} diff --git a/src/main/java/com/yourname/premiumah/economy/EconomyService.java b/src/main/java/com/yourname/premiumah/economy/EconomyService.java new file mode 100644 index 0000000..6dd02b3 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/economy/EconomyService.java @@ -0,0 +1,19 @@ +package com.yourname.premiumah.economy; + +import org.bukkit.OfflinePlayer; + +public interface EconomyService { + void reload(); + + boolean isAvailable(); + + String providerName(); + + boolean has(OfflinePlayer player, double amount); + + boolean withdraw(OfflinePlayer player, double amount); + + boolean deposit(OfflinePlayer player, double amount); + + String format(double amount); +} diff --git a/src/main/java/com/yourname/premiumah/economy/NoopEconomyService.java b/src/main/java/com/yourname/premiumah/economy/NoopEconomyService.java new file mode 100644 index 0000000..371c81e --- /dev/null +++ b/src/main/java/com/yourname/premiumah/economy/NoopEconomyService.java @@ -0,0 +1,43 @@ +package com.yourname.premiumah.economy; + +import org.bukkit.OfflinePlayer; + +import java.text.DecimalFormat; + +public final class NoopEconomyService implements EconomyService { + private static final DecimalFormat FORMAT = new DecimalFormat("#,##0.##"); + + @Override + public void reload() { + } + + @Override + public boolean isAvailable() { + return false; + } + + @Override + public String providerName() { + return "Unavailable"; + } + + @Override + public boolean has(OfflinePlayer player, double amount) { + return false; + } + + @Override + public boolean withdraw(OfflinePlayer player, double amount) { + return false; + } + + @Override + public boolean deposit(OfflinePlayer player, double amount) { + return false; + } + + @Override + public String format(double amount) { + return "$" + FORMAT.format(amount); + } +} diff --git a/src/main/java/com/yourname/premiumah/economy/VaultEconomyService.java b/src/main/java/com/yourname/premiumah/economy/VaultEconomyService.java new file mode 100644 index 0000000..cc699d4 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/economy/VaultEconomyService.java @@ -0,0 +1,87 @@ +package com.yourname.premiumah.economy; + +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; + +import java.text.DecimalFormat; + +public final class VaultEconomyService implements EconomyService { + private static final DecimalFormat FALLBACK_FORMAT = new DecimalFormat("#,##0.##"); + + private final JavaPlugin plugin; + private Economy economy; + + public VaultEconomyService(JavaPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void reload() { + this.economy = null; + if (Bukkit.getPluginManager().getPlugin("Vault") == null) { + plugin.getLogger().warning("Vault is not installed. Dirt Auctions marketplace functions are disabled."); + return; + } + RegisteredServiceProvider registration = Bukkit.getServicesManager().getRegistration(Economy.class); + if (registration == null || registration.getProvider() == null) { + plugin.getLogger().warning("Vault is installed, but no economy provider is registered. Dirt Auctions marketplace functions are disabled."); + return; + } + this.economy = registration.getProvider(); + plugin.getLogger().info("Hooked economy provider: " + economy.getName()); + } + + @Override + public boolean isAvailable() { + return economy != null; + } + + @Override + public String providerName() { + return economy == null ? "Vault" : economy.getName(); + } + + @Override + public boolean has(OfflinePlayer player, double amount) { + if (amount <= 0.0D) { + return true; + } + return economy != null && economy.has(player, amount); + } + + @Override + public boolean withdraw(OfflinePlayer player, double amount) { + if (amount <= 0.0D) { + return true; + } + if (economy == null) { + return false; + } + EconomyResponse response = economy.withdrawPlayer(player, amount); + return response.transactionSuccess(); + } + + @Override + public boolean deposit(OfflinePlayer player, double amount) { + if (amount <= 0.0D) { + return true; + } + if (economy == null) { + return false; + } + EconomyResponse response = economy.depositPlayer(player, amount); + return response.transactionSuccess(); + } + + @Override + public String format(double amount) { + if (economy == null) { + return "$" + FALLBACK_FORMAT.format(amount); + } + return economy.format(amount); + } +} diff --git a/src/main/java/com/yourname/premiumah/gui/BrowseState.java b/src/main/java/com/yourname/premiumah/gui/BrowseState.java new file mode 100644 index 0000000..8f5906c --- /dev/null +++ b/src/main/java/com/yourname/premiumah/gui/BrowseState.java @@ -0,0 +1,30 @@ +package com.yourname.premiumah.gui; + +import com.yourname.premiumah.model.SortMode; +import org.bukkit.Material; + +public final class BrowseState { + private SortMode sortMode; + private Material filter; + + public BrowseState(SortMode sortMode, Material filter) { + this.sortMode = sortMode; + this.filter = filter; + } + + public SortMode sortMode() { + return sortMode; + } + + public void sortMode(SortMode sortMode) { + this.sortMode = sortMode; + } + + public Material filter() { + return filter; + } + + public void filter(Material filter) { + this.filter = filter; + } +} diff --git a/src/main/java/com/yourname/premiumah/gui/GuiHolder.java b/src/main/java/com/yourname/premiumah/gui/GuiHolder.java new file mode 100644 index 0000000..a4c449e --- /dev/null +++ b/src/main/java/com/yourname/premiumah/gui/GuiHolder.java @@ -0,0 +1,72 @@ +package com.yourname.premiumah.gui; + +import com.yourname.premiumah.model.SortMode; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public final class GuiHolder implements InventoryHolder { + private final GuiType type; + private final UUID viewer; + private final int page; + private final String context; + private final SortMode sortMode; + private final Material filter; + private final Map listingSlots = new HashMap<>(); + private final Map claimSlots = new HashMap<>(); + private Inventory inventory; + + public GuiHolder(GuiType type, UUID viewer, int page, String context, SortMode sortMode, Material filter) { + this.type = type; + this.viewer = viewer; + this.page = page; + this.context = context; + this.sortMode = sortMode; + this.filter = filter; + } + + public GuiType type() { + return type; + } + + public UUID viewer() { + return viewer; + } + + public int page() { + return page; + } + + public String context() { + return context; + } + + public SortMode sortMode() { + return sortMode; + } + + public Material filter() { + return filter; + } + + public Map listingSlots() { + return listingSlots; + } + + public Map claimSlots() { + return claimSlots; + } + + public void inventory(Inventory inventory) { + this.inventory = inventory; + } + + @Override + public Inventory getInventory() { + return inventory; + } +} diff --git a/src/main/java/com/yourname/premiumah/gui/GuiManager.java b/src/main/java/com/yourname/premiumah/gui/GuiManager.java new file mode 100644 index 0000000..b1cae66 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/gui/GuiManager.java @@ -0,0 +1,764 @@ +package com.yourname.premiumah.gui; + +import com.yourname.premiumah.config.ButtonConfig; +import com.yourname.premiumah.config.ConfigManager; +import com.yourname.premiumah.config.MessageManager; +import com.yourname.premiumah.manager.AuctionHouseManager; +import com.yourname.premiumah.manager.ClaimManager; +import com.yourname.premiumah.manager.ListingManager; +import com.yourname.premiumah.model.ActionResult; +import com.yourname.premiumah.model.ClaimRecord; +import com.yourname.premiumah.model.ClaimType; +import com.yourname.premiumah.model.Listing; +import com.yourname.premiumah.model.ListingCreationResult; +import com.yourname.premiumah.model.SortMode; +import com.yourname.premiumah.util.InventoryUtil; +import com.yourname.premiumah.util.MaterialUtil; +import com.yourname.premiumah.util.TextUtil; +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Sound; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +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.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class GuiManager { + private static final int SELL_ITEM_SLOT = 22; + + private final JavaPlugin plugin; + private final ConfigManager config; + private final MessageManager messages; + private final ListingManager listings; + private final ClaimManager claims; + private final AuctionHouseManager auctionHouse; + private final Map browseStates = new ConcurrentHashMap<>(); + private final Map sellSessions = new ConcurrentHashMap<>(); + private final Map clickDebounce = new ConcurrentHashMap<>(); + + public GuiManager(JavaPlugin plugin, + ConfigManager config, + MessageManager messages, + ListingManager listings, + ClaimManager claims, + AuctionHouseManager auctionHouse) { + this.plugin = plugin; + this.config = config; + this.messages = messages; + this.listings = listings; + this.claims = claims; + this.auctionHouse = auctionHouse; + } + + public void openMain(Player player) { + GuiHolder holder = new GuiHolder(GuiType.MAIN, player.getUniqueId(), 0, null, null, null); + Inventory inventory = create(holder, config.guiSize("main"), config.guiTitle("main"), Map.of()); + fill(inventory); + inventory.setItem(11, button("browse", Map.of())); + inventory.setItem(13, button("sell", Map.of())); + inventory.setItem(15, button("my-listings", Map.of())); + inventory.setItem(29, button("claims", Map.of())); + if (player.hasPermission("premiumah.admin")) { + inventory.setItem(31, button("admin", Map.of())); + } + inventory.setItem(33, button("close", Map.of())); + player.openInventory(inventory); + play(player, "open"); + } + + public void openBrowse(Player player, int requestedPage) { + BrowseState state = browseState(player); + List active = listings.activeListings(state.sortMode(), state.filter()); + openListingView(player, GuiType.BROWSE, active, requestedPage, "browse", null, state.sortMode(), state.filter()); + } + + public void openMyListings(Player player, int requestedPage) { + List own = listings.activeListingsBySeller(player.getUniqueId(), SortMode.NEWEST); + openListingView(player, GuiType.MY_LISTINGS, own, requestedPage, "my-listings", null, SortMode.NEWEST, null); + } + + public void openAdmin(Player player, int requestedPage) { + BrowseState state = browseState(player); + List active = listings.activeListings(state.sortMode(), state.filter()); + openListingView(player, GuiType.ADMIN, active, requestedPage, "admin", null, state.sortMode(), state.filter()); + } + + public void openAdminForSeller(Player player, String sellerName, int requestedPage) { + List active = listings.activeListingsBySellerName(sellerName, SortMode.NEWEST); + openListingView(player, GuiType.ADMIN, active, requestedPage, "admin", sellerName, SortMode.NEWEST, null); + } + + public void openClaims(Player player, int requestedPage) { + List playerClaims = claims.claimsFor(player.getUniqueId()); + int page = normalizePage(requestedPage, playerClaims.size()); + GuiHolder holder = new GuiHolder(GuiType.CLAIMS, player.getUniqueId(), page, null, SortMode.NEWEST, null); + Inventory inventory = create(holder, config.guiSize("claims"), config.guiTitle("claims"), Map.of("page", String.valueOf(page + 1))); + fill(inventory); + List slots = config.listingSlots(); + int start = page * slots.size(); + for (int i = 0; i < slots.size(); i++) { + int index = start + i; + if (index >= playerClaims.size()) { + break; + } + ClaimRecord claim = playerClaims.get(index); + int slot = slots.get(i); + holder.claimSlots().put(slot, claim.id()); + inventory.setItem(slot, claimItem(claim)); + } + addPagedControls(inventory, page, playerClaims.size(), slots.size(), "back"); + player.openInventory(inventory); + play(player, "open"); + } + + public void openSell(Player player, boolean takeHeldItem) { + if (!player.hasPermission("premiumah.sell")) { + messages.send(player, "no-permission"); + play(player, "fail"); + return; + } + SellSession session = sellSessions.computeIfAbsent(player.getUniqueId(), ignored -> new SellSession()); + if (takeHeldItem && InventoryUtil.isAir(session.rawItem())) { + ItemStack held = player.getInventory().getItemInMainHand(); + if (!InventoryUtil.isAir(held)) { + session.item(held); + player.getInventory().setItemInMainHand(null); + } + } + session.awaitingPrice(false); + session.completed(false); + + GuiHolder holder = new GuiHolder(GuiType.SELL, player.getUniqueId(), 0, null, null, null); + Inventory inventory = create(holder, config.guiSize("sell"), config.guiTitle("sell"), Map.of()); + fill(inventory); + inventory.setItem(SELL_ITEM_SLOT, session.item()); + inventory.setItem(20, customItem(Material.GOLD_INGOT, "<#f5d58a>Set Price", List.of( + "<#9ca3af>Current: <#ffffff>" + (session.price() <= 0.0D ? "Not set" : auctionHouse.formatMoney(session.price())), + "<#6ee7b7>Click to type a price." + ), Map.of())); + inventory.setItem(24, button("confirm", Map.of())); + inventory.setItem(36, button("back", Map.of())); + inventory.setItem(40, button("cancel", Map.of())); + player.openInventory(inventory); + play(player, "open"); + } + + public void openConfirmBuy(Player player, String listingId) { + Listing listing = listings.get(listingId).orElse(null); + if (listing == null || !listing.isActive(System.currentTimeMillis())) { + messages.send(player, "listing-no-longer-available"); + play(player, "fail"); + openBrowse(player, 0); + return; + } + GuiHolder holder = new GuiHolder(GuiType.CONFIRM_BUY, player.getUniqueId(), 0, listing.id(), null, null); + Inventory inventory = create(holder, config.guiSize("confirm-buy"), config.guiTitle("confirm-buy"), Map.of()); + fill(inventory); + inventory.setItem(11, button("confirm", Map.of())); + inventory.setItem(13, listingItem(listing, "confirm-buy")); + inventory.setItem(15, button("cancel", Map.of())); + player.openInventory(inventory); + play(player, "open"); + } + + public void handleClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player) || !(event.getView().getTopInventory().getHolder() instanceof GuiHolder holder)) { + return; + } + int topSize = event.getView().getTopInventory().getSize(); + int rawSlot = event.getRawSlot(); + + if (holder.type() != GuiType.SELL && rawSlot >= topSize) { + if (event.isShiftClick()) { + event.setCancelled(true); + } + return; + } + if (holder.type() == GuiType.SELL && rawSlot >= topSize) { + if (event.isShiftClick()) { + event.setCancelled(true); + } + return; + } + + if (holder.type() == GuiType.SELL && rawSlot == SELL_ITEM_SLOT) { + event.setCancelled(false); + Bukkit.getScheduler().runTask(plugin, () -> syncSellSlot(player, event.getView().getTopInventory())); + return; + } + + event.setCancelled(true); + if (rawSlot < 0 || !canClick(player)) { + return; + } + + switch (holder.type()) { + case MAIN -> handleMainClick(player, rawSlot); + case BROWSE -> handleBrowseClick(player, holder, rawSlot, event.getClick()); + case MY_LISTINGS -> handleMyListingsClick(player, holder, rawSlot); + case CLAIMS -> handleClaimsClick(player, holder, rawSlot); + case SELL -> handleSellClick(player, event.getView().getTopInventory(), rawSlot); + case CONFIRM_BUY -> handleConfirmClick(player, holder, rawSlot); + case ADMIN -> handleAdminClick(player, holder, rawSlot, event.getClick()); + } + } + + public void handleDrag(InventoryDragEvent event) { + if (!(event.getWhoClicked() instanceof Player player) || !(event.getView().getTopInventory().getHolder() instanceof GuiHolder holder)) { + return; + } + int topSize = event.getView().getTopInventory().getSize(); + boolean touchesTop = event.getRawSlots().stream().anyMatch(slot -> slot < topSize); + if (!touchesTop) { + return; + } + if (holder.type() == GuiType.SELL && event.getRawSlots().stream().allMatch(slot -> slot == SELL_ITEM_SLOT || slot >= topSize)) { + Bukkit.getScheduler().runTask(plugin, () -> syncSellSlot(player, event.getView().getTopInventory())); + return; + } + event.setCancelled(true); + } + + public void handleClose(InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player player) || !(event.getInventory().getHolder() instanceof GuiHolder holder)) { + return; + } + if (holder.type() != GuiType.SELL) { + return; + } + SellSession session = sellSessions.get(player.getUniqueId()); + if (session == null || session.awaitingPrice() || session.completed()) { + return; + } + ItemStack item = event.getInventory().getItem(SELL_ITEM_SLOT); + if (InventoryUtil.isAir(item)) { + item = session.item(); + } + returnItem(player, item); + sellSessions.remove(player.getUniqueId()); + } + + public void handleChat(AsyncChatEvent event) { + Player player = event.getPlayer(); + SellSession session = sellSessions.get(player.getUniqueId()); + if (session == null || !session.awaitingPrice()) { + return; + } + event.setCancelled(true); + String input = PlainTextComponentSerializer.plainText().serialize(event.message()).trim(); + Bukkit.getScheduler().runTask(plugin, () -> completePricePrompt(player, input)); + } + + public void setSort(Player player, SortMode sortMode) { + browseState(player).sortMode(sortMode); + } + + public void shutdown() { + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getOpenInventory().getTopInventory().getHolder() instanceof GuiHolder) { + player.closeInventory(); + } + } + for (UUID uuid : new ArrayList<>(sellSessions.keySet())) { + Player player = Bukkit.getPlayer(uuid); + SellSession session = sellSessions.remove(uuid); + if (player != null && session != null && !InventoryUtil.isAir(session.rawItem())) { + returnItem(player, session.rawItem()); + } + if (session != null) { + session.cancelTimeout(); + } + } + sellSessions.clear(); + browseStates.clear(); + clickDebounce.clear(); + } + + public void handleQuit(Player player) { + SellSession session = sellSessions.remove(player.getUniqueId()); + if (session != null) { + session.cancelTimeout(); + if (!InventoryUtil.isAir(session.rawItem())) { + returnItem(player, session.rawItem()); + } + } + browseStates.remove(player.getUniqueId()); + clickDebounce.remove(player.getUniqueId()); + } + + private void handleMainClick(Player player, int slot) { + play(player, "click"); + if (slot == 11) { + openBrowse(player, 0); + } else if (slot == 13) { + openSell(player, true); + } else if (slot == 15) { + openMyListings(player, 0); + } else if (slot == 29) { + openClaims(player, 0); + } else if (slot == 31 && player.hasPermission("premiumah.admin")) { + openAdmin(player, 0); + } else if (slot == 33) { + player.closeInventory(); + } + } + + private void handleBrowseClick(Player player, GuiHolder holder, int slot, ClickType click) { + if (holder.listingSlots().containsKey(slot)) { + Listing listing = listings.get(holder.listingSlots().get(slot)).orElse(null); + if (listing == null) { + messages.send(player, "listing-no-longer-available"); + play(player, "fail"); + openBrowse(player, holder.page()); + return; + } + if (click.isRightClick() && player.hasPermission("premiumah.admin.remove")) { + ActionResult result = auctionHouse.adminRemoveListing(listing.id()); + messages.send(player, result.messageKey(), result.placeholders()); + play(player, result.success() ? "success" : "fail"); + openBrowse(player, holder.page()); + return; + } + if (click.isShiftClick()) { + browseState(player).filter(listing.rawItem().getType()); + openBrowse(player, 0); + return; + } + openConfirmBuy(player, listing.id()); + return; + } + handlePagedControl(player, holder, slot, GuiType.BROWSE); + } + + private void handleMyListingsClick(Player player, GuiHolder holder, int slot) { + if (holder.listingSlots().containsKey(slot)) { + ActionResult result = auctionHouse.cancelListing(player, holder.listingSlots().get(slot)); + messages.send(player, result.messageKey(), result.placeholders()); + play(player, result.success() ? "success" : "fail"); + openMyListings(player, holder.page()); + return; + } + handlePagedControl(player, holder, slot, GuiType.MY_LISTINGS); + } + + private void handleClaimsClick(Player player, GuiHolder holder, int slot) { + if (holder.claimSlots().containsKey(slot)) { + ActionResult result = auctionHouse.claim(player, holder.claimSlots().get(slot)); + messages.send(player, result.messageKey(), result.placeholders()); + play(player, result.success() ? "success" : "fail"); + openClaims(player, holder.page()); + return; + } + handlePagedControl(player, holder, slot, GuiType.CLAIMS); + } + + private void handleSellClick(Player player, Inventory inventory, int slot) { + play(player, "click"); + if (slot == 20) { + beginPricePrompt(player, inventory); + } else if (slot == 24) { + confirmSell(player, inventory); + } else if (slot == 36) { + closeSellReturningItem(player, inventory, true); + openMain(player); + } else if (slot == 40) { + closeSellReturningItem(player, inventory, false); + } + } + + private void handleConfirmClick(Player player, GuiHolder holder, int slot) { + if (slot == 11) { + ActionResult result = auctionHouse.buyListing(player, holder.context()); + messages.send(player, result.messageKey(), result.placeholders()); + play(player, result.success() ? "success" : "fail"); + if (result.success()) { + openBrowse(player, 0); + } else { + player.closeInventory(); + } + } else if (slot == 15) { + play(player, "click"); + openBrowse(player, 0); + } + } + + private void handleAdminClick(Player player, GuiHolder holder, int slot, ClickType click) { + if (holder.listingSlots().containsKey(slot)) { + Listing listing = listings.get(holder.listingSlots().get(slot)).orElse(null); + if (listing == null) { + messages.send(player, "listing-no-longer-available"); + openAdmin(player, holder.page()); + return; + } + if (click.isRightClick() || click.isShiftClick()) { + ActionResult result = auctionHouse.adminRemoveListing(listing.id()); + messages.send(player, result.messageKey(), result.placeholders()); + play(player, result.success() ? "success" : "fail"); + openAdmin(player, holder.page()); + } else { + openConfirmBuy(player, listing.id()); + } + return; + } + handlePagedControl(player, holder, slot, GuiType.ADMIN); + } + + private void handlePagedControl(Player player, GuiHolder holder, int slot, GuiType type) { + play(player, "click"); + if (slot == 45) { + openByType(player, type, Math.max(0, holder.page() - 1), holder.context()); + } else if (slot == 49) { + openMain(player); + } else if (slot == 53) { + openByType(player, type, holder.page() + 1, holder.context()); + } else if (slot == 50 && (type == GuiType.BROWSE || type == GuiType.ADMIN)) { + BrowseState state = browseState(player); + state.sortMode(state.sortMode().next()); + openByType(player, type, 0, holder.context()); + } else if (slot == 48 && (type == GuiType.BROWSE || type == GuiType.ADMIN)) { + browseState(player).filter(null); + openByType(player, type, 0, holder.context()); + } + } + + private void openByType(Player player, GuiType type, int page, String context) { + switch (type) { + case BROWSE -> openBrowse(player, page); + case MY_LISTINGS -> openMyListings(player, page); + case CLAIMS -> openClaims(player, page); + case ADMIN -> { + if (context == null || context.isBlank()) { + openAdmin(player, page); + } else { + openAdminForSeller(player, context, page); + } + } + default -> openMain(player); + } + } + + private void openListingView(Player player, + GuiType type, + List source, + int requestedPage, + String titleKey, + String context, + SortMode sortMode, + Material filter) { + int page = normalizePage(requestedPage, source.size()); + GuiHolder holder = new GuiHolder(type, player.getUniqueId(), page, context, sortMode, filter); + Inventory inventory = create(holder, config.guiSize(titleKey), config.guiTitle(titleKey), Map.of("page", String.valueOf(page + 1))); + fill(inventory); + List slots = config.listingSlots(); + int start = page * slots.size(); + String loreKey = type == GuiType.MY_LISTINGS ? "my-listing" : "listing"; + for (int i = 0; i < slots.size(); i++) { + int index = start + i; + if (index >= source.size()) { + break; + } + Listing listing = source.get(index); + int slot = slots.get(i); + holder.listingSlots().put(slot, listing.id()); + inventory.setItem(slot, listingItem(listing, loreKey)); + } + addPagedControls(inventory, page, source.size(), slots.size(), "back"); + if (type == GuiType.BROWSE || type == GuiType.ADMIN) { + BrowseState state = browseState(player); + inventory.setItem(48, button("filter", Map.of("filter", state.filter() == null ? "All" : MaterialUtil.pretty(state.filter())))); + inventory.setItem(50, button("sort", Map.of("sort", state.sortMode().displayName()))); + } + player.openInventory(inventory); + play(player, "open"); + } + + private void addPagedControls(Inventory inventory, int page, int totalItems, int pageSize, String backKey) { + if (page > 0) { + inventory.setItem(45, button("previous", Map.of())); + } + inventory.setItem(49, button(backKey, Map.of())); + if ((page + 1) * pageSize < totalItems) { + inventory.setItem(53, button("next", Map.of())); + } + } + + private Inventory create(GuiHolder holder, int size, String title, Map placeholders) { + Inventory inventory = Bukkit.createInventory(holder, size, messages.component(messages.apply(title, placeholders))); + holder.inventory(inventory); + return inventory; + } + + private void fill(Inventory inventory) { + if (!config.fillerEnabled()) { + return; + } + ItemStack filler = customItem(config.fillerMaterial(), config.fillerName(), List.of(), Map.of()); + for (int i = 0; i < inventory.getSize(); i++) { + if (inventory.getItem(i) == null) { + inventory.setItem(i, filler); + } + } + } + + private ItemStack button(String key, Map placeholders) { + ButtonConfig button = config.button(key); + return customItem(button.material(), button.name(), button.lore(), placeholders); + } + + private ItemStack customItem(Material material, String name, List lore, Map placeholders) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(messages.component(messages.apply(name, placeholders))); + if (lore != null && !lore.isEmpty()) { + meta.lore(lore.stream().map(line -> messages.component(messages.apply(line, placeholders))).toList()); + } + meta.addItemFlags(ItemFlag.values()); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack listingItem(Listing listing, String loreKey) { + ItemStack item = listing.item(); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + List lore = new ArrayList<>(); + if (meta.lore() != null && !meta.lore().isEmpty()) { + lore.addAll(meta.lore()); + lore.add(TextUtil.component("")); + } + lore.addAll(messages.guiLore(loreKey, listingPlaceholders(listing))); + meta.lore(lore); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack claimItem(ClaimRecord claim) { + ItemStack item; + if (claim.type() == ClaimType.MONEY) { + item = customItem(Material.EMERALD, "<#6ee7b7>Sale Payment <#ffffff>{amount}", messages.rawGuiLore("claim"), claimPlaceholders(claim)); + } else { + item = claim.item(); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + List lore = new ArrayList<>(); + if (meta.lore() != null && !meta.lore().isEmpty()) { + lore.addAll(meta.lore()); + lore.add(TextUtil.component("")); + } + lore.addAll(messages.guiLore("claim", claimPlaceholders(claim))); + meta.lore(lore); + item.setItemMeta(meta); + } + } + return item; + } + + private Map listingPlaceholders(Listing listing) { + return Map.of( + "seller", listing.sellerName(), + "price", auctionHouse.formatMoney(listing.price()), + "remaining", com.yourname.premiumah.util.TimeUtil.remaining(listing.expiresAt()), + "id", listing.id() + ); + } + + private Map claimPlaceholders(ClaimRecord claim) { + String itemName = claim.type() == ClaimType.MONEY ? auctionHouse.formatMoney(claim.moneyAmount()) : auctionHouse.itemName(claim.item()); + Map placeholders = new HashMap<>(); + placeholders.put("reason", claim.reason().displayName()); + placeholders.put("id", claim.listingId() == null ? "N/A" : claim.listingId()); + placeholders.put("age", com.yourname.premiumah.util.TimeUtil.age(claim.createdAt())); + placeholders.put("item", itemName); + placeholders.put("amount", itemName); + return placeholders; + } + + private BrowseState browseState(Player player) { + return browseStates.computeIfAbsent(player.getUniqueId(), ignored -> new BrowseState(config.defaultSort(), null)); + } + + private int normalizePage(int requestedPage, int totalItems) { + int pageSize = Math.max(1, config.listingSlots().size()); + int maxPage = Math.max(0, (int) Math.ceil(totalItems / (double) pageSize) - 1); + return Math.max(0, Math.min(requestedPage, maxPage)); + } + + private boolean canClick(Player player) { + long now = System.currentTimeMillis(); + long last = clickDebounce.getOrDefault(player.getUniqueId(), 0L); + if (now - last < config.clickDebounceMillis()) { + return false; + } + clickDebounce.put(player.getUniqueId(), now); + return true; + } + + private void syncSellSlot(Player player, Inventory inventory) { + SellSession session = sellSessions.get(player.getUniqueId()); + if (session != null) { + session.item(inventory.getItem(SELL_ITEM_SLOT)); + } + } + + private void beginPricePrompt(Player player, Inventory inventory) { + SellSession session = sellSessions.computeIfAbsent(player.getUniqueId(), ignored -> new SellSession()); + session.item(inventory.getItem(SELL_ITEM_SLOT)); + inventory.setItem(SELL_ITEM_SLOT, null); + session.awaitingPrice(true); + session.completed(false); + session.cancelTimeout(); + session.timeoutTask(Bukkit.getScheduler().runTaskLater(plugin, () -> { + SellSession current = sellSessions.get(player.getUniqueId()); + if (current != null && current.awaitingPrice()) { + current.awaitingPrice(false); + returnItem(player, current.rawItem()); + current.cancelTimeout(); + sellSessions.remove(player.getUniqueId()); + messages.send(player, "price-prompt-timeout"); + play(player, "fail"); + } + }, config.chatPriceTimeoutSeconds() * 20L)); + player.closeInventory(); + messages.send(player, "price-prompt"); + } + + private void completePricePrompt(Player player, String input) { + SellSession session = sellSessions.get(player.getUniqueId()); + if (session == null || !session.awaitingPrice()) { + return; + } + session.cancelTimeout(); + if (input.equalsIgnoreCase("cancel")) { + session.awaitingPrice(false); + returnItem(player, session.rawItem()); + sellSessions.remove(player.getUniqueId()); + messages.send(player, "price-prompt-cancelled"); + play(player, "click"); + return; + } + double price; + try { + price = Double.parseDouble(input.replace(",", "")); + } catch (NumberFormatException exception) { + messages.send(player, "invalid-price"); + play(player, "fail"); + beginPricePrompt(player, createTempSellInventory(player, session)); + return; + } + if (!Double.isFinite(price) || price <= 0.0D) { + messages.send(player, "invalid-price"); + play(player, "fail"); + beginPricePrompt(player, createTempSellInventory(player, session)); + return; + } + session.price(price); + session.awaitingPrice(false); + messages.send(player, "price-set", Map.of("price", auctionHouse.formatMoney(price))); + openSell(player, false); + } + + private Inventory createTempSellInventory(Player player, SellSession session) { + GuiHolder holder = new GuiHolder(GuiType.SELL, player.getUniqueId(), 0, null, null, null); + Inventory inventory = create(holder, config.guiSize("sell"), config.guiTitle("sell"), Map.of()); + inventory.setItem(SELL_ITEM_SLOT, session.item()); + return inventory; + } + + private void confirmSell(Player player, Inventory inventory) { + SellSession session = sellSessions.computeIfAbsent(player.getUniqueId(), ignored -> new SellSession()); + ItemStack item = inventory.getItem(SELL_ITEM_SLOT); + if (InventoryUtil.isAir(item)) { + messages.send(player, "no-item-in-hand"); + play(player, "fail"); + return; + } + if (session.price() <= 0.0D) { + messages.send(player, "invalid-price"); + play(player, "fail"); + return; + } + inventory.setItem(SELL_ITEM_SLOT, null); + ListingCreationResult result = auctionHouse.createListing(player, item, session.price()); + if (!result.success()) { + inventory.setItem(SELL_ITEM_SLOT, item); + messages.send(player, result.messageKey(), result.placeholders()); + play(player, "fail"); + return; + } + session.completed(true); + session.cancelTimeout(); + sellSessions.remove(player.getUniqueId()); + messages.send(player, result.messageKey(), result.placeholders()); + play(player, "success"); + openBrowse(player, 0); + } + + private void closeSellReturningItem(Player player, Inventory inventory, boolean keepOpen) { + SellSession session = sellSessions.remove(player.getUniqueId()); + ItemStack item = inventory.getItem(SELL_ITEM_SLOT); + inventory.setItem(SELL_ITEM_SLOT, null); + if (session != null) { + session.completed(true); + session.cancelTimeout(); + if (InventoryUtil.isAir(item)) { + item = session.rawItem(); + } + } + returnItem(player, item); + if (!keepOpen) { + player.closeInventory(); + } + } + + private void returnItem(Player player, ItemStack item) { + if (InventoryUtil.isAir(item)) { + return; + } + Map leftovers = InventoryUtil.addItem(player.getInventory(), item); + for (ItemStack leftover : leftovers.values()) { + player.getWorld().dropItemNaturally(player.getLocation(), leftover); + } + } + + private void play(Player player, String key) { + if (!config.soundsEnabled()) { + return; + } + String soundName = config.soundName(key); + if (soundName == null || soundName.isBlank()) { + return; + } + try { + String soundKey = soundName.toLowerCase(Locale.ROOT).replace('_', '.'); + Sound sound = Registry.SOUNDS.get(NamespacedKey.minecraft(soundKey)); + if (sound == null) { + return; + } + player.playSound(player.getLocation(), sound, 0.7F, 1.0F); + } catch (IllegalArgumentException ignored) { + } + } +} diff --git a/src/main/java/com/yourname/premiumah/gui/GuiType.java b/src/main/java/com/yourname/premiumah/gui/GuiType.java new file mode 100644 index 0000000..cb1a7ca --- /dev/null +++ b/src/main/java/com/yourname/premiumah/gui/GuiType.java @@ -0,0 +1,11 @@ +package com.yourname.premiumah.gui; + +public enum GuiType { + MAIN, + BROWSE, + MY_LISTINGS, + CLAIMS, + SELL, + CONFIRM_BUY, + ADMIN +} diff --git a/src/main/java/com/yourname/premiumah/gui/SellSession.java b/src/main/java/com/yourname/premiumah/gui/SellSession.java new file mode 100644 index 0000000..d772004 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/gui/SellSession.java @@ -0,0 +1,63 @@ +package com.yourname.premiumah.gui; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitTask; + +public final class SellSession { + private ItemStack item; + private double price; + private volatile boolean awaitingPrice; + private volatile boolean completed; + private BukkitTask timeoutTask; + + public ItemStack item() { + return item == null ? null : item.clone(); + } + + public ItemStack rawItem() { + return item; + } + + public void item(ItemStack item) { + this.item = item == null ? null : item.clone(); + } + + public double price() { + return price; + } + + public void price(double price) { + this.price = price; + } + + public boolean awaitingPrice() { + return awaitingPrice; + } + + public void awaitingPrice(boolean awaitingPrice) { + this.awaitingPrice = awaitingPrice; + } + + public boolean completed() { + return completed; + } + + public void completed(boolean completed) { + this.completed = completed; + } + + public BukkitTask timeoutTask() { + return timeoutTask; + } + + public void timeoutTask(BukkitTask timeoutTask) { + this.timeoutTask = timeoutTask; + } + + public void cancelTimeout() { + if (timeoutTask != null) { + timeoutTask.cancel(); + timeoutTask = null; + } + } +} diff --git a/src/main/java/com/yourname/premiumah/listener/ChatInputListener.java b/src/main/java/com/yourname/premiumah/listener/ChatInputListener.java new file mode 100644 index 0000000..c2c2216 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/listener/ChatInputListener.java @@ -0,0 +1,20 @@ +package com.yourname.premiumah.listener; + +import com.yourname.premiumah.gui.GuiManager; +import io.papermc.paper.event.player.AsyncChatEvent; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; + +public final class ChatInputListener implements Listener { + private final GuiManager guiManager; + + public ChatInputListener(GuiManager guiManager) { + this.guiManager = guiManager; + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onAsyncChat(AsyncChatEvent event) { + guiManager.handleChat(event); + } +} diff --git a/src/main/java/com/yourname/premiumah/listener/InventoryGuiListener.java b/src/main/java/com/yourname/premiumah/listener/InventoryGuiListener.java new file mode 100644 index 0000000..5c3dcb9 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/listener/InventoryGuiListener.java @@ -0,0 +1,31 @@ +package com.yourname.premiumah.listener; + +import com.yourname.premiumah.gui.GuiManager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; + +public final class InventoryGuiListener implements Listener { + private final GuiManager guiManager; + + public InventoryGuiListener(GuiManager guiManager) { + this.guiManager = guiManager; + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + guiManager.handleClick(event); + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + guiManager.handleDrag(event); + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + guiManager.handleClose(event); + } +} diff --git a/src/main/java/com/yourname/premiumah/listener/PlayerSessionListener.java b/src/main/java/com/yourname/premiumah/listener/PlayerSessionListener.java new file mode 100644 index 0000000..08133b4 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/listener/PlayerSessionListener.java @@ -0,0 +1,19 @@ +package com.yourname.premiumah.listener; + +import com.yourname.premiumah.gui.GuiManager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public final class PlayerSessionListener implements Listener { + private final GuiManager guiManager; + + public PlayerSessionListener(GuiManager guiManager) { + this.guiManager = guiManager; + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + guiManager.handleQuit(event.getPlayer()); + } +} diff --git a/src/main/java/com/yourname/premiumah/manager/AuctionHouseManager.java b/src/main/java/com/yourname/premiumah/manager/AuctionHouseManager.java new file mode 100644 index 0000000..f952b3f --- /dev/null +++ b/src/main/java/com/yourname/premiumah/manager/AuctionHouseManager.java @@ -0,0 +1,437 @@ +package com.yourname.premiumah.manager; + +import com.yourname.premiumah.config.ConfigManager; +import com.yourname.premiumah.config.MessageManager; +import com.yourname.premiumah.economy.EconomyService; +import com.yourname.premiumah.model.ActionResult; +import com.yourname.premiumah.model.ClaimReason; +import com.yourname.premiumah.model.ClaimRecord; +import com.yourname.premiumah.model.ClaimType; +import com.yourname.premiumah.model.Listing; +import com.yourname.premiumah.model.ListingCreationResult; +import com.yourname.premiumah.model.ListingStatus; +import com.yourname.premiumah.storage.StorageManager; +import com.yourname.premiumah.util.IdGenerator; +import com.yourname.premiumah.util.InventoryUtil; +import com.yourname.premiumah.util.MaterialUtil; +import com.yourname.premiumah.util.TextUtil; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public final class AuctionHouseManager { + private final JavaPlugin plugin; + private final ConfigManager config; + private final MessageManager messages; + private final StorageManager storage; + private final ListingManager listings; + private final ClaimManager claims; + private final EconomyService economy; + private BukkitTask expirationTask; + + public AuctionHouseManager(JavaPlugin plugin, + ConfigManager config, + MessageManager messages, + StorageManager storage, + ListingManager listings, + ClaimManager claims, + EconomyService economy) { + this.plugin = plugin; + this.config = config; + this.messages = messages; + this.storage = storage; + this.listings = listings; + this.claims = claims; + this.economy = economy; + } + + public void startTasks() { + stopTasks(); + expirationTask = Bukkit.getScheduler().runTaskTimer(plugin, this::expireListings, 100L, config.listingExpireCheckTicks()); + } + + public void stopTasks() { + if (expirationTask != null) { + expirationTask.cancel(); + expirationTask = null; + } + } + + public boolean marketplaceReady() { + return config.economyEnabled() && economy.isAvailable(); + } + + public synchronized void saveAll() { + listings.saveToStorage(); + claims.saveToStorage(); + storage.save(); + } + + public synchronized ListingCreationResult createListing(Player seller, ItemStack item, double price) { + if (!seller.hasPermission("premiumah.sell")) { + return ListingCreationResult.fail("no-permission", Map.of()); + } + if (!marketplaceReady()) { + return ListingCreationResult.fail("economy-unavailable", Map.of()); + } + if (InventoryUtil.isAir(item)) { + return ListingCreationResult.fail("no-item-in-hand", Map.of()); + } + if (!Double.isFinite(price) || price <= 0.0D) { + return ListingCreationResult.fail("invalid-price", Map.of()); + } + + boolean bypassRestrictions = seller.hasPermission("premiumah.bypass.restrictions"); + if (!bypassRestrictions && price < config.minPrice()) { + return ListingCreationResult.fail("price-too-low", Map.of("min", economy.format(config.minPrice()))); + } + if (!bypassRestrictions && price > config.maxPrice()) { + return ListingCreationResult.fail("price-too-high", Map.of("max", economy.format(config.maxPrice()))); + } + if (!config.isItemAllowed(seller, item.getType())) { + return ListingCreationResult.fail("item-blocked", Map.of()); + } + int limit = config.listingLimit(seller); + if (listings.activeCount(seller.getUniqueId()) >= limit) { + return ListingCreationResult.fail("listing-limit", Map.of("limit", String.valueOf(limit))); + } + + double fee = seller.hasPermission("premiumah.bypass.fees") ? 0.0D : (config.listingFeeEnabled() ? config.listingFee() : 0.0D); + if (fee > 0.0D) { + if (!economy.has(seller, fee)) { + return ListingCreationResult.fail("not-enough-money", Map.of("price", economy.format(fee))); + } + if (!economy.withdraw(seller, fee)) { + return ListingCreationResult.fail("economy-unavailable", Map.of()); + } + } + + String id = nextListingId(); + long now = System.currentTimeMillis(); + Listing listing = new Listing( + id, + seller.getUniqueId(), + seller.getName(), + item.clone(), + price, + now, + now + config.defaultListingDurationMillis(), + ListingStatus.ACTIVE, + null, + null, + now + ); + listings.add(listing); + saveAll(); + + Map placeholders = placeholders( + "id", listing.id(), + "item", itemName(item), + "price", economy.format(price), + "fee", economy.format(fee) + ); + return ListingCreationResult.success(listing, fee > 0.0D ? "listing-created-fee" : "listing-created", placeholders); + } + + public synchronized ActionResult buyListing(Player buyer, String listingId) { + if (!buyer.hasPermission("premiumah.buy")) { + return ActionResult.fail("no-permission", Map.of()); + } + if (!marketplaceReady()) { + return ActionResult.fail("economy-unavailable", Map.of()); + } + + Optional optional = listings.get(listingId); + if (optional.isEmpty()) { + return ActionResult.fail("listing-not-found", Map.of()); + } + Listing listing = optional.get(); + long now = System.currentTimeMillis(); + if (!listing.isActive(now)) { + if (listing.status() == ListingStatus.ACTIVE && listing.expiresAt() <= now) { + expireListingInternal(listing); + saveAll(); + } + return ActionResult.fail("listing-no-longer-available", Map.of()); + } + if (!config.allowSellerSelfPurchase() && listing.sellerUuid().equals(buyer.getUniqueId())) { + return ActionResult.fail("cannot-buy-own", Map.of()); + } + + ItemStack item = listing.item(); + boolean fitsInventory = InventoryUtil.canFit(buyer.getInventory(), item); + if (!fitsInventory && (config.requireInventorySpaceToBuy() || !config.claimFullInventoryPurchases())) { + return ActionResult.fail("inventory-full", Map.of()); + } + if (!economy.has(buyer, listing.price())) { + return ActionResult.fail("not-enough-money", Map.of("price", economy.format(listing.price()))); + } + if (!economy.withdraw(buyer, listing.price())) { + return ActionResult.fail("economy-unavailable", Map.of()); + } + + double sellerAmount = sellerProceeds(listing); + OfflinePlayer seller = Bukkit.getOfflinePlayer(listing.sellerUuid()); + if (config.instantSellerPayment()) { + if (!economy.deposit(seller, sellerAmount)) { + economy.deposit(buyer, listing.price()); + return ActionResult.fail("economy-unavailable", Map.of()); + } + } else { + addMoneyClaim(listing.sellerUuid(), listing.sellerName(), listing.id(), sellerAmount, ClaimReason.SALE_PAYMENT); + } + + listing.soldTo(buyer.getUniqueId(), buyer.getName()); + + boolean deliveredToInventory = true; + if (fitsInventory) { + Map leftovers = InventoryUtil.addItem(buyer.getInventory(), item); + if (!leftovers.isEmpty()) { + deliveredToInventory = false; + for (ItemStack leftover : leftovers.values()) { + addItemClaim(buyer.getUniqueId(), buyer.getName(), listing.id(), leftover, ClaimReason.PURCHASE_DELIVERY); + } + } + } else { + deliveredToInventory = false; + addItemClaim(buyer.getUniqueId(), buyer.getName(), listing.id(), item, ClaimReason.PURCHASE_DELIVERY); + } + + saveAll(); + Player onlineSeller = Bukkit.getPlayer(listing.sellerUuid()); + if (onlineSeller != null) { + messages.send(onlineSeller, "sold-notify", placeholders( + "buyer", buyer.getName(), + "price", economy.format(listing.price()), + "item", itemName(item), + "id", listing.id() + )); + } + + return ActionResult.success(deliveredToInventory ? "purchase-success" : "purchase-claim", placeholders( + "price", economy.format(listing.price()), + "item", itemName(item), + "seller", listing.sellerName(), + "id", listing.id() + )); + } + + public synchronized ActionResult cancelListing(Player player, String listingId) { + if (!config.allowCancelActiveListings()) { + return ActionResult.fail("listing-no-longer-available", Map.of()); + } + Optional optional = listings.get(listingId); + if (optional.isEmpty()) { + return ActionResult.fail("listing-not-found", Map.of()); + } + Listing listing = optional.get(); + if (!listing.sellerUuid().equals(player.getUniqueId()) && !player.hasPermission("premiumah.admin.remove")) { + return ActionResult.fail("no-permission", Map.of()); + } + if (!listing.isActive(System.currentTimeMillis())) { + return ActionResult.fail("listing-no-longer-available", Map.of()); + } + listing.status(ListingStatus.CANCELLED); + addItemClaim(listing.sellerUuid(), listing.sellerName(), listing.id(), listing.item(), ClaimReason.CANCELLED_LISTING); + saveAll(); + return ActionResult.success("listing-cancelled", placeholders("id", listing.id())); + } + + public synchronized ActionResult adminRemoveListing(String listingId) { + Optional optional = listings.get(listingId); + if (optional.isEmpty()) { + return ActionResult.fail("listing-not-found", Map.of()); + } + Listing listing = optional.get(); + if (listing.status() == ListingStatus.ACTIVE) { + listing.status(ListingStatus.REMOVED); + if (config.reclaimAdminRemovedItems()) { + addItemClaim(listing.sellerUuid(), listing.sellerName(), listing.id(), listing.item(), ClaimReason.ADMIN_REMOVED); + } + } else { + listing.status(ListingStatus.REMOVED); + } + saveAll(); + Player seller = Bukkit.getPlayer(listing.sellerUuid()); + if (seller != null) { + messages.send(seller, "listing-removed-admin", placeholders("id", listing.id())); + } + return ActionResult.success("listing-removed-admin", placeholders("id", listing.id())); + } + + public synchronized ActionResult forceExpireListing(String listingId) { + Optional optional = listings.get(listingId); + if (optional.isEmpty()) { + return ActionResult.fail("listing-not-found", Map.of()); + } + Listing listing = optional.get(); + if (listing.status() != ListingStatus.ACTIVE) { + return ActionResult.fail("listing-no-longer-available", Map.of()); + } + expireListingInternal(listing); + saveAll(); + return ActionResult.success("listing-expired", placeholders("id", listing.id())); + } + + public synchronized ActionResult claim(Player player, String claimId) { + if (!player.hasPermission("premiumah.expired")) { + return ActionResult.fail("no-permission", Map.of()); + } + Optional optional = claims.get(claimId); + if (optional.isEmpty()) { + return ActionResult.fail("nothing-to-claim", Map.of()); + } + ClaimRecord claim = optional.get(); + if (!claim.ownerUuid().equals(player.getUniqueId())) { + return ActionResult.fail("no-permission", Map.of()); + } + + if (claim.type() == ClaimType.MONEY) { + if (!marketplaceReady()) { + return ActionResult.fail("economy-unavailable", Map.of()); + } + if (!economy.deposit(player, claim.moneyAmount())) { + return ActionResult.fail("economy-unavailable", Map.of()); + } + claims.remove(claim.id()); + saveAll(); + return ActionResult.success("item-reclaimed", placeholders("item", economy.format(claim.moneyAmount()), "id", claim.listingId())); + } + + ItemStack item = claim.item(); + if (InventoryUtil.isAir(item)) { + claims.remove(claim.id()); + saveAll(); + return ActionResult.fail("nothing-to-claim", Map.of()); + } + if (!InventoryUtil.canFit(player.getInventory(), item)) { + return ActionResult.fail("inventory-full", Map.of()); + } + Map leftovers = InventoryUtil.addItem(player.getInventory(), item); + if (!leftovers.isEmpty()) { + return ActionResult.fail("inventory-full", Map.of()); + } + claims.remove(claim.id()); + markListingClaimedIfComplete(claim.listingId()); + saveAll(); + return ActionResult.success("item-reclaimed", placeholders("item", itemName(item), "id", claim.listingId())); + } + + public synchronized void expireListings() { + long now = System.currentTimeMillis(); + boolean changed = false; + for (Listing listing : listings.expiredActiveListings(now)) { + expireListingInternal(listing); + Player seller = Bukkit.getPlayer(listing.sellerUuid()); + if (seller != null) { + messages.send(seller, "listing-expired", placeholders("id", listing.id())); + } + changed = true; + } + if (changed) { + saveAll(); + } + } + + public ClaimRecord addItemClaim(UUID ownerUuid, String ownerName, String listingId, ItemStack item, ClaimReason reason) { + ClaimRecord claim = new ClaimRecord(nextClaimId(), ownerUuid, ownerName, listingId, ClaimType.ITEM, reason, item, 0.0D, System.currentTimeMillis()); + claims.add(claim); + return claim; + } + + public ClaimRecord addMoneyClaim(UUID ownerUuid, String ownerName, String listingId, double amount, ClaimReason reason) { + ClaimRecord claim = new ClaimRecord(nextClaimId(), ownerUuid, ownerName, listingId, ClaimType.MONEY, reason, null, amount, System.currentTimeMillis()); + claims.add(claim); + return claim; + } + + public double sellerProceeds(Listing listing) { + double amount = listing.price(); + if (!config.salesTaxEnabled()) { + return amount; + } + Player seller = Bukkit.getPlayer(listing.sellerUuid()); + if (seller != null && seller.hasPermission("premiumah.bypass.tax")) { + return amount; + } + double tax = amount * (config.salesTaxPercent() / 100.0D); + return Math.max(0.0D, amount - tax); + } + + public String formatMoney(double amount) { + return economy.format(amount); + } + + public String itemName(ItemStack item) { + if (InventoryUtil.isAir(item)) { + return "Air"; + } + ItemMeta meta = item.getItemMeta(); + String name; + if (meta != null && meta.hasDisplayName()) { + name = TextUtil.plain(meta.displayName()); + } else { + name = MaterialUtil.pretty(item.getType()); + } + if (item.getAmount() > 1) { + return name + " x" + item.getAmount(); + } + return name; + } + + private void expireListingInternal(Listing listing) { + if (listing.status() != ListingStatus.ACTIVE) { + return; + } + listing.status(ListingStatus.EXPIRED); + if (!claims.hasItemClaimForListing(listing.id())) { + addItemClaim(listing.sellerUuid(), listing.sellerName(), listing.id(), listing.item(), ClaimReason.EXPIRED_LISTING); + } + } + + private void markListingClaimedIfComplete(String listingId) { + if (listingId == null || claims.hasItemClaimForListing(listingId)) { + return; + } + listings.get(listingId).ifPresent(listing -> { + if (listing.status() == ListingStatus.EXPIRED + || listing.status() == ListingStatus.CANCELLED + || listing.status() == ListingStatus.REMOVED) { + listing.status(ListingStatus.CLAIMED); + } + }); + } + + private String nextListingId() { + String id; + do { + id = IdGenerator.listingId(); + } while (listings.contains(id)); + return id; + } + + private String nextClaimId() { + String id; + do { + id = IdGenerator.claimId(); + } while (claims.get(id).isPresent()); + return id; + } + + private Map placeholders(String... pairs) { + Map map = new HashMap<>(); + for (int i = 0; i + 1 < pairs.length; i += 2) { + map.put(pairs[i], pairs[i + 1] == null ? "" : pairs[i + 1]); + } + return map; + } +} diff --git a/src/main/java/com/yourname/premiumah/manager/ClaimManager.java b/src/main/java/com/yourname/premiumah/manager/ClaimManager.java new file mode 100644 index 0000000..0daa746 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/manager/ClaimManager.java @@ -0,0 +1,67 @@ +package com.yourname.premiumah.manager; + +import com.yourname.premiumah.model.ClaimRecord; +import com.yourname.premiumah.model.ClaimType; +import com.yourname.premiumah.storage.StorageManager; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public final class ClaimManager { + private final StorageManager storageManager; + private final Map claims = new LinkedHashMap<>(); + + public ClaimManager(StorageManager storageManager) { + this.storageManager = storageManager; + } + + public synchronized void loadFromStorage() { + claims.clear(); + claims.putAll(storageManager.claimsSnapshot()); + } + + public synchronized void saveToStorage() { + storageManager.replaceClaims(claims); + } + + public synchronized void clear() { + claims.clear(); + } + + public synchronized void add(ClaimRecord claim) { + claims.put(claim.id(), claim); + } + + public synchronized Optional get(String id) { + if (id == null) { + return Optional.empty(); + } + return Optional.ofNullable(claims.get(id)); + } + + public synchronized void remove(String id) { + claims.remove(id); + } + + public synchronized List claimsFor(UUID ownerUuid) { + return claims.values().stream() + .filter(claim -> claim.ownerUuid().equals(ownerUuid)) + .sorted(Comparator.comparingLong(ClaimRecord::createdAt).reversed()) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public synchronized boolean hasClaims(UUID ownerUuid) { + return claims.values().stream().anyMatch(claim -> claim.ownerUuid().equals(ownerUuid)); + } + + public synchronized boolean hasItemClaimForListing(String listingId) { + return claims.values().stream() + .anyMatch(claim -> claim.type() == ClaimType.ITEM && listingId != null && listingId.equals(claim.listingId())); + } +} diff --git a/src/main/java/com/yourname/premiumah/manager/ListingManager.java b/src/main/java/com/yourname/premiumah/manager/ListingManager.java new file mode 100644 index 0000000..078d15b --- /dev/null +++ b/src/main/java/com/yourname/premiumah/manager/ListingManager.java @@ -0,0 +1,110 @@ +package com.yourname.premiumah.manager; + +import com.yourname.premiumah.model.Listing; +import com.yourname.premiumah.model.ListingStatus; +import com.yourname.premiumah.model.SortMode; +import com.yourname.premiumah.storage.StorageManager; +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public final class ListingManager { + private final StorageManager storageManager; + private final Map listings = new LinkedHashMap<>(); + + public ListingManager(StorageManager storageManager) { + this.storageManager = storageManager; + } + + public synchronized void loadFromStorage() { + listings.clear(); + listings.putAll(storageManager.listingsSnapshot()); + } + + public synchronized void saveToStorage() { + storageManager.replaceListings(listings); + } + + public synchronized void clear() { + listings.clear(); + } + + public synchronized Optional get(String id) { + if (id == null) { + return Optional.empty(); + } + return Optional.ofNullable(listings.get(id.toUpperCase())); + } + + public synchronized boolean contains(String id) { + return listings.containsKey(id.toUpperCase()); + } + + public synchronized void add(Listing listing) { + listings.put(listing.id(), listing); + } + + public synchronized Collection all() { + return new ArrayList<>(listings.values()); + } + + public synchronized List activeListings(SortMode sortMode, Material filter) { + long now = System.currentTimeMillis(); + return listings.values().stream() + .filter(listing -> listing.isActive(now)) + .filter(listing -> filter == null || listing.rawItem().getType() == filter) + .sorted(comparator(sortMode)) + .collect(Collectors.toList()); + } + + public synchronized List activeListingsBySeller(UUID sellerUuid, SortMode sortMode) { + long now = System.currentTimeMillis(); + return listings.values().stream() + .filter(listing -> listing.sellerUuid().equals(sellerUuid)) + .filter(listing -> listing.isActive(now)) + .sorted(comparator(sortMode)) + .collect(Collectors.toList()); + } + + public synchronized List activeListingsBySellerName(String sellerName, SortMode sortMode) { + long now = System.currentTimeMillis(); + String normalized = sellerName == null ? "" : sellerName.toLowerCase(); + return listings.values().stream() + .filter(listing -> listing.sellerName().toLowerCase().equals(normalized)) + .filter(listing -> listing.isActive(now)) + .sorted(comparator(sortMode)) + .collect(Collectors.toList()); + } + + public synchronized int activeCount(UUID sellerUuid) { + long now = System.currentTimeMillis(); + return (int) listings.values().stream() + .filter(listing -> listing.sellerUuid().equals(sellerUuid)) + .filter(listing -> listing.isActive(now)) + .count(); + } + + public synchronized List expiredActiveListings(long now) { + return listings.values().stream() + .filter(listing -> listing.status() == ListingStatus.ACTIVE) + .filter(listing -> listing.expiresAt() <= now) + .collect(Collectors.toList()); + } + + private Comparator comparator(SortMode sortMode) { + return switch (sortMode == null ? SortMode.NEWEST : sortMode) { + case OLDEST -> Comparator.comparingLong(Listing::createdAt); + case LOWEST_PRICE -> Comparator.comparingDouble(Listing::price).thenComparing(Comparator.comparingLong(Listing::createdAt).reversed()); + case HIGHEST_PRICE -> Comparator.comparingDouble(Listing::price).reversed().thenComparing(Comparator.comparingLong(Listing::createdAt).reversed()); + case NEWEST -> Comparator.comparingLong(Listing::createdAt).reversed(); + }; + } +} diff --git a/src/main/java/com/yourname/premiumah/model/ActionResult.java b/src/main/java/com/yourname/premiumah/model/ActionResult.java new file mode 100644 index 0000000..57009ed --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/ActionResult.java @@ -0,0 +1,14 @@ +package com.yourname.premiumah.model; + +import java.util.Collections; +import java.util.Map; + +public record ActionResult(boolean success, String messageKey, Map placeholders) { + public static ActionResult success(String messageKey, Map placeholders) { + return new ActionResult(true, messageKey, placeholders == null ? Collections.emptyMap() : placeholders); + } + + public static ActionResult fail(String messageKey, Map placeholders) { + return new ActionResult(false, messageKey, placeholders == null ? Collections.emptyMap() : placeholders); + } +} diff --git a/src/main/java/com/yourname/premiumah/model/ClaimReason.java b/src/main/java/com/yourname/premiumah/model/ClaimReason.java new file mode 100644 index 0000000..61757c3 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/ClaimReason.java @@ -0,0 +1,19 @@ +package com.yourname.premiumah.model; + +public enum ClaimReason { + EXPIRED_LISTING("Expired Listing"), + ADMIN_REMOVED("Admin Removed"), + CANCELLED_LISTING("Cancelled Listing"), + PURCHASE_DELIVERY("Purchased Item"), + SALE_PAYMENT("Sale Payment"); + + private final String displayName; + + ClaimReason(String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } +} diff --git a/src/main/java/com/yourname/premiumah/model/ClaimRecord.java b/src/main/java/com/yourname/premiumah/model/ClaimRecord.java new file mode 100644 index 0000000..b015bc5 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/ClaimRecord.java @@ -0,0 +1,86 @@ +package com.yourname.premiumah.model; + +import org.bukkit.inventory.ItemStack; + +import java.util.Objects; +import java.util.UUID; + +public final class ClaimRecord { + private final String id; + private final UUID ownerUuid; + private final String ownerName; + private final String listingId; + private final ClaimType type; + private final ClaimReason reason; + private ItemStack item; + private double moneyAmount; + private final long createdAt; + + public ClaimRecord(String id, + UUID ownerUuid, + String ownerName, + String listingId, + ClaimType type, + ClaimReason reason, + ItemStack item, + double moneyAmount, + long createdAt) { + this.id = Objects.requireNonNull(id, "id"); + this.ownerUuid = Objects.requireNonNull(ownerUuid, "ownerUuid"); + this.ownerName = Objects.requireNonNullElse(ownerName, "Unknown"); + this.listingId = listingId; + this.type = Objects.requireNonNull(type, "type"); + this.reason = Objects.requireNonNull(reason, "reason"); + this.item = item == null ? null : item.clone(); + this.moneyAmount = moneyAmount; + this.createdAt = createdAt; + } + + public String id() { + return id; + } + + public UUID ownerUuid() { + return ownerUuid; + } + + public String ownerName() { + return ownerName; + } + + public String listingId() { + return listingId; + } + + public ClaimType type() { + return type; + } + + public ClaimReason reason() { + return reason; + } + + public ItemStack item() { + return item == null ? null : item.clone(); + } + + public ItemStack rawItem() { + return item; + } + + public void item(ItemStack item) { + this.item = item == null ? null : item.clone(); + } + + public double moneyAmount() { + return moneyAmount; + } + + public void moneyAmount(double moneyAmount) { + this.moneyAmount = moneyAmount; + } + + public long createdAt() { + return createdAt; + } +} diff --git a/src/main/java/com/yourname/premiumah/model/ClaimType.java b/src/main/java/com/yourname/premiumah/model/ClaimType.java new file mode 100644 index 0000000..611602e --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/ClaimType.java @@ -0,0 +1,6 @@ +package com.yourname.premiumah.model; + +public enum ClaimType { + ITEM, + MONEY +} diff --git a/src/main/java/com/yourname/premiumah/model/Listing.java b/src/main/java/com/yourname/premiumah/model/Listing.java new file mode 100644 index 0000000..c6d3e59 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/Listing.java @@ -0,0 +1,112 @@ +package com.yourname.premiumah.model; + +import org.bukkit.inventory.ItemStack; + +import java.util.Objects; +import java.util.UUID; + +public final class Listing { + private final String id; + private final UUID sellerUuid; + private final String sellerName; + private final ItemStack item; + private final double price; + private final long createdAt; + private final long expiresAt; + private ListingStatus status; + private UUID buyerUuid; + private String buyerName; + private long updatedAt; + + public Listing(String id, + UUID sellerUuid, + String sellerName, + ItemStack item, + double price, + long createdAt, + long expiresAt, + ListingStatus status, + UUID buyerUuid, + String buyerName, + long updatedAt) { + this.id = Objects.requireNonNull(id, "id"); + this.sellerUuid = Objects.requireNonNull(sellerUuid, "sellerUuid"); + this.sellerName = Objects.requireNonNullElse(sellerName, "Unknown"); + this.item = Objects.requireNonNull(item, "item").clone(); + this.price = price; + this.createdAt = createdAt; + this.expiresAt = expiresAt; + this.status = Objects.requireNonNull(status, "status"); + this.buyerUuid = buyerUuid; + this.buyerName = buyerName; + this.updatedAt = updatedAt; + } + + public String id() { + return id; + } + + public UUID sellerUuid() { + return sellerUuid; + } + + public String sellerName() { + return sellerName; + } + + public ItemStack item() { + return item.clone(); + } + + public ItemStack rawItem() { + return item; + } + + public double price() { + return price; + } + + public long createdAt() { + return createdAt; + } + + public long expiresAt() { + return expiresAt; + } + + public ListingStatus status() { + return status; + } + + public void status(ListingStatus status) { + this.status = Objects.requireNonNull(status, "status"); + touch(); + } + + public UUID buyerUuid() { + return buyerUuid; + } + + public String buyerName() { + return buyerName; + } + + public long updatedAt() { + return updatedAt; + } + + public boolean isActive(long now) { + return status == ListingStatus.ACTIVE && expiresAt > now; + } + + public void soldTo(UUID buyerUuid, String buyerName) { + this.status = ListingStatus.SOLD; + this.buyerUuid = buyerUuid; + this.buyerName = buyerName; + touch(); + } + + private void touch() { + this.updatedAt = System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/yourname/premiumah/model/ListingCreationResult.java b/src/main/java/com/yourname/premiumah/model/ListingCreationResult.java new file mode 100644 index 0000000..26392a4 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/ListingCreationResult.java @@ -0,0 +1,14 @@ +package com.yourname.premiumah.model; + +import java.util.Collections; +import java.util.Map; + +public record ListingCreationResult(boolean success, Listing listing, String messageKey, Map placeholders) { + public static ListingCreationResult success(Listing listing, String messageKey, Map placeholders) { + return new ListingCreationResult(true, listing, messageKey, placeholders == null ? Collections.emptyMap() : placeholders); + } + + public static ListingCreationResult fail(String messageKey, Map placeholders) { + return new ListingCreationResult(false, null, messageKey, placeholders == null ? Collections.emptyMap() : placeholders); + } +} diff --git a/src/main/java/com/yourname/premiumah/model/ListingStatus.java b/src/main/java/com/yourname/premiumah/model/ListingStatus.java new file mode 100644 index 0000000..c8e6e53 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/ListingStatus.java @@ -0,0 +1,10 @@ +package com.yourname.premiumah.model; + +public enum ListingStatus { + ACTIVE, + SOLD, + EXPIRED, + CLAIMED, + REMOVED, + CANCELLED +} diff --git a/src/main/java/com/yourname/premiumah/model/SortMode.java b/src/main/java/com/yourname/premiumah/model/SortMode.java new file mode 100644 index 0000000..273602e --- /dev/null +++ b/src/main/java/com/yourname/premiumah/model/SortMode.java @@ -0,0 +1,36 @@ +package com.yourname.premiumah.model; + +import java.util.Locale; + +public enum SortMode { + NEWEST("Newest"), + OLDEST("Oldest"), + LOWEST_PRICE("Lowest Price"), + HIGHEST_PRICE("Highest Price"); + + private final String displayName; + + SortMode(String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } + + public SortMode next() { + SortMode[] values = values(); + return values[(ordinal() + 1) % values.length]; + } + + public static SortMode fromString(String value, SortMode fallback) { + if (value == null || value.isBlank()) { + return fallback; + } + try { + return SortMode.valueOf(value.trim().toUpperCase(Locale.ROOT).replace('-', '_')); + } catch (IllegalArgumentException ignored) { + return fallback; + } + } +} diff --git a/src/main/java/com/yourname/premiumah/storage/ItemStackSerializer.java b/src/main/java/com/yourname/premiumah/storage/ItemStackSerializer.java new file mode 100644 index 0000000..81383f7 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/storage/ItemStackSerializer.java @@ -0,0 +1,34 @@ +package com.yourname.premiumah.storage; + +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; + +final class ItemStackSerializer { + private ItemStackSerializer() { + } + + static String toBase64(ItemStack item) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (BukkitObjectOutputStream data = new BukkitObjectOutputStream(output)) { + data.writeObject(item); + } + return Base64.getEncoder().encodeToString(output.toByteArray()); + } + + static ItemStack fromBase64(String encoded) throws IOException, ClassNotFoundException { + byte[] bytes = Base64.getDecoder().decode(encoded); + try (BukkitObjectInputStream data = new BukkitObjectInputStream(new ByteArrayInputStream(bytes))) { + Object object = data.readObject(); + if (!(object instanceof ItemStack item)) { + throw new IOException("Serialized object was not an ItemStack."); + } + return item; + } + } +} diff --git a/src/main/java/com/yourname/premiumah/storage/StorageManager.java b/src/main/java/com/yourname/premiumah/storage/StorageManager.java new file mode 100644 index 0000000..c21772e --- /dev/null +++ b/src/main/java/com/yourname/premiumah/storage/StorageManager.java @@ -0,0 +1,448 @@ +package com.yourname.premiumah.storage; + +import com.yourname.premiumah.model.ClaimReason; +import com.yourname.premiumah.model.ClaimRecord; +import com.yourname.premiumah.model.ClaimType; +import com.yourname.premiumah.model.Listing; +import com.yourname.premiumah.model.ListingStatus; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +public final class StorageManager { + private static final String LEGACY_IMPORT_KEY = "legacy_yaml_imported"; + + private final JavaPlugin plugin; + private File databaseFile; + private File legacyYamlFile; + private Connection connection; + private final Map listings = new LinkedHashMap<>(); + private final Map claims = new LinkedHashMap<>(); + + public StorageManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + public synchronized void load() { + plugin.getDataFolder().mkdirs(); + String databaseName = plugin.getConfig().getString("storage.sqlite-file", "auctionhouse.db"); + if (databaseName == null || databaseName.isBlank()) { + databaseName = "auctionhouse.db"; + } + this.databaseFile = new File(plugin.getDataFolder(), databaseName); + this.legacyYamlFile = new File(plugin.getDataFolder(), "data.yml"); + + listings.clear(); + claims.clear(); + try { + connect(); + createSchema(); + loadListingsFromDatabase(); + loadClaimsFromDatabase(); + importLegacyYamlIfNeeded(); + plugin.getLogger().info("Loaded " + listings.size() + " listings and " + claims.size() + " claims from SQLite."); + } catch (SQLException exception) { + plugin.getLogger().severe("Failed to load SQLite storage: " + exception.getMessage()); + } + } + + public synchronized void save() { + try { + connect(); + connection.setAutoCommit(false); + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("DELETE FROM listings"); + statement.executeUpdate("DELETE FROM claims"); + } + saveListings(); + saveClaims(); + connection.commit(); + } catch (SQLException | IOException exception) { + rollbackQuietly(); + plugin.getLogger().severe("Failed to save SQLite storage: " + exception.getMessage()); + } finally { + setAutoCommitQuietly(true); + } + } + + public synchronized void close() { + if (connection == null) { + return; + } + try { + connection.close(); + } catch (SQLException exception) { + plugin.getLogger().warning("Failed to close SQLite connection: " + exception.getMessage()); + } finally { + connection = null; + } + } + + public synchronized Map listingsSnapshot() { + return new LinkedHashMap<>(listings); + } + + public synchronized Map claimsSnapshot() { + return new LinkedHashMap<>(claims); + } + + public synchronized void replaceListings(Map replacement) { + listings.clear(); + listings.putAll(replacement); + } + + public synchronized void replaceClaims(Map replacement) { + claims.clear(); + claims.putAll(replacement); + } + + private void connect() throws SQLException { + if (connection != null && !connection.isClosed()) { + return; + } + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException exception) { + throw new SQLException("SQLite JDBC driver is missing from the plugin jar.", exception); + } + connection = DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getAbsolutePath()); + try (Statement statement = connection.createStatement()) { + statement.execute("PRAGMA busy_timeout = 5000"); + statement.execute("PRAGMA foreign_keys = ON"); + statement.execute("PRAGMA journal_mode = WAL"); + } + } + + private void createSchema() throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate(""" + CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """); + statement.executeUpdate(""" + CREATE TABLE IF NOT EXISTS listings ( + id TEXT PRIMARY KEY, + seller_uuid TEXT NOT NULL, + seller_name TEXT NOT NULL, + item TEXT NOT NULL, + price REAL NOT NULL, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL, + status TEXT NOT NULL, + buyer_uuid TEXT, + buyer_name TEXT, + updated_at INTEGER NOT NULL + ) + """); + statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_listings_status_expires ON listings(status, expires_at)"); + statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_listings_seller ON listings(seller_uuid, status)"); + statement.executeUpdate(""" + CREATE TABLE IF NOT EXISTS claims ( + id TEXT PRIMARY KEY, + owner_uuid TEXT NOT NULL, + owner_name TEXT NOT NULL, + listing_id TEXT, + type TEXT NOT NULL, + reason TEXT NOT NULL, + item TEXT, + money_amount REAL NOT NULL, + created_at INTEGER NOT NULL + ) + """); + statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_claims_owner ON claims(owner_uuid, created_at)"); + statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_claims_listing ON claims(listing_id)"); + } + } + + private void loadListingsFromDatabase() throws SQLException { + String sql = """ + SELECT id, seller_uuid, seller_name, item, price, created_at, expires_at, status, + buyer_uuid, buyer_name, updated_at + FROM listings + ORDER BY created_at DESC + """; + try (PreparedStatement statement = connection.prepareStatement(sql); + ResultSet result = statement.executeQuery()) { + while (result.next()) { + String id = result.getString("id"); + try { + ItemStack item = ItemStackSerializer.fromBase64(result.getString("item")); + Listing listing = new Listing( + id, + UUID.fromString(result.getString("seller_uuid")), + result.getString("seller_name"), + item, + result.getDouble("price"), + result.getLong("created_at"), + result.getLong("expires_at"), + parseEnum(ListingStatus.class, result.getString("status"), ListingStatus.ACTIVE), + parseUuid(result.getString("buyer_uuid")), + result.getString("buyer_name"), + result.getLong("updated_at") + ); + listings.put(id, listing); + } catch (RuntimeException | IOException | ClassNotFoundException exception) { + plugin.getLogger().warning("Skipping invalid SQLite listing " + id + ": " + exception.getMessage()); + } + } + } + } + + private void loadClaimsFromDatabase() throws SQLException { + String sql = """ + SELECT id, owner_uuid, owner_name, listing_id, type, reason, item, money_amount, created_at + FROM claims + ORDER BY created_at DESC + """; + try (PreparedStatement statement = connection.prepareStatement(sql); + ResultSet result = statement.executeQuery()) { + while (result.next()) { + String id = result.getString("id"); + try { + ClaimType type = parseEnum(ClaimType.class, result.getString("type"), ClaimType.ITEM); + ItemStack item = null; + String encodedItem = result.getString("item"); + if (encodedItem != null && !encodedItem.isBlank()) { + item = ItemStackSerializer.fromBase64(encodedItem); + } + if (type == ClaimType.ITEM && item == null) { + plugin.getLogger().warning("Skipping SQLite item claim " + id + " because its item is missing."); + continue; + } + ClaimRecord claim = new ClaimRecord( + id, + UUID.fromString(result.getString("owner_uuid")), + result.getString("owner_name"), + result.getString("listing_id"), + type, + parseEnum(ClaimReason.class, result.getString("reason"), ClaimReason.EXPIRED_LISTING), + item, + result.getDouble("money_amount"), + result.getLong("created_at") + ); + claims.put(id, claim); + } catch (RuntimeException | IOException | ClassNotFoundException exception) { + plugin.getLogger().warning("Skipping invalid SQLite claim " + id + ": " + exception.getMessage()); + } + } + } + } + + private void saveListings() throws SQLException, IOException { + String sql = """ + INSERT INTO listings + (id, seller_uuid, seller_name, item, price, created_at, expires_at, status, buyer_uuid, buyer_name, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + for (Listing listing : listings.values()) { + statement.setString(1, listing.id()); + statement.setString(2, listing.sellerUuid().toString()); + statement.setString(3, listing.sellerName()); + statement.setString(4, ItemStackSerializer.toBase64(listing.rawItem())); + statement.setDouble(5, listing.price()); + statement.setLong(6, listing.createdAt()); + statement.setLong(7, listing.expiresAt()); + statement.setString(8, listing.status().name()); + statement.setString(9, listing.buyerUuid() == null ? null : listing.buyerUuid().toString()); + statement.setString(10, listing.buyerName()); + statement.setLong(11, listing.updatedAt()); + statement.addBatch(); + } + statement.executeBatch(); + } + } + + private void saveClaims() throws SQLException, IOException { + String sql = """ + INSERT INTO claims + (id, owner_uuid, owner_name, listing_id, type, reason, item, money_amount, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + for (ClaimRecord claim : claims.values()) { + statement.setString(1, claim.id()); + statement.setString(2, claim.ownerUuid().toString()); + statement.setString(3, claim.ownerName()); + statement.setString(4, claim.listingId()); + statement.setString(5, claim.type().name()); + statement.setString(6, claim.reason().name()); + statement.setString(7, claim.rawItem() == null ? null : ItemStackSerializer.toBase64(claim.rawItem())); + statement.setDouble(8, claim.moneyAmount()); + statement.setLong(9, claim.createdAt()); + statement.addBatch(); + } + statement.executeBatch(); + } + } + + private void importLegacyYamlIfNeeded() throws SQLException { + if (!plugin.getConfig().getBoolean("storage.import-legacy-yaml", true) + || !listings.isEmpty() + || !claims.isEmpty() + || getMetadata(LEGACY_IMPORT_KEY).equals("true")) { + return; + } + if (legacyYamlFile == null || !legacyYamlFile.exists() || legacyYamlFile.length() == 0L) { + return; + } + + YamlConfiguration data = YamlConfiguration.loadConfiguration(legacyYamlFile); + int beforeListings = listings.size(); + int beforeClaims = claims.size(); + loadLegacyListings(data.getConfigurationSection("listings")); + loadLegacyClaims(data.getConfigurationSection("claims")); + setMetadata(LEGACY_IMPORT_KEY, "true"); + + int importedListings = listings.size() - beforeListings; + int importedClaims = claims.size() - beforeClaims; + if (importedListings > 0 || importedClaims > 0) { + plugin.getLogger().info("Imported " + importedListings + " listings and " + importedClaims + " claims from legacy data.yml."); + save(); + } + } + + private void loadLegacyListings(ConfigurationSection root) { + if (root == null) { + return; + } + for (String id : root.getKeys(false)) { + ConfigurationSection section = root.getConfigurationSection(id); + if (section == null) { + continue; + } + try { + ItemStack item = section.getItemStack("item"); + if (item == null) { + plugin.getLogger().warning("Skipping legacy listing " + id + " because its item is missing."); + continue; + } + Listing listing = new Listing( + id, + UUID.fromString(section.getString("seller-uuid", "")), + section.getString("seller-name", "Unknown"), + item, + section.getDouble("price"), + section.getLong("created-at"), + section.getLong("expires-at"), + parseEnum(ListingStatus.class, section.getString("status"), ListingStatus.ACTIVE), + parseUuid(section.getString("buyer-uuid")), + section.getString("buyer-name", null), + section.getLong("updated-at", section.getLong("created-at")) + ); + listings.put(id, listing); + } catch (RuntimeException exception) { + plugin.getLogger().warning("Skipping invalid legacy listing " + id + ": " + exception.getMessage()); + } + } + } + + private void loadLegacyClaims(ConfigurationSection root) { + if (root == null) { + return; + } + for (String id : root.getKeys(false)) { + ConfigurationSection section = root.getConfigurationSection(id); + if (section == null) { + continue; + } + try { + ClaimType type = parseEnum(ClaimType.class, section.getString("type"), ClaimType.ITEM); + ItemStack item = section.getItemStack("item"); + if (type == ClaimType.ITEM && item == null) { + plugin.getLogger().warning("Skipping legacy item claim " + id + " because its item is missing."); + continue; + } + ClaimRecord claim = new ClaimRecord( + id, + UUID.fromString(section.getString("owner-uuid", "")), + section.getString("owner-name", "Unknown"), + section.getString("listing-id", null), + type, + parseEnum(ClaimReason.class, section.getString("reason"), ClaimReason.EXPIRED_LISTING), + item, + section.getDouble("money-amount"), + section.getLong("created-at") + ); + claims.put(id, claim); + } catch (RuntimeException exception) { + plugin.getLogger().warning("Skipping invalid legacy claim " + id + ": " + exception.getMessage()); + } + } + } + + private String getMetadata(String key) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement("SELECT value FROM metadata WHERE key = ?")) { + statement.setString(1, key); + try (ResultSet result = statement.executeQuery()) { + if (result.next()) { + return result.getString("value"); + } + } + } + return ""; + } + + private void setMetadata(String key, String value) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(""" + INSERT INTO metadata(key, value) + VALUES(?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + """)) { + statement.setString(1, key); + statement.setString(2, value); + statement.executeUpdate(); + } + } + + private void rollbackQuietly() { + try { + if (connection != null) { + connection.rollback(); + } + } catch (SQLException ignored) { + } + } + + private void setAutoCommitQuietly(boolean autoCommit) { + try { + if (connection != null) { + connection.setAutoCommit(autoCommit); + } + } catch (SQLException ignored) { + } + } + + private UUID parseUuid(String raw) { + if (raw == null || raw.isBlank()) { + return null; + } + return UUID.fromString(raw); + } + + private > E parseEnum(Class type, String raw, E fallback) { + if (raw == null || raw.isBlank()) { + return fallback; + } + try { + return Enum.valueOf(type, raw); + } catch (IllegalArgumentException ignored) { + return fallback; + } + } +} diff --git a/src/main/java/com/yourname/premiumah/util/IdGenerator.java b/src/main/java/com/yourname/premiumah/util/IdGenerator.java new file mode 100644 index 0000000..3c283b7 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/util/IdGenerator.java @@ -0,0 +1,27 @@ +package com.yourname.premiumah.util; + +import java.security.SecureRandom; + +public final class IdGenerator { + private static final char[] ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray(); + private static final SecureRandom RANDOM = new SecureRandom(); + + private IdGenerator() { + } + + public static String listingId() { + return "AH-" + random(8); + } + + public static String claimId() { + return "CL-" + random(10); + } + + private static String random(int length) { + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + chars[i] = ALPHABET[RANDOM.nextInt(ALPHABET.length)]; + } + return new String(chars); + } +} diff --git a/src/main/java/com/yourname/premiumah/util/InventoryUtil.java b/src/main/java/com/yourname/premiumah/util/InventoryUtil.java new file mode 100644 index 0000000..ad35959 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/util/InventoryUtil.java @@ -0,0 +1,43 @@ +package com.yourname.premiumah.util; + +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.Map; + +public final class InventoryUtil { + private InventoryUtil() { + } + + public static boolean isAir(ItemStack item) { + return item == null || item.getType() == Material.AIR || item.getAmount() <= 0; + } + + public static boolean canFit(Inventory inventory, ItemStack item) { + if (isAir(item)) { + return true; + } + int remaining = item.getAmount(); + int max = item.getMaxStackSize(); + for (ItemStack slot : inventory.getStorageContents()) { + if (isAir(slot)) { + remaining -= max; + } else if (slot.isSimilar(item)) { + remaining -= Math.max(0, max - slot.getAmount()); + } + if (remaining <= 0) { + return true; + } + } + return false; + } + + public static Map addItem(Inventory inventory, ItemStack item) { + if (isAir(item)) { + return new HashMap<>(); + } + return inventory.addItem(item.clone()); + } +} diff --git a/src/main/java/com/yourname/premiumah/util/MaterialUtil.java b/src/main/java/com/yourname/premiumah/util/MaterialUtil.java new file mode 100644 index 0000000..a302d9b --- /dev/null +++ b/src/main/java/com/yourname/premiumah/util/MaterialUtil.java @@ -0,0 +1,42 @@ +package com.yourname.premiumah.util; + +import org.bukkit.Material; + +import java.util.Locale; +import java.util.Optional; + +public final class MaterialUtil { + private MaterialUtil() { + } + + public static Optional parse(String value) { + if (value == null || value.isBlank()) { + return Optional.empty(); + } + String normalized = value.trim().toUpperCase(Locale.ROOT).replace(' ', '_').replace('-', '_'); + try { + Material material = Material.valueOf(normalized); + return Optional.of(material); + } catch (IllegalArgumentException ignored) { + return Optional.empty(); + } + } + + public static String pretty(Material material) { + String lower = material.name().toLowerCase(Locale.ROOT).replace('_', ' '); + StringBuilder builder = new StringBuilder(lower.length()); + boolean cap = true; + for (char c : lower.toCharArray()) { + if (cap && Character.isLetter(c)) { + builder.append(Character.toUpperCase(c)); + cap = false; + } else { + builder.append(c); + } + if (c == ' ') { + cap = true; + } + } + return builder.toString(); + } +} diff --git a/src/main/java/com/yourname/premiumah/util/TextUtil.java b/src/main/java/com/yourname/premiumah/util/TextUtil.java new file mode 100644 index 0000000..2553e00 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/util/TextUtil.java @@ -0,0 +1,100 @@ +package com.yourname.premiumah.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +import java.util.Locale; + +public final class TextUtil { + private static final MiniMessage MINI = MiniMessage.miniMessage(); + private static final LegacyComponentSerializer LEGACY_AMPERSAND = LegacyComponentSerializer.legacyAmpersand(); + private static final LegacyComponentSerializer LEGACY_SECTION_HEX = LegacyComponentSerializer.builder() + .character('§') + .hexColors() + .useUnusualXRepeatedCharacterHexFormat() + .build(); + + private TextUtil() { + } + + public static Component component(String raw) { + if (raw == null || raw.isEmpty()) { + return Component.empty(); + } + String normalized = normalizeHexAmpersand(raw); + if (normalized.contains("<")) { + return MINI.deserialize(normalized); + } + return LEGACY_AMPERSAND.deserialize(normalized); + } + + public static String legacy(String raw) { + return LEGACY_SECTION_HEX.serialize(component(raw)); + } + + public static String plain(Component component) { + if (component == null) { + return ""; + } + return PlainTextComponentSerializer.plainText().serialize(component); + } + + public static String normalizeHexAmpersand(String raw) { + StringBuilder builder = new StringBuilder(raw.length() + 16); + for (int i = 0; i < raw.length(); i++) { + char c = raw.charAt(i); + if (c == '&' && i + 7 < raw.length() && raw.charAt(i + 1) == '#') { + String hex = raw.substring(i + 2, i + 8); + if (hex.chars().allMatch(TextUtil::isHex)) { + builder.append("<#").append(hex.toLowerCase(Locale.ROOT)).append(">"); + i += 7; + continue; + } + } + if (c == '&' && i + 1 < raw.length()) { + String tag = legacyTag(raw.charAt(i + 1)); + if (tag != null) { + builder.append(tag); + i++; + continue; + } + } + builder.append(c); + } + return builder.toString(); + } + + private static boolean isHex(int c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + private static String legacyTag(char code) { + return switch (Character.toLowerCase(code)) { + case '0' -> ""; + case '1' -> ""; + case '2' -> ""; + case '3' -> ""; + case '4' -> ""; + case '5' -> ""; + case '6' -> ""; + case '7' -> ""; + case '8' -> ""; + case '9' -> ""; + case 'a' -> ""; + case 'b' -> ""; + case 'c' -> ""; + case 'd' -> ""; + case 'e' -> ""; + case 'f' -> ""; + case 'k' -> ""; + case 'l' -> ""; + case 'm' -> ""; + case 'n' -> ""; + case 'o' -> ""; + case 'r' -> ""; + default -> null; + }; + } +} diff --git a/src/main/java/com/yourname/premiumah/util/TimeUtil.java b/src/main/java/com/yourname/premiumah/util/TimeUtil.java new file mode 100644 index 0000000..3c1d262 --- /dev/null +++ b/src/main/java/com/yourname/premiumah/util/TimeUtil.java @@ -0,0 +1,39 @@ +package com.yourname.premiumah.util; + +import java.time.Duration; + +public final class TimeUtil { + private TimeUtil() { + } + + public static String remaining(long untilMillis) { + long millis = Math.max(0L, untilMillis - System.currentTimeMillis()); + return compact(Duration.ofMillis(millis)); + } + + public static String age(long sinceMillis) { + long millis = Math.max(0L, System.currentTimeMillis() - sinceMillis); + return compact(Duration.ofMillis(millis)); + } + + public static String compact(Duration duration) { + long seconds = Math.max(0L, duration.toSeconds()); + long days = seconds / 86400; + seconds %= 86400; + long hours = seconds / 3600; + seconds %= 3600; + long minutes = seconds / 60; + seconds %= 60; + + if (days > 0) { + return days + "d " + hours + "h"; + } + if (hours > 0) { + return hours + "h " + minutes + "m"; + } + if (minutes > 0) { + return minutes + "m " + seconds + "s"; + } + return seconds + "s"; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..2d196bd --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,168 @@ +settings: + debug: false + command-prefix: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ&r <#6A3F24>»" + allow-seller-self-purchase: false + require-inventory-space-to-buy: false + chat-price-timeout-seconds: 45 + click-debounce-millis: 350 + listing-expire-check-seconds: 60 + default-sort: "NEWEST" + +storage: + sqlite-file: "auctionhouse.db" + import-legacy-yaml: true + +economy: + enabled: true + require-economy: true + provider: "Vault" + instant-seller-payment: true + listing-fee: + enabled: false + amount: 0.0 + sales-tax: + enabled: false + percent: 0.0 + price: + min: 1.0 + max: 1000000000.0 + +listings: + default-duration-seconds: 604800 + allow-cancel-active-listings: true + reclaim-admin-removed-items: true + cleanup-claimed-after-days: 30 + +listing-limits: + default: 5 + permissions: + - permission: "premiumah.limit.10" + amount: 10 + - permission: "premiumah.limit.25" + amount: 25 + - permission: "premiumah.limit.50" + amount: 50 + +item-restrictions: + mode: "BLACKLIST" + materials: + - BEDROCK + - BARRIER + - COMMAND_BLOCK + - CHAIN_COMMAND_BLOCK + - REPEATING_COMMAND_BLOCK + - STRUCTURE_BLOCK + - STRUCTURE_VOID + - JIGSAW + - DEBUG_STICK + +claims: + buyer-full-inventory-action: "CLAIM" + seller-payment-action: "INSTANT" + +sounds: + enabled: true + open: "BLOCK_CHEST_OPEN" + click: "UI_BUTTON_CLICK" + success: "ENTITY_PLAYER_LEVELUP" + fail: "ENTITY_VILLAGER_NO" + +gui: + filler: + enabled: true + material: "BROWN_STAINED_GLASS_PANE" + name: " " + titles: + main: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ &#D4AF37&lᴀᴜᴄᴛɪᴏɴs" + browse: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>Auctions <#7A4A2A>• Page {page}" + my-listings: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>My Listings <#7A4A2A>• Page {page}" + claims: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#B9C63F>Claims <#7A4A2A>• Page {page}" + sell: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>Create Listing" + confirm-buy: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>Confirm Purchase" + admin: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#C1432E>Admin <#7A4A2A>• Page {page}" + size: + main: 45 + browse: 54 + my-listings: 54 + claims: 54 + sell: 54 + confirm-buy: 27 + listing-slots: + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + - 25 + - 28 + - 29 + - 30 + - 31 + - 32 + - 33 + - 34 + buttons: + browse: + material: "CHEST" + name: "<#D4AF37>Browse Auctions" + lore: + - "<#C7BCA8>View every active listing." + - "<#B9C63F>Click to browse." + sell: + material: "EMERALD" + name: "<#B9C63F>Sell Held Item" + lore: + - "<#C7BCA8>Create a listing from your hand." + - "<#D4AF37>GUI flow with price confirmation." + my-listings: + material: "BOOK" + name: "<#A8873F>My Listings" + lore: + - "<#C7BCA8>Manage your active listings." + claims: + material: "ENDER_CHEST" + name: "<#B9C63F>Claims" + lore: + - "<#C7BCA8>Reclaim expired or undelivered items." + admin: + material: "ANVIL" + name: "<#C1432E>Admin Panel" + lore: + - "<#C7BCA8>Moderate active listings." + close: + material: "BARRIER" + name: "<#C1432E>Close" + back: + material: "ARROW" + name: "<#D4AF37>Back" + next: + material: "SPECTRAL_ARROW" + name: "<#B9C63F>Next Page" + previous: + material: "ARROW" + name: "<#D4AF37>Previous Page" + sort: + material: "HOPPER" + name: "<#A8873F>Sort: <#D4AF37>{sort}" + lore: + - "<#C7BCA8>Click to rotate sorting." + filter: + material: "COMPASS" + name: "<#A8873F>Filter: <#D4AF37>{filter}" + lore: + - "<#C7BCA8>Shift-click a listing to filter." + - "<#C7BCA8>Right-click to clear filter." + confirm: + material: "LIME_STAINED_GLASS_PANE" + name: "<#B9C63F>Confirm" + cancel: + material: "RED_STAINED_GLASS_PANE" + name: "<#C1432E>Cancel" diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..11262fe --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,65 @@ +messages: + no-permission: "{prefix} <#C1432E>You do not have permission." + player-only: "{prefix} <#C1432E>Only players can use that." + economy-unavailable: "{prefix} <#C1432E>The auction economy is unavailable. Try again later." + no-item-in-hand: "{prefix} <#C1432E>Hold the item you want to list." + invalid-price: "{prefix} <#C1432E>Enter a valid positive price." + invalid-sort: "{prefix} <#C1432E>Use newest, oldest, lowest_price, or highest_price." + price-too-low: "{prefix} <#C1432E>The minimum listing price is <#D4AF37>{min}<#C1432E>." + price-too-high: "{prefix} <#C1432E>The maximum listing price is <#D4AF37>{max}<#C1432E>." + item-blocked: "{prefix} <#C1432E>That item cannot be listed." + listing-limit: "{prefix} <#C1432E>You have reached your active listing limit of <#D4AF37>{limit}<#C1432E>." + listing-created: "{prefix} <#B9C63F>Listed <#D4AF37>{item} <#B9C63F>for <#D4AF37>{price}<#B9C63F>." + listing-created-fee: "{prefix} <#B9C63F>Listed <#D4AF37>{item} <#B9C63F>for <#D4AF37>{price}<#B9C63F>. Fee charged: <#D4AF37>{fee}<#B9C63F>." + listing-cancelled: "{prefix} <#D4AF37>Your listing was cancelled and moved to claims." + listing-expired: "{prefix} <#D4AF37>A listing expired and was moved to your claims." + listing-removed-admin: "{prefix} <#D4AF37>Listing <#A8873F>{id} <#D4AF37>was removed." + listing-no-longer-available: "{prefix} <#C1432E>That listing is no longer available." + cannot-buy-own: "{prefix} <#C1432E>You cannot buy your own listing." + not-enough-money: "{prefix} <#C1432E>You need <#D4AF37>{price}<#C1432E> to buy this." + purchase-success: "{prefix} <#B9C63F>You bought <#D4AF37>{item} <#B9C63F>for <#D4AF37>{price}<#B9C63F>." + purchase-claim: "{prefix} <#B9C63F>Purchase complete. Your inventory was full, so the item was moved to claims." + sold-notify: "{prefix} <#B9C63F>Your listing sold to <#D4AF37>{buyer} <#B9C63F>for <#D4AF37>{price}<#B9C63F>." + inventory-full: "{prefix} <#C1432E>Your inventory is full." + item-reclaimed: "{prefix} <#B9C63F>Claimed <#D4AF37>{item}<#B9C63F>." + nothing-to-claim: "{prefix} <#D4AF37>You do not have anything to claim." + reload-complete: "{prefix} <#B9C63F>Configuration, messages, and SQLite storage were reloaded." + listing-not-found: "{prefix} <#C1432E>Listing not found." + admin-help: "{prefix} <#D4AF37>/ahadmin reload<#7A4A2A>, <#D4AF37>/ahadmin remove <#7A4A2A>, <#D4AF37>/ahadmin view <#7A4A2A>, <#D4AF37>/ahadmin forceexpire " + price-prompt: "{prefix} <#D4AF37>Type the listing price in chat. Type <#B9C63F>cancel <#D4AF37>to stop." + price-prompt-cancelled: "{prefix} <#D4AF37>Listing price entry cancelled." + price-prompt-timeout: "{prefix} <#C1432E>Listing price entry timed out." + price-set: "{prefix} <#B9C63F>Price set to <#D4AF37>{price}<#B9C63F>." + usage-sell: "{prefix} <#D4AF37>Usage: /ah sell " + storage-save-failed: "{prefix} <#C1432E>Storage could not be saved. Check console." + seller-paid-claim: "{prefix} <#B9C63F>Your sale funds were moved to claims." +gui-lore: + listing: + - "<#7A4A2A>Seller: <#C7BCA8>{seller}" + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>Remaining: <#D4AF37>{remaining}" + - "<#7A4A2A>ID: <#A8873F>{id}" + - "" + - "<#B9C63F>Click to inspect and buy." + - "<#C1432E>Admin right-click removes." + my-listing: + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>Remaining: <#D4AF37>{remaining}" + - "<#7A4A2A>ID: <#A8873F>{id}" + - "" + - "<#D4AF37>Click to cancel and reclaim." + claim: + - "<#7A4A2A>Reason: <#C7BCA8>{reason}" + - "<#7A4A2A>From Listing: <#A8873F>{id}" + - "<#7A4A2A>Added: <#D4AF37>{age} ago" + - "" + - "<#B9C63F>Click to claim." + confirm-buy: + - "<#7A4A2A>Seller: <#C7BCA8>{seller}" + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>ID: <#A8873F>{id}" + sell-item: + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>Fee: <#D4AF37>{fee}" + - "" + - "<#C7BCA8>Use Set Price, then Confirm." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..b7ac0ae --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,64 @@ +name: DirtAuctions +version: ${project.version} +main: com.yourname.premiumah.PremiumAHPlugin +api-version: '1.21' +author: yourname +description: Premium GUI-first auction house for Paper SMP servers. +softdepend: + - Vault + - CMI +commands: + ah: + description: Open the Dirt Auctions GUI. + usage: /ah [browse|sell|listings|expired|claims|sort] + aliases: + - auctionhouse + - auctions + ahadmin: + description: Dirt Auctions administration. + usage: /ahadmin +permissions: + premiumah.use: + description: Allows opening the auction house. + default: true + premiumah.sell: + description: Allows creating auction listings. + default: true + premiumah.buy: + description: Allows buying listings. + default: true + premiumah.listings: + description: Allows opening personal listings. + default: true + premiumah.expired: + description: Allows reclaiming expired and claimable items. + default: true + premiumah.admin: + description: Full Dirt Auctions administration. + default: op + children: + premiumah.admin.remove: true + premiumah.admin.reload: true + premiumah.admin.view: true + premiumah.admin.forceexpire: true + premiumah.admin.remove: + description: Allows removing listings. + default: op + premiumah.admin.reload: + description: Allows reloading Dirt Auctions. + default: op + premiumah.admin.view: + description: Allows viewing player listings. + default: op + premiumah.admin.forceexpire: + description: Allows expiring listings. + default: op + premiumah.bypass.restrictions: + description: Bypass configured item and price restrictions. + default: op + premiumah.bypass.fees: + description: Bypass listing fees. + default: op + premiumah.bypass.tax: + description: Bypass sales tax. + default: op diff --git a/target/DirtAuctions-1.0.0.jar b/target/DirtAuctions-1.0.0.jar new file mode 100644 index 0000000..d5d615e Binary files /dev/null and b/target/DirtAuctions-1.0.0.jar differ diff --git a/target/classes/com/yourname/premiumah/PremiumAHPlugin.class b/target/classes/com/yourname/premiumah/PremiumAHPlugin.class new file mode 100644 index 0000000..aae3035 Binary files /dev/null and b/target/classes/com/yourname/premiumah/PremiumAHPlugin.class differ diff --git a/target/classes/com/yourname/premiumah/command/AhAdminCommand.class b/target/classes/com/yourname/premiumah/command/AhAdminCommand.class new file mode 100644 index 0000000..51cd01f Binary files /dev/null and b/target/classes/com/yourname/premiumah/command/AhAdminCommand.class differ diff --git a/target/classes/com/yourname/premiumah/command/AhCommand.class b/target/classes/com/yourname/premiumah/command/AhCommand.class new file mode 100644 index 0000000..ffcfee8 Binary files /dev/null and b/target/classes/com/yourname/premiumah/command/AhCommand.class differ diff --git a/target/classes/com/yourname/premiumah/config/ButtonConfig.class b/target/classes/com/yourname/premiumah/config/ButtonConfig.class new file mode 100644 index 0000000..9b85dcd Binary files /dev/null and b/target/classes/com/yourname/premiumah/config/ButtonConfig.class differ diff --git a/target/classes/com/yourname/premiumah/config/ConfigManager.class b/target/classes/com/yourname/premiumah/config/ConfigManager.class new file mode 100644 index 0000000..6f7279c Binary files /dev/null and b/target/classes/com/yourname/premiumah/config/ConfigManager.class differ diff --git a/target/classes/com/yourname/premiumah/config/LimitPermission.class b/target/classes/com/yourname/premiumah/config/LimitPermission.class new file mode 100644 index 0000000..bc7e177 Binary files /dev/null and b/target/classes/com/yourname/premiumah/config/LimitPermission.class differ diff --git a/target/classes/com/yourname/premiumah/config/MessageManager.class b/target/classes/com/yourname/premiumah/config/MessageManager.class new file mode 100644 index 0000000..fbe5664 Binary files /dev/null and b/target/classes/com/yourname/premiumah/config/MessageManager.class differ diff --git a/target/classes/com/yourname/premiumah/economy/EconomyService.class b/target/classes/com/yourname/premiumah/economy/EconomyService.class new file mode 100644 index 0000000..e749757 Binary files /dev/null and b/target/classes/com/yourname/premiumah/economy/EconomyService.class differ diff --git a/target/classes/com/yourname/premiumah/economy/NoopEconomyService.class b/target/classes/com/yourname/premiumah/economy/NoopEconomyService.class new file mode 100644 index 0000000..2b11928 Binary files /dev/null and b/target/classes/com/yourname/premiumah/economy/NoopEconomyService.class differ diff --git a/target/classes/com/yourname/premiumah/economy/VaultEconomyService.class b/target/classes/com/yourname/premiumah/economy/VaultEconomyService.class new file mode 100644 index 0000000..09adf4a Binary files /dev/null and b/target/classes/com/yourname/premiumah/economy/VaultEconomyService.class differ diff --git a/target/classes/com/yourname/premiumah/gui/BrowseState.class b/target/classes/com/yourname/premiumah/gui/BrowseState.class new file mode 100644 index 0000000..3b03ab7 Binary files /dev/null and b/target/classes/com/yourname/premiumah/gui/BrowseState.class differ diff --git a/target/classes/com/yourname/premiumah/gui/GuiHolder.class b/target/classes/com/yourname/premiumah/gui/GuiHolder.class new file mode 100644 index 0000000..0cc8945 Binary files /dev/null and b/target/classes/com/yourname/premiumah/gui/GuiHolder.class differ diff --git a/target/classes/com/yourname/premiumah/gui/GuiManager$1.class b/target/classes/com/yourname/premiumah/gui/GuiManager$1.class new file mode 100644 index 0000000..494ed0b Binary files /dev/null and b/target/classes/com/yourname/premiumah/gui/GuiManager$1.class differ diff --git a/target/classes/com/yourname/premiumah/gui/GuiManager.class b/target/classes/com/yourname/premiumah/gui/GuiManager.class new file mode 100644 index 0000000..2919c19 Binary files /dev/null and b/target/classes/com/yourname/premiumah/gui/GuiManager.class differ diff --git a/target/classes/com/yourname/premiumah/gui/GuiType.class b/target/classes/com/yourname/premiumah/gui/GuiType.class new file mode 100644 index 0000000..b36a4eb Binary files /dev/null and b/target/classes/com/yourname/premiumah/gui/GuiType.class differ diff --git a/target/classes/com/yourname/premiumah/gui/SellSession.class b/target/classes/com/yourname/premiumah/gui/SellSession.class new file mode 100644 index 0000000..4e82891 Binary files /dev/null and b/target/classes/com/yourname/premiumah/gui/SellSession.class differ diff --git a/target/classes/com/yourname/premiumah/listener/ChatInputListener.class b/target/classes/com/yourname/premiumah/listener/ChatInputListener.class new file mode 100644 index 0000000..0b0d988 Binary files /dev/null and b/target/classes/com/yourname/premiumah/listener/ChatInputListener.class differ diff --git a/target/classes/com/yourname/premiumah/listener/InventoryGuiListener.class b/target/classes/com/yourname/premiumah/listener/InventoryGuiListener.class new file mode 100644 index 0000000..f9d7c66 Binary files /dev/null and b/target/classes/com/yourname/premiumah/listener/InventoryGuiListener.class differ diff --git a/target/classes/com/yourname/premiumah/listener/PlayerSessionListener.class b/target/classes/com/yourname/premiumah/listener/PlayerSessionListener.class new file mode 100644 index 0000000..8adc2fc Binary files /dev/null and b/target/classes/com/yourname/premiumah/listener/PlayerSessionListener.class differ diff --git a/target/classes/com/yourname/premiumah/manager/AuctionHouseManager.class b/target/classes/com/yourname/premiumah/manager/AuctionHouseManager.class new file mode 100644 index 0000000..6e65d92 Binary files /dev/null and b/target/classes/com/yourname/premiumah/manager/AuctionHouseManager.class differ diff --git a/target/classes/com/yourname/premiumah/manager/ClaimManager.class b/target/classes/com/yourname/premiumah/manager/ClaimManager.class new file mode 100644 index 0000000..1a8b99b Binary files /dev/null and b/target/classes/com/yourname/premiumah/manager/ClaimManager.class differ diff --git a/target/classes/com/yourname/premiumah/manager/ListingManager$1.class b/target/classes/com/yourname/premiumah/manager/ListingManager$1.class new file mode 100644 index 0000000..c94c20a Binary files /dev/null and b/target/classes/com/yourname/premiumah/manager/ListingManager$1.class differ diff --git a/target/classes/com/yourname/premiumah/manager/ListingManager.class b/target/classes/com/yourname/premiumah/manager/ListingManager.class new file mode 100644 index 0000000..36cff45 Binary files /dev/null and b/target/classes/com/yourname/premiumah/manager/ListingManager.class differ diff --git a/target/classes/com/yourname/premiumah/model/ActionResult.class b/target/classes/com/yourname/premiumah/model/ActionResult.class new file mode 100644 index 0000000..6d9954f Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/ActionResult.class differ diff --git a/target/classes/com/yourname/premiumah/model/ClaimReason.class b/target/classes/com/yourname/premiumah/model/ClaimReason.class new file mode 100644 index 0000000..def7b7c Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/ClaimReason.class differ diff --git a/target/classes/com/yourname/premiumah/model/ClaimRecord.class b/target/classes/com/yourname/premiumah/model/ClaimRecord.class new file mode 100644 index 0000000..e1686b0 Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/ClaimRecord.class differ diff --git a/target/classes/com/yourname/premiumah/model/ClaimType.class b/target/classes/com/yourname/premiumah/model/ClaimType.class new file mode 100644 index 0000000..7605910 Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/ClaimType.class differ diff --git a/target/classes/com/yourname/premiumah/model/Listing.class b/target/classes/com/yourname/premiumah/model/Listing.class new file mode 100644 index 0000000..8f9bc9c Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/Listing.class differ diff --git a/target/classes/com/yourname/premiumah/model/ListingCreationResult.class b/target/classes/com/yourname/premiumah/model/ListingCreationResult.class new file mode 100644 index 0000000..53c8a3c Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/ListingCreationResult.class differ diff --git a/target/classes/com/yourname/premiumah/model/ListingStatus.class b/target/classes/com/yourname/premiumah/model/ListingStatus.class new file mode 100644 index 0000000..9c43d29 Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/ListingStatus.class differ diff --git a/target/classes/com/yourname/premiumah/model/SortMode.class b/target/classes/com/yourname/premiumah/model/SortMode.class new file mode 100644 index 0000000..0e83ef7 Binary files /dev/null and b/target/classes/com/yourname/premiumah/model/SortMode.class differ diff --git a/target/classes/com/yourname/premiumah/storage/ItemStackSerializer.class b/target/classes/com/yourname/premiumah/storage/ItemStackSerializer.class new file mode 100644 index 0000000..48993df Binary files /dev/null and b/target/classes/com/yourname/premiumah/storage/ItemStackSerializer.class differ diff --git a/target/classes/com/yourname/premiumah/storage/StorageManager.class b/target/classes/com/yourname/premiumah/storage/StorageManager.class new file mode 100644 index 0000000..d905f7c Binary files /dev/null and b/target/classes/com/yourname/premiumah/storage/StorageManager.class differ diff --git a/target/classes/com/yourname/premiumah/util/IdGenerator.class b/target/classes/com/yourname/premiumah/util/IdGenerator.class new file mode 100644 index 0000000..b4eed6d Binary files /dev/null and b/target/classes/com/yourname/premiumah/util/IdGenerator.class differ diff --git a/target/classes/com/yourname/premiumah/util/InventoryUtil.class b/target/classes/com/yourname/premiumah/util/InventoryUtil.class new file mode 100644 index 0000000..e04107e Binary files /dev/null and b/target/classes/com/yourname/premiumah/util/InventoryUtil.class differ diff --git a/target/classes/com/yourname/premiumah/util/MaterialUtil.class b/target/classes/com/yourname/premiumah/util/MaterialUtil.class new file mode 100644 index 0000000..9d1a1cf Binary files /dev/null and b/target/classes/com/yourname/premiumah/util/MaterialUtil.class differ diff --git a/target/classes/com/yourname/premiumah/util/TextUtil.class b/target/classes/com/yourname/premiumah/util/TextUtil.class new file mode 100644 index 0000000..2f47745 Binary files /dev/null and b/target/classes/com/yourname/premiumah/util/TextUtil.class differ diff --git a/target/classes/com/yourname/premiumah/util/TimeUtil.class b/target/classes/com/yourname/premiumah/util/TimeUtil.class new file mode 100644 index 0000000..4e1aaf3 Binary files /dev/null and b/target/classes/com/yourname/premiumah/util/TimeUtil.class differ diff --git a/target/classes/config.yml b/target/classes/config.yml new file mode 100644 index 0000000..2d196bd --- /dev/null +++ b/target/classes/config.yml @@ -0,0 +1,168 @@ +settings: + debug: false + command-prefix: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ&r <#6A3F24>»" + allow-seller-self-purchase: false + require-inventory-space-to-buy: false + chat-price-timeout-seconds: 45 + click-debounce-millis: 350 + listing-expire-check-seconds: 60 + default-sort: "NEWEST" + +storage: + sqlite-file: "auctionhouse.db" + import-legacy-yaml: true + +economy: + enabled: true + require-economy: true + provider: "Vault" + instant-seller-payment: true + listing-fee: + enabled: false + amount: 0.0 + sales-tax: + enabled: false + percent: 0.0 + price: + min: 1.0 + max: 1000000000.0 + +listings: + default-duration-seconds: 604800 + allow-cancel-active-listings: true + reclaim-admin-removed-items: true + cleanup-claimed-after-days: 30 + +listing-limits: + default: 5 + permissions: + - permission: "premiumah.limit.10" + amount: 10 + - permission: "premiumah.limit.25" + amount: 25 + - permission: "premiumah.limit.50" + amount: 50 + +item-restrictions: + mode: "BLACKLIST" + materials: + - BEDROCK + - BARRIER + - COMMAND_BLOCK + - CHAIN_COMMAND_BLOCK + - REPEATING_COMMAND_BLOCK + - STRUCTURE_BLOCK + - STRUCTURE_VOID + - JIGSAW + - DEBUG_STICK + +claims: + buyer-full-inventory-action: "CLAIM" + seller-payment-action: "INSTANT" + +sounds: + enabled: true + open: "BLOCK_CHEST_OPEN" + click: "UI_BUTTON_CLICK" + success: "ENTITY_PLAYER_LEVELUP" + fail: "ENTITY_VILLAGER_NO" + +gui: + filler: + enabled: true + material: "BROWN_STAINED_GLASS_PANE" + name: " " + titles: + main: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ &#D4AF37&lᴀᴜᴄᴛɪᴏɴs" + browse: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>Auctions <#7A4A2A>• Page {page}" + my-listings: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>My Listings <#7A4A2A>• Page {page}" + claims: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#B9C63F>Claims <#7A4A2A>• Page {page}" + sell: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>Create Listing" + confirm-buy: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#D4AF37>Confirm Purchase" + admin: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛ <#C1432E>Admin <#7A4A2A>• Page {page}" + size: + main: 45 + browse: 54 + my-listings: 54 + claims: 54 + sell: 54 + confirm-buy: 27 + listing-slots: + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + - 25 + - 28 + - 29 + - 30 + - 31 + - 32 + - 33 + - 34 + buttons: + browse: + material: "CHEST" + name: "<#D4AF37>Browse Auctions" + lore: + - "<#C7BCA8>View every active listing." + - "<#B9C63F>Click to browse." + sell: + material: "EMERALD" + name: "<#B9C63F>Sell Held Item" + lore: + - "<#C7BCA8>Create a listing from your hand." + - "<#D4AF37>GUI flow with price confirmation." + my-listings: + material: "BOOK" + name: "<#A8873F>My Listings" + lore: + - "<#C7BCA8>Manage your active listings." + claims: + material: "ENDER_CHEST" + name: "<#B9C63F>Claims" + lore: + - "<#C7BCA8>Reclaim expired or undelivered items." + admin: + material: "ANVIL" + name: "<#C1432E>Admin Panel" + lore: + - "<#C7BCA8>Moderate active listings." + close: + material: "BARRIER" + name: "<#C1432E>Close" + back: + material: "ARROW" + name: "<#D4AF37>Back" + next: + material: "SPECTRAL_ARROW" + name: "<#B9C63F>Next Page" + previous: + material: "ARROW" + name: "<#D4AF37>Previous Page" + sort: + material: "HOPPER" + name: "<#A8873F>Sort: <#D4AF37>{sort}" + lore: + - "<#C7BCA8>Click to rotate sorting." + filter: + material: "COMPASS" + name: "<#A8873F>Filter: <#D4AF37>{filter}" + lore: + - "<#C7BCA8>Shift-click a listing to filter." + - "<#C7BCA8>Right-click to clear filter." + confirm: + material: "LIME_STAINED_GLASS_PANE" + name: "<#B9C63F>Confirm" + cancel: + material: "RED_STAINED_GLASS_PANE" + name: "<#C1432E>Cancel" diff --git a/target/classes/messages.yml b/target/classes/messages.yml new file mode 100644 index 0000000..11262fe --- /dev/null +++ b/target/classes/messages.yml @@ -0,0 +1,65 @@ +messages: + no-permission: "{prefix} <#C1432E>You do not have permission." + player-only: "{prefix} <#C1432E>Only players can use that." + economy-unavailable: "{prefix} <#C1432E>The auction economy is unavailable. Try again later." + no-item-in-hand: "{prefix} <#C1432E>Hold the item you want to list." + invalid-price: "{prefix} <#C1432E>Enter a valid positive price." + invalid-sort: "{prefix} <#C1432E>Use newest, oldest, lowest_price, or highest_price." + price-too-low: "{prefix} <#C1432E>The minimum listing price is <#D4AF37>{min}<#C1432E>." + price-too-high: "{prefix} <#C1432E>The maximum listing price is <#D4AF37>{max}<#C1432E>." + item-blocked: "{prefix} <#C1432E>That item cannot be listed." + listing-limit: "{prefix} <#C1432E>You have reached your active listing limit of <#D4AF37>{limit}<#C1432E>." + listing-created: "{prefix} <#B9C63F>Listed <#D4AF37>{item} <#B9C63F>for <#D4AF37>{price}<#B9C63F>." + listing-created-fee: "{prefix} <#B9C63F>Listed <#D4AF37>{item} <#B9C63F>for <#D4AF37>{price}<#B9C63F>. Fee charged: <#D4AF37>{fee}<#B9C63F>." + listing-cancelled: "{prefix} <#D4AF37>Your listing was cancelled and moved to claims." + listing-expired: "{prefix} <#D4AF37>A listing expired and was moved to your claims." + listing-removed-admin: "{prefix} <#D4AF37>Listing <#A8873F>{id} <#D4AF37>was removed." + listing-no-longer-available: "{prefix} <#C1432E>That listing is no longer available." + cannot-buy-own: "{prefix} <#C1432E>You cannot buy your own listing." + not-enough-money: "{prefix} <#C1432E>You need <#D4AF37>{price}<#C1432E> to buy this." + purchase-success: "{prefix} <#B9C63F>You bought <#D4AF37>{item} <#B9C63F>for <#D4AF37>{price}<#B9C63F>." + purchase-claim: "{prefix} <#B9C63F>Purchase complete. Your inventory was full, so the item was moved to claims." + sold-notify: "{prefix} <#B9C63F>Your listing sold to <#D4AF37>{buyer} <#B9C63F>for <#D4AF37>{price}<#B9C63F>." + inventory-full: "{prefix} <#C1432E>Your inventory is full." + item-reclaimed: "{prefix} <#B9C63F>Claimed <#D4AF37>{item}<#B9C63F>." + nothing-to-claim: "{prefix} <#D4AF37>You do not have anything to claim." + reload-complete: "{prefix} <#B9C63F>Configuration, messages, and SQLite storage were reloaded." + listing-not-found: "{prefix} <#C1432E>Listing not found." + admin-help: "{prefix} <#D4AF37>/ahadmin reload<#7A4A2A>, <#D4AF37>/ahadmin remove <#7A4A2A>, <#D4AF37>/ahadmin view <#7A4A2A>, <#D4AF37>/ahadmin forceexpire " + price-prompt: "{prefix} <#D4AF37>Type the listing price in chat. Type <#B9C63F>cancel <#D4AF37>to stop." + price-prompt-cancelled: "{prefix} <#D4AF37>Listing price entry cancelled." + price-prompt-timeout: "{prefix} <#C1432E>Listing price entry timed out." + price-set: "{prefix} <#B9C63F>Price set to <#D4AF37>{price}<#B9C63F>." + usage-sell: "{prefix} <#D4AF37>Usage: /ah sell " + storage-save-failed: "{prefix} <#C1432E>Storage could not be saved. Check console." + seller-paid-claim: "{prefix} <#B9C63F>Your sale funds were moved to claims." +gui-lore: + listing: + - "<#7A4A2A>Seller: <#C7BCA8>{seller}" + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>Remaining: <#D4AF37>{remaining}" + - "<#7A4A2A>ID: <#A8873F>{id}" + - "" + - "<#B9C63F>Click to inspect and buy." + - "<#C1432E>Admin right-click removes." + my-listing: + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>Remaining: <#D4AF37>{remaining}" + - "<#7A4A2A>ID: <#A8873F>{id}" + - "" + - "<#D4AF37>Click to cancel and reclaim." + claim: + - "<#7A4A2A>Reason: <#C7BCA8>{reason}" + - "<#7A4A2A>From Listing: <#A8873F>{id}" + - "<#7A4A2A>Added: <#D4AF37>{age} ago" + - "" + - "<#B9C63F>Click to claim." + confirm-buy: + - "<#7A4A2A>Seller: <#C7BCA8>{seller}" + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>ID: <#A8873F>{id}" + sell-item: + - "<#7A4A2A>Price: <#B9C63F>{price}" + - "<#7A4A2A>Fee: <#D4AF37>{fee}" + - "" + - "<#C7BCA8>Use Set Price, then Confirm." diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml new file mode 100644 index 0000000..3a779b5 --- /dev/null +++ b/target/classes/plugin.yml @@ -0,0 +1,64 @@ +name: DirtAuctions +version: 1.0.0 +main: com.yourname.premiumah.PremiumAHPlugin +api-version: '1.21' +author: yourname +description: Premium GUI-first auction house for Paper SMP servers. +softdepend: + - Vault + - CMI +commands: + ah: + description: Open the Dirt Auctions GUI. + usage: /ah [browse|sell|listings|expired|claims|sort] + aliases: + - auctionhouse + - auctions + ahadmin: + description: Dirt Auctions administration. + usage: /ahadmin +permissions: + premiumah.use: + description: Allows opening the auction house. + default: true + premiumah.sell: + description: Allows creating auction listings. + default: true + premiumah.buy: + description: Allows buying listings. + default: true + premiumah.listings: + description: Allows opening personal listings. + default: true + premiumah.expired: + description: Allows reclaiming expired and claimable items. + default: true + premiumah.admin: + description: Full Dirt Auctions administration. + default: op + children: + premiumah.admin.remove: true + premiumah.admin.reload: true + premiumah.admin.view: true + premiumah.admin.forceexpire: true + premiumah.admin.remove: + description: Allows removing listings. + default: op + premiumah.admin.reload: + description: Allows reloading Dirt Auctions. + default: op + premiumah.admin.view: + description: Allows viewing player listings. + default: op + premiumah.admin.forceexpire: + description: Allows expiring listings. + default: op + premiumah.bypass.restrictions: + description: Bypass configured item and price restrictions. + default: op + premiumah.bypass.fees: + description: Bypass listing fees. + default: op + premiumah.bypass.tax: + description: Bypass sales tax. + default: op diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..d5b4b22 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Tue Jun 23 17:40:08 EDT 2026 +artifactId=dirt-auctions +groupId=com.yourname +version=1.0.0 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..03b20a1 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,38 @@ +com/yourname/premiumah/util/IdGenerator.class +com/yourname/premiumah/model/ClaimReason.class +com/yourname/premiumah/gui/GuiManager$1.class +com/yourname/premiumah/listener/PlayerSessionListener.class +com/yourname/premiumah/manager/ListingManager$1.class +com/yourname/premiumah/model/SortMode.class +com/yourname/premiumah/command/AhAdminCommand.class +com/yourname/premiumah/storage/ItemStackSerializer.class +com/yourname/premiumah/gui/GuiManager.class +com/yourname/premiumah/manager/AuctionHouseManager.class +com/yourname/premiumah/listener/InventoryGuiListener.class +com/yourname/premiumah/util/InventoryUtil.class +com/yourname/premiumah/util/TextUtil.class +com/yourname/premiumah/model/ListingCreationResult.class +com/yourname/premiumah/PremiumAHPlugin.class +com/yourname/premiumah/config/MessageManager.class +com/yourname/premiumah/config/ConfigManager.class +com/yourname/premiumah/listener/ChatInputListener.class +com/yourname/premiumah/model/ActionResult.class +com/yourname/premiumah/gui/GuiHolder.class +com/yourname/premiumah/model/ClaimRecord.class +com/yourname/premiumah/gui/BrowseState.class +com/yourname/premiumah/storage/StorageManager.class +com/yourname/premiumah/manager/ListingManager.class +com/yourname/premiumah/command/AhCommand.class +com/yourname/premiumah/manager/ClaimManager.class +com/yourname/premiumah/gui/GuiType.class +com/yourname/premiumah/gui/SellSession.class +com/yourname/premiumah/economy/NoopEconomyService.class +com/yourname/premiumah/economy/EconomyService.class +com/yourname/premiumah/model/ClaimType.class +com/yourname/premiumah/economy/VaultEconomyService.class +com/yourname/premiumah/config/LimitPermission.class +com/yourname/premiumah/util/TimeUtil.class +com/yourname/premiumah/util/MaterialUtil.class +com/yourname/premiumah/config/ButtonConfig.class +com/yourname/premiumah/model/ListingStatus.class +com/yourname/premiumah/model/Listing.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..4ab23b2 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,36 @@ +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/PremiumAHPlugin.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/command/AhAdminCommand.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/command/AhCommand.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/config/ButtonConfig.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/config/ConfigManager.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/config/LimitPermission.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/config/MessageManager.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/economy/EconomyService.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/economy/NoopEconomyService.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/economy/VaultEconomyService.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/gui/BrowseState.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/gui/GuiHolder.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/gui/GuiManager.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/gui/GuiType.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/gui/SellSession.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/listener/ChatInputListener.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/listener/InventoryGuiListener.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/listener/PlayerSessionListener.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/manager/AuctionHouseManager.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/manager/ClaimManager.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/manager/ListingManager.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/ActionResult.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/ClaimReason.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/ClaimRecord.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/ClaimType.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/Listing.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/ListingCreationResult.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/ListingStatus.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/model/SortMode.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/storage/ItemStackSerializer.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/storage/StorageManager.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/util/IdGenerator.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/util/InventoryUtil.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/util/MaterialUtil.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/util/TextUtil.java +/home/bitnix/Desktop/DirtAuctions/src/main/java/com/yourname/premiumah/util/TimeUtil.java diff --git a/target/original-DirtAuctions-1.0.0.jar b/target/original-DirtAuctions-1.0.0.jar new file mode 100644 index 0000000..09eb18e Binary files /dev/null and b/target/original-DirtAuctions-1.0.0.jar differ