This commit is contained in:
2026-06-23 18:06:43 -04:00
commit c712003509
87 changed files with 4340 additions and 0 deletions
View File
+96
View File
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yourname</groupId>
<artifactId>dirt-auctions</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Dirt Auctions</name>
<description>Premium GUI-first auction house plugin for modern Paper servers.</description>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.1.0</version>
</dependency>
</dependencies>
<build>
<finalName>DirtAuctions-${project.version}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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<String> 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<String> filter(List<String> values, String prefix) {
String normalized = prefix.toLowerCase(Locale.ROOT);
List<String> result = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase(Locale.ROOT).startsWith(normalized)) {
result.add(value);
}
}
return result;
}
}
@@ -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<String> 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<String> filter(List<String> values, String prefix) {
String normalized = prefix.toLowerCase(Locale.ROOT);
List<String> result = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase(Locale.ROOT).startsWith(normalized)) {
result.add(value);
}
}
return result;
}
}
@@ -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<String> lore) {
}
@@ -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<Material> restrictedMaterials = Set.of();
private List<LimitPermission> limitPermissions = List.of();
private List<Integer> 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<Integer> 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<String> lore = config.getStringList(path + ".lore");
return new ButtonConfig(material, name, lore);
}
public List<String> 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<Material> loadRestrictedMaterials() {
Set<Material> materials = new HashSet<>();
for (String entry : config.getStringList("item-restrictions.materials")) {
MaterialUtil.parse(entry).ifPresent(materials::add);
}
return Collections.unmodifiableSet(materials);
}
private List<LimitPermission> loadLimitPermissions() {
List<LimitPermission> 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<Integer> loadListingSlots() {
List<Integer> 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);
}
}
@@ -0,0 +1,4 @@
package com.yourname.premiumah.config;
public record LimitPermission(String permission, int amount) {
}
@@ -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<String, String> placeholders) {
sender.sendMessage(message(key, placeholders));
}
public void action(Player player, String key, Map<String, String> placeholders) {
player.sendActionBar(message(key, placeholders));
}
public Component message(String key, Map<String, String> placeholders) {
String raw = messages.getString("messages." + key, "<#ef4444>Missing message: " + key);
return component(apply(raw, placeholders));
}
public List<Component> guiLore(String key, Map<String, String> placeholders) {
return messages.getStringList("gui-lore." + key).stream()
.map(line -> component(apply(line, placeholders)))
.toList();
}
public List<String> 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<String, String> placeholders) {
return TextUtil.legacy(apply(raw, placeholders));
}
public Map<String, String> placeholders(String... pairs) {
Map<String, String> 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<String, String> placeholders) {
String result = raw == null ? "" : raw;
result = result.replace("{prefix}", configManager.prefix());
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
result = result.replace("{" + entry.getKey() + "}", entry.getValue());
}
return result;
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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<Economy> 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);
}
}
@@ -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;
}
}
@@ -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<Integer, String> listingSlots = new HashMap<>();
private final Map<Integer, String> 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<Integer, String> listingSlots() {
return listingSlots;
}
public Map<Integer, String> claimSlots() {
return claimSlots;
}
public void inventory(Inventory inventory) {
this.inventory = inventory;
}
@Override
public Inventory getInventory() {
return inventory;
}
}
@@ -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<UUID, BrowseState> browseStates = new ConcurrentHashMap<>();
private final Map<UUID, SellSession> sellSessions = new ConcurrentHashMap<>();
private final Map<UUID, Long> 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<Listing> 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<Listing> 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<Listing> 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<Listing> 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<ClaimRecord> 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<Integer> 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<Listing> 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<Integer> 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<String, String> 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<String, String> placeholders) {
ButtonConfig button = config.button(key);
return customItem(button.material(), button.name(), button.lore(), placeholders);
}
private ItemStack customItem(Material material, String name, List<String> lore, Map<String, String> 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<net.kyori.adventure.text.Component> 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<net.kyori.adventure.text.Component> 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<String, String> 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<String, String> claimPlaceholders(ClaimRecord claim) {
String itemName = claim.type() == ClaimType.MONEY ? auctionHouse.formatMoney(claim.moneyAmount()) : auctionHouse.itemName(claim.item());
Map<String, String> 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<Integer, ItemStack> 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) {
}
}
}
@@ -0,0 +1,11 @@
package com.yourname.premiumah.gui;
public enum GuiType {
MAIN,
BROWSE,
MY_LISTINGS,
CLAIMS,
SELL,
CONFIRM_BUY,
ADMIN
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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<String, String> 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<Listing> 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<Integer, ItemStack> 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<Listing> 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<Listing> 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<Listing> 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<ClaimRecord> 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<Integer, ItemStack> 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<String, String> placeholders(String... pairs) {
Map<String, String> 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;
}
}
@@ -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<String, ClaimRecord> 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<ClaimRecord> 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<ClaimRecord> 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()));
}
}
@@ -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<String, Listing> 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<Listing> 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<Listing> all() {
return new ArrayList<>(listings.values());
}
public synchronized List<Listing> 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<Listing> 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<Listing> 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<Listing> expiredActiveListings(long now) {
return listings.values().stream()
.filter(listing -> listing.status() == ListingStatus.ACTIVE)
.filter(listing -> listing.expiresAt() <= now)
.collect(Collectors.toList());
}
private Comparator<Listing> 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();
};
}
}
@@ -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<String, String> placeholders) {
public static ActionResult success(String messageKey, Map<String, String> placeholders) {
return new ActionResult(true, messageKey, placeholders == null ? Collections.emptyMap() : placeholders);
}
public static ActionResult fail(String messageKey, Map<String, String> placeholders) {
return new ActionResult(false, messageKey, placeholders == null ? Collections.emptyMap() : placeholders);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,6 @@
package com.yourname.premiumah.model;
public enum ClaimType {
ITEM,
MONEY
}
@@ -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();
}
}
@@ -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<String, String> placeholders) {
public static ListingCreationResult success(Listing listing, String messageKey, Map<String, String> placeholders) {
return new ListingCreationResult(true, listing, messageKey, placeholders == null ? Collections.emptyMap() : placeholders);
}
public static ListingCreationResult fail(String messageKey, Map<String, String> placeholders) {
return new ListingCreationResult(false, null, messageKey, placeholders == null ? Collections.emptyMap() : placeholders);
}
}
@@ -0,0 +1,10 @@
package com.yourname.premiumah.model;
public enum ListingStatus {
ACTIVE,
SOLD,
EXPIRED,
CLAIMED,
REMOVED,
CANCELLED
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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<String, Listing> listings = new LinkedHashMap<>();
private final Map<String, ClaimRecord> 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<String, Listing> listingsSnapshot() {
return new LinkedHashMap<>(listings);
}
public synchronized Map<String, ClaimRecord> claimsSnapshot() {
return new LinkedHashMap<>(claims);
}
public synchronized void replaceListings(Map<String, Listing> replacement) {
listings.clear();
listings.putAll(replacement);
}
public synchronized void replaceClaims(Map<String, ClaimRecord> 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 extends Enum<E>> E parseEnum(Class<E> type, String raw, E fallback) {
if (raw == null || raw.isBlank()) {
return fallback;
}
try {
return Enum.valueOf(type, raw);
} catch (IllegalArgumentException ignored) {
return fallback;
}
}
}
@@ -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);
}
}
@@ -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<Integer, ItemStack> addItem(Inventory inventory, ItemStack item) {
if (isAir(item)) {
return new HashMap<>();
}
return inventory.addItem(item.clone());
}
}
@@ -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<Material> 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();
}
}
@@ -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' -> "<black>";
case '1' -> "<dark_blue>";
case '2' -> "<dark_green>";
case '3' -> "<dark_aqua>";
case '4' -> "<dark_red>";
case '5' -> "<dark_purple>";
case '6' -> "<gold>";
case '7' -> "<gray>";
case '8' -> "<dark_gray>";
case '9' -> "<blue>";
case 'a' -> "<green>";
case 'b' -> "<aqua>";
case 'c' -> "<red>";
case 'd' -> "<light_purple>";
case 'e' -> "<yellow>";
case 'f' -> "<white>";
case 'k' -> "<obfuscated>";
case 'l' -> "<bold>";
case 'm' -> "<strikethrough>";
case 'n' -> "<underlined>";
case 'o' -> "<italic>";
case 'r' -> "<reset>";
default -> null;
};
}
}
@@ -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";
}
}
+168
View File
@@ -0,0 +1,168 @@
settings:
debug: false
command-prefix: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ&#7A4A2A&lʙ&#8B6B35&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: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ &#D4AF37&lᴀᴜᴄᴛɪᴏɴs"
browse: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>Auctions <#7A4A2A>• Page {page}"
my-listings: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>My Listings <#7A4A2A>• Page {page}"
claims: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#B9C63F>Claims <#7A4A2A>• Page {page}"
sell: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>Create Listing"
confirm-buy: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>Confirm Purchase"
admin: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&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"
+65
View File
@@ -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 <id><#7A4A2A>, <#D4AF37>/ahadmin view <player><#7A4A2A>, <#D4AF37>/ahadmin forceexpire <id>"
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 <price>"
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."
+64
View File
@@ -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 <reload|remove|view|forceexpire|help>
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
Binary file not shown.
+168
View File
@@ -0,0 +1,168 @@
settings:
debug: false
command-prefix: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ&#7A4A2A&lʙ&#8B6B35&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: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ &#D4AF37&lᴀᴜᴄᴛɪᴏɴs"
browse: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>Auctions <#7A4A2A>• Page {page}"
my-listings: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>My Listings <#7A4A2A>• Page {page}"
claims: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#B9C63F>Claims <#7A4A2A>• Page {page}"
sell: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>Create Listing"
confirm-buy: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&lᴛ <#D4AF37>Confirm Purchase"
admin: "&#3A2416&lᴅ&#4A2D1B&lɪ&#5A351F&lʀ&#6A3F24&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"
+65
View File
@@ -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 <id><#7A4A2A>, <#D4AF37>/ahadmin view <player><#7A4A2A>, <#D4AF37>/ahadmin forceexpire <id>"
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 <price>"
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."
+64
View File
@@ -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 <reload|remove|view|forceexpire|help>
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
+5
View File
@@ -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
@@ -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
@@ -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
Binary file not shown.