From 4510b3bbd1086e3f618d948bc9ca480aada3473c Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 18 Feb 2026 04:55:03 -0500 Subject: [PATCH] Large Auction Cleanup + Auction Events - Auction Enums - Better Notification Handling - Auction Cancelation Events - Lang Key Cleanup --- src/api | 2 +- .../kamkeel/npcs/command/AuctionCommand.java | 10 +- .../packets/player/AuctionActionPacket.java | 13 +- src/main/java/noppes/npcs/EventHooks.java | 28 + .../npcs/client/AuctionClientConfig.java | 6 +- .../gui/player/AuctionTooltipHandler.java | 69 +- .../client/gui/player/GuiAuctionBidding.java | 19 +- .../gui/player/GuiAuctionInterface.java | 38 +- .../client/gui/player/GuiAuctionListing.java | 5 +- .../client/gui/player/GuiAuctionSell.java | 8 +- .../client/gui/player/SubGuiAuctionBid.java | 20 +- .../gui/player/SubGuiAuctionBuyNow.java | 18 +- .../java/noppes/npcs/config/ConfigMarket.java | 33 +- .../npcs/constants/EnumAuctionLogAction.java | 34 + .../npcs/constants/EnumAuctionPage.java | 30 + .../noppes/npcs/constants/EnumScriptType.java | 9 +- .../npcs/constants/EnumTradeSlotType.java | 35 + .../noppes/npcs/constants/ScriptContext.java | 7 +- .../containers/ContainerAuctionBidding.java | 10 +- .../npcs/containers/ContainerAuctionSell.java | 18 +- .../containers/ContainerAuctionTrades.java | 38 +- .../npcs/controllers/AuctionConfigSync.java | 2 +- .../npcs/controllers/AuctionController.java | 611 ++++++++++-------- .../controllers/ScriptHookController.java | 21 + .../controllers/data/AuctionBlacklist.java | 79 ++- .../npcs/controllers/data/AuctionClaim.java | 2 +- .../npcs/controllers/data/AuctionFilter.java | 11 +- .../controllers/data/AuctionNotification.java | 64 -- .../controllers/data/PlayerTradeData.java | 28 +- .../scripted/event/player/AuctionEvent.java | 131 ++++ .../noppes/npcs/util/AuctionFormatUtil.java | 68 ++ 31 files changed, 889 insertions(+), 578 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionPage.java create mode 100644 src/main/java/noppes/npcs/constants/EnumTradeSlotType.java delete mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionNotification.java create mode 100644 src/main/java/noppes/npcs/scripted/event/player/AuctionEvent.java create mode 100644 src/main/java/noppes/npcs/util/AuctionFormatUtil.java diff --git a/src/api b/src/api index 28c0ca840..ae4b84f3b 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 28c0ca840f3d726c07b47aafa29d97b51a69a0cd +Subproject commit ae4b84f3baf19c2998449e4b40ac33bc6a92555f diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java index 87d7e4385..a56dce671 100644 --- a/src/main/java/kamkeel/npcs/command/AuctionCommand.java +++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java @@ -20,6 +20,7 @@ import static kamkeel.npcs.util.ColorUtil.sendMessage; import static kamkeel.npcs.util.ColorUtil.sendResult; public class AuctionCommand extends CommandKamkeelBase { + private static final int COMMAND_PAGE_SIZE = 10; public AuctionCommand() { super(); @@ -62,9 +63,10 @@ public class AuctionCommand extends CommandKamkeelBase { } } - List listings = controller.getActiveListings(null, page - 1, 10); + page = Math.max(1, page); + List listings = controller.getActiveListings(null, page - 1, COMMAND_PAGE_SIZE); int total = controller.getTotalActiveListings(null); - int totalPages = Math.max(1, (int) Math.ceil((double) total / 10)); + int totalPages = Math.max(1, (int) Math.ceil((double) total / COMMAND_PAGE_SIZE)); if (listings.isEmpty()) { sendMessage(sender, "\u00A77No active auctions found"); @@ -98,6 +100,10 @@ public class AuctionCommand extends CommandKamkeelBase { return; } + if (args.length < 1) { + throw new CommandException("Usage: /auction view "); + } + String playername = args[0]; List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index 3ce74eb4c..b15ff8992 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -17,6 +17,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import noppes.npcs.NoppesUtilServer; import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionPage; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.AuctionController; @@ -315,13 +316,8 @@ public class AuctionActionPacket extends AbstractPacket { private void handleOpenPage(ByteBuf in, EntityPlayerMP player) { int page = in.readInt(); - EnumGuiType guiType; - switch (page) { - case 0: guiType = EnumGuiType.PlayerAuction; break; - case 1: guiType = EnumGuiType.PlayerAuctionSell; break; - case 2: guiType = EnumGuiType.PlayerAuctionTrades; break; - default: guiType = EnumGuiType.PlayerAuction; break; - } + EnumAuctionPage auctionPage = EnumAuctionPage.fromOrdinal(page); + EnumGuiType guiType = auctionPage.getGuiType(); NoppesUtilServer.sendOpenGui(player, guiType, npc); // Send trades data when opening the trades page @@ -481,6 +477,9 @@ public class AuctionActionPacket extends AbstractPacket { } else { // Need only part of stack stack.splitStack(needed); + if (stack.stackSize <= 0) { + player.inventory.setInventorySlotContents(i, null); + } needed = 0; } } diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 2fefb23ac..2d2496fc6 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -67,6 +67,7 @@ import noppes.npcs.scripted.event.RecipeScriptEvent; import noppes.npcs.scripted.event.player.CustomGuiEvent; import noppes.npcs.scripted.event.player.DialogEvent; import noppes.npcs.scripted.event.player.FactionEvent; +import noppes.npcs.scripted.event.player.AuctionEvent; import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ChatEvent; @@ -1253,4 +1254,31 @@ public class EventHooks { } postAnimationEvent(new AnimationEvent.FrameEvent.Exited(animation, frame)); } + + // ==================== AUCTION EVENTS ==================== + + public static boolean onAuctionCreate(PlayerDataScript handler, AuctionEvent.CreateEvent event) { + handler.callScript(EnumScriptType.AUCTION_CREATE, event); + return NpcAPI.EVENT_BUS.post(event); + } + + public static boolean onAuctionBid(PlayerDataScript handler, AuctionEvent.BidEvent event) { + handler.callScript(EnumScriptType.AUCTION_BID, event); + return NpcAPI.EVENT_BUS.post(event); + } + + public static boolean onAuctionBuyout(PlayerDataScript handler, AuctionEvent.BuyoutEvent event) { + handler.callScript(EnumScriptType.AUCTION_BUYOUT, event); + return NpcAPI.EVENT_BUS.post(event); + } + + public static boolean onAuctionCancel(PlayerDataScript handler, AuctionEvent.CancelEvent event) { + handler.callScript(EnumScriptType.AUCTION_CANCEL, event); + return NpcAPI.EVENT_BUS.post(event); + } + + public static boolean onAuctionClaim(PlayerDataScript handler, AuctionEvent.ClaimEvent event) { + handler.callScript(EnumScriptType.AUCTION_CLAIM, event); + return NpcAPI.EVENT_BUS.post(event); + } } diff --git a/src/main/java/noppes/npcs/client/AuctionClientConfig.java b/src/main/java/noppes/npcs/client/AuctionClientConfig.java index 7d6854e4c..f993318ed 100644 --- a/src/main/java/noppes/npcs/client/AuctionClientConfig.java +++ b/src/main/java/noppes/npcs/client/AuctionClientConfig.java @@ -14,10 +14,10 @@ import net.minecraft.nbt.NBTTagCompound; @SideOnly(Side.CLIENT) public class AuctionClientConfig { // Cached values from server - private static long listingFee = 0; + private static volatile long listingFee = 0; private static String currencyName = "Coins"; private static int auctionDurationHours = 24; - private static double minBidIncrement = 0.05; + private static volatile double minBidIncrement = 0.05; private static int maxActiveListings = 8; private static int claimExpirationDays = 20; private static boolean auctionEnabled = false; @@ -38,7 +38,7 @@ public class AuctionClientConfig { claimExpirationDays = compound.getInteger("ClaimExpirationDays"); if (currencyName.isEmpty()) { - currencyName = "Gold"; + currencyName = "Coins"; } } diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index 11ffa198d..a9473f2e9 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -12,6 +12,7 @@ import net.minecraft.util.StatCollector; import net.minecraftforge.event.entity.player.ItemTooltipEvent; import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.util.AuctionFormatUtil; import noppes.npcs.containers.ContainerAuctionListing; import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerAuctionTrades; @@ -88,25 +89,25 @@ public class AuctionTooltipHandler { // Current bid or starting price if (listing.hasBids()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid))); tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); } else { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.startingPrice") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.startingPrice))); + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.startingPrice))); tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); } // Buyout price if (listing.hasBuyout()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout") - .replace("%s", EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice))); + .replace("%s", EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(listing.buyoutPrice))); } // Time remaining long timeRemaining = listing.getTimeRemaining(); String timeText = StatCollector.translateToLocal("auction.timeLeft") - .replace("%s", formatTimeRemaining(timeRemaining)); - tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + .replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining)); + tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); } // ========== Trades Page ========== @@ -144,7 +145,7 @@ public class AuctionTooltipHandler { if (listing.hasBids()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid))); tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); } else { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); @@ -152,8 +153,8 @@ public class AuctionTooltipHandler { long timeRemaining = listing.getTimeRemaining(); String timeText = StatCollector.translateToLocal("auction.timeLeft") - .replace("%s", formatTimeRemaining(timeRemaining)); - tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + .replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining)); + tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); tooltip.add(""); tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel")); @@ -164,13 +165,13 @@ public class AuctionTooltipHandler { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding")); tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid))); tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); long timeRemaining = listing.getTimeRemaining(); String timeText = StatCollector.translateToLocal("auction.timeLeft") - .replace("%s", formatTimeRemaining(timeRemaining)); - tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + .replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining)); + tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); tooltip.add(""); tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid")); @@ -210,7 +211,7 @@ public class AuctionTooltipHandler { // Amount for currency claims if (claim.type.isCurrency()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency))); + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency))); } // Expiration @@ -254,48 +255,4 @@ public class AuctionTooltipHandler { } } - // ========== Formatting Utilities ========== - - private static String formatCurrency(long amount) { - if (amount < 1000) { - return amount + " " + AuctionClientConfig.getCurrencyName(); - } - StringBuilder sb = new StringBuilder(); - String str = "" + amount; - int count = 0; - for (int i = str.length() - 1; i >= 0; i--) { - if (count > 0 && count % 3 == 0) { - sb.insert(0, ','); - } - sb.insert(0, str.charAt(i)); - count++; - } - return sb.toString() + " " + AuctionClientConfig.getCurrencyName(); - } - - private static String formatTimeRemaining(long milliseconds) { - if (milliseconds <= 0) { - return StatCollector.translateToLocal("auction.ended"); - } - - long seconds = (milliseconds / 1000) % 60; - long minutes = (milliseconds / 60000) % 60; - long hours = (milliseconds / 3600000) % 24; - long days = milliseconds / 86400000; - - StringBuilder sb = new StringBuilder(); - if (days > 0) { - sb.append(days).append("d "); - } - if (hours > 0 || days > 0) { - sb.append(hours).append("h "); - } - if (minutes > 0 || hours > 0 || days > 0) { - sb.append(minutes).append("m"); - } else { - sb.append(seconds).append("s"); - } - - return sb.toString(); - } } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java index afe47fb44..f74a354df 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -15,6 +15,7 @@ import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.containers.ContainerAuctionBidding; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.AuctionFormatUtil; import org.lwjgl.opengl.GL11; import java.util.ArrayList; @@ -36,7 +37,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis private int btnBidY = 110; private int btnBuyoutX = 82; private int btnBuyoutY = 110; - private int infoX = 110; + private int infoX = 105; private int infoY = 50; // Button IDs @@ -186,7 +187,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis /** Draw auction listing information on the right side */ private void drawAuctionInfo() { - int x = guiLeft + 105; + int x = guiLeft + infoX; int y = guiTop + infoY; int xRight = guiLeft + 215; @@ -206,7 +207,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis String bidLabel = StatCollector.translateToLocal("auction.info.currentBid") + ":"; fontRendererObj.drawString(EnumChatFormatting.YELLOW + bidLabel, x, y, 0xFFFFFF); - label = formatCurrency(listing.currentBid); + label = AuctionFormatUtil.formatCurrency(listing.currentBid); length = fontRendererObj.getStringWidth(label); fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; @@ -223,7 +224,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis String startLabel = StatCollector.translateToLocal("auction.info.startingPrice"); fontRendererObj.drawString(EnumChatFormatting.YELLOW + startLabel, x, y, 0xFFFFFF); - label = formatCurrency(listing.startingPrice); + label = AuctionFormatUtil.formatCurrency(listing.startingPrice); length = fontRendererObj.getStringWidth(label); fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; @@ -234,7 +235,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis String buyoutLabel = StatCollector.translateToLocal("auction.info.buyoutPrice"); fontRendererObj.drawString(EnumChatFormatting.GREEN + buyoutLabel, x, y, 0xFFFFFF); - label = formatCurrency(listing.buyoutPrice); + label = AuctionFormatUtil.formatCurrency(listing.buyoutPrice); length = fontRendererObj.getStringWidth(label); fontRendererObj.drawString(EnumChatFormatting.GREEN + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; @@ -244,9 +245,9 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis String timeLabel = StatCollector.translateToLocal("auction.info.timeLeft"); fontRendererObj.drawString(EnumChatFormatting.GRAY + timeLabel, x, y, 0xFFFFFF); long timeRemaining = listing.getTimeRemaining(); - EnumChatFormatting timeColor = timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; + EnumChatFormatting timeColor = AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; - label = timeColor + formatTimeRemaining(timeRemaining); + label = timeColor + AuctionFormatUtil.formatTimeRemaining(timeRemaining); length = fontRendererObj.getStringWidth(label); fontRendererObj.drawString(label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; @@ -278,14 +279,14 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis if (listing != null) { long minBid = listing.getMinimumBid(AuctionClientConfig.getMinBidIncrement()); tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.bid.minimum") - .replace("%s", EnumChatFormatting.WHITE + formatCurrency(minBid) + " " + AuctionClientConfig.getCurrencyName())); + .replace("%s", EnumChatFormatting.WHITE + AuctionFormatUtil.formatCurrency(minBid) + " " + AuctionClientConfig.getCurrencyName())); } } else if (btnBuyout != null && btnBuyout.visible && btnBuyout.isHovered()) { tooltip = new ArrayList<>(); tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.bid.buyout")); if (listing != null && listing.hasBuyout()) { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.buyoutPrice") + ":"); - tooltip.add(EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice) + " " + AuctionClientConfig.getCurrencyName()); + tooltip.add(EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrency(listing.buyoutPrice) + " " + AuctionClientConfig.getCurrencyName()); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java index c34223550..e6e87383a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java @@ -34,6 +34,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { public static final int PAGE_SELL = 1; public static final int PAGE_CLAIMS = 2; + // Overlay color + protected static final int OVERLAY_DARK = 0xC0000000; + // Navigation button IDs protected static final int BTN_NAV_LISTINGS = 100; protected static final int BTN_NAV_SELL = 101; @@ -172,7 +175,7 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { /** Draw dark overlay for unavailable slots */ protected void drawDarkenedOverlay(int x, int y) { - drawColoredOverlay(x, y, 0xC0000000); + drawColoredOverlay(x, y, OVERLAY_DARK); } /** Draw icon texture on slot (16x16) */ @@ -192,39 +195,6 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { /** Subclasses implement this to draw specific content */ protected abstract void drawAuctionContent(float partialTicks, int mouseX, int mouseY); - // ========== Formatting Utilities ========== - - /** Format currency with commas (e.g., 1000 -> "1,000") */ - protected String formatCurrency(long amount) { - if (amount < 1000) return "" + amount; - StringBuilder sb = new StringBuilder(); - String str = "" + amount; - int count = 0; - for (int i = str.length() - 1; i >= 0; i--) { - if (count > 0 && count % 3 == 0) sb.insert(0, ','); - sb.insert(0, str.charAt(i)); - count++; - } - return sb.toString(); - } - - /** Format time remaining (e.g., "2d 5h 30m") */ - protected String formatTimeRemaining(long ms) { - if (ms <= 0) return "Ended"; - - long seconds = (ms / 1000) % 60; - long minutes = (ms / 60000) % 60; - long hours = (ms / 3600000) % 24; - long days = ms / 86400000; - - StringBuilder sb = new StringBuilder(); - if (days > 0) sb.append(days).append("d "); - if (hours > 0 || days > 0) sb.append(hours).append("h "); - if (minutes > 0 || hours > 0 || days > 0) sb.append(minutes).append("m"); - else sb.append(seconds).append("s"); - return sb.toString(); - } - @Override public void save() { // Override in subclasses if needed diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java index 381747c6a..c08569598 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java @@ -58,8 +58,6 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis private AuctionFilter filter; private int currentPage = 0; private int totalPages = 1; - private int totalListings = 0; - private boolean dataLoaded = false; // Buttons defined on this page private GuiAuctionNavButton btnFilter; @@ -204,7 +202,6 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis if (compound.hasKey("ListingsData")) { currentPage = compound.getInteger("Page"); totalPages = compound.getInteger("TotalPages"); - totalListings = compound.getInteger("TotalListings"); // Load listings listingContainer.displayInventory.clear(); @@ -213,7 +210,7 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis AuctionListing listing = AuctionListing.fromNBT(list.getCompoundTagAt(i)); listingContainer.displayInventory.setListing(i, listing); } - dataLoaded = true; + updatePaginationVisibility(); } else if (compound.hasKey("ListingsRefresh")) { requestListings(); } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index a8784b3b0..5b514cff6 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -15,7 +15,9 @@ import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.containers.ContainerAuctionSell; +import noppes.npcs.util.AuctionFormatUtil; import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.List; @@ -239,7 +241,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Draw listing fee info (uses cached config from server) String feeText = StatCollector.translateToLocal("auction.sell.fee") - .replace("%s", formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName()); + .replace("%s", AuctionFormatUtil.formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName()); fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 77, 0xFFFFFF); // Draw error message if any @@ -326,7 +328,11 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis } if (tooltip != null && !tooltip.isEmpty()) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); } } } diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index a1793c545..ae24a9ae9 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -10,6 +10,7 @@ import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.util.AuctionFormatUtil; /** * Sub GUI for placing a bid on an auction. @@ -49,13 +50,13 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe // Minimum bid info String minText = StatCollector.translateToLocal("auction.bid.minimum") - .replace("%s", EnumChatFormatting.DARK_RED + formatCurrency(minimumBid) + " " + currencyName); + .replace("%s", EnumChatFormatting.DARK_RED + AuctionFormatUtil.formatCurrency(minimumBid) + " " + currencyName); addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y)); y += 14; // Your balance String balanceText = StatCollector.translateToLocal("auction.bid.yourBalance") - .replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + .replace("%s", EnumChatFormatting.DARK_GREEN + AuctionFormatUtil.formatCurrency(playerBalance) + " " + currencyName); addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y)); y += 18; @@ -117,7 +118,7 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe // Validate if (bidAmount < minimumBid) { errorMessage = StatCollector.translateToLocal("auction.error.bidTooLow") - .replace("%s", formatCurrency(minimumBid)); + .replace("%s", AuctionFormatUtil.formatCurrency(minimumBid)); updateErrorLabel(); return; } @@ -144,17 +145,4 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe public boolean wasSuccessful() { return successful; } - - private String formatCurrency(long amount) { - if (amount < 1000) return "" + amount; - StringBuilder sb = new StringBuilder(); - String str = "" + amount; - int count = 0; - for (int i = str.length() - 1; i >= 0; i--) { - if (count > 0 && count % 3 == 0) sb.insert(0, ','); - sb.insert(0, str.charAt(i)); - count++; - } - return sb.toString(); - } } diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java index b63471047..4884c7ae4 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -8,6 +8,7 @@ import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.util.AuctionFormatUtil; /** * Sub GUI for confirming a buyout purchase. @@ -54,14 +55,14 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { y += 16; // Buyout price - String priceText = EnumChatFormatting.DARK_RED + formatCurrency(buyoutPrice) + " " + currencyName; + String priceText = EnumChatFormatting.DARK_RED + AuctionFormatUtil.formatCurrency(buyoutPrice) + " " + currencyName; int priceWidth = fontRendererObj.getStringWidth(priceText); addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y)); y += 20; // Your balance String balanceLabel = StatCollector.translateToLocal("auction.bid.yourBalance"); - String balanceText = balanceLabel.replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + String balanceText = balanceLabel.replace("%s", EnumChatFormatting.DARK_GREEN + AuctionFormatUtil.formatCurrency(playerBalance) + " " + currencyName); addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y)); y += 14; @@ -105,17 +106,4 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { public boolean wasSuccessful() { return successful; } - - private String formatCurrency(long amount) { - if (amount < 1000) return "" + amount; - StringBuilder sb = new StringBuilder(); - String str = "" + amount; - int count = 0; - for (int i = str.length() - 1; i >= 0; i--) { - if (count > 0 && count % 3 == 0) sb.insert(0, ','); - sb.insert(0, str.charAt(i)); - count++; - } - return sb.toString(); - } } diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index e14861033..2909ec2a6 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -79,11 +79,12 @@ public class ConfigMarket { CurrencyName = config.get(CURRENCY, "Currency Name", "Coins", "Display name for the currency (e.g. 'Coins', 'Gold', 'Credits')").getString(); - StartingBalance = config.get(CURRENCY, "Starting Balance", 0, - "Starting currency balance for new players").getInt(0); + StartingBalance = parseLongConfig(config.get(CURRENCY, "Starting Balance", "0", + "Starting currency balance for new players").getString(), 0); - MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE, - "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE); + MaxBalance = parseLongConfig(config.get(CURRENCY, "Max Balance", String.valueOf(Long.MAX_VALUE), + "Maximum currency balance a player can have").getString(), Long.MAX_VALUE); + if (MaxBalance < 0) MaxBalance = Long.MAX_VALUE; // ========================================= // Auction Settings @@ -95,16 +96,19 @@ public class ConfigMarket { AuctionDurationHours = config.get(AUCTION, "Auction Duration Hours", 24, "Default duration for auctions in hours").getInt(24); + if (AuctionDurationHours < 1) AuctionDurationHours = 1; - ListingFee = config.get(AUCTION, "Listing Fee", 10, - "Flat fee charged when creating a listing").getInt(10); + ListingFee = parseLongConfig(config.get(AUCTION, "Listing Fee", "10", + "Flat fee charged when creating a listing").getString(), 10); + if (ListingFee < 0) ListingFee = 0; - MinimumListingPrice = config.get(AUCTION, "Minimum Listing Price", 1, - "Minimum starting price for auction listings").getInt(1); + MinimumListingPrice = parseLongConfig(config.get(AUCTION, "Minimum Listing Price", "1", + "Minimum starting price for auction listings").getString(), 1); if (MinimumListingPrice < 1) MinimumListingPrice = 1; SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, "Percentage of sale price taken as tax (0.05 = 5%). Tax is deleted as a currency sink.").getDouble(0.05); + SalesTaxPercent = Math.max(0.0, Math.min(1.0, SalesTaxPercent)); int maxTrades = config.get(AUCTION, "Default Max Trades", 8, "Default maximum number of trade slots per player (listings + bids + claims). Min: 1, Max: 45. Players can have more via customnpcs.auction.trades.X permissions.").getInt(8); @@ -112,15 +116,19 @@ public class ConfigMarket { SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 2, "When a bid is placed with less than this many minutes remaining, the auction is extended to this duration").getInt(2); + if (SnipeProtectionMinutes < 0) SnipeProtectionMinutes = 0; ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 20, "Number of days before unclaimed items/currency are deleted").getInt(20); + if (ClaimExpirationDays < 1) ClaimExpirationDays = 1; MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, "Minimum bid increment as a percentage of current bid (0.05 = 5%)").getDouble(0.05); + MinBidIncrementPercent = Math.max(0.0, Math.min(1.0, MinBidIncrementPercent)); CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10, "Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10); + CancellationPenaltyPercent = Math.max(0.0, Math.min(1.0, CancellationPenaltyPercent)); // ========================================= // Auction Blacklist Settings @@ -237,4 +245,13 @@ public class ConfigMarket { } } } + + private static long parseLongConfig(String value, long defaultValue) { + if (value == null || value.trim().isEmpty()) return defaultValue; + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } } diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java b/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java new file mode 100644 index 000000000..7e48589f4 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java @@ -0,0 +1,34 @@ +package noppes.npcs.constants; + +import noppes.npcs.config.ConfigMarket; + +/** + * Enum for auction log action types. + * Used by AuctionController.logAuction() for type-safe logging. + */ +public enum EnumAuctionLogAction { + CREATED, + BID, + BUYOUT, + SOLD, + EXPIRED, + CANCELLED, + CLAIMED, + CLAIM_EXPIRED; + + /** Check if this action should be logged based on config settings */ + public boolean shouldLog() { + if (!ConfigMarket.AuctionLoggingEnabled) return false; + switch (this) { + case CREATED: return ConfigMarket.LogAuctionCreated; + case BID: return ConfigMarket.LogAuctionBid; + case BUYOUT: return ConfigMarket.LogAuctionBuyout; + case SOLD: return ConfigMarket.LogAuctionSold; + case EXPIRED: return ConfigMarket.LogAuctionExpired; + case CANCELLED: return ConfigMarket.LogAuctionCancelled; + case CLAIMED: + case CLAIM_EXPIRED: return ConfigMarket.LogAuctionClaimed; + default: return false; + } + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionPage.java b/src/main/java/noppes/npcs/constants/EnumAuctionPage.java new file mode 100644 index 000000000..c6c2c2a36 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionPage.java @@ -0,0 +1,30 @@ +package noppes.npcs.constants; + +import noppes.npcs.constants.EnumGuiType; + +/** + * Enum for auction GUI page navigation. + * Maps page indices to GUI types for packet-based navigation. + */ +public enum EnumAuctionPage { + LISTINGS(EnumGuiType.PlayerAuction), + SELL(EnumGuiType.PlayerAuctionSell), + TRADES(EnumGuiType.PlayerAuctionTrades); + + private final EnumGuiType guiType; + + EnumAuctionPage(EnumGuiType guiType) { + this.guiType = guiType; + } + + public EnumGuiType getGuiType() { + return guiType; + } + + public static EnumAuctionPage fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return LISTINGS; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index 9e2588103..1088ef83a 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -137,7 +137,14 @@ public enum EnumScriptType { ENERGY_BARRIER_SPAWNED("energyBarrierSpawned"), ENERGY_BARRIER_TICK("energyBarrierTick"), ENERGY_BARRIER_HIT("energyBarrierHit"), - ENERGY_BARRIER_DESTROYED("energyBarrierDestroyed"); + ENERGY_BARRIER_DESTROYED("energyBarrierDestroyed"), + + // Auction Events + AUCTION_CREATE("auctionCreate"), + AUCTION_BID("auctionBid"), + AUCTION_BUYOUT("auctionBuyout"), + AUCTION_CANCEL("auctionCancel"), + AUCTION_CLAIM("auctionClaim"); public String function; diff --git a/src/main/java/noppes/npcs/constants/EnumTradeSlotType.java b/src/main/java/noppes/npcs/constants/EnumTradeSlotType.java new file mode 100644 index 000000000..16850a79e --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumTradeSlotType.java @@ -0,0 +1,35 @@ +package noppes.npcs.constants; + +/** + * Enum for trade slot types in the player's auction trades view. + * Used by ContainerAuctionTrades to identify what each slot contains. + */ +public enum EnumTradeSlotType { + EMPTY, + SELLING, + BIDDING, + CLAIM; + + public boolean isSelling() { + return this == SELLING; + } + + public boolean isBidding() { + return this == BIDDING; + } + + public boolean isClaim() { + return this == CLAIM; + } + + public boolean isEmpty() { + return this == EMPTY; + } + + public static EnumTradeSlotType fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return EMPTY; + } +} diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index c5861927a..9b06001ff 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -2,9 +2,11 @@ package noppes.npcs.constants; import noppes.npcs.api.event.IAbilityEvent; import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IAuctionEvent; import noppes.npcs.api.event.IBlockEvent; import noppes.npcs.api.event.ICustomGuiEvent; import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IEnergyBarrierEvent; import noppes.npcs.api.event.IEnergyProjectileEvent; import noppes.npcs.api.event.IFactionEvent; import noppes.npcs.api.event.IForgeEvent; @@ -57,6 +59,7 @@ public class ScriptContext { INpcEvent.class, IProjectileEvent.class, IEnergyProjectileEvent.class, + IEnergyBarrierEvent.class, IAbilityEvent.class, IAnimationEvent.class ); @@ -70,7 +73,9 @@ public class ScriptContext { IFactionEvent.class, ICustomGuiEvent.class, IEnergyProjectileEvent.class, - IPlayerAbilityEvent.class + IEnergyBarrierEvent.class, + IPlayerAbilityEvent.class, + IAuctionEvent.class ); public static final ScriptContext BLOCK = register("BLOCK", "block", diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java index 2b40000ac..0e46b6b9b 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java @@ -14,9 +14,9 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class ContainerAuctionBidding extends ContainerAuction { // Display slot layout - private int itemSlotX = 71; - private int itemSlotY = 76; - private int itemSlotIndex = PLAYER_INV_SLOT_COUNT; + private static final int ITEM_SLOT_X = 71; + private static final int ITEM_SLOT_Y = 76; + private static final int ITEM_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; private final IInventory displayInventory; private AuctionListing listing; @@ -26,7 +26,7 @@ public class ContainerAuctionBidding extends ContainerAuction { displayInventory = new InventoryBasic("Display", false, 1); // Display slot (read-only) - addSlotToContainer(new Slot(displayInventory, 0, itemSlotX, itemSlotY) { + addSlotToContainer(new Slot(displayInventory, 0, ITEM_SLOT_X, ITEM_SLOT_Y) { @Override public boolean isItemValid(ItemStack stack) { return false; } @Override @@ -58,6 +58,6 @@ public class ContainerAuctionBidding extends ContainerAuction { * Check if this is the display slot. */ public boolean isDisplaySlot(int slotIndex) { - return slotIndex == itemSlotIndex; + return slotIndex == ITEM_SLOT_INDEX; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java index 14b381f40..e5a01dd6f 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java @@ -13,10 +13,10 @@ import noppes.npcs.entity.EntityNPCInterface; * Items are cloned from inventory, not moved. Validation happens on submit. */ public class ContainerAuctionSell extends ContainerAuction { - private int sellSlotX = 59; - private int sellSlotY = 101; - private int sellSlotIndex = PLAYER_INV_SLOT_COUNT; - private int maxStack = 64; + private static final int SELL_SLOT_X = 59; + private static final int SELL_SLOT_Y = 101; + private static final int SELL_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; + private static final int MAX_STACK = 64; private final IInventory sellInventory; @@ -25,7 +25,7 @@ public class ContainerAuctionSell extends ContainerAuction { sellInventory = new InventoryBasic("Sell", false, 1); // Sell slot blocks direct interaction - addSlotToContainer(new Slot(sellInventory, 0, sellSlotX, sellSlotY) { + addSlotToContainer(new Slot(sellInventory, 0, SELL_SLOT_X, SELL_SLOT_Y) { @Override public boolean isItemValid(ItemStack stack) { return false; } @Override @@ -59,11 +59,11 @@ public class ContainerAuctionSell extends ContainerAuction { if (sellStack == null) { // Empty slot - create new stack ItemStack newStack = source.copy(); - newStack.stackSize = Math.min(toAdd, maxStack); + newStack.stackSize = Math.min(toAdd, MAX_STACK); sellInventory.setInventorySlotContents(0, newStack); } else if (itemsMatch(sellStack, source)) { // Same item - add to existing (capped at 64) - int space = maxStack - sellStack.stackSize; + int space = MAX_STACK - sellStack.stackSize; int add = Math.min(toAdd, space); if (add > 0) { sellStack.stackSize += add; @@ -71,7 +71,7 @@ public class ContainerAuctionSell extends ContainerAuction { } else { // Different item - replace ItemStack newStack = source.copy(); - newStack.stackSize = Math.min(toAdd, maxStack); + newStack.stackSize = Math.min(toAdd, MAX_STACK); sellInventory.setInventorySlotContents(0, newStack); } } @@ -93,7 +93,7 @@ public class ContainerAuctionSell extends ContainerAuction { /** Check if sell slot is this container slot index */ public boolean isSellSlot(int slotIndex) { - return slotIndex == sellSlotIndex; + return slotIndex == SELL_SLOT_INDEX; } /** Check if two stacks match using NoppesUtilPlayer.compareItems (same item, damage, and NBT) */ diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java index eab491c70..43e91f428 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java @@ -6,11 +6,13 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.controllers.AuctionController; +import noppes.npcs.constants.EnumTradeSlotType; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -34,7 +36,7 @@ public class ContainerAuctionTrades extends ContainerAuction { private int maxTradeSlots = 8; // Player's max trade slots (permission-aware, synced from server) // Slot type tracking for proper accessor methods - private int[] slotTypes = new int[SLOT_COUNT]; // 0=empty, 1=selling, 2=bidding, 3=claim + private EnumTradeSlotType[] slotTypes = new EnumTradeSlotType[SLOT_COUNT]; private int[] slotDataIndex = new int[SLOT_COUNT]; // Index into respective list public ContainerAuctionTrades(EntityNPCInterface npc, EntityPlayer player) { @@ -117,8 +119,8 @@ public class ContainerAuctionTrades extends ContainerAuction { displayInventory.clear(); // Reset slot tracking + Arrays.fill(slotTypes, EnumTradeSlotType.EMPTY); for (int i = 0; i < SLOT_COUNT; i++) { - slotTypes[i] = 0; slotDataIndex[i] = -1; } } @@ -156,7 +158,7 @@ public class ContainerAuctionTrades extends ContainerAuction { if (slot >= SLOT_COUNT) break; claims.add(claim); displayInventory.setClaimItem(slot, claim); - slotTypes[slot] = 3; // claim + slotTypes[slot] = EnumTradeSlotType.CLAIM; slotDataIndex[slot] = claims.size() - 1; slot++; } @@ -166,7 +168,7 @@ public class ContainerAuctionTrades extends ContainerAuction { if (slot >= SLOT_COUNT) break; claims.add(claim); displayInventory.setClaimItem(slot, claim); - slotTypes[slot] = 3; // claim + slotTypes[slot] = EnumTradeSlotType.CLAIM; slotDataIndex[slot] = claims.size() - 1; slot++; } @@ -176,7 +178,7 @@ public class ContainerAuctionTrades extends ContainerAuction { if (slot >= SLOT_COUNT) break; claims.add(claim); displayInventory.setClaimItem(slot, claim); - slotTypes[slot] = 3; // claim + slotTypes[slot] = EnumTradeSlotType.CLAIM; slotDataIndex[slot] = claims.size() - 1; slot++; } @@ -186,7 +188,7 @@ public class ContainerAuctionTrades extends ContainerAuction { for (AuctionListing listing : activeListings) { if (slot >= SLOT_COUNT) break; if (listing.item != null) displayInventory.setInventorySlotContents(slot, listing.item.copy()); - slotTypes[slot] = 1; // selling + slotTypes[slot] = EnumTradeSlotType.SELLING; slotDataIndex[slot] = listingIdx; slot++; listingIdx++; @@ -197,7 +199,7 @@ public class ContainerAuctionTrades extends ContainerAuction { for (AuctionListing bid : activeBids) { if (slot >= SLOT_COUNT) break; if (bid.item != null) displayInventory.setInventorySlotContents(slot, bid.item.copy()); - slotTypes[slot] = 2; // bidding + slotTypes[slot] = EnumTradeSlotType.BIDDING; slotDataIndex[slot] = bidIdx; slot++; bidIdx++; @@ -208,7 +210,7 @@ public class ContainerAuctionTrades extends ContainerAuction { if (slot >= SLOT_COUNT) break; claims.add(claim); displayInventory.setClaimItem(slot, claim); - slotTypes[slot] = 3; // claim + slotTypes[slot] = EnumTradeSlotType.CLAIM; slotDataIndex[slot] = claims.size() - 1; slot++; } @@ -219,13 +221,13 @@ public class ContainerAuctionTrades extends ContainerAuction { /** Get listing at slot (or null if claim/empty) */ public AuctionListing getListingAt(int slot) { if (slot < 0 || slot >= SLOT_COUNT) return null; - int type = slotTypes[slot]; + EnumTradeSlotType type = slotTypes[slot]; int idx = slotDataIndex[slot]; if (idx < 0) return null; - if (type == 1 && idx < activeListings.size()) { + if (type == EnumTradeSlotType.SELLING && idx < activeListings.size()) { return activeListings.get(idx); - } else if (type == 2 && idx < activeBids.size()) { + } else if (type == EnumTradeSlotType.BIDDING && idx < activeBids.size()) { return activeBids.get(idx); } return null; @@ -234,7 +236,7 @@ public class ContainerAuctionTrades extends ContainerAuction { /** Get claim at slot (or null if listing/empty) */ public AuctionClaim getClaimAt(int slot) { if (slot < 0 || slot >= SLOT_COUNT) return null; - if (slotTypes[slot] == 3) { + if (slotTypes[slot] == EnumTradeSlotType.CLAIM) { int idx = slotDataIndex[slot]; if (idx >= 0 && idx < claims.size()) { return claims.get(idx); @@ -245,12 +247,12 @@ public class ContainerAuctionTrades extends ContainerAuction { /** Check if slot is player's active listing */ public boolean isSellingAt(int slot) { - return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 1; + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == EnumTradeSlotType.SELLING; } /** Check if slot is player's active bid */ public boolean isBiddingAt(int slot) { - return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 2; + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == EnumTradeSlotType.BIDDING; } /** Find listing matching item (searches both selling and bidding) */ @@ -324,23 +326,23 @@ public class ContainerAuctionTrades extends ContainerAuction { private void restoreSlotItem(int slot) { if (slot < 0 || slot >= SLOT_COUNT) return; - int type = slotTypes[slot]; + EnumTradeSlotType type = slotTypes[slot]; int idx = slotDataIndex[slot]; if (idx < 0) return; - if (type == 1 && idx < activeListings.size()) { + if (type == EnumTradeSlotType.SELLING && idx < activeListings.size()) { // Selling AuctionListing listing = activeListings.get(idx); if (listing != null && listing.item != null) { displayInventory.setInventorySlotContents(slot, listing.item.copy()); } - } else if (type == 2 && idx < activeBids.size()) { + } else if (type == EnumTradeSlotType.BIDDING && idx < activeBids.size()) { // Bidding AuctionListing bid = activeBids.get(idx); if (bid != null && bid.item != null) { displayInventory.setInventorySlotContents(slot, bid.item.copy()); } - } else if (type == 3 && idx < claims.size()) { + } else if (type == EnumTradeSlotType.CLAIM && idx < claims.size()) { // Claim displayInventory.setClaimItem(slot, claims.get(idx)); } diff --git a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java index 9b8c1f995..2b921aad6 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java +++ b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java @@ -40,7 +40,7 @@ public class AuctionConfigSync { public static String getEffectiveCurrencyName() { if (ConfigMarket.UseVault && VaultUtil.isEnabled()) { String vaultName = VaultUtil.getCurrencyNamePlural(); - if (vaultName != null && !vaultName.isEmpty() && !vaultName.equals("coins")) { + if (vaultName != null && !vaultName.isEmpty()) { return vaultName; } } diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index e6ca0c6cf..2d02cd9ce 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -11,22 +11,27 @@ import net.minecraft.util.ChatComponentText; import net.minecraft.util.EnumChatFormatting; import noppes.npcs.CustomNpcs; import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.EventHooks; import noppes.npcs.LogWriter; 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; import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionLogAction; import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.constants.EnumNotificationType; +import noppes.npcs.controllers.data.PlayerDataScript; import noppes.npcs.controllers.data.AuctionBlacklist; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.PlayerTradeData; +import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.scripted.event.player.AuctionEvent; import noppes.npcs.util.CustomNPCsThreader; import java.io.File; @@ -138,52 +143,56 @@ public class AuctionController implements IAuctionHandler { } private void endAuction(AuctionListing listing) { - listing.status = EnumAuctionStatus.ENDED; + synchronized (listing) { + if (listing.status != EnumAuctionStatus.ACTIVE) return; - // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); + listing.status = EnumAuctionStatus.ENDED; - // Remove from bidder's active bids if any - if (listing.highBidderUUID != null) { - removeFromPlayerBids(listing.highBidderUUID, listing.id); - } + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); - // Get item display name safely - String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; + // Remove from bidder's active bids if any + if (listing.highBidderUUID != null) { + removeFromPlayerBids(listing.highBidderUUID, listing.id); + } - if (listing.hasBids()) { - // Auction sold - create claims - AuctionClaim itemClaim = AuctionClaim.createItemWonClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); - addClaimToPlayer(listing.highBidderUUID, itemClaim); + // Get item display name safely + String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; - long saleAmount = listing.currentBid; - long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); - long sellerReceives = saleAmount - tax; + if (listing.hasBids()) { + // Auction sold - create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); + addClaimToPlayer(listing.highBidderUUID, itemClaim); - AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( - listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, - itemDisplayName, listing.highBidderName); - addClaimToPlayer(listing.sellerUUID, currencyClaim); + long saleAmount = listing.currentBid; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; - // 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 + "!"); + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + itemDisplayName, listing.highBidderName); + addClaimToPlayer(listing.sellerUUID, currencyClaim); - logAuction("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); + // 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 + "!"); - sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, - "Your auction for " + itemDisplayName + " expired with no bids."); + 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); - logAuction("EXPIRED", listing.sellerName, itemDisplayName, listing.startingPrice, "No bids"); + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, + "Your auction for " + itemDisplayName + " expired with no bids."); + + logAuction(EnumAuctionLogAction.EXPIRED, listing.sellerName, itemDisplayName, listing.startingPrice, "No bids"); + } } } @@ -280,6 +289,15 @@ public class AuctionController implements IAuctionHandler { return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; } + // 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."; + } + // Deduct fee if (!currency.withdraw(fee)) { return "Failed to deduct listing fee."; @@ -295,7 +313,7 @@ public class AuctionController implements IAuctionHandler { markDirty(); - logAuction("CREATED", playerName, item.getDisplayName(), startingPrice, + logAuction(EnumAuctionLogAction.CREATED, playerName, item.getDisplayName(), startingPrice, "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); return null; // Success @@ -307,55 +325,74 @@ public class AuctionController implements IAuctionHandler { return "Listing not found."; } - if (!listing.status.canCancel()) { - return "This auction cannot be cancelled."; + synchronized (listing) { + if (!listing.status.canCancel()) { + return "This auction cannot be cancelled."; + } + + UUID playerUUID = player.getUniqueID(); + if (!isAdmin && !listing.isSeller(playerUUID)) { + return "You can only cancel your own listings."; + } + + // 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."; + } + + // 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(); + } + } + + 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); + + logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, + "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.CANCELLED; + markDirty(); } - UUID playerUUID = player.getUniqueID(); - if (!isAdmin && !listing.isSeller(playerUUID)) { - return "You can only cancel your own listings."; - } - - // 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); - - logAuction("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); - - logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, - "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); - } - - // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); - - listing.status = EnumAuctionStatus.CANCELLED; - markDirty(); - return null; // Success } @@ -376,75 +413,85 @@ public class AuctionController implements IAuctionHandler { return "Listing not found."; } - // Check status and expiry - if (!listing.status.canBid()) { - return "This auction has ended."; + synchronized (listing) { + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + // CRITICAL: Prevent bidding on own items + if (listing.isSeller(playerUUID)) { + return "You cannot bid on your own auction."; + } + + // 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."; + } + + // Check minimum bid + long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); + if (bidAmount < minBid) { + return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // Store previous bidder info + UUID previousBidder = listing.highBidderUUID; + long previousBid = listing.currentBid; + + // Full bid amount charged since we don't allow rebidding + long amountToCharge = bidAmount; + + if (!currency.canAfford(amountToCharge)) { + return "You cannot afford this bid."; + } + + // 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."; + } + + // Deduct bid from new bidder FIRST + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct bid amount."; + } + + // 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); + + // Remove from previous bidder's active bids + removeFromPlayerBids(previousBidder, listing.id); + + 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++; } - if (listing.isExpired()) { - return "This auction has ended."; - } - - // CRITICAL: Prevent bidding on own items - if (listing.isSeller(playerUUID)) { - return "You cannot bid on your own auction."; - } - - // 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."; - } - - // Check minimum bid - long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); - if (bidAmount < minBid) { - return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; - } - - // Check currency - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerTradeData currency = playerData.tradeData; - - // Store previous bidder info - UUID previousBidder = listing.highBidderUUID; - long previousBid = listing.currentBid; - - // Full bid amount charged since we don't allow rebidding - long amountToCharge = bidAmount; - - if (!currency.canAfford(amountToCharge)) { - return "You cannot afford this bid."; - } - - // Deduct bid from new bidder FIRST - if (!currency.withdraw(amountToCharge)) { - return "Failed to deduct bid amount."; - } - - // 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); - - // Remove from previous bidder's active bids - removeFromPlayerBids(previousBidder, listing.id); - - 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++; - // Add to new bidder's active bids addToPlayerBids(playerUUID, listing.id); @@ -453,7 +500,7 @@ public class AuctionController implements IAuctionHandler { markDirty(); - logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount, + logAuction(EnumAuctionLogAction.BID, playerName, listing.item.getDisplayName(), bidAmount, "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); return null; // Success @@ -472,97 +519,107 @@ public class AuctionController implements IAuctionHandler { return "Listing not found."; } - // Check status and expiry - if (!listing.status.canBid()) { - return "This auction has ended."; - } - - if (listing.isExpired()) { - return "This auction has ended."; - } - - if (!listing.hasBuyout()) { - return "This auction does not have a buyout price."; - } - - // CRITICAL: Prevent buying own items - if (listing.isSeller(playerUUID)) { - return "You cannot buy your own auction."; - } - - // Check currency - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerTradeData currency = playerData.tradeData; - - // 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; - - if (!currency.canAfford(amountToCharge)) { - if (isCurrentBidder) { - return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout."; + synchronized (listing) { + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; } - return "You cannot afford the buyout price."; + + if (listing.isExpired()) { + return "This auction has ended."; + } + + if (!listing.hasBuyout()) { + return "This auction does not have a buyout price."; + } + + // CRITICAL: Prevent buying own items + if (listing.isSeller(playerUUID)) { + return "You cannot buy your own auction."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // 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; + + if (!currency.canAfford(amountToCharge)) { + if (isCurrentBidder) { + return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout."; + } + return "You cannot afford the buyout price."; + } + + // 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."; + } + + // Deduct buyout amount + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct buyout amount."; + } + + // 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); + } + + // Create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); + addClaimToPlayer(playerUUID, itemClaim); + + long saleAmount = listing.buyoutPrice; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + listing.item.getDisplayName(), playerName); + addClaimToPlayer(listing.sellerUUID, currencyClaim); + + // Notifications + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.ENDED; + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = listing.buyoutPrice; + + markDirty(); + + logAuction(EnumAuctionLogAction.BUYOUT, playerName, listing.item.getDisplayName(), listing.buyoutPrice, + "Seller: " + listing.sellerName + ", Tax: " + tax); } - // Deduct buyout amount - if (!currency.withdraw(amountToCharge)) { - return "Failed to deduct buyout amount."; - } - - // 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); - } - - // Create claims - AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); - addClaimToPlayer(playerUUID, itemClaim); - - long saleAmount = listing.buyoutPrice; - long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); - long sellerReceives = saleAmount - tax; - - AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( - listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, - listing.item.getDisplayName(), playerName); - addClaimToPlayer(listing.sellerUUID, currencyClaim); - - // Notifications - sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, - "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); - - // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); - - listing.status = EnumAuctionStatus.ENDED; - listing.highBidderUUID = playerUUID; - listing.highBidderName = playerName; - listing.currentBid = listing.buyoutPrice; - - markDirty(); - - logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice, - "Seller: " + listing.sellerName + ", Tax: " + tax); - return null; // Success } @@ -615,6 +672,14 @@ public class AuctionController implements IAuctionHandler { return "Item data is missing."; } + // 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."; + } + // Check inventory space if (!player.inventory.addItemStackToInventory(claim.item.copy())) { return "Your inventory is full."; @@ -628,7 +693,7 @@ public class AuctionController implements IAuctionHandler { // Cleanup listing if no more claims cleanupListingIfComplete(claim.listingId); - logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); return null; // Success } @@ -652,6 +717,14 @@ public class AuctionController implements IAuctionHandler { return "This is not a currency claim."; } + // 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."; + } + if (!playerData.tradeData.deposit(claim.currency)) { return "Failed to deposit currency."; } @@ -664,7 +737,7 @@ public class AuctionController implements IAuctionHandler { // Cleanup listing if no more claims cleanupListingIfComplete(claim.listingId); - logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); return null; // Success @@ -993,7 +1066,7 @@ public class AuctionController implements IAuctionHandler { if (expired > 0) { playerData.updateClient = true; playerData.save(); - logAuction("CLAIM_EXPIRED", player.getCommandSenderName(), "Multiple", expired, + logAuction(EnumAuctionLogAction.CLAIM_EXPIRED, player.getCommandSenderName(), "Multiple", expired, expired + " claims expired on login"); } } @@ -1041,40 +1114,12 @@ public class AuctionController implements IAuctionHandler { // Logging // ========================================= - private void logAuction(String action, String playerName, String itemName, long amount, String details) { - if (!ConfigMarket.AuctionLoggingEnabled) return; + private void logAuction(EnumAuctionLogAction action, String playerName, String itemName, long amount, String details) { + if (!action.shouldLog()) return; - boolean shouldLog = false; - switch (action) { - case "CREATED": - shouldLog = ConfigMarket.LogAuctionCreated; - break; - case "BID": - shouldLog = ConfigMarket.LogAuctionBid; - break; - case "BUYOUT": - shouldLog = ConfigMarket.LogAuctionBuyout; - break; - case "SOLD": - shouldLog = ConfigMarket.LogAuctionSold; - break; - case "EXPIRED": - shouldLog = ConfigMarket.LogAuctionExpired; - break; - case "CANCELLED": - shouldLog = ConfigMarket.LogAuctionCancelled; - break; - case "CLAIMED": - case "CLAIM_EXPIRED": - shouldLog = ConfigMarket.LogAuctionClaimed; - break; - } - - if (shouldLog) { - String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s", - action, playerName, itemName, amount, details); - LogWriter.info(logMessage); - } + String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s", + action.name(), playerName, itemName, amount, details); + LogWriter.info(logMessage); } // ========================================= @@ -1141,7 +1186,9 @@ public class AuctionController implements IAuctionHandler { File fileOld = new File(saveDir, "auction.dat_old"); File fileCurrent = new File(saveDir, "auction.dat"); - CompressedStreamTools.writeCompressed(compound, new FileOutputStream(fileNew)); + try (FileOutputStream fos = new FileOutputStream(fileNew)) { + CompressedStreamTools.writeCompressed(compound, fos); + } // Backup rotation if (fileOld.exists()) fileOld.delete(); @@ -1210,7 +1257,10 @@ public class AuctionController implements IAuctionHandler { } private void loadFromFile(File file) throws Exception { - NBTTagCompound compound = CompressedStreamTools.readCompressed(new FileInputStream(file)); + NBTTagCompound compound; + try (FileInputStream fis = new FileInputStream(file)) { + compound = CompressedStreamTools.readCompressed(fis); + } readFromNBT(compound); } @@ -1398,7 +1448,12 @@ public class AuctionController implements IAuctionHandler { } public void adminCancelListing(String listingId, EntityPlayer admin, String reason) { + AuctionListing listing = listings.get(listingId); + String itemName = listing != null && listing.item != null ? listing.item.getDisplayName() : "Unknown"; cancelListing(listingId, admin, true); + if (reason != null && !reason.isEmpty()) { + logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, 0, "Admin cancel - Reason: " + reason); + } } public void clearEndedListings() { diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index 88a12e20d..3b79637d8 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -2,6 +2,8 @@ package noppes.npcs.controllers; import noppes.npcs.api.event.IAbilityEvent; import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IAuctionEvent; +import noppes.npcs.api.event.IEnergyBarrierEvent; import noppes.npcs.api.event.IPlayerAbilityEvent; import noppes.npcs.api.event.IBlockEvent; import noppes.npcs.api.event.ICustomGuiEvent; @@ -123,6 +125,12 @@ public class ScriptHookController implements IScriptHookHandler { hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + // Energy Barrier + hook(NPC, ENERGY_BARRIER_SPAWNED, IEnergyBarrierEvent.SpawnedEvent.class); + hook(NPC, ENERGY_BARRIER_TICK, IEnergyBarrierEvent.UpdateEvent.class); + hook(NPC, ENERGY_BARRIER_HIT, IEnergyBarrierEvent.HitEvent.class); + hook(NPC, ENERGY_BARRIER_DESTROYED, IEnergyBarrierEvent.DestroyedEvent.class); + // Animation hook(NPC, ANIMATION_START, IAnimationEvent.Started.class); hook(NPC, ANIMATION_END, IAnimationEvent.Ended.class); @@ -230,6 +238,12 @@ public class ScriptHookController implements IScriptHookHandler { hook(PLAYER, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class); hook(PLAYER, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class); + // Energy Barrier + hook(PLAYER, ENERGY_BARRIER_SPAWNED, IEnergyBarrierEvent.SpawnedEvent.class); + hook(PLAYER, ENERGY_BARRIER_TICK, IEnergyBarrierEvent.UpdateEvent.class); + hook(PLAYER, ENERGY_BARRIER_HIT, IEnergyBarrierEvent.HitEvent.class); + hook(PLAYER, ENERGY_BARRIER_DESTROYED, IEnergyBarrierEvent.DestroyedEvent.class); + // Ability hook(PLAYER, ABILITY_START, IPlayerAbilityEvent.StartEvent.class); hook(PLAYER, ABILITY_EXECUTE, IPlayerAbilityEvent.ExecuteEvent.class); @@ -238,6 +252,13 @@ public class ScriptHookController implements IScriptHookHandler { hook(PLAYER, ABILITY_INTERRUPT, IPlayerAbilityEvent.InterruptEvent.class); hook(PLAYER, ABILITY_COMPLETE, IPlayerAbilityEvent.CompleteEvent.class); + // Auction + hook(PLAYER, AUCTION_CREATE, IAuctionEvent.CreateEvent.class); + hook(PLAYER, AUCTION_BID, IAuctionEvent.BidEvent.class); + hook(PLAYER, AUCTION_BUYOUT, IAuctionEvent.BuyoutEvent.class); + hook(PLAYER, AUCTION_CANCEL, IAuctionEvent.CancelEvent.class); + hook(PLAYER, AUCTION_CLAIM, IAuctionEvent.ClaimEvent.class); + // Profile hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class); hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class); diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java index 75a1a714a..5a950816c 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java @@ -13,12 +13,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; +import java.util.Iterator; public class AuctionBlacklist { - private static Set blacklistedItems = new HashSet<>(); - private static Set wildcardPatterns = new HashSet<>(); - private static Set blacklistedMods = new HashSet<>(); - private static Set blacklistedNBTTags = new HashSet<>(); + private static volatile Set blacklistedItems = new HashSet<>(); + private static volatile Set wildcardPatterns = new HashSet<>(); + private static volatile Set blacklistedMods = new HashSet<>(); + private static volatile Set blacklistedNBTTags = new HashSet<>(); // Built-in bound item requirement keys (always blocked) private static final String SOULBIND_KEY = "cnpc_soulbind"; @@ -28,10 +29,10 @@ public class AuctionBlacklist { private static final String BYPASS_PERMISSION = "customnpcs.auction.blacklist.bypass"; public static void reload() { - blacklistedItems.clear(); - wildcardPatterns.clear(); - blacklistedMods.clear(); - blacklistedNBTTags.clear(); + Set newItems = new HashSet<>(); + Set newWildcards = new HashSet<>(); + Set newMods = new HashSet<>(); + Set newNBTTags = new HashSet<>(); // Load blacklisted items for (String item : ConfigMarket.BlacklistedItems) { @@ -40,23 +41,29 @@ public class AuctionBlacklist { if (item.contains("*")) { String regex = "^" + item.replace(".", "\\.").replace("*", ".*") + "$"; - wildcardPatterns.add(Pattern.compile(regex)); + newWildcards.add(Pattern.compile(regex)); } else { - blacklistedItems.add(item); + newItems.add(item); } } // Load blacklisted mods for (String mod : ConfigMarket.BlacklistedMods) { if (mod == null || mod.isEmpty()) continue; - blacklistedMods.add(mod.toLowerCase().trim()); + newMods.add(mod.toLowerCase().trim()); } // Load blacklisted NBT tags for (String tag : ConfigMarket.BlacklistedNBTTags) { if (tag == null || tag.isEmpty()) continue; - blacklistedNBTTags.add(tag.trim()); + newNBTTags.add(tag.trim()); } + + // Atomic swap + blacklistedItems = newItems; + wildcardPatterns = newWildcards; + blacklistedMods = newMods; + blacklistedNBTTags = newNBTTags; } /** @@ -78,7 +85,6 @@ public class AuctionBlacklist { * Check if item is blacklisted (without bypass check) */ public static boolean isBlacklisted(ItemStack item) { - if (!ConfigMarket.BlacklistEnabled) return false; if (item == null || item.getItem() == null) return true; // Check bound items (always blocked, even if blacklist is "disabled") @@ -86,6 +92,8 @@ public class AuctionBlacklist { return true; } + if (!ConfigMarket.BlacklistEnabled) return false; + // Get registry name GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); if (uid == null) return true; @@ -172,9 +180,13 @@ public class AuctionBlacklist { if (registryName.contains("*")) { String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; - wildcardPatterns.add(Pattern.compile(regex)); + Set newWildcards = new HashSet<>(wildcardPatterns); + newWildcards.add(Pattern.compile(regex)); + wildcardPatterns = newWildcards; } else { - blacklistedItems.add(registryName); + Set newItems = new HashSet<>(blacklistedItems); + newItems.add(registryName); + blacklistedItems = newItems; } return true; } @@ -185,30 +197,55 @@ public class AuctionBlacklist { if (registryName.contains("*")) { String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; - return wildcardPatterns.removeIf(p -> p.pattern().equals(regex)); + Set newWildcards = new HashSet<>(wildcardPatterns); + boolean removed = false; + Iterator it = newWildcards.iterator(); + while (it.hasNext()) { + if (it.next().pattern().equals(regex)) { + it.remove(); + removed = true; + } + } + wildcardPatterns = newWildcards; + return removed; } else { - return blacklistedItems.remove(registryName); + Set newItems = new HashSet<>(blacklistedItems); + boolean removed = newItems.remove(registryName); + blacklistedItems = newItems; + return removed; } } public static boolean addMod(String modId) { if (modId == null || modId.isEmpty()) return false; - return blacklistedMods.add(modId.toLowerCase().trim()); + Set newMods = new HashSet<>(blacklistedMods); + boolean added = newMods.add(modId.toLowerCase().trim()); + blacklistedMods = newMods; + return added; } public static boolean removeMod(String modId) { if (modId == null || modId.isEmpty()) return false; - return blacklistedMods.remove(modId.toLowerCase().trim()); + Set newMods = new HashSet<>(blacklistedMods); + boolean removed = newMods.remove(modId.toLowerCase().trim()); + blacklistedMods = newMods; + return removed; } public static boolean addNBTTag(String tag) { if (tag == null || tag.isEmpty()) return false; - return blacklistedNBTTags.add(tag.trim()); + Set newTags = new HashSet<>(blacklistedNBTTags); + boolean added = newTags.add(tag.trim()); + blacklistedNBTTags = newTags; + return added; } public static boolean removeNBTTag(String tag) { if (tag == null || tag.isEmpty()) return false; - return blacklistedNBTTags.remove(tag.trim()); + Set newTags = new HashSet<>(blacklistedNBTTags); + boolean removed = newTags.remove(tag.trim()); + blacklistedNBTTags = newTags; + return removed; } // ========================================= diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java index ad4ae3238..c76aece11 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -140,7 +140,7 @@ public class AuctionClaim implements IAuctionClaim { @Override public boolean isCurrencyClaim() { - return type == EnumClaimType.CURRENCY; + return type.isCurrency(); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java index 95bf2ed24..5018ec2e0 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -4,16 +4,12 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.EnumChatFormatting; import noppes.npcs.constants.EnumAuctionSort; -import java.util.regex.Pattern; - /** * Filter for auction listings. * Supports search by item name or seller name and sorting options. */ public class AuctionFilter { - private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("ยง[0-9a-fk-or]", Pattern.CASE_INSENSITIVE); - - public String searchText; + private String searchText; public EnumAuctionSort sortBy; // Cached normalized search for efficiency @@ -32,9 +28,8 @@ public class AuctionFilter { /** Normalize string for search (strip colors, lowercase, trim) */ public static String normalizeForSearch(String input) { if (input == null || input.isEmpty()) return ""; - String stripped = COLOR_CODE_PATTERN.matcher(input).replaceAll(""); - stripped = EnumChatFormatting.getTextWithoutFormattingCodes(stripped); - return stripped.toLowerCase().trim(); + String stripped = EnumChatFormatting.getTextWithoutFormattingCodes(input); + return stripped != null ? stripped.toLowerCase().trim() : input.toLowerCase().trim(); } /** Check if item name or seller name matches search text */ diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java b/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java deleted file mode 100644 index 01ebd1e47..000000000 --- a/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java +++ /dev/null @@ -1,64 +0,0 @@ -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.constants.EnumNotificationType; - -import java.util.UUID; - -public class AuctionNotification { - public String id; - public UUID playerUUID; - public EnumNotificationType type; - public String listingId; - public String message; - public long timestamp; - public boolean sent; - - public AuctionNotification() { - this.id = UUID.randomUUID().toString(); - this.timestamp = System.currentTimeMillis(); - this.sent = false; - } - - public AuctionNotification(UUID playerUUID, EnumNotificationType type, String listingId, String message) { - this(); - this.playerUUID = playerUUID; - this.type = type; - this.listingId = listingId; - this.message = message; - } - - public boolean isForPlayer(UUID uuid) { - return playerUUID != null && playerUUID.equals(uuid); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ID", id); - compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); - compound.setInteger("Type", type.ordinal()); - compound.setString("ListingID", listingId != null ? listingId : ""); - compound.setString("Message", message != null ? message : ""); - compound.setLong("Timestamp", timestamp); - compound.setBoolean("Sent", sent); - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - id = compound.getString("ID"); - - String playerUUIDStr = compound.getString("PlayerUUID"); - playerUUID = playerUUIDStr.isEmpty() ? null : UUID.fromString(playerUUIDStr); - - type = EnumNotificationType.fromOrdinal(compound.getInteger("Type")); - listingId = compound.getString("ListingID"); - message = compound.getString("Message"); - timestamp = compound.getLong("Timestamp"); - sent = compound.getBoolean("Sent"); - } - - public static AuctionNotification fromNBT(NBTTagCompound compound) { - AuctionNotification notification = new AuctionNotification(); - notification.readFromNBT(compound); - return notification; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java index a0a8e0bb7..2850ddb90 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java @@ -8,7 +8,6 @@ import noppes.npcs.api.handler.data.IAuctionClaim; import noppes.npcs.config.ConfigMarket; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -129,12 +128,13 @@ public class PlayerTradeData implements IPlayerTradeData { // Built-in currency long newBalance = this.balance + amount; - if (newBalance > ConfigMarket.MaxBalance) { - return false; - } + // Overflow protection if (newBalance < this.balance) { - // Overflow protection - return false; + newBalance = ConfigMarket.MaxBalance; + } + // Cap at MaxBalance instead of rejecting + if (newBalance > ConfigMarket.MaxBalance) { + newBalance = ConfigMarket.MaxBalance; } this.balance = newBalance; this.lifetimeEarned += amount; @@ -341,18 +341,16 @@ public class PlayerTradeData implements IPlayerTradeData { */ public int processExpiredClaims() { int expirationDays = ConfigMarket.ClaimExpirationDays; - int removed = 0; - - Iterator iterator = claims.iterator(); - while (iterator.hasNext()) { - AuctionClaim claim = iterator.next(); + List expired = new ArrayList<>(); + for (AuctionClaim claim : claims) { if (!claim.claimed && claim.isExpired(expirationDays)) { - claims.remove(claim); - removed++; + expired.add(claim); } } - - return removed; + if (!expired.isEmpty()) { + claims.removeAll(expired); + } + return expired.size(); } // ========================================= diff --git a/src/main/java/noppes/npcs/scripted/event/player/AuctionEvent.java b/src/main/java/noppes/npcs/scripted/event/player/AuctionEvent.java new file mode 100644 index 000000000..8cebf5a6c --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/player/AuctionEvent.java @@ -0,0 +1,131 @@ +package noppes.npcs.scripted.event.player; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.event.IAuctionEvent; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.constants.EnumScriptType; + +public class AuctionEvent extends PlayerEvent implements IAuctionEvent { + + public AuctionEvent(IPlayer player) { + super(player); + } + + @Override + public String getHookName() { + return "auctionEvent"; + } + + @Cancelable + public static class CreateEvent extends AuctionEvent implements IAuctionEvent.CreateEvent { + public final IItemStack item; + public final long startingPrice; + public final long buyoutPrice; + + public CreateEvent(IPlayer player, IItemStack item, long startingPrice, long buyoutPrice) { + super(player); + this.item = item; + this.startingPrice = startingPrice; + this.buyoutPrice = buyoutPrice; + } + + @Override + public String getHookName() { + return EnumScriptType.AUCTION_CREATE.function; + } + + @Override + public IItemStack getItem() { return item; } + + @Override + public long getStartingPrice() { return startingPrice; } + + @Override + public long getBuyoutPrice() { return buyoutPrice; } + } + + @Cancelable + public static class BidEvent extends AuctionEvent implements IAuctionEvent.BidEvent { + public final IAuctionListing listing; + public final long bidAmount; + + public BidEvent(IPlayer player, IAuctionListing listing, long bidAmount) { + super(player); + this.listing = listing; + this.bidAmount = bidAmount; + } + + @Override + public String getHookName() { + return EnumScriptType.AUCTION_BID.function; + } + + @Override + public IAuctionListing getListing() { return listing; } + + @Override + public long getBidAmount() { return bidAmount; } + } + + @Cancelable + public static class BuyoutEvent extends AuctionEvent implements IAuctionEvent.BuyoutEvent { + public final IAuctionListing listing; + + public BuyoutEvent(IPlayer player, IAuctionListing listing) { + super(player); + this.listing = listing; + } + + @Override + public String getHookName() { + return EnumScriptType.AUCTION_BUYOUT.function; + } + + @Override + public IAuctionListing getListing() { return listing; } + } + + @Cancelable + public static class CancelEvent extends AuctionEvent implements IAuctionEvent.CancelEvent { + public final IAuctionListing listing; + public final boolean admin; + + public CancelEvent(IPlayer player, IAuctionListing listing, boolean isAdmin) { + super(player); + this.listing = listing; + this.admin = isAdmin; + } + + @Override + public String getHookName() { + return EnumScriptType.AUCTION_CANCEL.function; + } + + @Override + public IAuctionListing getListing() { return listing; } + + @Override + public boolean isAdmin() { return admin; } + } + + @Cancelable + public static class ClaimEvent extends AuctionEvent implements IAuctionEvent.ClaimEvent { + public final IAuctionClaim claim; + + public ClaimEvent(IPlayer player, IAuctionClaim claim) { + super(player); + this.claim = claim; + } + + @Override + public String getHookName() { + return EnumScriptType.AUCTION_CLAIM.function; + } + + @Override + public IAuctionClaim getClaim() { return claim; } + } +} diff --git a/src/main/java/noppes/npcs/util/AuctionFormatUtil.java b/src/main/java/noppes/npcs/util/AuctionFormatUtil.java new file mode 100644 index 000000000..a92193815 --- /dev/null +++ b/src/main/java/noppes/npcs/util/AuctionFormatUtil.java @@ -0,0 +1,68 @@ +package noppes.npcs.util; + +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; + +/** + * Shared formatting utilities for the auction system. + * Consolidates duplicated format methods from GUI classes. + */ +public class AuctionFormatUtil { + // Time constants in milliseconds + public static final long MS_PER_SECOND = 1000L; + public static final long MS_PER_MINUTE = 60L * MS_PER_SECOND; + public static final long MS_PER_HOUR = 60L * MS_PER_MINUTE; + public static final long MS_PER_DAY = 24L * MS_PER_HOUR; + + /** + * Format currency amount with commas (e.g., 1000 -> "1,000"). + * Does NOT append currency name. + */ + public static String formatCurrency(long amount) { + if (amount < 1000) return "" + amount; + StringBuilder sb = new StringBuilder(); + String str = "" + amount; + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) sb.insert(0, ','); + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); + } + + /** + * Format currency amount with commas and append currency name. + * (e.g., 1000 -> "1,000 Gold") + */ + public static String formatCurrencyWithName(long amount) { + return formatCurrency(amount) + " " + AuctionClientConfig.getCurrencyName(); + } + + /** + * Format time remaining as a compact string (e.g., "2d 5h 30m"). + * Uses localized "Ended" text when time is <= 0. + */ + public static String formatTimeRemaining(long ms) { + if (ms <= 0) return StatCollector.translateToLocal("auction.ended"); + + long seconds = (ms / MS_PER_SECOND) % 60; + long minutes = (ms / MS_PER_MINUTE) % 60; + long hours = (ms / MS_PER_HOUR) % 24; + long days = ms / MS_PER_DAY; + + StringBuilder sb = new StringBuilder(); + if (days > 0) sb.append(days).append("d "); + if (hours > 0 || days > 0) sb.append(hours).append("h "); + if (minutes > 0 || hours > 0 || days > 0) sb.append(minutes).append("m"); + else sb.append(seconds).append("s"); + return sb.toString(); + } + + /** + * Check if time remaining is urgent (less than 1 hour). + */ + public static boolean isTimeUrgent(long ms) { + return ms > 0 && ms < MS_PER_HOUR; + } +}