diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 745962aba..b22537070 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -49,6 +49,7 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; +import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; @@ -611,6 +612,9 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new PartyAcceptInvitePacket()); PLAYER_PACKET.registerPacket(new PartyIgnoreInvitePacket()); PLAYER_PACKET.registerPacket(new PartyLogToServerPacket()); + + // Auction Packets + PLAYER_PACKET.registerPacket(new AuctionActionPacket()); } public void registerChannels() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 0c1db3cd9..15c90fce5 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -33,4 +33,7 @@ public enum EnumPlayerPacket { ProfileGet, ProfileGetInfo, SyncRevisionInfo, + + // Auction + AuctionAction, } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java new file mode 100644 index 000000000..4aa56ab57 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -0,0 +1,441 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.ChatComponentText; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.IOException; + +/** + * Packet for all auction-related player actions. + * Client -> Server for actions, Server -> Client for responses. + */ +public class AuctionActionPacket extends AbstractPacket { + public static final String packetName = "Player|Auction"; + + private Action action; + private int listingId; + private long amount; + private long buyoutPrice; + private int durationOrdinal; + private NBTTagCompound itemData; + private String responseMessage; + private boolean success; + + public AuctionActionPacket() { + } + + private AuctionActionPacket(Action action) { + this.action = action; + } + + @Override + public boolean needsNPC() { + return true; + } + + // ==================== Client -> Server Actions ==================== + + /** + * Create a new auction listing + */ + @SideOnly(Side.CLIENT) + public static void CreateListing(ItemStack item, long startingPrice, long buyoutPrice, EnumAuctionDuration duration) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); + packet.amount = startingPrice; + packet.buyoutPrice = buyoutPrice; + packet.durationOrdinal = duration.ordinal(); + packet.itemData = item.writeToNBT(new NBTTagCompound()); + PacketClient.sendClient(packet); + } + + /** + * Place a bid on an auction + */ + @SideOnly(Side.CLIENT) + public static void PlaceBid(int listingId, long bidAmount) { + AuctionActionPacket packet = new AuctionActionPacket(Action.PlaceBid); + packet.listingId = listingId; + packet.amount = bidAmount; + PacketClient.sendClient(packet); + } + + /** + * Execute buyout on an auction + */ + @SideOnly(Side.CLIENT) + public static void Buyout(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Cancel own listing + */ + @SideOnly(Side.CLIENT) + public static void CancelListing(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CancelListing); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Claim item from won/expired/cancelled auction + */ + @SideOnly(Side.CLIENT) + public static void ClaimItem(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Claim proceeds from sold auction + */ + @SideOnly(Side.CLIENT) + public static void ClaimProceeds(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimProceeds); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + @Override + public Enum getType() { + return EnumPlayerPacket.AuctionAction; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeInt(action.ordinal()); + + switch (action) { + case CreateListing: + out.writeLong(amount); + out.writeLong(buyoutPrice); + out.writeInt(durationOrdinal); + String itemJson = NBTJsonUtil.Convert(itemData); + byte[] itemBytes = itemJson.getBytes("UTF-8"); + out.writeInt(itemBytes.length); + out.writeBytes(itemBytes); + break; + + case PlaceBid: + out.writeInt(listingId); + out.writeLong(amount); + break; + + case Buyout: + case CancelListing: + case ClaimItem: + case ClaimProceeds: + out.writeInt(listingId); + break; + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + Action requestedAction = Action.values()[in.readInt()]; + + if (!(player instanceof EntityPlayerMP)) { + return; + } + + EntityPlayerMP playerMP = (EntityPlayerMP) player; + + // Verify NPC is an Auctioneer + if (npc == null || npc.advanced.role != EnumRoleType.Auctioneer) { + return; + } + + // Check if auction system is enabled + if (!ConfigMarket.AuctionEnabled) { + sendResponse(playerMP, false, "Auction system is disabled"); + return; + } + + // Check controller exists + if (AuctionController.Instance == null) { + sendResponse(playerMP, false, "Auction controller not initialized"); + return; + } + + switch (requestedAction) { + case CreateListing: + handleCreateListing(in, playerMP); + break; + case PlaceBid: + handlePlaceBid(in, playerMP); + break; + case Buyout: + handleBuyout(in, playerMP); + break; + case CancelListing: + handleCancelListing(in, playerMP); + break; + case ClaimItem: + handleClaimItem(in, playerMP); + break; + case ClaimProceeds: + handleClaimProceeds(in, playerMP); + break; + } + } + + private void handleCreateListing(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_LIST)) { + sendResponse(player, false, "You don't have permission to create listings"); + return; + } + + long startingPrice = in.readLong(); + long buyoutPrice = in.readLong(); + int durationOrdinal = in.readInt(); + + int itemBytesLength = in.readInt(); + byte[] itemBytes = new byte[itemBytesLength]; + in.readBytes(itemBytes); + String itemJson = new String(itemBytes, "UTF-8"); + + try { + NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); + ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); + + if (item == null) { + sendResponse(player, false, "Invalid item"); + return; + } + + // Validate duration + EnumAuctionDuration duration; + if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { + duration = EnumAuctionDuration.values()[durationOrdinal]; + } else { + duration = EnumAuctionDuration.MEDIUM; + } + + // Validate prices + if (startingPrice < 1) { + sendResponse(player, false, "Starting price must be at least 1"); + return; + } + + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + sendResponse(player, false, "Buyout price must be higher than starting price"); + return; + } + + // Create the listing + AuctionListing listing = AuctionController.Instance.createListing( + player, item, startingPrice, buyoutPrice, duration); + + if (listing != null) { + // Remove item from player's inventory + // Note: The GUI should have already collected the item, this is validation + sendResponse(player, true, "Listing created successfully"); + } else { + sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); + } + } catch (Exception e) { + sendResponse(player, false, "Error creating listing: " + e.getMessage()); + } + } + + private void handlePlaceBid(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BID)) { + sendResponse(player, false, "You don't have permission to bid"); + return; + } + + int listingId = in.readInt(); + long bidAmount = in.readLong(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (!listing.isActive()) { + sendResponse(player, false, "This auction has ended"); + return; + } + + if (bidAmount < listing.getMinimumBid()) { + sendResponse(player, false, "Bid must be at least " + listing.getMinimumBid()); + return; + } + + if (!CurrencyController.Instance.canAfford(player, bidAmount)) { + sendResponse(player, false, "You cannot afford this bid"); + return; + } + + if (AuctionController.Instance.placeBid(listingId, player, bidAmount)) { + sendResponse(player, true, "Bid placed successfully"); + } else { + sendResponse(player, false, "Failed to place bid"); + } + } + + private void handleBuyout(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BUYOUT)) { + sendResponse(player, false, "You don't have permission to use buyout"); + return; + } + + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (!listing.isActive()) { + sendResponse(player, false, "This auction has ended"); + return; + } + + if (listing.buyoutPrice <= 0) { + sendResponse(player, false, "This listing has no buyout option"); + return; + } + + if (!CurrencyController.Instance.canAfford(player, listing.buyoutPrice)) { + sendResponse(player, false, "You cannot afford the buyout price"); + return; + } + + if (AuctionController.Instance.buyout(listingId, player)) { + sendResponse(player, true, "Buyout successful! Claim your item."); + } else { + sendResponse(player, false, "Failed to execute buyout"); + } + } + + private void handleCancelListing(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + // Admin can cancel any listing + boolean isAdmin = CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_ADMIN); + + if (!listing.sellerUUID.equals(player.getUniqueID()) && !isAdmin) { + sendResponse(player, false, "You can only cancel your own listings"); + return; + } + + if (listing.hasBids() && !isAdmin) { + sendResponse(player, false, "Cannot cancel listing with active bids"); + return; + } + + if (AuctionController.Instance.cancelListing(listingId, player)) { + sendResponse(player, true, "Listing cancelled. Claim your item."); + } else { + sendResponse(player, false, "Failed to cancel listing"); + } + } + + private void handleClaimItem(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + ItemStack claimed = null; + + // Try buyer claim first + if (listing.highBidderUUID != null && listing.highBidderUUID.equals(player.getUniqueID())) { + claimed = AuctionController.Instance.claimBuyerItem(listingId, player); + } + // Then try seller reclaim (for expired/cancelled) + else if (listing.sellerUUID.equals(player.getUniqueID())) { + claimed = AuctionController.Instance.claimExpiredItem(listingId, player); + } + + if (claimed != null) { + // Give item to player + if (!player.inventory.addItemStackToInventory(claimed)) { + // Drop if inventory full + player.entityDropItem(claimed, 0.5f); + } + sendResponse(player, true, "Item claimed"); + } else { + sendResponse(player, false, "Cannot claim item"); + } + } + + private void handleClaimProceeds(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (AuctionController.Instance.claimSellerProceeds(listingId, player)) { + long proceeds = listing.getSellerProceeds(); + sendResponse(player, true, "Claimed " + proceeds + " " + ConfigMarket.CurrencyName); + } else { + sendResponse(player, false, "Cannot claim proceeds"); + } + } + + /** + * Send a response message to the player + */ + private void sendResponse(EntityPlayerMP player, boolean success, String message) { + // Use chat message for now; can be extended to use GUI updates + if (message != null && !message.isEmpty()) { + player.addChatMessage(new ChatComponentText("[Auction] " + message)); + } + } + + private enum Action { + CreateListing, + PlaceBid, + Buyout, + CancelListing, + ClaimItem, + ClaimProceeds + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 3363c0351..1f09e466f 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -90,6 +90,13 @@ public class CustomNpcsPermissions { public static final Permission SOULSTONE_ALL = new Permission("customnpcs.soulstone.all"); + // Auction System + public static final Permission AUCTION_USE = new Permission("customnpcs.auction.use"); + public static final Permission AUCTION_LIST = new Permission("customnpcs.auction.list"); + public static final Permission AUCTION_BID = new Permission("customnpcs.auction.bid"); + public static final Permission AUCTION_BUYOUT = new Permission("customnpcs.auction.buyout"); + public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); + // Modify / Save Scripts public static final Permission SCRIPT = new Permission("customnpcs.script"); public static final Permission SCRIPT_NPC = new Permission("customnpcs.script.npc"); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 5bfaba816..9fc547260 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,6 +49,7 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; @@ -1013,4 +1014,70 @@ public class ScriptPlayer extends ScriptLivingBase } return CurrencyController.Instance.claimAll(player); } + + // ==================== AUCTION METHODS ==================== + + /** + * Get the number of active auction listings the player has. + * @return Number of active listings + * @since 1.11 + */ + public int getAuctionListingCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); + } + + /** + * Get the maximum number of auction listings the player is allowed. + * @return Maximum listing count + * @since 1.11 + */ + public int getMaxAuctionListings() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getMaxListings(player); + } + + /** + * Check if the player has auction items waiting to be claimed. + * This includes won items, expired listings, and sale proceeds. + * @return true if there are claimable items + * @since 1.11 + */ + public boolean hasAuctionClaims() { + if (AuctionController.Instance == null) { + return false; + } + return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); + } + + /** + * Get the number of auction items waiting to be claimed. + * @return Number of claimable items/listings + * @since 1.11 + */ + public int getAuctionClaimCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getClaimableListings(player.getUniqueID()).size(); + } + + /** + * Get the number of auctions where the player is the current high bidder. + * @return Number of active bids + * @since 1.11 + */ + public int getActiveAuctionBidCount() { + if (AuctionController.Instance == null) { + return 0; + } + return (int) AuctionController.Instance.getListingsAsBidder(player.getUniqueID()) + .stream() + .filter(l -> l.isActive()) + .count(); + } }