2026-01-23 21:53:12 -05:00
|
|
|
package noppes.npcs.controllers;
|
|
|
|
|
|
|
|
|
|
import net.minecraft.entity.player.EntityPlayer;
|
|
|
|
|
import net.minecraft.entity.player.EntityPlayerMP;
|
|
|
|
|
import net.minecraft.item.ItemStack;
|
|
|
|
|
import net.minecraft.nbt.CompressedStreamTools;
|
|
|
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
|
|
|
import net.minecraft.nbt.NBTTagList;
|
|
|
|
|
import net.minecraft.server.MinecraftServer;
|
|
|
|
|
import net.minecraft.util.ChatComponentText;
|
|
|
|
|
import net.minecraft.util.EnumChatFormatting;
|
|
|
|
|
import noppes.npcs.CustomNpcs;
|
2026-01-24 15:11:50 -05:00
|
|
|
import noppes.npcs.CustomNpcsPermissions;
|
2026-02-18 04:55:03 -05:00
|
|
|
import noppes.npcs.EventHooks;
|
2026-01-23 21:53:12 -05:00
|
|
|
import noppes.npcs.LogWriter;
|
2026-01-24 15:11:50 -05:00
|
|
|
import noppes.npcs.api.entity.IPlayer;
|
|
|
|
|
import noppes.npcs.api.handler.IAuctionHandler;
|
|
|
|
|
import noppes.npcs.api.handler.data.IAuctionListing;
|
|
|
|
|
import noppes.npcs.api.item.IItemStack;
|
2026-01-23 21:53:12 -05:00
|
|
|
import noppes.npcs.config.ConfigMarket;
|
2026-02-18 04:55:03 -05:00
|
|
|
import noppes.npcs.constants.EnumAuctionLogAction;
|
2026-01-23 21:53:12 -05:00
|
|
|
import noppes.npcs.constants.EnumAuctionSort;
|
|
|
|
|
import noppes.npcs.constants.EnumAuctionStatus;
|
|
|
|
|
import noppes.npcs.constants.EnumClaimType;
|
|
|
|
|
import noppes.npcs.constants.EnumNotificationType;
|
2026-02-18 04:55:03 -05:00
|
|
|
import noppes.npcs.controllers.data.PlayerDataScript;
|
2026-01-24 16:03:54 -05:00
|
|
|
import noppes.npcs.controllers.data.AuctionBlacklist;
|
2026-01-23 21:53:12 -05:00
|
|
|
import noppes.npcs.controllers.data.AuctionClaim;
|
|
|
|
|
import noppes.npcs.controllers.data.AuctionFilter;
|
|
|
|
|
import noppes.npcs.controllers.data.AuctionListing;
|
|
|
|
|
import noppes.npcs.controllers.data.PlayerData;
|
2026-01-24 15:11:50 -05:00
|
|
|
import noppes.npcs.controllers.data.PlayerTradeData;
|
2026-02-18 04:55:03 -05:00
|
|
|
import noppes.npcs.scripted.NpcAPI;
|
|
|
|
|
import noppes.npcs.scripted.event.player.AuctionEvent;
|
2026-01-23 21:53:12 -05:00
|
|
|
import noppes.npcs.util.CustomNPCsThreader;
|
|
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.FileInputStream;
|
|
|
|
|
import java.io.FileOutputStream;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-24 15:11:50 -05:00
|
|
|
* Main controller for the Auction system.
|
2026-01-23 21:53:12 -05:00
|
|
|
*
|
|
|
|
|
* Thread Safety:
|
2026-01-24 15:11:50 -05:00
|
|
|
* - Uses ConcurrentHashMap for all data stores - no locking required
|
|
|
|
|
* - Claims are stored in PlayerData, not here - reduces contention
|
2026-01-23 21:53:12 -05:00
|
|
|
* - Saves are performed asynchronously on the CNPC+ thread
|
|
|
|
|
*
|
|
|
|
|
* Performance:
|
2026-01-24 15:11:50 -05:00
|
|
|
* - Auction processing runs every 30 seconds (600 ticks)
|
|
|
|
|
* - Save interval is every 30 seconds (600 ticks)
|
2026-01-23 21:53:12 -05:00
|
|
|
* - Player-specific queries use indexed maps for fast lookups
|
|
|
|
|
*/
|
2026-01-24 15:11:50 -05:00
|
|
|
public class AuctionController implements IAuctionHandler {
|
2026-01-23 21:53:12 -05:00
|
|
|
public static AuctionController Instance;
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Main data stores - listings only, claims are stored in PlayerData
|
2026-01-23 21:53:12 -05:00
|
|
|
private final Map<String, AuctionListing> listings = new ConcurrentHashMap<>();
|
2026-01-24 15:11:50 -05:00
|
|
|
// Claims are now stored in PlayerData.tradeData, not here
|
2026-01-23 21:53:12 -05:00
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Player Indices for O(1) Lookups
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
/** Listings by seller UUID - for quick "my listings" lookups */
|
|
|
|
|
private final Map<UUID, Set<String>> playerListingIds = new ConcurrentHashMap<>();
|
|
|
|
|
|
|
|
|
|
/** Listings where player is current high bidder */
|
|
|
|
|
private final Map<UUID, Set<String>> playerBidIds = new ConcurrentHashMap<>();
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
/** Cached max trade slots per player - computed on login, cleared on logout */
|
|
|
|
|
private final Map<UUID, Integer> playerMaxTradesCache = new ConcurrentHashMap<>();
|
2026-01-23 21:53:12 -05:00
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Threading & State
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
/** Flag indicating data needs to be saved */
|
|
|
|
|
private final AtomicBoolean dirty = new AtomicBoolean(false);
|
|
|
|
|
|
|
|
|
|
/** Flag to prevent concurrent saves */
|
|
|
|
|
private final AtomicBoolean saving = new AtomicBoolean(false);
|
|
|
|
|
|
|
|
|
|
private String filePath = "";
|
|
|
|
|
private int tickCounter = 0;
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
/** Process auctions and save every 30 seconds (600 ticks) */
|
|
|
|
|
private static final int TICK_INTERVAL = 600;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
|
|
|
|
public AuctionController() {
|
|
|
|
|
Instance = this;
|
2026-01-24 16:03:54 -05:00
|
|
|
AuctionBlacklist.reload();
|
2026-01-23 21:53:12 -05:00
|
|
|
load();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static AuctionController getInstance() {
|
|
|
|
|
if (Instance == null || needsNewInstance()) {
|
|
|
|
|
Instance = new AuctionController();
|
|
|
|
|
}
|
|
|
|
|
return Instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static boolean needsNewInstance() {
|
|
|
|
|
if (Instance == null) return true;
|
|
|
|
|
File file = CustomNpcs.getWorldSaveDirectory();
|
|
|
|
|
if (file == null) return false;
|
|
|
|
|
return !Instance.filePath.equals(file.getAbsolutePath());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Tick Processing
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
public void onServerTick() {
|
|
|
|
|
if (!ConfigMarket.AuctionEnabled) return;
|
|
|
|
|
|
|
|
|
|
tickCounter++;
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Process auctions and save every 30 seconds
|
|
|
|
|
if (tickCounter >= TICK_INTERVAL) {
|
2026-01-23 21:53:12 -05:00
|
|
|
tickCounter = 0;
|
|
|
|
|
processEndedAuctions();
|
|
|
|
|
if (dirty.get()) {
|
|
|
|
|
saveAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void processEndedAuctions() {
|
2026-01-24 15:11:50 -05:00
|
|
|
boolean changed = false;
|
|
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) {
|
|
|
|
|
endAuction(listing);
|
|
|
|
|
changed = true;
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
}
|
|
|
|
|
if (changed) {
|
|
|
|
|
markDirty();
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void endAuction(AuctionListing listing) {
|
2026-02-18 04:55:03 -05:00
|
|
|
synchronized (listing) {
|
|
|
|
|
if (listing.status != EnumAuctionStatus.ACTIVE) return;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
listing.status = EnumAuctionStatus.ENDED;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Remove from seller's active listings
|
|
|
|
|
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Remove from bidder's active bids if any
|
|
|
|
|
if (listing.highBidderUUID != null) {
|
|
|
|
|
removeFromPlayerBids(listing.highBidderUUID, listing.id);
|
|
|
|
|
}
|
2026-01-24 01:57:46 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Get item display name safely
|
|
|
|
|
String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item";
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
if (listing.hasBids()) {
|
|
|
|
|
// Auction sold - create claims
|
|
|
|
|
AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(
|
|
|
|
|
listing.highBidderUUID, listing.highBidderName, listing.id, listing.item);
|
|
|
|
|
addClaimToPlayer(listing.highBidderUUID, itemClaim);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
long saleAmount = listing.currentBid;
|
|
|
|
|
long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent);
|
|
|
|
|
long sellerReceives = saleAmount - tax;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
|
|
|
|
|
listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
|
|
|
|
|
itemDisplayName, listing.highBidderName);
|
|
|
|
|
addClaimToPlayer(listing.sellerUUID, currencyClaim);
|
|
|
|
|
|
|
|
|
|
// Notifications
|
|
|
|
|
sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id,
|
|
|
|
|
"You won the auction for " + itemDisplayName + "!");
|
|
|
|
|
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
|
|
|
|
|
"Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.SOLD, listing.sellerName, itemDisplayName, saleAmount,
|
|
|
|
|
"Winner: " + listing.highBidderName + ", Tax: " + tax);
|
|
|
|
|
} else {
|
|
|
|
|
// No bids - return item to seller
|
|
|
|
|
AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim(
|
|
|
|
|
listing.sellerUUID, listing.sellerName, listing.id, listing.item);
|
|
|
|
|
addClaimToPlayer(listing.sellerUUID, returnClaim);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id,
|
|
|
|
|
"Your auction for " + itemDisplayName + " expired with no bids.");
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.EXPIRED, listing.sellerName, itemDisplayName, listing.startingPrice, "No bids");
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Index Management
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
private void addToPlayerListings(UUID playerUUID, String listingId) {
|
|
|
|
|
playerListingIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void removeFromPlayerListings(UUID playerUUID, String listingId) {
|
|
|
|
|
Set<String> ids = playerListingIds.get(playerUUID);
|
|
|
|
|
if (ids != null) {
|
|
|
|
|
ids.remove(listingId);
|
|
|
|
|
if (ids.isEmpty()) {
|
|
|
|
|
playerListingIds.remove(playerUUID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void addToPlayerBids(UUID playerUUID, String listingId) {
|
|
|
|
|
playerBidIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void removeFromPlayerBids(UUID playerUUID, String listingId) {
|
|
|
|
|
Set<String> ids = playerBidIds.get(playerUUID);
|
|
|
|
|
if (ids != null) {
|
|
|
|
|
ids.remove(listingId);
|
|
|
|
|
if (ids.isEmpty()) {
|
|
|
|
|
playerBidIds.remove(playerUUID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
/**
|
|
|
|
|
* Add a claim to a player's trade data.
|
|
|
|
|
* If player is online, adds directly. If offline, loads their data file and updates it.
|
|
|
|
|
*/
|
|
|
|
|
private void addClaimToPlayer(UUID playerUUID, AuctionClaim claim) {
|
|
|
|
|
if (playerUUID == null || claim == null) return;
|
|
|
|
|
|
|
|
|
|
PlayerData data = PlayerDataController.Instance.getData(playerUUID);
|
|
|
|
|
if (data != null) {
|
|
|
|
|
data.tradeData.addClaim(claim);
|
|
|
|
|
data.updateClient = true;
|
|
|
|
|
data.save();
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Listing Management
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
public String createListing(EntityPlayer player, ItemStack item, long startingPrice, long buyoutPrice) {
|
|
|
|
|
if (!ConfigMarket.AuctionEnabled) {
|
2026-01-24 15:11:50 -05:00
|
|
|
return "Auction is disabled.";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 16:03:54 -05:00
|
|
|
// Check blacklist (with bypass for admins)
|
|
|
|
|
if (AuctionBlacklist.isBlacklistedForPlayer(item, player)) {
|
|
|
|
|
return "This item cannot be listed on the Auction House.";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Validate minimum price
|
|
|
|
|
if (startingPrice < ConfigMarket.MinimumListingPrice) {
|
|
|
|
|
return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + ".";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate buyout price if set
|
|
|
|
|
if (buyoutPrice > 0 && buyoutPrice < startingPrice) {
|
|
|
|
|
return "Buyout price must be higher than starting price.";
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UUID playerUUID = player.getUniqueID();
|
|
|
|
|
String playerName = player.getCommandSenderName();
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Check trade slot limit (listings + bids + claims)
|
|
|
|
|
int currentTrades = getPlayerTradeSlotCount(player);
|
|
|
|
|
int maxTrades = getMaxTradesForPlayer(player);
|
|
|
|
|
if (currentTrades >= maxTrades) {
|
|
|
|
|
return "You have reached your maximum trade slots (" + maxTrades + ").";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Check listing fee
|
|
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData == null) {
|
|
|
|
|
return "Could not access player data.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
PlayerTradeData currency = playerData.tradeData;
|
|
|
|
|
long fee = ConfigMarket.ListingFee;
|
|
|
|
|
if (!currency.canAfford(fee)) {
|
|
|
|
|
return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ").";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Fire auction create event (cancelable)
|
|
|
|
|
PlayerDataScript createHandler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
IPlayer iCreator = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
AuctionEvent.CreateEvent createEvent = new AuctionEvent.CreateEvent(
|
|
|
|
|
iCreator, NpcAPI.Instance().getIItemStack(item), startingPrice, buyoutPrice);
|
|
|
|
|
if (EventHooks.onAuctionCreate(createHandler, createEvent)) {
|
|
|
|
|
return "Auction creation was cancelled.";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Deduct fee
|
|
|
|
|
if (!currency.withdraw(fee)) {
|
|
|
|
|
return "Failed to deduct listing fee.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Calculate duration
|
|
|
|
|
long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Create listing
|
|
|
|
|
AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs);
|
|
|
|
|
listings.put(listing.id, listing);
|
|
|
|
|
addToPlayerListings(playerUUID, listing.id);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
markDirty();
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.CREATED, playerName, item.getDisplayName(), startingPrice,
|
2026-01-24 15:11:50 -05:00
|
|
|
"Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
return null; // Success
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String cancelListing(String listingId, EntityPlayer player, boolean isAdmin) {
|
2026-01-24 15:11:50 -05:00
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
if (listing == null) {
|
|
|
|
|
return "Listing not found.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
synchronized (listing) {
|
|
|
|
|
if (!listing.status.canCancel()) {
|
|
|
|
|
return "This auction cannot be cancelled.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
UUID playerUUID = player.getUniqueID();
|
|
|
|
|
if (!isAdmin && !listing.isSeller(playerUUID)) {
|
|
|
|
|
return "You can only cancel your own listings.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Fire auction cancel event (cancelable)
|
|
|
|
|
PlayerDataScript cancelHandler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
IPlayer iCanceller = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
AuctionEvent.CancelEvent cancelEvent = new AuctionEvent.CancelEvent(iCanceller, listing, isAdmin);
|
|
|
|
|
if (EventHooks.onAuctionCancel(cancelHandler, cancelEvent)) {
|
|
|
|
|
return "Auction cancellation was prevented.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Handle penalty if there are bids
|
|
|
|
|
if (listing.hasBids()) {
|
|
|
|
|
long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent);
|
|
|
|
|
|
|
|
|
|
// Refund bidder (no item - can't rebid on cancelled auction)
|
|
|
|
|
AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
|
|
|
|
|
listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid,
|
|
|
|
|
listing.item.getDisplayName(), listing.sellerName, null);
|
|
|
|
|
addClaimToPlayer(listing.highBidderUUID, refundClaim);
|
|
|
|
|
|
|
|
|
|
// Remove from bidder's active bids
|
|
|
|
|
removeFromPlayerBids(listing.highBidderUUID, listing.id);
|
|
|
|
|
|
|
|
|
|
sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id,
|
|
|
|
|
"The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded.");
|
|
|
|
|
|
|
|
|
|
// Return item to seller
|
|
|
|
|
AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim(
|
|
|
|
|
listing.sellerUUID, listing.sellerName, listing.id, listing.item);
|
|
|
|
|
addClaimToPlayer(listing.sellerUUID, itemClaim);
|
|
|
|
|
|
|
|
|
|
// Deduct cancellation penalty from seller
|
|
|
|
|
if (penalty > 0) {
|
|
|
|
|
PlayerData sellerData = PlayerDataController.Instance.getData(listing.sellerUUID);
|
|
|
|
|
if (sellerData != null) {
|
|
|
|
|
sellerData.tradeData.withdraw(penalty);
|
|
|
|
|
sellerData.save();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.currentBid,
|
|
|
|
|
"Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName +
|
|
|
|
|
(isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : ""));
|
|
|
|
|
} else {
|
|
|
|
|
// No bids - just return item
|
|
|
|
|
AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim(
|
|
|
|
|
listing.sellerUUID, listing.sellerName, listing.id, listing.item);
|
|
|
|
|
addClaimToPlayer(listing.sellerUUID, itemClaim);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.startingPrice,
|
|
|
|
|
"No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : ""));
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Remove from seller's active listings
|
|
|
|
|
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
listing.status = EnumAuctionStatus.CANCELLED;
|
|
|
|
|
markDirty();
|
2026-01-24 15:11:50 -05:00
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
return null; // Success
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Bidding
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
public String placeBid(String listingId, EntityPlayer player, long bidAmount) {
|
|
|
|
|
if (!ConfigMarket.AuctionEnabled) {
|
2026-01-24 15:11:50 -05:00
|
|
|
return "Auction is disabled.";
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UUID playerUUID = player.getUniqueID();
|
|
|
|
|
String playerName = player.getCommandSenderName();
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
if (listing == null) {
|
|
|
|
|
return "Listing not found.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
synchronized (listing) {
|
|
|
|
|
// Check status and expiry
|
|
|
|
|
if (!listing.status.canBid()) {
|
|
|
|
|
return "This auction has ended.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
if (listing.isExpired()) {
|
|
|
|
|
return "This auction has ended.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// CRITICAL: Prevent bidding on own items
|
|
|
|
|
if (listing.isSeller(playerUUID)) {
|
|
|
|
|
return "You cannot bid on your own auction.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Prevent rebidding if already highest bidder
|
|
|
|
|
if (listing.isHighBidder(playerUUID)) {
|
|
|
|
|
return "You are already the highest bidder. Use buyout if you want to purchase immediately.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Check minimum bid
|
|
|
|
|
long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent);
|
|
|
|
|
if (bidAmount < minBid) {
|
|
|
|
|
return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + ".";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Check currency
|
|
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData == null) {
|
|
|
|
|
return "Could not access player data.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
PlayerTradeData currency = playerData.tradeData;
|
2026-01-24 03:24:57 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Store previous bidder info
|
|
|
|
|
UUID previousBidder = listing.highBidderUUID;
|
|
|
|
|
long previousBid = listing.currentBid;
|
2026-01-24 03:24:57 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Full bid amount charged since we don't allow rebidding
|
|
|
|
|
long amountToCharge = bidAmount;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
if (!currency.canAfford(amountToCharge)) {
|
|
|
|
|
return "You cannot afford this bid.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Fire auction bid event (cancelable)
|
|
|
|
|
PlayerDataScript bidHandler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
IPlayer iBidder = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
AuctionEvent.BidEvent bidEvent = new AuctionEvent.BidEvent(iBidder, listing, bidAmount);
|
|
|
|
|
if (EventHooks.onAuctionBid(bidHandler, bidEvent)) {
|
|
|
|
|
return "Bid was cancelled.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Deduct bid from new bidder FIRST
|
|
|
|
|
if (!currency.withdraw(amountToCharge)) {
|
|
|
|
|
return "Failed to deduct bid amount.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Refund previous bidder (include item for rebid option)
|
|
|
|
|
if (listing.hasBids() && previousBidder != null) {
|
|
|
|
|
AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
|
|
|
|
|
previousBidder, listing.highBidderName, listing.id, previousBid,
|
|
|
|
|
listing.item.getDisplayName(), playerName, listing.item);
|
|
|
|
|
addClaimToPlayer(previousBidder, refundClaim);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Remove from previous bidder's active bids
|
|
|
|
|
removeFromPlayerBids(previousBidder, listing.id);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
sendNotificationToPlayer(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id,
|
|
|
|
|
"You were outbid on " + listing.item.getDisplayName() + "!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update listing
|
|
|
|
|
listing.highBidderUUID = playerUUID;
|
|
|
|
|
listing.highBidderName = playerName;
|
|
|
|
|
listing.currentBid = bidAmount;
|
|
|
|
|
listing.bidCount++;
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Add to new bidder's active bids
|
|
|
|
|
addToPlayerBids(playerUUID, listing.id);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Snipe protection
|
|
|
|
|
listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
markDirty();
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.BID, playerName, listing.item.getDisplayName(), bidAmount,
|
2026-01-24 15:11:50 -05:00
|
|
|
"Bids: " + listing.bidCount + ", Seller: " + listing.sellerName);
|
|
|
|
|
|
|
|
|
|
return null; // Success
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String buyout(String listingId, EntityPlayer player) {
|
|
|
|
|
if (!ConfigMarket.AuctionEnabled) {
|
2026-01-24 15:11:50 -05:00
|
|
|
return "Auction is disabled.";
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UUID playerUUID = player.getUniqueID();
|
|
|
|
|
String playerName = player.getCommandSenderName();
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
if (listing == null) {
|
|
|
|
|
return "Listing not found.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
synchronized (listing) {
|
|
|
|
|
// Check status and expiry
|
|
|
|
|
if (!listing.status.canBid()) {
|
|
|
|
|
return "This auction has ended.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
if (listing.isExpired()) {
|
|
|
|
|
return "This auction has ended.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
if (!listing.hasBuyout()) {
|
|
|
|
|
return "This auction does not have a buyout price.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// CRITICAL: Prevent buying own items
|
|
|
|
|
if (listing.isSeller(playerUUID)) {
|
|
|
|
|
return "You cannot buy your own auction.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Check currency
|
|
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData == null) {
|
|
|
|
|
return "Could not access player data.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
PlayerTradeData currency = playerData.tradeData;
|
2026-01-24 15:11:50 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// If buyer is already the high bidder, only charge the difference
|
|
|
|
|
boolean isCurrentBidder = listing.isHighBidder(playerUUID);
|
|
|
|
|
long amountToCharge = isCurrentBidder
|
|
|
|
|
? (listing.buyoutPrice - listing.currentBid)
|
|
|
|
|
: listing.buyoutPrice;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
if (!currency.canAfford(amountToCharge)) {
|
|
|
|
|
if (isCurrentBidder) {
|
|
|
|
|
return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout.";
|
|
|
|
|
}
|
|
|
|
|
return "You cannot afford the buyout price.";
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Fire auction buyout event (cancelable)
|
|
|
|
|
PlayerDataScript buyoutHandler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
IPlayer iBuyer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
AuctionEvent.BuyoutEvent buyoutEvent = new AuctionEvent.BuyoutEvent(iBuyer, listing);
|
|
|
|
|
if (EventHooks.onAuctionBuyout(buyoutHandler, buyoutEvent)) {
|
|
|
|
|
return "Buyout was cancelled.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Deduct buyout amount
|
|
|
|
|
if (!currency.withdraw(amountToCharge)) {
|
|
|
|
|
return "Failed to deduct buyout amount.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Refund previous bidder if any AND they're not the buyer (no item - auction ended via buyout)
|
|
|
|
|
if (listing.hasBids() && listing.highBidderUUID != null && !isCurrentBidder) {
|
|
|
|
|
AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
|
|
|
|
|
listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid,
|
|
|
|
|
listing.item.getDisplayName(), playerName, null);
|
|
|
|
|
addClaimToPlayer(listing.highBidderUUID, refundClaim);
|
|
|
|
|
|
|
|
|
|
// Remove from previous bidder's active bids
|
|
|
|
|
removeFromPlayerBids(listing.highBidderUUID, listing.id);
|
|
|
|
|
|
|
|
|
|
sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id,
|
|
|
|
|
"The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded.");
|
|
|
|
|
} else if (isCurrentBidder) {
|
|
|
|
|
// Current bidder is buying out - just remove from their active bids
|
|
|
|
|
removeFromPlayerBids(playerUUID, listing.id);
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Create claims
|
|
|
|
|
AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item);
|
|
|
|
|
addClaimToPlayer(playerUUID, itemClaim);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
long saleAmount = listing.buyoutPrice;
|
|
|
|
|
long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent);
|
|
|
|
|
long sellerReceives = saleAmount - tax;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
|
|
|
|
|
listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
|
|
|
|
|
listing.item.getDisplayName(), playerName);
|
|
|
|
|
addClaimToPlayer(listing.sellerUUID, currencyClaim);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Notifications
|
|
|
|
|
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
|
|
|
|
|
"Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Remove from seller's active listings
|
|
|
|
|
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
listing.status = EnumAuctionStatus.ENDED;
|
|
|
|
|
listing.highBidderUUID = playerUUID;
|
|
|
|
|
listing.highBidderName = playerName;
|
|
|
|
|
listing.currentBid = listing.buyoutPrice;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
markDirty();
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.BUYOUT, playerName, listing.item.getDisplayName(), listing.buyoutPrice,
|
|
|
|
|
"Seller: " + listing.sellerName + ", Tax: " + tax);
|
|
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
|
|
|
|
|
return null; // Success
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
2026-01-24 15:11:50 -05:00
|
|
|
// Claims - Now stored in PlayerData
|
2026-01-23 21:53:12 -05:00
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-24 15:11:50 -05:00
|
|
|
* Get player claims from PlayerData.
|
2026-01-23 21:53:12 -05:00
|
|
|
*/
|
2026-01-24 15:11:50 -05:00
|
|
|
public List<AuctionClaim> getPlayerClaims(EntityPlayer player) {
|
|
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData == null) {
|
2026-01-23 21:53:12 -05:00
|
|
|
return new ArrayList<>();
|
|
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
return playerData.tradeData.getClaimsList();
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
/**
|
|
|
|
|
* Get player claims by UUID (for login notification).
|
|
|
|
|
*/
|
|
|
|
|
public List<AuctionClaim> getPlayerClaims(UUID playerUUID) {
|
|
|
|
|
EntityPlayerMP onlinePlayer = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID);
|
|
|
|
|
if (onlinePlayer != null) {
|
|
|
|
|
return getPlayerClaims(onlinePlayer);
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
return new ArrayList<>();
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String claimItem(String claimId, EntityPlayer player) {
|
2026-01-24 15:11:50 -05:00
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData == null) {
|
|
|
|
|
return "Could not access player data.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId);
|
|
|
|
|
if (claim == null) {
|
|
|
|
|
return "Claim not found.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
if (claim.claimed) {
|
|
|
|
|
return "Already claimed.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
if (!claim.type.isItem()) {
|
|
|
|
|
return "This is not an item claim.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
if (claim.item == null) {
|
|
|
|
|
return "Item data is missing.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Fire auction claim event (cancelable)
|
|
|
|
|
PlayerDataScript itemClaimHandler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
IPlayer iItemClaimer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
AuctionEvent.ClaimEvent itemClaimEvent = new AuctionEvent.ClaimEvent(iItemClaimer, claim);
|
|
|
|
|
if (EventHooks.onAuctionClaim(itemClaimHandler, itemClaimEvent)) {
|
|
|
|
|
return "Claim was prevented.";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Check inventory space
|
|
|
|
|
if (!player.inventory.addItemStackToInventory(claim.item.copy())) {
|
|
|
|
|
return "Your inventory is full.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Remove claim from player's auction data
|
|
|
|
|
playerData.tradeData.claimAndRemove(claimId);
|
|
|
|
|
playerData.updateClient = true;
|
|
|
|
|
playerData.save();
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Cleanup listing if no more claims
|
|
|
|
|
cleanupListingIfComplete(claim.listingId);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed");
|
2026-01-24 15:11:50 -05:00
|
|
|
|
|
|
|
|
return null; // Success
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String claimCurrency(String claimId, EntityPlayer player) {
|
2026-01-24 15:11:50 -05:00
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData == null) {
|
|
|
|
|
return "Could not access player data.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId);
|
|
|
|
|
if (claim == null) {
|
|
|
|
|
return "Claim not found.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
if (claim.claimed) {
|
|
|
|
|
return "Already claimed.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
if (!claim.type.isCurrency()) {
|
|
|
|
|
return "This is not a currency claim.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
// Fire auction claim event (cancelable)
|
|
|
|
|
PlayerDataScript currencyClaimHandler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
IPlayer iCurrencyClaimer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
AuctionEvent.ClaimEvent currencyClaimEvent = new AuctionEvent.ClaimEvent(iCurrencyClaimer, claim);
|
|
|
|
|
if (EventHooks.onAuctionClaim(currencyClaimHandler, currencyClaimEvent)) {
|
|
|
|
|
return "Claim was prevented.";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
if (!playerData.tradeData.deposit(claim.currency)) {
|
|
|
|
|
return "Failed to deposit currency.";
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Remove claim from player's auction data
|
|
|
|
|
playerData.tradeData.claimAndRemove(claimId);
|
|
|
|
|
playerData.updateClient = true;
|
|
|
|
|
playerData.save();
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Cleanup listing if no more claims
|
|
|
|
|
cleanupListingIfComplete(claim.listingId);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency,
|
2026-01-24 15:11:50 -05:00
|
|
|
claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed");
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
return null; // Success
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clean up a listing if it's ended and no longer needed.
|
|
|
|
|
* Now just removes ended listings after some delay since claims are in PlayerData.
|
|
|
|
|
*/
|
|
|
|
|
private void cleanupListingIfComplete(String listingId) {
|
|
|
|
|
if (listingId == null || listingId.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
if (listing == null) return;
|
|
|
|
|
|
|
|
|
|
// Only cleanup ended/cancelled listings
|
|
|
|
|
if (listing.status == EnumAuctionStatus.ACTIVE) return;
|
|
|
|
|
|
|
|
|
|
// Remove ended listings (claims are now in PlayerData, not here)
|
|
|
|
|
listings.remove(listingId);
|
|
|
|
|
markDirty();
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int claimAll(EntityPlayer player) {
|
|
|
|
|
int claimedCount = 0;
|
2026-01-24 15:11:50 -05:00
|
|
|
List<AuctionClaim> playerClaims = getPlayerClaims(player);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
|
|
|
|
for (AuctionClaim claim : playerClaims) {
|
|
|
|
|
String result;
|
|
|
|
|
if (claim.type.isItem()) {
|
|
|
|
|
result = claimItem(claim.id, player);
|
|
|
|
|
} else {
|
|
|
|
|
result = claimCurrency(claim.id, player);
|
|
|
|
|
}
|
|
|
|
|
if (result == null) {
|
|
|
|
|
claimedCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return claimedCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Queries - Using Indexed Lookups
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
public List<AuctionListing> getActiveListings(AuctionFilter filter, int page, int pageSize) {
|
|
|
|
|
List<AuctionListing> result = new ArrayList<>();
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
if (listing.status != EnumAuctionStatus.ACTIVE) continue;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Apply search filter (searches item name AND seller name)
|
|
|
|
|
if (filter.hasSearchText()) {
|
|
|
|
|
String itemName = listing.item != null ? listing.item.getDisplayName() : "";
|
|
|
|
|
String sellerName = listing.sellerName != null ? listing.sellerName : "";
|
|
|
|
|
if (!filter.matchesSearch(itemName, sellerName)) continue;
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
|
|
|
|
|
result.add(listing);
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort
|
|
|
|
|
sortListings(result, filter.sortBy);
|
|
|
|
|
|
|
|
|
|
// Paginate
|
|
|
|
|
int start = page * pageSize;
|
|
|
|
|
int end = Math.min(start + pageSize, result.size());
|
|
|
|
|
if (start >= result.size()) {
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
return new ArrayList<>(result.subList(start, end));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void sortListings(List<AuctionListing> list, EnumAuctionSort sortBy) {
|
|
|
|
|
Comparator<AuctionListing> comparator;
|
|
|
|
|
switch (sortBy) {
|
|
|
|
|
case ENDING_SOON:
|
|
|
|
|
comparator = Comparator.comparingLong(AuctionListing::getTimeRemaining);
|
|
|
|
|
break;
|
|
|
|
|
case PRICE_LOW:
|
|
|
|
|
comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice);
|
|
|
|
|
break;
|
|
|
|
|
case PRICE_HIGH:
|
|
|
|
|
comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice).reversed();
|
|
|
|
|
break;
|
|
|
|
|
case MOST_BIDS:
|
|
|
|
|
comparator = Comparator.comparingInt(l -> -l.bidCount);
|
|
|
|
|
break;
|
|
|
|
|
case NEWEST:
|
|
|
|
|
default:
|
|
|
|
|
comparator = Comparator.comparingLong(l -> -l.createdTime);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
list.sort(comparator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getTotalActiveListings(AuctionFilter filter) {
|
|
|
|
|
int count = 0;
|
2026-01-24 15:11:50 -05:00
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
if (listing.status != EnumAuctionStatus.ACTIVE) continue;
|
|
|
|
|
if (filter.hasSearchText()) {
|
|
|
|
|
String itemName = listing.item != null ? listing.item.getDisplayName() : "";
|
|
|
|
|
String sellerName = listing.sellerName != null ? listing.sellerName : "";
|
|
|
|
|
if (!filter.matchesSearch(itemName, sellerName)) continue;
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
count++;
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
@Override
|
2026-01-23 21:53:12 -05:00
|
|
|
public AuctionListing getListing(String listingId) {
|
|
|
|
|
return listings.get(listingId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a player's current currency balance.
|
|
|
|
|
*/
|
|
|
|
|
public long getPlayerBalance(EntityPlayer player) {
|
|
|
|
|
PlayerData playerData = PlayerData.get(player);
|
2026-01-24 15:11:50 -05:00
|
|
|
if (playerData == null || playerData.tradeData == null) {
|
2026-01-23 21:53:12 -05:00
|
|
|
return 0;
|
|
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
return playerData.tradeData.getBalance();
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-24 15:11:50 -05:00
|
|
|
* Get player listing count using index
|
2026-01-23 21:53:12 -05:00
|
|
|
*/
|
2026-01-24 15:11:50 -05:00
|
|
|
public int getPlayerListingCount(UUID playerUUID) {
|
2026-01-23 21:53:12 -05:00
|
|
|
Set<String> ids = playerListingIds.get(playerUUID);
|
|
|
|
|
return ids != null ? ids.size() : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get player's active listings using indexed lookup
|
|
|
|
|
*/
|
|
|
|
|
public List<AuctionListing> getPlayerActiveListings(UUID playerUUID) {
|
|
|
|
|
Set<String> listingIds = playerListingIds.get(playerUUID);
|
|
|
|
|
if (listingIds == null || listingIds.isEmpty()) {
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<AuctionListing> result = new ArrayList<>();
|
|
|
|
|
for (String listingId : listingIds) {
|
|
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
if (listing != null && listing.status == EnumAuctionStatus.ACTIVE) {
|
|
|
|
|
result.add(listing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all active auctions where the player is currently the highest bidder.
|
|
|
|
|
* Uses indexed lookup for O(1) access to player's bid set.
|
|
|
|
|
*/
|
|
|
|
|
public List<AuctionListing> getPlayerActiveBids(UUID playerUUID) {
|
|
|
|
|
Set<String> bidListingIds = playerBidIds.get(playerUUID);
|
|
|
|
|
if (bidListingIds == null || bidListingIds.isEmpty()) {
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<AuctionListing> result = new ArrayList<>();
|
|
|
|
|
for (String listingId : bidListingIds) {
|
|
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
if (listing != null && listing.status == EnumAuctionStatus.ACTIVE &&
|
|
|
|
|
listing.highBidderUUID != null && listing.highBidderUUID.equals(playerUUID)) {
|
|
|
|
|
result.add(listing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the total number of trade slots used by a player.
|
2026-01-24 15:11:50 -05:00
|
|
|
* Includes active listings, active bids, and pending claims.
|
|
|
|
|
*/
|
|
|
|
|
public int getPlayerTradeSlotCount(EntityPlayer player) {
|
|
|
|
|
UUID playerUUID = player.getUniqueID();
|
|
|
|
|
int count = 0;
|
|
|
|
|
// Active listings
|
|
|
|
|
Set<String> listingIds = playerListingIds.get(playerUUID);
|
|
|
|
|
if (listingIds != null) count += listingIds.size();
|
|
|
|
|
// Active bids
|
|
|
|
|
Set<String> bidIds = playerBidIds.get(playerUUID);
|
|
|
|
|
if (bidIds != null) count += bidIds.size();
|
|
|
|
|
// Pending claims (from PlayerData)
|
|
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData != null) {
|
|
|
|
|
count += playerData.tradeData.getClaimCount();
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the total number of trade slots used by a player (UUID version).
|
|
|
|
|
* For offline players, only returns listings + bids (claims are in their PlayerData).
|
2026-01-23 21:53:12 -05:00
|
|
|
*/
|
|
|
|
|
public int getPlayerTradeSlotCount(UUID playerUUID) {
|
|
|
|
|
int count = 0;
|
|
|
|
|
// Active listings
|
|
|
|
|
Set<String> listingIds = playerListingIds.get(playerUUID);
|
|
|
|
|
if (listingIds != null) count += listingIds.size();
|
|
|
|
|
// Active bids
|
|
|
|
|
Set<String> bidIds = playerBidIds.get(playerUUID);
|
|
|
|
|
if (bidIds != null) count += bidIds.size();
|
2026-01-24 15:11:50 -05:00
|
|
|
// Can't get claims for offline player without loading their data
|
2026-01-23 21:53:12 -05:00
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
/**
|
|
|
|
|
* Get the maximum trade slots for a player.
|
|
|
|
|
* Uses cached value if available, otherwise computes from permissions.
|
|
|
|
|
*/
|
|
|
|
|
public int getMaxTradesForPlayer(EntityPlayer player) {
|
|
|
|
|
UUID playerUUID = player.getUniqueID();
|
|
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
Integer cached = playerMaxTradesCache.get(playerUUID);
|
|
|
|
|
if (cached != null) {
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute and cache
|
|
|
|
|
int maxTrades = computeMaxTradesForPlayer(player);
|
|
|
|
|
playerMaxTradesCache.put(playerUUID, maxTrades);
|
|
|
|
|
return maxTrades;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compute max trade slots from permissions.
|
|
|
|
|
* Checks for customnpcs.auction.trades.X permissions where X is the slot count.
|
|
|
|
|
* Uses the highest permission found, or DefaultMaxTrades if no permissions or lower.
|
|
|
|
|
* Capped at MAX_TRADE_SLOTS (45).
|
|
|
|
|
*/
|
|
|
|
|
private int computeMaxTradesForPlayer(EntityPlayer player) {
|
|
|
|
|
// Check for unlimited permission
|
|
|
|
|
if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.auction.trades.*")) {
|
|
|
|
|
return ConfigMarket.MAX_TRADE_SLOTS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find highest allowed slot count from permissions
|
|
|
|
|
int highestAllowed = 0;
|
|
|
|
|
for (int i = 1; i <= ConfigMarket.MAX_TRADE_SLOTS; i++) {
|
|
|
|
|
String perm = "customnpcs.auction.trades." + i;
|
|
|
|
|
if (CustomNpcsPermissions.hasCustomPermission(player, perm)) {
|
|
|
|
|
highestAllowed = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use default if no permissions found or permission is lower than default
|
|
|
|
|
if (highestAllowed == 0 || highestAllowed < ConfigMarket.DefaultMaxTrades) {
|
|
|
|
|
highestAllowed = ConfigMarket.DefaultMaxTrades;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cap at maximum
|
|
|
|
|
return Math.min(highestAllowed, ConfigMarket.MAX_TRADE_SLOTS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Refresh a player's cached max trades (call when permissions change).
|
|
|
|
|
*/
|
|
|
|
|
public void refreshPlayerMaxTrades(EntityPlayer player) {
|
|
|
|
|
UUID playerUUID = player.getUniqueID();
|
|
|
|
|
int maxTrades = computeMaxTradesForPlayer(player);
|
|
|
|
|
playerMaxTradesCache.put(playerUUID, maxTrades);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear a player's cached max trades (call on logout).
|
|
|
|
|
*/
|
|
|
|
|
public void clearPlayerMaxTradesCache(UUID playerUUID) {
|
|
|
|
|
playerMaxTradesCache.remove(playerUUID);
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Notifications
|
|
|
|
|
// =========================================
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
/**
|
|
|
|
|
* Send a notification to a player.
|
|
|
|
|
* If player is online, sends immediately. Otherwise, discards (claims will notify on login).
|
|
|
|
|
*/
|
|
|
|
|
private void sendNotificationToPlayer(UUID playerUUID, EnumNotificationType type, String listingId, String message) {
|
2026-01-23 21:53:12 -05:00
|
|
|
// Try to send immediately if player is online
|
2026-01-24 15:11:50 -05:00
|
|
|
EntityPlayerMP player = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID);
|
2026-01-23 21:53:12 -05:00
|
|
|
if (player != null) {
|
2026-01-24 15:11:50 -05:00
|
|
|
sendNotificationMessage(player, type, message);
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
// If offline, no need to store - they'll get notified about claims on login
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
/**
|
|
|
|
|
* Send a notification chat message to a player.
|
|
|
|
|
*/
|
|
|
|
|
private void sendNotificationMessage(EntityPlayerMP player, EnumNotificationType type, String message) {
|
|
|
|
|
EnumChatFormatting color;
|
|
|
|
|
switch (type) {
|
|
|
|
|
case AUCTION_WON:
|
|
|
|
|
case AUCTION_SOLD:
|
|
|
|
|
color = EnumChatFormatting.GREEN;
|
|
|
|
|
break;
|
|
|
|
|
case AUCTION_OUTBID:
|
|
|
|
|
color = EnumChatFormatting.YELLOW;
|
|
|
|
|
break;
|
|
|
|
|
case AUCTION_EXPIRED:
|
|
|
|
|
color = EnumChatFormatting.RED;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
color = EnumChatFormatting.GRAY;
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
player.addChatMessage(new ChatComponentText(
|
|
|
|
|
EnumChatFormatting.GOLD + "[Auction] " + color + message));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void onPlayerLogin(EntityPlayer player) {
|
|
|
|
|
// Cache player's max trades permission
|
|
|
|
|
refreshPlayerMaxTrades(player);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Process expired claims for this player
|
|
|
|
|
PlayerData playerData = PlayerData.get(player);
|
|
|
|
|
if (playerData != null) {
|
|
|
|
|
int expired = playerData.tradeData.processExpiredClaims();
|
|
|
|
|
if (expired > 0) {
|
|
|
|
|
playerData.updateClient = true;
|
|
|
|
|
playerData.save();
|
2026-02-18 04:55:03 -05:00
|
|
|
logAuction(EnumAuctionLogAction.CLAIM_EXPIRED, player.getCommandSenderName(), "Multiple", expired,
|
2026-01-24 15:11:50 -05:00
|
|
|
expired + " claims expired on login");
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Check for pending claims and notify
|
|
|
|
|
List<AuctionClaim> claims = getPlayerClaims(player);
|
|
|
|
|
if (!claims.isEmpty()) {
|
|
|
|
|
int itemClaims = 0;
|
|
|
|
|
int currencyClaims = 0;
|
|
|
|
|
long totalCurrency = 0;
|
|
|
|
|
|
|
|
|
|
for (AuctionClaim claim : claims) {
|
|
|
|
|
if (claim.type.isItem()) {
|
|
|
|
|
itemClaims++;
|
|
|
|
|
} else {
|
|
|
|
|
currencyClaims++;
|
|
|
|
|
totalCurrency += claim.currency;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
StringBuilder msg = new StringBuilder();
|
|
|
|
|
msg.append(EnumChatFormatting.GOLD).append("[Auction] ");
|
|
|
|
|
msg.append(EnumChatFormatting.YELLOW).append("You have ");
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
if (itemClaims > 0 && currencyClaims > 0) {
|
|
|
|
|
msg.append(itemClaims).append(" item(s) and ");
|
|
|
|
|
msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName);
|
|
|
|
|
} else if (itemClaims > 0) {
|
|
|
|
|
msg.append(itemClaims).append(" item(s)");
|
|
|
|
|
} else {
|
|
|
|
|
msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName);
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
msg.append(" to claim!");
|
|
|
|
|
|
|
|
|
|
player.addChatMessage(new ChatComponentText(msg.toString()));
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void onPlayerLogout(UUID playerUUID) {
|
|
|
|
|
// Clear cached permission
|
|
|
|
|
clearPlayerMaxTradesCache(playerUUID);
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Logging
|
|
|
|
|
// =========================================
|
|
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
private void logAuction(EnumAuctionLogAction action, String playerName, String itemName, long amount, String details) {
|
|
|
|
|
if (!action.shouldLog()) return;
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s",
|
|
|
|
|
action.name(), playerName, itemName, amount, details);
|
|
|
|
|
LogWriter.info(logMessage);
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// Save / Load - Async on CNPC+ Thread
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
/** Mark data as dirty - will be saved on next save cycle */
|
|
|
|
|
private void markDirty() {
|
|
|
|
|
dirty.set(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Save synchronously - used for server shutdown */
|
|
|
|
|
public void save() {
|
|
|
|
|
if (!dirty.compareAndSet(true, false)) {
|
|
|
|
|
return; // Nothing to save
|
|
|
|
|
}
|
|
|
|
|
saveInternal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Save asynchronously on CNPC+ thread */
|
|
|
|
|
public void saveAsync() {
|
|
|
|
|
if (!dirty.compareAndSet(true, false)) {
|
|
|
|
|
return; // Nothing to save
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!saving.compareAndSet(false, true)) {
|
|
|
|
|
// Another save is in progress, mark dirty again
|
|
|
|
|
dirty.set(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Take a snapshot of the data for async save
|
2026-01-24 15:11:50 -05:00
|
|
|
final NBTTagCompound compound = writeToNBT(new NBTTagCompound());
|
2026-01-23 21:53:12 -05:00
|
|
|
|
|
|
|
|
CustomNPCsThreader.customNPCThread.execute(() -> {
|
|
|
|
|
try {
|
|
|
|
|
saveToFile(compound);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error saving auction data asynchronously", e);
|
|
|
|
|
// Mark dirty again so we retry
|
|
|
|
|
dirty.set(true);
|
|
|
|
|
} finally {
|
|
|
|
|
saving.set(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Internal synchronous save */
|
|
|
|
|
private void saveInternal() {
|
|
|
|
|
try {
|
|
|
|
|
NBTTagCompound compound = writeToNBT(new NBTTagCompound());
|
|
|
|
|
saveToFile(compound);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error saving auction data", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void saveToFile(NBTTagCompound compound) {
|
|
|
|
|
try {
|
|
|
|
|
File saveDir = CustomNpcs.getWorldSaveDirectory();
|
|
|
|
|
if (saveDir == null) return;
|
|
|
|
|
|
|
|
|
|
File fileNew = new File(saveDir, "auction.dat_new");
|
|
|
|
|
File fileOld = new File(saveDir, "auction.dat_old");
|
|
|
|
|
File fileCurrent = new File(saveDir, "auction.dat");
|
|
|
|
|
|
2026-02-18 04:55:03 -05:00
|
|
|
try (FileOutputStream fos = new FileOutputStream(fileNew)) {
|
|
|
|
|
CompressedStreamTools.writeCompressed(compound, fos);
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
|
|
|
|
// Backup rotation
|
|
|
|
|
if (fileOld.exists()) fileOld.delete();
|
|
|
|
|
if (fileCurrent.exists()) fileCurrent.renameTo(fileOld);
|
|
|
|
|
if (fileCurrent.exists()) fileCurrent.delete();
|
|
|
|
|
fileNew.renameTo(fileCurrent);
|
|
|
|
|
if (fileNew.exists()) fileNew.delete();
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error saving auction data to file", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void load() {
|
|
|
|
|
listings.clear();
|
|
|
|
|
playerListingIds.clear();
|
|
|
|
|
playerBidIds.clear();
|
|
|
|
|
|
|
|
|
|
File saveDir = CustomNpcs.getWorldSaveDirectory();
|
|
|
|
|
if (saveDir == null) return;
|
|
|
|
|
filePath = saveDir.getAbsolutePath();
|
|
|
|
|
|
|
|
|
|
// Try current file
|
|
|
|
|
try {
|
|
|
|
|
File file = new File(saveDir, "auction.dat");
|
|
|
|
|
if (file.exists()) {
|
|
|
|
|
loadFromFile(file);
|
|
|
|
|
rebuildIndices();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error loading auction.dat, trying backup", e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try backup file
|
|
|
|
|
try {
|
|
|
|
|
File file = new File(saveDir, "auction.dat_old");
|
|
|
|
|
if (file.exists()) {
|
|
|
|
|
loadFromFile(file);
|
|
|
|
|
rebuildIndices();
|
|
|
|
|
LogWriter.info("Loaded auction data from backup file");
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Error loading auction.dat_old", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Rebuild all indices from loaded data */
|
|
|
|
|
private void rebuildIndices() {
|
|
|
|
|
playerListingIds.clear();
|
|
|
|
|
playerBidIds.clear();
|
|
|
|
|
|
|
|
|
|
// Index listings
|
|
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
if (listing.status == EnumAuctionStatus.ACTIVE) {
|
|
|
|
|
addToPlayerListings(listing.sellerUUID, listing.id);
|
|
|
|
|
if (listing.highBidderUUID != null) {
|
|
|
|
|
addToPlayerBids(listing.highBidderUUID, listing.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Claims are now stored in PlayerData, not indexed here
|
2026-01-23 21:53:12 -05:00
|
|
|
LogWriter.info("Rebuilt auction indices: " + playerListingIds.size() + " sellers, " +
|
2026-01-24 15:11:50 -05:00
|
|
|
playerBidIds.size() + " bidders");
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void loadFromFile(File file) throws Exception {
|
2026-02-18 04:55:03 -05:00
|
|
|
NBTTagCompound compound;
|
|
|
|
|
try (FileInputStream fis = new FileInputStream(file)) {
|
|
|
|
|
compound = CompressedStreamTools.readCompressed(fis);
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
readFromNBT(compound);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
2026-01-24 15:11:50 -05:00
|
|
|
// Listings only - claims are stored in PlayerData
|
2026-01-23 21:53:12 -05:00
|
|
|
NBTTagList listingsList = new NBTTagList();
|
|
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
listingsList.appendTag(listing.writeToNBT(new NBTTagCompound()));
|
|
|
|
|
}
|
|
|
|
|
compound.setTag("Listings", listingsList);
|
|
|
|
|
|
|
|
|
|
return compound;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void readFromNBT(NBTTagCompound compound) {
|
|
|
|
|
NBTTagList listingsList = compound.getTagList("Listings", 10);
|
|
|
|
|
for (int i = 0; i < listingsList.tagCount(); i++) {
|
|
|
|
|
AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i));
|
|
|
|
|
listings.put(listing.id, listing);
|
|
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================
|
|
|
|
|
// IAuctionHandler Implementation
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean isEnabled() {
|
|
|
|
|
return ConfigMarket.AuctionEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public IAuctionListing[] getActiveListings() {
|
|
|
|
|
List<AuctionListing> active = new ArrayList<>();
|
|
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) {
|
|
|
|
|
active.add(listing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return active.toArray(new IAuctionListing[0]);
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
@Override
|
|
|
|
|
public IAuctionListing[] getListingsBySeller(String sellerUUID) {
|
|
|
|
|
if (sellerUUID == null) return new IAuctionListing[0];
|
|
|
|
|
try {
|
|
|
|
|
UUID uuid = UUID.fromString(sellerUUID);
|
|
|
|
|
List<AuctionListing> result = getPlayerActiveListings(uuid);
|
|
|
|
|
return result.toArray(new IAuctionListing[0]);
|
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
|
return new IAuctionListing[0];
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
2026-01-24 15:11:50 -05:00
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
@Override
|
|
|
|
|
public IAuctionListing[] getListingsByBidder(String bidderUUID) {
|
|
|
|
|
if (bidderUUID == null) return new IAuctionListing[0];
|
|
|
|
|
try {
|
|
|
|
|
UUID uuid = UUID.fromString(bidderUUID);
|
|
|
|
|
List<AuctionListing> result = getPlayerActiveBids(uuid);
|
|
|
|
|
return result.toArray(new IAuctionListing[0]);
|
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
|
return new IAuctionListing[0];
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
@Override
|
|
|
|
|
public int getActiveListingCount() {
|
|
|
|
|
int count = 0;
|
|
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) {
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public IAuctionListing createListing(IPlayer<?> player, IItemStack item, long startingPrice, long buyoutPrice) {
|
|
|
|
|
if (player == null || item == null) return null;
|
|
|
|
|
EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity();
|
|
|
|
|
ItemStack mcItem = item.getMCItemStack();
|
|
|
|
|
String result = createListing(entityPlayer, mcItem, startingPrice, buyoutPrice);
|
|
|
|
|
if (result != null) {
|
|
|
|
|
return null; // Failed
|
|
|
|
|
}
|
|
|
|
|
// Find the just-created listing
|
|
|
|
|
Set<String> playerListings = playerListingIds.get(entityPlayer.getUniqueID());
|
|
|
|
|
if (playerListings != null && !playerListings.isEmpty()) {
|
|
|
|
|
// Get the most recent one
|
|
|
|
|
long latestTime = 0;
|
|
|
|
|
AuctionListing latestListing = null;
|
|
|
|
|
for (String id : playerListings) {
|
|
|
|
|
AuctionListing listing = listings.get(id);
|
|
|
|
|
if (listing != null && listing.createdTime > latestTime) {
|
|
|
|
|
latestTime = listing.createdTime;
|
|
|
|
|
latestListing = listing;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return latestListing;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String placeBid(String listingId, IPlayer<?> player, long amount) {
|
|
|
|
|
if (player == null) return "Player is null";
|
|
|
|
|
EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity();
|
|
|
|
|
return placeBid(listingId, entityPlayer, amount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String buyout(String listingId, IPlayer<?> player) {
|
|
|
|
|
if (player == null) return "Player is null";
|
|
|
|
|
EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity();
|
|
|
|
|
return buyout(listingId, entityPlayer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String cancelListing(String listingId, IPlayer<?> player, boolean isAdmin) {
|
|
|
|
|
if (player == null) return "Player is null";
|
|
|
|
|
EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity();
|
|
|
|
|
return cancelListing(listingId, entityPlayer, isAdmin);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public long getListingFee() {
|
|
|
|
|
return ConfigMarket.ListingFee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public double getSalesTaxPercent() {
|
|
|
|
|
return ConfigMarket.SalesTaxPercent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public double getMinBidIncrementPercent() {
|
|
|
|
|
return ConfigMarket.MinBidIncrementPercent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getAuctionDurationHours() {
|
|
|
|
|
return ConfigMarket.AuctionDurationHours;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getSnipeProtectionMinutes() {
|
|
|
|
|
return ConfigMarket.SnipeProtectionMinutes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String getCurrencyName() {
|
|
|
|
|
return ConfigMarket.CurrencyName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public long getMinimumListingPrice() {
|
|
|
|
|
return ConfigMarket.MinimumListingPrice;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public IAuctionListing[] searchListings(String searchText) {
|
|
|
|
|
if (searchText == null || searchText.trim().isEmpty()) {
|
|
|
|
|
return getActiveListings();
|
|
|
|
|
}
|
|
|
|
|
String search = searchText.toLowerCase().trim();
|
|
|
|
|
List<AuctionListing> result = new ArrayList<>();
|
|
|
|
|
for (AuctionListing listing : listings.values()) {
|
|
|
|
|
if (listing.status != EnumAuctionStatus.ACTIVE || listing.isExpired()) continue;
|
|
|
|
|
String itemName = listing.item != null ? listing.item.getDisplayName().toLowerCase() : "";
|
|
|
|
|
String sellerName = listing.sellerName != null ? listing.sellerName.toLowerCase() : "";
|
|
|
|
|
if (itemName.contains(search) || sellerName.contains(search)) {
|
|
|
|
|
result.add(listing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result.toArray(new IAuctionListing[0]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 21:53:12 -05:00
|
|
|
// =========================================
|
|
|
|
|
// Admin Methods
|
|
|
|
|
// =========================================
|
|
|
|
|
|
|
|
|
|
public List<AuctionListing> getAllListings() {
|
|
|
|
|
return new ArrayList<>(listings.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void adminCancelListing(String listingId, EntityPlayer admin, String reason) {
|
2026-02-18 04:55:03 -05:00
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
String itemName = listing != null && listing.item != null ? listing.item.getDisplayName() : "Unknown";
|
2026-01-23 21:53:12 -05:00
|
|
|
cancelListing(listingId, admin, true);
|
2026-02-18 04:55:03 -05:00
|
|
|
if (reason != null && !reason.isEmpty()) {
|
|
|
|
|
logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, 0, "Admin cancel - Reason: " + reason);
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
public void clearEndedListings() {
|
|
|
|
|
// Remove ended/cancelled listings (claims are in PlayerData now)
|
|
|
|
|
listings.entrySet().removeIf(entry ->
|
|
|
|
|
entry.getValue().status == EnumAuctionStatus.CLAIMED ||
|
|
|
|
|
entry.getValue().status == EnumAuctionStatus.ENDED ||
|
|
|
|
|
entry.getValue().status == EnumAuctionStatus.CANCELLED);
|
2026-01-23 21:53:12 -05:00
|
|
|
|
2026-01-24 15:11:50 -05:00
|
|
|
// Rebuild indices
|
|
|
|
|
rebuildIndices();
|
|
|
|
|
markDirty();
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a player is the seller of a listing.
|
|
|
|
|
* Used for GUI visibility checks.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isPlayerSeller(String listingId, UUID playerUUID) {
|
|
|
|
|
AuctionListing listing = listings.get(listingId);
|
|
|
|
|
return listing != null && listing.isSeller(playerUUID);
|
|
|
|
|
}
|
2026-01-24 16:03:54 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reload the item blacklist from config.
|
|
|
|
|
*/
|
|
|
|
|
public void reloadBlacklist() {
|
|
|
|
|
AuctionBlacklist.reload();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if an item is blacklisted.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isItemBlacklisted(ItemStack item) {
|
|
|
|
|
return AuctionBlacklist.isBlacklisted(item);
|
|
|
|
|
}
|
2026-01-23 21:53:12 -05:00
|
|
|
}
|