diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index b71021b37..787c6e204 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -99,6 +99,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; +import kamkeel.npcs.network.packets.request.auction.ManageAuctionPacket; import kamkeel.npcs.network.packets.request.bank.BankGetPacket; import kamkeel.npcs.network.packets.request.bank.BankRemovePacket; import kamkeel.npcs.network.packets.request.bank.BankSavePacket; @@ -470,6 +471,7 @@ public class PacketHandler { // Trader Packets REQUEST_PACKET.registerPacket(new TraderMarketSavePacket()); + REQUEST_PACKET.registerPacket(new ManageAuctionPacket()); // Party Packets moved to player channel diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index e0f3451a1..8592163be 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -213,4 +213,5 @@ public enum EnumRequestPacket { Hammer, RecipeScript, + ManageAuctionAction, } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java index fbfae0a6e..b880d141a 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java @@ -11,6 +11,9 @@ import kamkeel.npcs.network.enums.EnumItemPacketType; import kamkeel.npcs.network.enums.EnumRequestPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; +import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NoppesUtilServer; import noppes.npcs.constants.EnumGuiType; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java new file mode 100644 index 000000000..10b4504b9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java @@ -0,0 +1,522 @@ +package kamkeel.npcs.network.packets.request.auction; + +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.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +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.nbt.NBTTagList; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Admin-only global auction management packet. + */ +public class ManageAuctionPacket extends AbstractPacket { + private static final int PAGE_SIZE = 45; + + private Action action; + private int page; + private boolean flag; + private String text; + private long amount; + private long amount2; + private int amountInt; + private NBTTagCompound data; + + public ManageAuctionPacket() { + } + + private ManageAuctionPacket(Action action) { + this.action = action; + } + + @Override + public Enum getType() { + return EnumRequestPacket.ManageAuctionAction; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_AUCTION; + } + + @Override + public boolean needsNPC() { + return false; + } + + // ========================================= + // Client Send Helpers + // ========================================= + + @SideOnly(Side.CLIENT) + public static void requestListings(AuctionFilter filter, int page) { + ManageAuctionPacket packet = new ManageAuctionPacket(Action.REQUEST_LISTINGS); + packet.page = page; + packet.data = filter != null ? filter.writeToNBT(new NBTTagCompound()) : new NBTTagCompound(); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void requestGlobalClaims(int page) { + ManageAuctionPacket packet = new ManageAuctionPacket(Action.REQUEST_CLAIMS); + packet.page = page; + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void createFakeListing(ItemStack item, String sellerName, long startingPrice, long buyoutPrice, int durationHours) { + if (item == null) return; + + ManageAuctionPacket packet = new ManageAuctionPacket(Action.CREATE_FAKE); + packet.text = sellerName != null ? sellerName : ""; + packet.amount = startingPrice; + packet.amount2 = buyoutPrice; + packet.amountInt = durationHours; + packet.data = new NBTTagCompound(); + item.writeToNBT(packet.data); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void manageListing(String listingId, boolean cancelCompletely) { + ManageAuctionPacket packet = new ManageAuctionPacket(Action.MANAGE_LISTING); + packet.text = listingId; + packet.flag = cancelCompletely; + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void claimGlobalItem(String claimId) { + ManageAuctionPacket packet = new ManageAuctionPacket(Action.CLAIM_ITEM); + packet.text = claimId; + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void claimGlobalCurrency(String claimId) { + ManageAuctionPacket packet = new ManageAuctionPacket(Action.CLAIM_CURRENCY); + packet.text = claimId; + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void openBidding(String listingId) { + ManageAuctionPacket packet = new ManageAuctionPacket(Action.OPEN_BIDDING); + packet.text = listingId; + PacketClient.sendClient(packet); + } + + // ========================================= + // Serialization + // ========================================= + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeInt(action.ordinal()); + + switch (action) { + case REQUEST_LISTINGS: + out.writeInt(page); + ByteBufUtils.writeNBT(out, data); + break; + case REQUEST_CLAIMS: + out.writeInt(page); + break; + case CREATE_FAKE: + ByteBufUtils.writeString(out, text); + out.writeLong(amount); + out.writeLong(amount2); + out.writeInt(amountInt); + ByteBufUtils.writeNBT(out, data); + break; + case MANAGE_LISTING: + ByteBufUtils.writeString(out, text); + out.writeBoolean(flag); + break; + case CLAIM_ITEM: + case CLAIM_CURRENCY: + case OPEN_BIDDING: + ByteBufUtils.writeString(out, text); + break; + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) return; + EntityPlayerMP playerMP = (EntityPlayerMP) player; + + if (!CustomNpcsPermissions.hasCustomPermission(playerMP, "customnpcs.global.auction")) { + sendError(playerMP, "Missing permission: customnpcs.global.auction"); + return; + } + + int actionOrdinal = in.readInt(); + if (actionOrdinal < 0 || actionOrdinal >= Action.values().length) return; + Action requestedAction = Action.values()[actionOrdinal]; + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(playerMP, "Auction system is not available."); + return; + } + + String result = null; + switch (requestedAction) { + case REQUEST_LISTINGS: + handleRequestListings(in, playerMP, controller); + break; + case REQUEST_CLAIMS: + handleRequestClaims(in, playerMP, controller); + break; + case CREATE_FAKE: + result = handleCreateFake(in, playerMP, controller); + break; + case MANAGE_LISTING: + result = handleManageListing(in, playerMP, controller); + break; + case CLAIM_ITEM: + result = handleClaimItem(in, playerMP, controller); + break; + case CLAIM_CURRENCY: + result = handleClaimCurrency(in, playerMP, controller); + break; + case OPEN_BIDDING: + handleOpenBidding(in, playerMP, controller); + break; + } + + if (result != null) { + sendError(playerMP, result); + } + } + + // ========================================= + // Handlers + // ========================================= + + private void handleRequestListings(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + int requestPage = Math.max(0, in.readInt()); + NBTTagCompound filterNbt = ByteBufUtils.readNBT(in); + + AuctionFilter filter = new AuctionFilter(); + if (filterNbt != null) { + filter.readFromNBT(filterNbt); + } + + int totalListings = controller.getTotalActiveListings(filter); + int totalPages = Math.max(1, (int) Math.ceil((double) totalListings / PAGE_SIZE)); + int safePage = Math.min(requestPage, totalPages - 1); + List listings = controller.getActiveListings(filter, safePage, PAGE_SIZE); + + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("ManageAuctionListingsData", true); + response.setInteger("Page", safePage); + response.setInteger("TotalPages", totalPages); + response.setInteger("TotalListings", totalListings); + + NBTTagList list = new NBTTagList(); + for (AuctionListing listing : listings) { + list.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + response.setTag("Listings", list); + GuiDataPacket.sendGuiData(player, response); + } + + private void handleRequestClaims(ByteBuf in, EntityPlayerMP player, AuctionController controller) { + int requestPage = Math.max(0, in.readInt()); + + // Global "trades" page contains global claims + active global listings, + // ordered like My Trades: sold claims, outbid claims, won claims, active listings, expired claims. + List allClaims = controller.getAllGlobalClaims(); + List allListings = controller.getActiveGlobalListings(); + int totalClaims = allClaims.size(); + int totalListings = allListings.size(); + + List soldClaims = new ArrayList(); + List outbidClaims = new ArrayList(); + List wonClaims = new ArrayList(); + List expiredClaims = new ArrayList(); + for (AuctionClaim claim : allClaims) { + if (claim == null) continue; + if (claim.type == EnumClaimType.CURRENCY) { + soldClaims.add(claim); + } else if (claim.type == EnumClaimType.REFUND) { + outbidClaims.add(claim); + } else if (claim.type == EnumClaimType.ITEM) { + if (claim.isReturned) { + expiredClaims.add(claim); + } else { + wonClaims.add(claim); + } + } else { + wonClaims.add(claim); + } + } + + List ordered = new ArrayList(); + appendClaims(ordered, soldClaims); + appendClaims(ordered, outbidClaims); + appendClaims(ordered, wonClaims); + for (AuctionListing listing : allListings) { + ordered.add(Entry.forListing(listing)); + } + appendClaims(ordered, expiredClaims); + + int totalEntries = ordered.size(); + + int totalPages = Math.max(1, (int) Math.ceil((double) totalEntries / PAGE_SIZE)); + int safePage = Math.min(requestPage, totalPages - 1); + + int start = safePage * PAGE_SIZE; + int end = Math.min(totalEntries, start + PAGE_SIZE); + List pageClaims = new ArrayList(); + List pageListings = new ArrayList(); + + for (int i = start; i < end; i++) { + Entry entry = ordered.get(i); + if (entry.claim != null) { + pageClaims.add(entry.claim); + } else if (entry.listing != null) { + pageListings.add(entry.listing); + } + } + + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("ManageAuctionClaimsData", true); + response.setInteger("Page", safePage); + response.setInteger("TotalPages", totalPages); + response.setInteger("TotalClaims", totalClaims); + response.setInteger("TotalListings", totalListings); + response.setInteger("TotalEntries", totalEntries); + + NBTTagList claimList = new NBTTagList(); + for (AuctionClaim claim : pageClaims) { + claimList.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + response.setTag("Claims", claimList); + + NBTTagList listingList = new NBTTagList(); + for (AuctionListing listing : pageListings) { + listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + response.setTag("Listings", listingList); + GuiDataPacket.sendGuiData(player, response); + } + + private static void appendClaims(List out, List claims) { + if (claims == null || claims.isEmpty()) return; + for (AuctionClaim claim : claims) { + out.add(Entry.forClaim(claim)); + } + } + + private String handleCreateFake(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + + String sellerName = ByteBufUtils.readString(in); + long startingPrice = in.readLong(); + long buyoutPrice = in.readLong(); + int durationHours = in.readInt(); + NBTTagCompound itemNbt = ByteBufUtils.readNBT(in); + + if (itemNbt == null) { + return "No item data received."; + } + ItemStack item = ItemStack.loadItemStackFromNBT(itemNbt); + if (item == null) { + return "Invalid item data."; + } + + // Consume staged item from admin inventory like normal auction sell flow. + if (!removeItemFromInventory(player, item)) { + return "Item not found in inventory."; + } + + String result = controller.createGlobalListing(player, sellerName, item, startingPrice, buyoutPrice, durationHours); + if (result != null) { + // Failed create, return item (or drop to avoid loss). + if (!player.inventory.addItemStackToInventory(item.copy())) { + player.entityDropItem(item.copy(), 0.5f); + } + } + if (result == null) { + sendRefresh(player, true); + } + return result; + } + + private String handleManageListing(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String listingId = ByteBufUtils.readString(in); + boolean cancelCompletely = in.readBoolean(); + String result = controller.adminStopListingToGlobal(listingId, player, cancelCompletely); + if (result == null) { + sendRefresh(player, false); + } + return result; + } + + private String handleClaimItem(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String claimId = ByteBufUtils.readString(in); + String result = controller.claimGlobalItem(claimId, player); + if (result == null) { + sendRefresh(player, false); + } + return result; + } + + private String handleClaimCurrency(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String claimId = ByteBufUtils.readString(in); + String result = controller.claimGlobalCurrency(claimId, player); + if (result == null) { + sendRefresh(player, false); + } + return result; + } + + private void handleOpenBidding(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String listingId = ByteBufUtils.readString(in); + AuctionListing listing = controller.getListing(listingId); + if (listing == null || !listing.isActive()) { + sendError(player, "Listing not found or has ended."); + return; + } + + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuctionBidding, npc); + + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("BiddingData", true); + response.setTag("Listing", listing.writeToNBT(new NBTTagCompound())); + response.setLong("Balance", controller.getPlayerBalance(player)); + response.setBoolean("IsOwnListing", listing.isSeller(player.getUniqueID())); + GuiDataPacket.sendGuiData(player, response); + } + + private void sendError(EntityPlayerMP player, String message) { + player.addChatMessage(new net.minecraft.util.ChatComponentText( + net.minecraft.util.EnumChatFormatting.RED + "[Global Auction] " + message)); + } + + private void sendRefresh(EntityPlayerMP player, boolean created) { + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("ManageAuctionRefresh", true); + response.setBoolean("ManageAuctionCreateSuccess", created); + GuiDataPacket.sendGuiData(player, response); + } + + /** + * Checks if two ItemStacks are the same type (item, damage, NBT) ignoring stack size. + */ + private boolean areItemsSameType(ItemStack item1, ItemStack item2) { + if (item1 == null || item2 == null) + return item1 == item2; + if (item1.getItem() != item2.getItem()) + return false; + if (item1.getItemDamage() != item2.getItemDamage()) + return false; + return ItemStack.areItemStackTagsEqual(item1, item2); + } + + private int countItemsInInventory(EntityPlayerMP player, ItemStack target) { + int count = 0; + for (int i = 0; i < player.inventory.mainInventory.length; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack != null && areItemsSameType(stack, target)) { + count += stack.stackSize; + } + } + return count; + } + + /** + * Removes items from inventory, consuming from multiple stacks if needed. + * Returns true if successful, false if not enough items. + */ + private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) { + if (target == null) return false; + + int needed = target.stackSize; + int available = countItemsInInventory(player, target); + if (available < needed) { + return false; + } + + for (int i = 0; i < player.inventory.mainInventory.length && needed > 0; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack == null || !areItemsSameType(stack, target)) + continue; + + if (needed >= stack.stackSize) { + needed -= stack.stackSize; + player.inventory.mainInventory[i] = null; + } else { + stack.splitStack(needed); + if (stack.stackSize <= 0) { + player.inventory.setInventorySlotContents(i, null); + } + needed = 0; + } + } + + player.inventory.markDirty(); + return true; + } + + private static class Entry { + final AuctionClaim claim; + final AuctionListing listing; + + private Entry(AuctionClaim claim, AuctionListing listing) { + this.claim = claim; + this.listing = listing; + } + + static Entry forClaim(AuctionClaim claim) { + return new Entry(claim, null); + } + + static Entry forListing(AuctionListing listing) { + return new Entry(null, listing); + } + } + + private enum Action { + REQUEST_LISTINGS, + REQUEST_CLAIMS, + CREATE_FAKE, + MANAGE_LISTING, + CLAIM_ITEM, + CLAIM_CURRENCY, + OPEN_BIDDING + } +} diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index e059e9104..6229b0f4e 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -25,6 +25,7 @@ import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; import noppes.npcs.containers.ContainerMail; import noppes.npcs.containers.ContainerManageBanks; +import noppes.npcs.containers.ContainerManageAuction; import noppes.npcs.containers.ContainerManageRecipes; import noppes.npcs.containers.ContainerMerchantAdd; import noppes.npcs.containers.ContainerNPCBankLarge; @@ -136,6 +137,9 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.ManageBanks) return new ContainerManageBanks(player); + if (gui == EnumGuiType.ManageAuction) + return new ContainerManageAuction(player); + if (gui == EnumGuiType.MerchantAdd) return new ContainerMerchantAdd(player, ServerEventsHandler.Merchant, player.worldObj); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index af9b2b9af..d27a509e5 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -69,6 +69,7 @@ public class CustomNpcsPermissions { public static final Permission GLOBAL_MAGIC = new Permission("customnpcs.global.magic"); public static final Permission GLOBAL_ABILITY = new Permission("customnpcs.global.ability"); public static final Permission GLOBAL_EFFECT = new Permission("customnpcs.global.effect"); + public static final Permission GLOBAL_AUCTION = new Permission("customnpcs.global.auction"); public static final Permission SPAWNER_MOB = new Permission("customnpcs.spawner.mob"); public static final Permission SPAWNER_CREATE = new Permission("customnpcs.spawner.create"); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 1cff1e838..44ccd806a 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -117,6 +117,7 @@ import noppes.npcs.client.gui.global.GuiNPCManageQuest; import noppes.npcs.client.gui.global.GuiNPCManageTags; import noppes.npcs.client.gui.global.GuiNPCManageTransporters; import noppes.npcs.client.gui.global.GuiNpcManageAbilities; +import noppes.npcs.client.gui.global.GuiNpcManageAuction; import noppes.npcs.client.gui.global.GuiNpcManageMagic; import noppes.npcs.client.gui.global.GuiNpcManageRecipes; import noppes.npcs.client.gui.global.GuiNpcQuestReward; @@ -219,6 +220,7 @@ import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; import noppes.npcs.containers.ContainerMail; import noppes.npcs.containers.ContainerManageBanks; +import noppes.npcs.containers.ContainerManageAuction; import noppes.npcs.containers.ContainerManageRecipes; import noppes.npcs.containers.ContainerNPCBankInterface; import noppes.npcs.containers.ContainerNPCCompanion; @@ -510,6 +512,9 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.ManageRecipes) return new GuiNpcManageRecipes(npc, (ContainerManageRecipes) container); + else if (gui == EnumGuiType.ManageAuction) + return new GuiNpcManageAuction(npc, (ContainerManageAuction) container); + else if (gui == EnumGuiType.ManageDialogs) return new GuiNPCManageDialogs(npc); diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAuction.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAuction.java new file mode 100644 index 000000000..84e16d24e --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAuction.java @@ -0,0 +1,1096 @@ +package noppes.npcs.client.gui.global; + +import kamkeel.npcs.network.packets.request.auction.ManageAuctionPacket; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.CustomNpcResourceListener; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.containers.ContainerManageAuction; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionFilter; +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; +import java.util.List; + +public class GuiNpcManageAuction extends GuiContainerNPCInterface2 implements IGuiData, ITextfieldListener { + private static final ResourceLocation SLOT_DEFAULT = new ResourceLocation("customnpcs", "textures/gui/slot.png"); + private static final ResourceLocation SLOT_AUCTION = new ResourceLocation("customnpcs", "textures/gui/auction/auction_slot.png"); + private static final ResourceLocation ICON_X = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon.png"); + private static final ResourceLocation ICON_X_CANCEL = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon_blue.png"); + private static final ResourceLocation ICON_CHECK = new ResourceLocation("customnpcs", "textures/gui/auction/check_icon.png"); + private static final ResourceLocation COIN_ICON = new ResourceLocation("customnpcs", "textures/items/npcCoinGold.png"); + + // Slot tints (ARGB) aligned with My Trades style. + private static final int TINT_BLUE = 0x605080FF; + private static final int TINT_GREEN = 0x6050FF50; + private static final int TINT_RED = 0x60FF5050; + + public enum Tab { + LISTINGS, + CREATE, + CLAIMS + } + + private enum PendingAction { + NONE, + STOP, + CANCEL, + CLAIM_ITEM, + CLAIM_CURRENCY + } + + private static final int BTN_TAB_LISTINGS = 100; + private static final int BTN_TAB_CREATE = 101; + private static final int BTN_TAB_CLAIMS = 102; + private static final int BTN_PAGE_PREV = 110; + private static final int BTN_PAGE_NEXT = 111; + private static final int BTN_SORT = 112; + private static final int BTN_CREATE = 120; + + private static final int TXT_SEARCH = 1; + private static final int TXT_FAKE_SELLER = 2; + private static final int TXT_FAKE_START = 3; + private static final int TXT_FAKE_BUYOUT = 4; + private static final int TXT_FAKE_DURATION = 5; + + private static final int RIGHT_X = 188; + private static final int RIGHT_WIDTH = 220; + private static final int TAB_Y = 10; + + private static final int LISTINGS_SORT_Y = 40; + private static final int LISTINGS_SEARCH_LABEL_Y = 66; + private static final int LISTINGS_SEARCH_FIELD_Y = 76; + private static final int LISTINGS_HINT_Y = 98; + private static final int LISTINGS_HINT2_Y = 110; + private static final int LISTINGS_HINT3_Y = 122; + private static final int LISTINGS_HINT4_Y = 134; + private static final int LISTINGS_DETAILS_TITLE_Y = 42; + private static final int DETAILS_PANEL_X = RIGHT_X; + private static final int DETAILS_PANEL_Y = 40; + private static final int DETAILS_PANEL_W = RIGHT_WIDTH - 8; + private static final int DETAILS_PANEL_H = 138; + + private static final int CREATE_FIELD_X = RIGHT_X + 82; + private static final int CREATE_FIELD_WIDTH = 126; + private static final int CREATE_SELLER_Y = 52; + private static final int CREATE_START_Y = 78; + private static final int CREATE_BUYOUT_Y = 104; + private static final int CREATE_DURATION_Y = 130; + private static final int CREATE_BUTTON_Y = 178; + + private final ContainerManageAuction manageContainer; + private final AuctionFilter filter = new AuctionFilter(); + + private Tab activeTab = Tab.LISTINGS; + private String searchText = ""; + + private String fakeSeller = "Server"; + private String fakeStartPrice = "1"; + private String fakeBuyoutPrice = ""; + private String fakeDurationHours = Integer.toString(Math.max(1, AuctionClientConfig.getAuctionDurationHours())); + + private int listingsPage = 0; + private int listingsTotalPages = 1; + private int listingsTotalCount = 0; + + private int claimsPage = 0; + private int claimsTotalPages = 1; + private int claimsTotalCount = 0; + private int claimsListingCount = 0; + private int claimsClaimCount = 0; + + private int pendingDisplaySlot = -1; + private PendingAction pendingAction = PendingAction.NONE; + + private String selectedListingId = null; + private AuctionListing selectedListing = null; + private int selectedDisplaySlot = -1; + private boolean listingsDetailsView = false; + + private String errorMessage = null; + private boolean createRequestPending = false; + + public GuiNpcManageAuction(EntityNPCInterface npc, ContainerManageAuction container) { + super(npc, container); + this.manageContainer = container; + this.drawDefaultBackground = false; + this.ySize = 200; + this.title = ""; + } + + @Override + public void initGui() { + super.initGui(); + + addButton(new GuiNpcButton(BTN_TAB_LISTINGS, guiLeft + RIGHT_X, guiTop + TAB_Y, 70, 20, "global.auction.manage.listings")); + addButton(new GuiNpcButton(BTN_TAB_CREATE, guiLeft + RIGHT_X + 74, guiTop + TAB_Y, 70, 20, "global.auction.manage.create")); + addButton(new GuiNpcButton(BTN_TAB_CLAIMS, guiLeft + RIGHT_X + 148, guiTop + TAB_Y, 70, 20, "global.auction.manage.claims")); + updateTabButtonState(); + + int pageButtonY = guiTop + ySize - 24; + addButton(new GuiNpcButton(BTN_PAGE_PREV, guiLeft + RIGHT_X, pageButtonY, 20, 20, "<")); + addButton(new GuiNpcButton(BTN_PAGE_NEXT, guiLeft + RIGHT_X + 24, pageButtonY, 20, 20, ">")); + + if (activeTab == Tab.LISTINGS && !listingsDetailsView) { + String sortName = filter.getSortBy().getDisplayName(); + addButton(new GuiNpcButton(BTN_SORT, guiLeft + RIGHT_X, guiTop + LISTINGS_SORT_Y, RIGHT_WIDTH - 8, 20, + StatCollector.translateToLocal("global.auction.manage.sort") + ": " + sortName)); + + addTextField(new GuiNpcTextField(TXT_SEARCH, this, fontRendererObj, + guiLeft + RIGHT_X, guiTop + LISTINGS_SEARCH_FIELD_Y, RIGHT_WIDTH - 8, 18, searchText)); + requestListings(); + } else if (activeTab == Tab.LISTINGS) { + manageContainer.setDisplayMode(ContainerManageAuction.DisplayMode.LISTINGS); + } else if (activeTab == Tab.CREATE) { + addTextField(new GuiNpcTextField(TXT_FAKE_SELLER, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_SELLER_Y, CREATE_FIELD_WIDTH, 18, fakeSeller)); + + GuiNpcTextField startField = new GuiNpcTextField(TXT_FAKE_START, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_START_Y, CREATE_FIELD_WIDTH, 18, fakeStartPrice); + startField.setIntegersOnly(); + addTextField(startField); + + GuiNpcTextField buyoutField = new GuiNpcTextField(TXT_FAKE_BUYOUT, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_BUYOUT_Y, CREATE_FIELD_WIDTH, 18, fakeBuyoutPrice); + buyoutField.setIntegersOnly(); + addTextField(buyoutField); + + GuiNpcTextField durationField = new GuiNpcTextField(TXT_FAKE_DURATION, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_DURATION_Y, CREATE_FIELD_WIDTH, 18, fakeDurationHours); + durationField.setIntegersOnly(); + addTextField(durationField); + + addButton(new GuiNpcButton(BTN_CREATE, guiLeft + RIGHT_X, guiTop + CREATE_BUTTON_Y, RIGHT_WIDTH - 8, 20, + "global.auction.manage.create.submit")); + } else if (activeTab == Tab.CLAIMS) { + requestClaims(); + } + + syncContainerSlots(); + updatePageButtons(); + } + + private void switchTab(Tab tab) { + if (activeTab == tab) return; + activeTab = tab; + listingsDetailsView = false; + if (tab != Tab.LISTINGS) { + selectedListing = null; + selectedListingId = null; + selectedDisplaySlot = -1; + } + clearPending(); + errorMessage = null; + initGui(); + } + + private void updateTabButtonState() { + GuiNpcButton listingsBtn = getButton(BTN_TAB_LISTINGS); + GuiNpcButton createBtn = getButton(BTN_TAB_CREATE); + GuiNpcButton claimsBtn = getButton(BTN_TAB_CLAIMS); + if (listingsBtn != null) { + listingsBtn.enabled = activeTab != Tab.LISTINGS || listingsDetailsView; + } + if (createBtn != null) { + createBtn.enabled = activeTab != Tab.CREATE; + } + if (claimsBtn != null) { + claimsBtn.enabled = activeTab != Tab.CLAIMS; + } + } + + private void syncContainerSlots() { + boolean showCreate = activeTab == Tab.CREATE; + manageContainer.setCreateSlotVisible(showCreate); + + boolean showDetail = activeTab == Tab.LISTINGS && listingsDetailsView && selectedListing != null; + manageContainer.setDetailSlotVisible(showDetail); + if (showDetail) { + manageContainer.setDetailItem(selectedListing.item); + } else { + manageContainer.clearDetailItem(); + } + } + + private void updatePageButtons() { + GuiNpcButton prev = getButton(BTN_PAGE_PREV); + GuiNpcButton next = getButton(BTN_PAGE_NEXT); + if (prev == null || next == null) return; + + if (activeTab == Tab.CREATE) { + prev.visible = false; + next.visible = false; + prev.enabled = false; + next.enabled = false; + return; + } + + if (activeTab == Tab.LISTINGS && listingsDetailsView) { + prev.visible = false; + next.visible = false; + prev.enabled = false; + next.enabled = false; + return; + } + + prev.visible = true; + next.visible = true; + if (activeTab == Tab.LISTINGS) { + prev.enabled = listingsPage > 0; + next.enabled = listingsPage < listingsTotalPages - 1; + } else { + prev.enabled = claimsPage > 0; + next.enabled = claimsPage < claimsTotalPages - 1; + } + } + + private void requestListings() { + filter.setSearchText(searchText); + manageContainer.setDisplayMode(ContainerManageAuction.DisplayMode.LISTINGS); + ManageAuctionPacket.requestListings(filter, listingsPage); + } + + private void requestClaims() { + manageContainer.setDisplayMode(ContainerManageAuction.DisplayMode.CLAIMS); + ManageAuctionPacket.requestGlobalClaims(claimsPage); + } + + private void cycleSort() { + EnumAuctionSort[] values = EnumAuctionSort.values(); + int next = (filter.getSortBy().ordinal() + 1) % values.length; + filter.setSortBy(values[next]); + listingsPage = 0; + clearPending(); + requestListings(); + initGui(); + } + + private void createFakeAuction() { + GuiNpcTextField sellerField = getTextField(TXT_FAKE_SELLER); + GuiNpcTextField startField = getTextField(TXT_FAKE_START); + GuiNpcTextField buyoutField = getTextField(TXT_FAKE_BUYOUT); + GuiNpcTextField durationField = getTextField(TXT_FAKE_DURATION); + + if (sellerField != null) fakeSeller = sellerField.getText(); + if (startField != null) fakeStartPrice = startField.getText(); + if (buyoutField != null) fakeBuyoutPrice = buyoutField.getText(); + if (durationField != null) fakeDurationHours = durationField.getText(); + + ItemStack staged = manageContainer.getCreateItem(); + if (staged == null) { + errorMessage = StatCollector.translateToLocal("global.auction.manage.error.noItem"); + return; + } + + long start = parseLong(fakeStartPrice, 0); + long buyout = parseLong(fakeBuyoutPrice, 0); + int duration = (int) parseLong(fakeDurationHours, AuctionClientConfig.getAuctionDurationHours()); + if (duration <= 0) { + duration = Math.max(1, AuctionClientConfig.getAuctionDurationHours()); + } + + if (start <= 0) { + errorMessage = StatCollector.translateToLocal("global.auction.manage.error.invalidStart"); + return; + } + if (buyout > 0 && buyout < start) { + errorMessage = StatCollector.translateToLocal("global.auction.manage.error.invalidBuyout"); + return; + } + + String seller = fakeSeller != null ? fakeSeller.trim() : ""; + if (seller.isEmpty()) { + seller = "Server"; + fakeSeller = seller; + } + + ManageAuctionPacket.createFakeListing(staged, seller, start, buyout, duration); + createRequestPending = true; + errorMessage = null; + } + + private long parseLong(String value, long fallback) { + if (value == null || value.trim().isEmpty()) return fallback; + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException ignored) { + return fallback; + } + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + super.actionPerformed(guibutton); + if (!(guibutton instanceof GuiNpcButton)) return; + + if (guibutton.id == BTN_TAB_LISTINGS) { + if (activeTab == Tab.LISTINGS && listingsDetailsView) { + listingsDetailsView = false; + selectedListing = null; + selectedListingId = null; + selectedDisplaySlot = -1; + clearPending(); + initGui(); + } else { + switchTab(Tab.LISTINGS); + } + return; + } + if (guibutton.id == BTN_TAB_CREATE) { + switchTab(Tab.CREATE); + return; + } + if (guibutton.id == BTN_TAB_CLAIMS) { + switchTab(Tab.CLAIMS); + return; + } + + if (guibutton.id == BTN_PAGE_PREV) { + if (activeTab == Tab.LISTINGS && listingsPage > 0) { + listingsPage--; + clearPending(); + requestListings(); + NoppesUtil.clickSound(); + } else if (activeTab == Tab.CLAIMS && claimsPage > 0) { + claimsPage--; + clearPending(); + requestClaims(); + NoppesUtil.clickSound(); + } + updatePageButtons(); + return; + } + + if (guibutton.id == BTN_PAGE_NEXT) { + if (activeTab == Tab.LISTINGS && listingsPage < listingsTotalPages - 1) { + listingsPage++; + clearPending(); + requestListings(); + NoppesUtil.clickSound(); + } else if (activeTab == Tab.CLAIMS && claimsPage < claimsTotalPages - 1) { + claimsPage++; + clearPending(); + requestClaims(); + NoppesUtil.clickSound(); + } + updatePageButtons(); + return; + } + + if (guibutton.id == BTN_SORT) { + cycleSort(); + NoppesUtil.clickSound(); + return; + } + + if (guibutton.id == BTN_CREATE) { + createFakeAuction(); + NoppesUtil.clickSound(); + } + } + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + if (slot == null) { + clearPending(); + return; + } + + if (manageContainer.isDisplaySlot(slotIndex)) { + int displayIndex = manageContainer.toDisplayIndex(slotIndex); + if (activeTab == Tab.LISTINGS) { + handleListingsSlotClick(displayIndex, mouseButton); + return; + } + if (activeTab == Tab.CLAIMS) { + handleClaimsSlotClick(displayIndex, mouseButton); + return; + } + } + + if (activeTab == Tab.CREATE) { + if (manageContainer.isCreateSlot(slotIndex)) { + if (manageContainer.getCreateItem() != null) { + boolean removeAll = mouseButton == 0; + manageContainer.removeFromCreateSlot(removeAll); + NoppesUtil.clickSound(); + } + return; + } + + if (manageContainer.isPlayerSlot(slotIndex)) { + ItemStack stack = slot.getStack(); + if (stack != null) { + boolean fullStack = mouseButton == 0; + manageContainer.addToCreateSlot(slotIndex, fullStack); + NoppesUtil.clickSound(); + } + return; + } + } + + clearPending(); + } + + private void handleListingsSlotClick(int displayIndex, int mouseButton) { + AuctionListing listing = manageContainer.getListingAtDisplay(displayIndex); + if (listing == null) { + clearPending(); + return; + } + + // Any non-matching input while pending cancels the operation. + if (pendingAction != PendingAction.NONE) { + if (displayIndex == pendingDisplaySlot && isConfirmInput(pendingAction, mouseButton)) { + ManageAuctionPacket.manageListing(listing.id, pendingAction == PendingAction.CANCEL); + clearPending(); + playConfirmSound(); + } else { + clearPending(); + } + return; + } + + if (mouseButton == 0) { + selectListing(displayIndex, listing); + clearPending(); + listingsDetailsView = true; + initGui(); + NoppesUtil.clickSound(); + return; + } + + if (isStopStartInput(mouseButton) || isCancelStartInput(mouseButton)) { + PendingAction action = isShiftKeyDown() ? PendingAction.CANCEL : PendingAction.STOP; + setPending(displayIndex, action); + NoppesUtil.clickSound(); + return; + } + + clearPending(); + } + + private void handleClaimsSlotClick(int displayIndex, int mouseButton) { + AuctionListing listing = manageContainer.getListingAtDisplay(displayIndex); + if (listing != null) { + if (pendingAction != PendingAction.NONE) { + if (displayIndex == pendingDisplaySlot && isConfirmInput(pendingAction, mouseButton)) { + ManageAuctionPacket.manageListing(listing.id, pendingAction == PendingAction.CANCEL); + clearPending(); + playConfirmSound(); + } else { + clearPending(); + } + return; + } + + if (isStopStartInput(mouseButton) || isCancelStartInput(mouseButton)) { + PendingAction action = isShiftKeyDown() ? PendingAction.CANCEL : PendingAction.STOP; + setPending(displayIndex, action); + NoppesUtil.clickSound(); + } else { + clearPending(); + } + return; + } + + AuctionClaim claim = manageContainer.getClaimAtDisplay(displayIndex); + if (claim == null) { + clearPending(); + return; + } + + PendingAction action = claim.type.isItem() ? PendingAction.CLAIM_ITEM : PendingAction.CLAIM_CURRENCY; + if (pendingAction != PendingAction.NONE) { + if (displayIndex == pendingDisplaySlot && pendingAction == action && isConfirmInput(action, mouseButton)) { + if (action == PendingAction.CLAIM_ITEM) { + ManageAuctionPacket.claimGlobalItem(claim.id); + } else { + ManageAuctionPacket.claimGlobalCurrency(claim.id); + } + clearPending(); + playConfirmSound(); + } else { + clearPending(); + } + return; + } + + if (mouseButton == 0) { + setPending(displayIndex, action); + NoppesUtil.clickSound(); + return; + } + + clearPending(); + } + + private boolean isStopStartInput(int mouseButton) { + return mouseButton == 1 && !isShiftKeyDown(); + } + + private boolean isCancelStartInput(int mouseButton) { + return mouseButton == 1 && isShiftKeyDown(); + } + + private boolean isConfirmInput(PendingAction action, int mouseButton) { + if (action == PendingAction.CLAIM_ITEM || action == PendingAction.CLAIM_CURRENCY) { + return mouseButton == 0; + } + if (action == PendingAction.STOP) { + return mouseButton == 1 && !isShiftKeyDown(); + } + if (action == PendingAction.CANCEL) { + return mouseButton == 1 && isShiftKeyDown(); + } + return false; + } + + private void selectListing(int displayIndex, AuctionListing listing) { + selectedDisplaySlot = displayIndex; + selectedListingId = listing != null ? listing.id : null; + selectedListing = listing; + syncContainerSlots(); + updateTabButtonState(); + } + + private void refreshSelectedListing(List pageListings) { + selectedListing = null; + selectedDisplaySlot = -1; + + if (selectedListingId == null || pageListings == null) { + syncContainerSlots(); + return; + } + + for (int i = 0; i < pageListings.size(); i++) { + AuctionListing listing = pageListings.get(i); + if (listing != null && selectedListingId.equals(listing.id)) { + selectedListing = listing; + selectedDisplaySlot = i; + break; + } + } + + if (selectedListing == null) { + selectedListingId = null; + } + + if (selectedListing == null) { + listingsDetailsView = false; + } + + syncContainerSlots(); + updateTabButtonState(); + } + + private void setPending(int displaySlot, PendingAction action) { + this.pendingDisplaySlot = displaySlot; + this.pendingAction = action; + manageContainer.setHiddenDisplaySlot(displaySlot); + } + + private void clearPending() { + manageContainer.clearHiddenDisplaySlot(); + pendingDisplaySlot = -1; + pendingAction = PendingAction.NONE; + } + + private void playConfirmSound() { + mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a( + new ResourceLocation("random.orb"), 1.0F)); + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + + if (activeTab == Tab.LISTINGS) { + GuiNpcTextField search = getTextField(TXT_SEARCH); + if (search != null && search.isFocused()) { + String text = search.getText(); + if (!text.equals(searchText)) { + searchText = text; + listingsPage = 0; + clearPending(); + requestListings(); + } + } + } + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield == null) return; + + if (textfield.id == TXT_SEARCH) { + searchText = textfield.getText(); + } else if (textfield.id == TXT_FAKE_SELLER) { + fakeSeller = textfield.getText(); + } else if (textfield.id == TXT_FAKE_START) { + fakeStartPrice = textfield.getText(); + } else if (textfield.id == TXT_FAKE_BUYOUT) { + fakeBuyoutPrice = textfield.getText(); + } else if (textfield.id == TXT_FAKE_DURATION) { + fakeDurationHours = textfield.getText(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("ManageAuctionListingsData")) { + listingsPage = compound.getInteger("Page"); + listingsTotalPages = Math.max(1, compound.getInteger("TotalPages")); + listingsTotalCount = compound.getInteger("TotalListings"); + + NBTTagList list = compound.getTagList("Listings", 10); + List listings = new ArrayList(); + for (int i = 0; i < list.tagCount(); i++) { + listings.add(AuctionListing.fromNBT(list.getCompoundTagAt(i))); + } + manageContainer.setListingsPage(listings); + refreshSelectedListing(listings); + updatePageButtons(); + } + + if (compound.hasKey("ManageAuctionClaimsData")) { + claimsPage = compound.getInteger("Page"); + claimsTotalPages = Math.max(1, compound.getInteger("TotalPages")); + claimsTotalCount = compound.hasKey("TotalEntries") ? compound.getInteger("TotalEntries") : compound.getInteger("TotalClaims"); + claimsClaimCount = compound.getInteger("TotalClaims"); + claimsListingCount = compound.hasKey("TotalListings") ? compound.getInteger("TotalListings") : 0; + + NBTTagList claimList = compound.getTagList("Claims", 10); + List claims = new ArrayList(); + for (int i = 0; i < claimList.tagCount(); i++) { + claims.add(AuctionClaim.fromNBT(claimList.getCompoundTagAt(i))); + } + + NBTTagList listingList = compound.getTagList("Listings", 10); + List listings = new ArrayList(); + for (int i = 0; i < listingList.tagCount(); i++) { + listings.add(AuctionListing.fromNBT(listingList.getCompoundTagAt(i))); + } + + manageContainer.setClaimsAndListingsPage(claims, listings); + updatePageButtons(); + } + + if (compound.getBoolean("ManageAuctionRefresh")) { + boolean createSuccess = compound.getBoolean("ManageAuctionCreateSuccess"); + createRequestPending = false; + if (createSuccess) { + manageContainer.clearCreateSlot(); + listingsDetailsView = false; + selectedListing = null; + selectedListingId = null; + selectedDisplaySlot = -1; + activeTab = Tab.LISTINGS; + clearPending(); + initGui(); + return; + } + + clearPending(); + if (activeTab == Tab.LISTINGS) { + requestListings(); + } else if (activeTab == Tab.CLAIMS) { + requestClaims(); + } + } + } + + @Override + protected void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) { + super.drawGuiContainerBackgroundLayer(f, mouseX, mouseY); + + drawDisplaySlotBackgrounds(); + drawPlayerInventorySlotBackgrounds(); + drawRightPanelSlotBackgrounds(); + drawClaimsTintOverlay(); + drawSelectedListingOverlay(); + drawClaimIndicators(); + drawPendingOverlay(); + } + + private void drawDisplaySlotBackgrounds() { + for (int row = 0; row < ContainerManageAuction.DISPLAY_ROWS; row++) { + for (int col = 0; col < ContainerManageAuction.DISPLAY_COLS; col++) { + int x = guiLeft + ContainerManageAuction.DISPLAY_X + col * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + row * 18; + drawAuctionSlot(x, y); + } + } + } + + private void drawPlayerInventorySlotBackgrounds() { + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int x = guiLeft + ContainerManageAuction.PLAYER_INV_X + col * 18; + int y = guiTop + ContainerManageAuction.PLAYER_INV_Y + row * 18; + drawDefaultSlot(x, y); + } + } + for (int col = 0; col < 9; col++) { + int x = guiLeft + ContainerManageAuction.PLAYER_INV_X + col * 18; + int y = guiTop + ContainerManageAuction.HOTBAR_Y; + drawDefaultSlot(x, y); + } + } + + private void drawRightPanelSlotBackgrounds() { + if (activeTab == Tab.LISTINGS && listingsDetailsView) { + drawDetailsPanelBackground(); + } + if (activeTab == Tab.LISTINGS && listingsDetailsView && selectedListing != null) { + drawAuctionSlot(guiLeft + ContainerManageAuction.DETAIL_SLOT_X, guiTop + ContainerManageAuction.DETAIL_SLOT_Y); + } else if (activeTab == Tab.CREATE) { + drawAuctionSlot(guiLeft + ContainerManageAuction.CREATE_SLOT_X, guiTop + ContainerManageAuction.CREATE_SLOT_Y); + } + } + + private void drawDetailsPanelBackground() { + int x = guiLeft + DETAILS_PANEL_X; + int y = guiTop + DETAILS_PANEL_Y; + int x2 = x + DETAILS_PANEL_W; + int y2 = y + DETAILS_PANEL_H; + + Gui.drawRect(x, y, x2, y2, 0xCC0F0F0F); + Gui.drawRect(x + 1, y + 1, x2 - 1, y2 - 1, 0x88202020); + Gui.drawRect(x, y, x2, y + 1, 0xFF3A3A3A); + Gui.drawRect(x, y2 - 1, x2, y2, 0xFF3A3A3A); + Gui.drawRect(x, y, x + 1, y2, 0xFF3A3A3A); + Gui.drawRect(x2 - 1, y, x2, y2, 0xFF3A3A3A); + } + + private void drawSelectedListingOverlay() { + if (activeTab != Tab.LISTINGS || selectedDisplaySlot < 0 || selectedDisplaySlot >= ContainerManageAuction.DISPLAY_SLOT_COUNT) { + return; + } + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (selectedDisplaySlot % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (selectedDisplaySlot / ContainerManageAuction.DISPLAY_COLS) * 18; + drawColoredOverlay(x, y, 0x503080FF); + Gui.drawRect(x, y, x + 18, y + 1, 0xFF8AC4FF); + Gui.drawRect(x, y + 17, x + 18, y + 18, 0xFF8AC4FF); + Gui.drawRect(x, y, x + 1, y + 18, 0xFF8AC4FF); + Gui.drawRect(x + 17, y, x + 18, y + 18, 0xFF8AC4FF); + } + + private void drawClaimIndicators() { + if (activeTab != Tab.CLAIMS) return; + + for (int i = 0; i < ContainerManageAuction.DISPLAY_SLOT_COUNT; i++) { + AuctionClaim claim = manageContainer.getClaimAtDisplay(i); + if (claim == null) continue; + + boolean drawCoin = claim.type == EnumClaimType.CURRENCY + || (claim.type == EnumClaimType.REFUND && claim.item == null); + if (!drawCoin) continue; + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (i % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (i / ContainerManageAuction.DISPLAY_COLS) * 18; + drawIconOverlay(x, y, COIN_ICON); + } + } + + private void drawClaimsTintOverlay() { + if (activeTab != Tab.CLAIMS) return; + for (int i = 0; i < ContainerManageAuction.DISPLAY_SLOT_COUNT; i++) { + int tint = getClaimsSlotTint(i); + if (tint == 0) continue; + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (i % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (i / ContainerManageAuction.DISPLAY_COLS) * 18; + drawColoredOverlay(x, y, tint); + } + } + + private int getClaimsSlotTint(int displayIndex) { + AuctionListing listing = manageContainer.getListingAtDisplay(displayIndex); + if (listing != null) { + return TINT_BLUE; + } + + AuctionClaim claim = manageContainer.getClaimAtDisplay(displayIndex); + if (claim == null) return 0; + + if (claim.type == EnumClaimType.CURRENCY) { + return TINT_GREEN; + } + if (claim.type == EnumClaimType.REFUND) { + return TINT_RED; + } + if (claim.type == EnumClaimType.ITEM) { + return claim.isReturned ? TINT_RED : TINT_GREEN; + } + return TINT_GREEN; + } + + private void drawPendingOverlay() { + if (pendingDisplaySlot < 0 || pendingAction == PendingAction.NONE) return; + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (pendingDisplaySlot % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (pendingDisplaySlot / ContainerManageAuction.DISPLAY_COLS) * 18; + + int color = 0x80FFFF00; + ResourceLocation icon = ICON_X; + float iconR = 1f; + float iconG = 1f; + float iconB = 1f; + if (pendingAction == PendingAction.STOP) { + color = 0x805080FF; + icon = ICON_X; + } else if (pendingAction == PendingAction.CANCEL) { + color = 0x80A040D0; + icon = ICON_X_CANCEL; + } else if (pendingAction == PendingAction.CLAIM_ITEM || pendingAction == PendingAction.CLAIM_CURRENCY) { + color = 0x8055FF55; + icon = ICON_CHECK; + } + + drawColoredOverlay(x, y, color); + drawIconOverlay(x, y, icon, iconR, iconG, iconB, 1f); + } + + private void drawAuctionSlot(int x, int y) { + GL11.glColor4f(1f, 1f, 1f, 1f); + mc.renderEngine.bindTexture(SLOT_AUCTION); + Gui.func_146110_a(x, y, 0, 0, 18, 18, 18, 18); + } + + private void drawDefaultSlot(int x, int y) { + GL11.glColor4f(1f, 1f, 1f, 1f); + mc.renderEngine.bindTexture(SLOT_DEFAULT); + drawTexturedModalRect(x, y, 0, 0, 18, 18); + } + + private void drawColoredOverlay(int x, int y, int color) { + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + Gui.drawRect(x + 1, y + 1, x + 17, y + 17, color); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_DEPTH_TEST); + GL11.glColor4f(1f, 1f, 1f, 1f); + } + + private void drawIconOverlay(int x, int y, ResourceLocation icon) { + drawIconOverlay(x, y, icon, 1f, 1f, 1f, 1f); + } + + private void drawIconOverlay(int x, int y, ResourceLocation icon, float r, float g, float b, float a) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + mc.renderEngine.bindTexture(icon); + Gui.func_146110_a(x + 1, y + 1, 0, 0, 16, 16, 16, 16); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + int textColor = CustomNpcResourceListener.DefaultTextColor; + + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.title"), 8, 6, textColor); + + if (activeTab == Tab.LISTINGS) { + if (listingsDetailsView) { + drawListingsDetails(textColor); + } else { + drawPageText(listingsPage, listingsTotalPages, listingsTotalCount, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.search"), RIGHT_X, LISTINGS_SEARCH_LABEL_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.listings"), RIGHT_X, LISTINGS_HINT_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.stop"), RIGHT_X, LISTINGS_HINT2_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.cancel"), RIGHT_X, LISTINGS_HINT3_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.selectListing"), RIGHT_X, LISTINGS_HINT4_Y, textColor); + } + } else if (activeTab == Tab.CLAIMS) { + drawPageText(claimsPage, claimsTotalPages, claimsTotalCount, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.claims"), RIGHT_X, 58, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.stop"), RIGHT_X, 70, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.cancel"), RIGHT_X, 82, textColor); + String totals = claimsClaimCount + " claims | " + claimsListingCount + " listings"; + fontRendererObj.drawString(totals, RIGHT_X, 94, textColor); + } else { + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.seller"), RIGHT_X, CREATE_SELLER_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.startPrice"), RIGHT_X, CREATE_START_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.buyoutPrice"), RIGHT_X, CREATE_BUYOUT_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.durationHours"), RIGHT_X, CREATE_DURATION_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.stagedItem"), RIGHT_X + 22, ContainerManageAuction.CREATE_SLOT_Y + 5, textColor); + + ItemStack staged = manageContainer.getCreateItem(); + if (staged != null) { + int available = manageContainer.countItemInPlayerInventory(staged); + String countText = staged.stackSize + "/" + available; + int countColor = staged.stackSize <= available ? 0x55FF55 : 0xFF5555; + fontRendererObj.drawStringWithShadow(countText, RIGHT_X + 22, ContainerManageAuction.CREATE_SLOT_Y + 16, countColor); + } + } + + if (errorMessage != null && !errorMessage.isEmpty()) { + fontRendererObj.drawString(EnumChatFormatting.RED + errorMessage, RIGHT_X, ySize - 12, 0xFFFFFF); + } + + drawDynamicTooltip(mouseX, mouseY); + } + + private void drawPageText(int page, int totalPages, int totalCount, int textColor) { + String pageText = (page + 1) + "/" + totalPages + " (" + totalCount + ")"; + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.page") + ": " + pageText, + RIGHT_X + 48, ySize - 18, textColor); + } + + private void drawListingsDetails(int textColor) { + int panelLeft = DETAILS_PANEL_X + 6; + int panelRight = DETAILS_PANEL_X + DETAILS_PANEL_W - 6; + int y = LISTINGS_DETAILS_TITLE_Y; + + fontRendererObj.drawString(EnumChatFormatting.AQUA + StatCollector.translateToLocal("global.auction.manage.details"), panelLeft, y, 0xFFFFFF); + y += 12; + + if (selectedListing == null) { + fontRendererObj.drawString(EnumChatFormatting.GRAY + StatCollector.translateToLocal("global.auction.manage.selectListing"), panelLeft, y, 0xFFFFFF); + return; + } + + y = ContainerManageAuction.DETAIL_SLOT_Y + 1; + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller").replace("%s", ""), + EnumChatFormatting.WHITE + selectedListing.sellerName); + + y += 11; + long currentValue = selectedListing.hasBids() ? selectedListing.currentBid : selectedListing.startingPrice; + String currentLabel = selectedListing.hasBids() + ? StatCollector.translateToLocal("auction.info.currentBid") + : StatCollector.translateToLocal("auction.info.startingPrice"); + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.YELLOW + currentLabel, + EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(currentValue)); + + y += 11; + if (selectedListing.hasBuyout()) { + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.info.buyoutPrice"), + EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(selectedListing.buyoutPrice)); + y += 11; + } + + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.bidCount"), + EnumChatFormatting.WHITE + Integer.toString(selectedListing.bidCount)); + y += 11; + + long remaining = selectedListing.getTimeRemaining(); + EnumChatFormatting timeColor = AuctionFormatUtil.isTimeUrgent(remaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.timeLeft"), + timeColor + AuctionFormatUtil.formatTimeRemaining(remaining)); + } + + private void drawDetailsLine(int leftX, int rightX, int y, String label, String value) { + fontRendererObj.drawString(label, leftX, y, 0xFFFFFF); + int width = fontRendererObj.getStringWidth(value); + fontRendererObj.drawString(value, rightX - width, y, 0xFFFFFF); + } + + private int getHoveredDisplaySlot(int mouseX, int mouseY) { + int relX = mouseX - ContainerManageAuction.DISPLAY_X; + int relY = mouseY - ContainerManageAuction.DISPLAY_Y; + if (relX < 0 || relY < 0) return -1; + + int col = relX / 18; + int row = relY / 18; + if (col < 0 || col >= ContainerManageAuction.DISPLAY_COLS || row < 0 || row >= ContainerManageAuction.DISPLAY_ROWS) return -1; + return col + row * ContainerManageAuction.DISPLAY_COLS; + } + + private void drawDynamicTooltip(int mouseX, int mouseY) { + if (hasSubGui()) return; + + // GuiContainer foreground receives absolute mouse coords; convert to GUI-local like auction GUIs. + int guiMouseX = mouseX - guiLeft; + int guiMouseY = mouseY - guiTop; + int hovered = getHoveredDisplaySlot(guiMouseX, guiMouseY); + + List tooltip = new ArrayList(); + + if (hovered >= 0) { + if (hovered == pendingDisplaySlot && pendingAction != PendingAction.NONE) { + if (pendingAction == PendingAction.STOP) { + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("global.auction.manage.confirm.stop")); + } else if (pendingAction == PendingAction.CANCEL) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("global.auction.manage.confirm.cancel")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("global.auction.manage.confirm.claim")); + } + tooltip.add(EnumChatFormatting.DARK_GRAY + StatCollector.translateToLocal("auction.trades.otherClickCancel")); + } else if (activeTab == Tab.CLAIMS) { + AuctionClaim claim = manageContainer.getClaimAtDisplay(hovered); + if (claim != null && claim.item == null) { + if (claim.type == EnumClaimType.CURRENCY) { + tooltip.add(EnumChatFormatting.GREEN + "Sold Auction Claim"); + if (claim.itemName != null && !claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem") + .replace("%s", EnumChatFormatting.WHITE + claim.itemName)); + } + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency))); + } else if (claim.type == EnumClaimType.REFUND) { + tooltip.add(EnumChatFormatting.RED + "Outbid Refund Claim"); + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency))); + } else if (claim.type.isCurrency()) { + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + } + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + } + } + } + + if (!tooltip.isEmpty()) { + drawHoveringText(tooltip, guiMouseX, guiMouseY, fontRendererObj); + } + } + + public ContainerManageAuction getManageContainer() { + return manageContainer; + } + + public Tab getActiveTab() { + return activeTab; + } + + @Override + public void save() { + } +} diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java index b055cbf43..36370eb92 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java @@ -247,7 +247,7 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { NoppesUtil.requestOpenGUI(EnumGuiType.ManageAbilities); } if (id == 22) { - // NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction); + NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction); } } 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 f07cb5ac6..b61c1e1b7 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -11,10 +11,12 @@ import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.StatCollector; import net.minecraftforge.event.entity.player.ItemTooltipEvent; import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.gui.global.GuiNpcManageAuction; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.containers.ContainerAuctionListing; import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.containers.ContainerManageAuction; import noppes.npcs.containers.SlotAuctionDisplay; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -49,6 +51,8 @@ public class AuctionTooltipHandler { addTradesTooltip((GuiAuctionTrades) screen, event.itemStack, event.toolTip); } else if (screen instanceof GuiAuctionSell) { addSellTooltip((GuiAuctionSell) screen, event.itemStack, event.toolTip); + } else if (screen instanceof GuiNpcManageAuction) { + addManageAuctionTooltip((GuiNpcManageAuction) screen, event.itemStack, event.toolTip); } } @@ -255,4 +259,125 @@ public class AuctionTooltipHandler { } } + // ========== Global Manage Auction ========== + + private static void addManageAuctionTooltip(GuiNpcManageAuction gui, ItemStack stack, List tooltip) { + Slot slot = findHoveredSlot(gui, stack); + if (slot == null) return; + + ContainerManageAuction container = gui.getManageContainer(); + if (container == null) return; + + GuiNpcManageAuction.Tab tab = gui.getActiveTab(); + + if (tab == GuiNpcManageAuction.Tab.LISTINGS && container.isDisplaySlot(slot.slotNumber)) { + int displayIndex = container.toDisplayIndex(slot.slotNumber); + AuctionListing listing = container.getListingAtDisplay(displayIndex); + if (listing == null) return; + + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller") + .replace("%s", EnumChatFormatting.WHITE + listing.sellerName)); + + if (listing.hasBids()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.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 + AuctionFormatUtil.formatCurrencyWithName(listing.startingPrice))); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); + } + + if (listing.hasBuyout()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout") + .replace("%s", EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(listing.buyoutPrice))); + } + + long timeRemaining = listing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining)); + tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + + tooltip.add(""); + tooltip.add(EnumChatFormatting.AQUA + "Left-click: View details"); + tooltip.add(EnumChatFormatting.GOLD + "Right-click: Stop player"); + tooltip.add(EnumChatFormatting.LIGHT_PURPLE + "Shift+Right-click: Cancel to global"); + return; + } + + if (tab == GuiNpcManageAuction.Tab.CLAIMS && container.isDisplaySlot(slot.slotNumber)) { + int displayIndex = container.toDisplayIndex(slot.slotNumber); + AuctionClaim claim = container.getClaimAtDisplay(displayIndex); + if (claim != null) { + addGlobalClaimTooltip(claim, tooltip); + return; + } + + AuctionListing listing = container.getListingAtDisplay(displayIndex); + if (listing != null) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller") + .replace("%s", EnumChatFormatting.WHITE + listing.sellerName)); + if (listing.hasBids()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid))); + } else { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.startingPrice") + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.startingPrice))); + } + if (listing.hasBuyout()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout") + .replace("%s", EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(listing.buyoutPrice))); + } + tooltip.add(""); + tooltip.add(EnumChatFormatting.GOLD + "Right-click: Stop player"); + tooltip.add(EnumChatFormatting.LIGHT_PURPLE + "Shift+Right-click: Cancel to global"); + } + return; + } + + if (tab == GuiNpcManageAuction.Tab.CREATE) { + if (container.isCreateSlot(slot.slotNumber)) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); + } else if (container.isPlayerSlot(slot.slotNumber)) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); + } + } + } + + private static void addGlobalClaimTooltip(AuctionClaim claim, List tooltip) { + tooltip.add(""); + if (claim.type == EnumClaimType.ITEM) { + tooltip.add(EnumChatFormatting.AQUA + "Global Item Claim"); + } else if (claim.type == EnumClaimType.CURRENCY) { + tooltip.add(EnumChatFormatting.GOLD + "Global Currency Claim"); + if (claim.itemName != null && !claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem") + .replace("%s", EnumChatFormatting.WHITE + claim.itemName)); + } + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency))); + tooltip.add(EnumChatFormatting.GRAY + "Claiming this does not change your balance."); + } else if (claim.type == EnumClaimType.REFUND) { + tooltip.add(EnumChatFormatting.RED + "Global Refund Claim"); + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") + .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency))); + } + tooltip.add(""); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + } + } diff --git a/src/main/java/noppes/npcs/containers/ContainerManageAuction.java b/src/main/java/noppes/npcs/containers/ContainerManageAuction.java new file mode 100644 index 000000000..1b6b05684 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerManageAuction.java @@ -0,0 +1,384 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.InventoryBasic; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; + +import java.util.ArrayList; +import java.util.List; + +/** + * Global auction management container. + * Uses a 9x5 display grid plus staged/detail slots on the right panel and player inventory below the grid. + */ +public class ContainerManageAuction extends ContainerNpcInterface { + public enum DisplayMode { + LISTINGS, + CLAIMS + } + + public static final int DISPLAY_COLS = 9; + public static final int DISPLAY_ROWS = 5; + public static final int DISPLAY_SLOT_COUNT = DISPLAY_COLS * DISPLAY_ROWS; + + public static final int DISPLAY_X = 8; + public static final int DISPLAY_Y = 16; + + public static final int PLAYER_INV_X = 8; + public static final int PLAYER_INV_Y = 113; + public static final int HOTBAR_Y = 171; + public static final int PLAYER_SLOT_COUNT = 36; + + public static final int CREATE_SLOT_X = 188; + public static final int CREATE_SLOT_Y = 156; + public static final int DETAIL_SLOT_X = 194; + public static final int DETAIL_SLOT_Y = 56; + private static final int MAX_STACK = 64; + + public static final int DISPLAY_SLOT_START = 0; + public static final int PLAYER_SLOT_START = DISPLAY_SLOT_START + DISPLAY_SLOT_COUNT; + public static final int DETAIL_SLOT_INDEX = PLAYER_SLOT_START + PLAYER_SLOT_COUNT; + public static final int CREATE_SLOT_INDEX = DETAIL_SLOT_INDEX + 1; + + private static final int HIDDEN_SLOT_X = -1000; + private static final int HIDDEN_SLOT_Y = -1000; + + public final InventoryAuctionDisplay displayInventory; + private final IInventory detailInventory; + private final IInventory createInventory; + private DisplayMode displayMode = DisplayMode.LISTINGS; + private Slot detailSlot; + private Slot createSlot; + private int hiddenDisplaySlot = -1; + + public ContainerManageAuction(EntityPlayer player) { + super(player); + this.player = player; + + displayInventory = new InventoryAuctionDisplay(DISPLAY_SLOT_COUNT); + detailInventory = new InventoryBasic("GlobalAuctionDetail", false, 1); + createInventory = new InventoryBasic("GlobalAuctionCreate", false, 1); + + // 9x5 display grid (listings / global claims) + for (int row = 0; row < DISPLAY_ROWS; row++) { + for (int col = 0; col < DISPLAY_COLS; col++) { + int slotIndex = col + row * DISPLAY_COLS; + int x = DISPLAY_X + col * 18; + int y = DISPLAY_Y + row * 18; + addSlotToContainer(new SlotAuctionDisplay(displayInventory, slotIndex, x, y)); + } + } + + // Player inventory below display grid + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int playerIndex = col + row * 9 + 9; + int x = PLAYER_INV_X + col * 18; + int y = PLAYER_INV_Y + row * 18; + addSlotToContainer(new Slot(player.inventory, playerIndex, x, y)); + } + } + for (int col = 0; col < 9; col++) { + int x = PLAYER_INV_X + col * 18; + addSlotToContainer(new Slot(player.inventory, col, x, HOTBAR_Y)); + } + + // Listing details slot (right panel) + detailSlot = new SlotAuctionDisplay(detailInventory, 0, DETAIL_SLOT_X, DETAIL_SLOT_Y); + addSlotToContainer(detailSlot); + + // Staged fake listing slot + createSlot = new Slot(createInventory, 0, CREATE_SLOT_X, CREATE_SLOT_Y) { + @Override + public boolean isItemValid(ItemStack stack) { + return false; + } + + @Override + public boolean canTakeStack(EntityPlayer player) { + return false; + } + }; + addSlotToContainer(createSlot); + + setDetailSlotVisible(false); + setCreateSlotVisible(false); + } + + @Override + public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { + return null; + } + + @Override + public ItemStack slotClick(int slotIndex, int mouseButton, int mode, EntityPlayer player) { + // Block native container interactions. GUI handles all click behaviors. + return null; + } + + public void setDisplayMode(DisplayMode mode) { + this.displayMode = mode != null ? mode : DisplayMode.LISTINGS; + } + + public DisplayMode getDisplayMode() { + return displayMode; + } + + public void clearDisplay() { + displayInventory.clear(); + } + + public void setListingsPage(List listings) { + displayMode = DisplayMode.LISTINGS; + clearHiddenDisplaySlot(); + displayInventory.clear(); + if (listings == null) return; + + for (int i = 0; i < listings.size() && i < DISPLAY_SLOT_COUNT; i++) { + displayInventory.setListing(i, listings.get(i)); + } + } + + public void setClaimsPage(List claims) { + displayMode = DisplayMode.CLAIMS; + clearHiddenDisplaySlot(); + displayInventory.clear(); + if (claims == null) return; + + for (int i = 0; i < claims.size() && i < DISPLAY_SLOT_COUNT; i++) { + displayInventory.setClaimItem(i, claims.get(i)); + } + } + + /** + * Claims page can contain both global claims and active global listings. + * Order mirrors My Trades: + * sold claims, outbid claims, won claims, active listings, expired claims. + */ + public void setClaimsAndListingsPage(List claims, List listings) { + displayMode = DisplayMode.CLAIMS; + clearHiddenDisplaySlot(); + displayInventory.clear(); + + List soldClaims = new ArrayList(); + List outbidClaims = new ArrayList(); + List wonClaims = new ArrayList(); + List expiredClaims = new ArrayList(); + if (claims != null) { + for (AuctionClaim claim : claims) { + if (claim == null) continue; + if (claim.type == EnumClaimType.CURRENCY) { + soldClaims.add(claim); + } else if (claim.type == EnumClaimType.REFUND) { + outbidClaims.add(claim); + } else if (claim.type == EnumClaimType.ITEM) { + if (claim.isReturned) { + expiredClaims.add(claim); + } else { + wonClaims.add(claim); + } + } else { + wonClaims.add(claim); + } + } + } + + int slot = 0; + slot = fillClaims(slot, soldClaims); + slot = fillClaims(slot, outbidClaims); + slot = fillClaims(slot, wonClaims); + + if (listings != null) { + for (int i = 0; i < listings.size() && slot < DISPLAY_SLOT_COUNT; i++) { + displayInventory.setListing(slot, listings.get(i)); + slot++; + } + } + + fillClaims(slot, expiredClaims); + } + + private int fillClaims(int slot, List claims) { + if (claims == null) return slot; + for (int i = 0; i < claims.size() && slot < DISPLAY_SLOT_COUNT; i++) { + displayInventory.setClaimItem(slot, claims.get(i)); + slot++; + } + return slot; + } + + public boolean isDisplaySlot(int containerSlotIndex) { + return containerSlotIndex >= DISPLAY_SLOT_START && containerSlotIndex < DISPLAY_SLOT_START + DISPLAY_SLOT_COUNT; + } + + public int toDisplayIndex(int containerSlotIndex) { + if (!isDisplaySlot(containerSlotIndex)) return -1; + return containerSlotIndex - DISPLAY_SLOT_START; + } + + public AuctionListing getListingAtDisplay(int displayIndex) { + if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) return null; + return displayInventory.getListing(displayIndex); + } + + public AuctionClaim getClaimAtDisplay(int displayIndex) { + if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) return null; + return displayInventory.getClaim(displayIndex); + } + + public boolean isPlayerSlot(int containerSlotIndex) { + return containerSlotIndex >= PLAYER_SLOT_START && containerSlotIndex < PLAYER_SLOT_START + PLAYER_SLOT_COUNT; + } + + public boolean isCreateSlot(int containerSlotIndex) { + return containerSlotIndex == CREATE_SLOT_INDEX; + } + + public boolean isDetailSlot(int containerSlotIndex) { + return containerSlotIndex == DETAIL_SLOT_INDEX; + } + + public void setDetailItem(ItemStack stack) { + detailInventory.setInventorySlotContents(0, stack != null ? stack.copy() : null); + } + + public void clearDetailItem() { + detailInventory.setInventorySlotContents(0, null); + } + + public void setDetailSlotVisible(boolean visible) { + if (detailSlot == null) return; + detailSlot.xDisplayPosition = visible ? DETAIL_SLOT_X : HIDDEN_SLOT_X; + detailSlot.yDisplayPosition = visible ? DETAIL_SLOT_Y : HIDDEN_SLOT_Y; + } + + public void setCreateSlotVisible(boolean visible) { + if (createSlot == null) return; + createSlot.xDisplayPosition = visible ? CREATE_SLOT_X : HIDDEN_SLOT_X; + createSlot.yDisplayPosition = visible ? CREATE_SLOT_Y : HIDDEN_SLOT_Y; + } + + public void setHiddenDisplaySlot(int displayIndex) { + if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) { + clearHiddenDisplaySlot(); + return; + } + if (hiddenDisplaySlot == displayIndex) return; + + clearHiddenDisplaySlot(); + hiddenDisplaySlot = displayIndex; + displayInventory.setInventorySlotContents(displayIndex, null); + } + + public void clearHiddenDisplaySlot() { + if (hiddenDisplaySlot < 0 || hiddenDisplaySlot >= DISPLAY_SLOT_COUNT) { + hiddenDisplaySlot = -1; + return; + } + restoreDisplaySlot(hiddenDisplaySlot); + hiddenDisplaySlot = -1; + } + + public int getHiddenDisplaySlot() { + return hiddenDisplaySlot; + } + + private void restoreDisplaySlot(int displayIndex) { + if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) return; + + AuctionClaim claim = displayInventory.getClaim(displayIndex); + if (claim != null) { + displayInventory.setClaimItem(displayIndex, claim); + return; + } + + AuctionListing listing = displayInventory.getListing(displayIndex); + if (listing != null) { + displayInventory.setListing(displayIndex, listing); + return; + } + + displayInventory.setInventorySlotContents(displayIndex, null); + } + + public ItemStack getCreateItem() { + return createInventory.getStackInSlot(0); + } + + public void clearCreateSlot() { + createInventory.setInventorySlotContents(0, null); + } + + public void addToCreateSlot(int containerPlayerSlot, boolean fullStack) { + if (!isPlayerSlot(containerPlayerSlot)) return; + + Slot slot = (Slot) inventorySlots.get(containerPlayerSlot); + if (slot == null) return; + ItemStack source = slot.getStack(); + if (source == null) return; + + ItemStack staged = createInventory.getStackInSlot(0); + int toAdd = fullStack ? source.stackSize : 1; + + if (staged == null) { + ItemStack copy = source.copy(); + copy.stackSize = Math.min(MAX_STACK, toAdd); + createInventory.setInventorySlotContents(0, copy); + return; + } + + if (!itemsMatch(staged, source)) { + ItemStack copy = source.copy(); + copy.stackSize = Math.min(MAX_STACK, toAdd); + createInventory.setInventorySlotContents(0, copy); + return; + } + + int space = MAX_STACK - staged.stackSize; + int add = Math.min(space, toAdd); + if (add > 0) { + staged.stackSize += add; + } + } + + public void removeFromCreateSlot(boolean removeAll) { + ItemStack staged = createInventory.getStackInSlot(0); + if (staged == null) return; + + if (removeAll || staged.stackSize <= 1) { + createInventory.setInventorySlotContents(0, null); + } else { + staged.stackSize--; + } + } + + public int countItemInPlayerInventory(ItemStack target) { + if (target == null || player == null || player.inventory == null) return 0; + int total = 0; + for (int i = 0; i < player.inventory.mainInventory.length; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack != null && itemsMatch(target, stack)) { + total += stack.stackSize; + } + } + return total; + } + + private boolean itemsMatch(ItemStack a, ItemStack b) { + return NoppesUtilPlayer.compareItems(a, b, false, false); + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + clearHiddenDisplaySlot(); + clearDetailItem(); + clearCreateSlot(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 7ea79baeb..dfa64b4ac 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -43,6 +43,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -63,7 +64,8 @@ public class AuctionController implements IAuctionHandler { // Main data stores - listings only, claims are stored in PlayerData private final Map listings = new ConcurrentHashMap<>(); - // Claims are now stored in PlayerData.tradeData, not here + // Shared global claims for admin-managed auction actions + private final List globalClaims = new CopyOnWriteArrayList<>(); // ========================================= // Player Indices for O(1) Lookups @@ -163,9 +165,12 @@ public class AuctionController implements IAuctionHandler { if (listing.status != EnumAuctionStatus.ACTIVE) return; listing.status = EnumAuctionStatus.ENDED; + boolean isGlobalListing = listing.isGlobalListing; // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); + if (!isGlobalListing && listing.sellerUUID != null) { + removeFromPlayerListings(listing.sellerUUID, listing.id); + } // Remove from bidder's active bids if any if (listing.highBidderUUID != null) { @@ -188,26 +193,38 @@ public class AuctionController implements IAuctionHandler { AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, itemDisplayName, listing.highBidderName); - addClaimToPlayer(listing.sellerUUID, currencyClaim); + if (isGlobalListing) { + addGlobalClaim(currencyClaim); + } else { + addClaimToPlayer(listing.sellerUUID, currencyClaim); + } // Notifications sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, "You won the auction for " + itemDisplayName + "!"); - sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, - "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + if (!isGlobalListing && listing.sellerUUID != null) { + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + } logAuction(EnumAuctionLogAction.SOLD, listing.sellerName, itemDisplayName, saleAmount, - "Winner: " + listing.highBidderName + ", Tax: " + tax); + "Winner: " + listing.highBidderName + ", Tax: " + tax + (isGlobalListing ? ", Global listing" : "")); } else { - // No bids - return item to seller + // No bids - return item to seller/global claims AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim( listing.sellerUUID, listing.sellerName, listing.id, listing.item); - addClaimToPlayer(listing.sellerUUID, returnClaim); + if (isGlobalListing) { + addGlobalClaim(returnClaim); + } else { + addClaimToPlayer(listing.sellerUUID, returnClaim); + if (listing.sellerUUID != null) { + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, + "Your auction for " + itemDisplayName + " expired with 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"); + logAuction(EnumAuctionLogAction.EXPIRED, listing.sellerName, itemDisplayName, listing.startingPrice, + "No bids" + (isGlobalListing ? ", Global listing" : "")); } } } @@ -217,10 +234,12 @@ public class AuctionController implements IAuctionHandler { // ========================================= private void addToPlayerListings(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; playerListingIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); } private void removeFromPlayerListings(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; Set ids = playerListingIds.get(playerUUID); if (ids != null) { ids.remove(listingId); @@ -231,10 +250,12 @@ public class AuctionController implements IAuctionHandler { } private void addToPlayerBids(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; playerBidIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); } private void removeFromPlayerBids(UUID playerUUID, String listingId) { + if (playerUUID == null || listingId == null || listingId.isEmpty()) return; Set ids = playerBidIds.get(playerUUID); if (ids != null) { ids.remove(listingId); @@ -259,6 +280,70 @@ public class AuctionController implements IAuctionHandler { } } + /** + * Add a claim to the global admin claims pool. + */ + private void addGlobalClaim(AuctionClaim claim) { + if (claim == null || claim.claimed) return; + + // Global claims are not tied to a single player. + claim.playerUUID = null; + claim.playerName = "Global"; + globalClaims.add(claim); + markDirty(); + } + + private List getGlobalClaimsSnapshot() { + List result = new ArrayList<>(); + for (AuctionClaim claim : globalClaims) { + if (claim != null && !claim.claimed) { + result.add(claim); + } + } + return result; + } + + private AuctionClaim getGlobalClaimInternal(String claimId) { + if (claimId == null || claimId.isEmpty()) return null; + for (AuctionClaim claim : globalClaims) { + if (claim != null && !claim.claimed && claimId.equals(claim.id)) { + return claim; + } + } + return null; + } + + private boolean claimAndRemoveGlobal(String claimId) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) return false; + claim.claimed = true; + return globalClaims.remove(claim); + } + + /** + * Full list of unclaimed global claims sorted newest-first. + */ + public List getAllGlobalClaims() { + List snapshot = getGlobalClaimsSnapshot(); + snapshot.sort((a, b) -> Long.compare(b.createdTime, a.createdTime)); + return snapshot; + } + + /** + * Full list of active global listings sorted newest-first. + */ + public List getActiveGlobalListings() { + List result = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing == null) continue; + if (!listing.isGlobalListing) continue; + if (!listing.isActive()) continue; + result.add(listing); + } + result.sort((a, b) -> Long.compare(b.createdTime, a.createdTime)); + return result; + } + // ========================================= // Listing Management // ========================================= @@ -335,12 +420,57 @@ public class AuctionController implements IAuctionHandler { return null; // Success } + /** + * Create a fake/global listing managed through the global auction admin GUI. + * Global listings do not consume player currency and route proceeds to global claims. + */ + public String createGlobalListing(EntityPlayer admin, String sellerName, ItemStack item, long startingPrice, long buyoutPrice, int durationHours) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + if (admin == null) { + return "Admin is missing."; + } + if (item == null) { + return "No item provided."; + } + + if (startingPrice < ConfigMarket.MinimumListingPrice) { + return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; + } + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return "Buyout price must be higher than starting price."; + } + + String cleanSeller = sellerName != null ? sellerName.trim() : ""; + if (cleanSeller.isEmpty()) { + cleanSeller = "Server"; + } + + int hours = durationHours > 0 ? durationHours : ConfigMarket.AuctionDurationHours; + long durationMs = hours * 60L * 60L * 1000L; + + // Global listings do not belong to a player UUID, only to the global pool. + AuctionListing listing = new AuctionListing(null, cleanSeller, item, startingPrice, buyoutPrice, durationMs); + listing.isGlobalListing = true; + listings.put(listing.id, listing); + markDirty(); + + logAuction(EnumAuctionLogAction.CREATED, admin.getCommandSenderName(), item.getDisplayName(), startingPrice, + "Global listing, Seller: " + cleanSeller + ", Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", DurationHours: " + hours); + return null; + } + public String cancelListing(String listingId, EntityPlayer player, boolean isAdmin) { AuctionListing listing = listings.get(listingId); if (listing == null) { return "Listing not found."; } + if (isAdmin && listing.isGlobalListing) { + return adminStopListingToGlobal(listingId, player, true); + } + synchronized (listing) { if (!listing.status.canCancel()) { return "This auction cannot be cancelled."; @@ -616,14 +746,22 @@ public class AuctionController implements IAuctionHandler { AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, listing.item.getDisplayName(), playerName); - addClaimToPlayer(listing.sellerUUID, currencyClaim); + if (listing.isGlobalListing) { + addGlobalClaim(currencyClaim); + } else { + addClaimToPlayer(listing.sellerUUID, currencyClaim); + } // Notifications - sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, - "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + if (!listing.isGlobalListing && listing.sellerUUID != null) { + 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); + if (!listing.isGlobalListing && listing.sellerUUID != null) { + removeFromPlayerListings(listing.sellerUUID, listing.id); + } listing.status = EnumAuctionStatus.ENDED; listing.highBidderUUID = playerUUID; @@ -633,7 +771,7 @@ public class AuctionController implements IAuctionHandler { markDirty(); logAuction(EnumAuctionLogAction.BUYOUT, playerName, listing.item.getDisplayName(), listing.buyoutPrice, - "Seller: " + listing.sellerName + ", Tax: " + tax); + "Seller: " + listing.sellerName + ", Tax: " + tax + (listing.isGlobalListing ? ", Global listing" : "")); } return null; // Success @@ -665,6 +803,77 @@ public class AuctionController implements IAuctionHandler { return new ArrayList<>(); } + /** + * Paginated global claims used by the global auction admin GUI. + */ + public List getGlobalClaims(int page, int pageSize) { + List snapshot = getGlobalClaimsSnapshot(); + snapshot.sort((a, b) -> Long.compare(b.createdTime, a.createdTime)); + + int safePage = Math.max(0, page); + int safePageSize = Math.max(1, pageSize); + int start = safePage * safePageSize; + if (start >= snapshot.size()) { + return new ArrayList<>(); + } + int end = Math.min(start + safePageSize, snapshot.size()); + return new ArrayList<>(snapshot.subList(start, end)); + } + + public int getTotalGlobalClaims() { + return getGlobalClaimsSnapshot().size(); + } + + public boolean hasGlobalClaims() { + return getTotalGlobalClaims() > 0; + } + + public String claimGlobalItem(String claimId, EntityPlayer player) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) { + return "Global claim not found."; + } + if (!claim.type.isItem()) { + return "This global claim is not an item claim."; + } + if (claim.item == null) { + return "Item data is missing."; + } + + if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + return "Your inventory is full."; + } + + claimAndRemoveGlobal(claim.id); + cleanupListingIfComplete(claim.listingId); + markDirty(); + + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0, + "Global item claim"); + return null; + } + + /** + * Global currency claims are acknowledged/cleared and do not alter player balances. + */ + public String claimGlobalCurrency(String claimId, EntityPlayer player) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) { + return "Global claim not found."; + } + if (!claim.type.isCurrency()) { + return "This global claim is not a currency claim."; + } + + claimAndRemoveGlobal(claim.id); + cleanupListingIfComplete(claim.listingId); + markDirty(); + + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + "Global currency claim acknowledged (no balance change)"); + return null; + } + public String claimItem(String claimId, EntityPlayer player) { PlayerData playerData = PlayerData.get(player); if (playerData == null) { @@ -1119,6 +1328,30 @@ public class AuctionController implements IAuctionHandler { player.addChatMessage(new ChatComponentText(msg.toString())); } + + // Notify admins with permission about pending global claims. + if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.global.auction")) { + List global = getGlobalClaimsSnapshot(); + if (!global.isEmpty()) { + int itemClaims = 0; + int currencyClaims = 0; + for (AuctionClaim claim : global) { + if (claim.type.isItem()) { + itemClaims++; + } else { + currencyClaims++; + } + } + + StringBuilder msg = new StringBuilder(); + msg.append(EnumChatFormatting.GOLD).append("[Auction] "); + msg.append(EnumChatFormatting.AQUA).append("Global claims available: "); + msg.append(EnumChatFormatting.GREEN).append(itemClaims).append(" item claim(s)"); + msg.append(EnumChatFormatting.GRAY).append(", "); + msg.append(EnumChatFormatting.YELLOW).append(currencyClaims).append(" currency claim(s)"); + player.addChatMessage(new ChatComponentText(msg.toString())); + } + } } public void onPlayerLogout(UUID playerUUID) { @@ -1228,6 +1461,7 @@ public class AuctionController implements IAuctionHandler { public void load() { listings.clear(); + globalClaims.clear(); playerListingIds.clear(); playerBidIds.clear(); @@ -1270,7 +1504,9 @@ public class AuctionController implements IAuctionHandler { // Index listings for (AuctionListing listing : listings.values()) { if (listing.status == EnumAuctionStatus.ACTIVE) { - addToPlayerListings(listing.sellerUUID, listing.id); + if (!listing.isGlobalListing && listing.sellerUUID != null) { + addToPlayerListings(listing.sellerUUID, listing.id); + } if (listing.highBidderUUID != null) { addToPlayerBids(listing.highBidderUUID, listing.id); } @@ -1291,22 +1527,42 @@ public class AuctionController implements IAuctionHandler { } public NBTTagCompound writeToNBT(NBTTagCompound compound) { - // Listings only - claims are stored in PlayerData + // Listings + global claims (player claims are stored in PlayerData) NBTTagList listingsList = new NBTTagList(); for (AuctionListing listing : listings.values()) { listingsList.appendTag(listing.writeToNBT(new NBTTagCompound())); } compound.setTag("Listings", listingsList); + NBTTagList globalClaimsList = new NBTTagList(); + for (AuctionClaim claim : globalClaims) { + if (claim != null && !claim.claimed) { + globalClaimsList.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + } + compound.setTag("GlobalClaims", globalClaimsList); + return compound; } public void readFromNBT(NBTTagCompound compound) { + globalClaims.clear(); + NBTTagList listingsList = compound.getTagList("Listings", 10); for (int i = 0; i < listingsList.tagCount(); i++) { AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i)); listings.put(listing.id, listing); } + + if (compound.hasKey("GlobalClaims")) { + NBTTagList claimsList = compound.getTagList("GlobalClaims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); + if (!claim.claimed) { + globalClaims.add(claim); + } + } + } } // ========================================= @@ -1473,10 +1729,77 @@ public class AuctionController implements IAuctionHandler { return new ArrayList<>(listings.values()); } + /** + * Admin management flow for listings. + * cancelCompletely=false: stop listing and return assets to the original owners' claim pools. + * cancelCompletely=true: cancel listing and route assets to global claims. + */ + public String adminStopListingToGlobal(String listingId, EntityPlayer admin, boolean cancelCompletely) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + if (admin == null) { + return "Admin player is missing."; + } + + synchronized (listing) { + if (!listing.status.canCancel()) { + return "This auction cannot be managed."; + } + + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; + + // Return active highest bid. + if (listing.hasBids() && listing.highBidderUUID != null) { + AuctionClaim refundClaim; + if (cancelCompletely) { + refundClaim = AuctionClaim.createRefundClaim( + null, "Global", listing.id, listing.currentBid, itemName, + listing.highBidderName, null); + addGlobalClaim(refundClaim); + } else { + refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName, + listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + } + removeFromPlayerBids(listing.highBidderUUID, listing.id); + } + + // Return the listed item. + if (listing.item != null) { + AuctionClaim itemClaim; + if (cancelCompletely || listing.sellerUUID == null) { + itemClaim = AuctionClaim.createItemReturnedClaim( + null, "Global", listing.id, listing.item); + addGlobalClaim(itemClaim); + } else { + itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + } + } + + if (!listing.isGlobalListing && listing.sellerUUID != null) { + removeFromPlayerListings(listing.sellerUUID, listing.id); + } + + listing.status = cancelCompletely ? EnumAuctionStatus.CANCELLED : EnumAuctionStatus.ENDED; + markDirty(); + + logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, listing.currentBid, + (cancelCompletely ? "Admin cancelled to global claims" : "Admin stopped to owner claims") + + ", OriginalSeller: " + listing.sellerName); + } + + return null; + } + 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); + adminStopListingToGlobal(listingId, admin, true); if (reason != null && !reason.isEmpty()) { logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, 0, "Admin cancel - Reason: " + reason); } diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index 6d57942c1..93fc70674 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -24,6 +24,7 @@ public class AuctionListing implements IAuctionListing { public long createdTime; public long endTime; public EnumAuctionStatus status; + public boolean isGlobalListing; public AuctionListing() { this.id = UUID.randomUUID().toString(); @@ -31,6 +32,7 @@ public class AuctionListing implements IAuctionListing { this.bidCount = 0; this.currentBid = 0; this.createdTime = System.currentTimeMillis(); + this.isGlobalListing = false; } public AuctionListing(UUID sellerUUID, String sellerName, ItemStack item, long startingPrice, long buyoutPrice, long durationMs) { @@ -228,6 +230,7 @@ public class AuctionListing implements IAuctionListing { compound.setLong("CreatedTime", createdTime); compound.setLong("EndTime", endTime); compound.setInteger("Status", status.ordinal()); + compound.setBoolean("IsGlobalListing", isGlobalListing); return compound; } @@ -255,6 +258,7 @@ public class AuctionListing implements IAuctionListing { createdTime = compound.getLong("CreatedTime"); endTime = compound.getLong("EndTime"); status = EnumAuctionStatus.fromOrdinal(compound.getInteger("Status")); + isGlobalListing = compound.getBoolean("IsGlobalListing"); } public static AuctionListing fromNBT(NBTTagCompound compound) { diff --git a/src/main/java/noppes/npcs/mixin/MixinItemStack.java b/src/main/java/noppes/npcs/mixin/MixinItemStack.java index 4e82147f1..051bc4b46 100644 --- a/src/main/java/noppes/npcs/mixin/MixinItemStack.java +++ b/src/main/java/noppes/npcs/mixin/MixinItemStack.java @@ -5,6 +5,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.gui.global.GuiNpcManageAuction; import noppes.npcs.client.gui.player.GuiAuctionInterface; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -31,7 +32,7 @@ public abstract class MixinItemStack { @Inject(method = "getTooltip", at = @At("TAIL"), cancellable = true) public void getAttributeTooltip(EntityPlayer player, boolean advanced, CallbackInfoReturnable> cir) { boolean override = false; - if (Minecraft.getMinecraft().currentScreen instanceof GuiAuctionInterface) { + if (Minecraft.getMinecraft().currentScreen instanceof GuiAuctionInterface || Minecraft.getMinecraft().currentScreen instanceof GuiNpcManageAuction) { override = true; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index a4c8103dc..ff1899b19 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2337,3 +2337,33 @@ auction.bid.yourBalance=Your Balance: %s # Auction Buyout SubGui auction.buyout.title=Buy Now auction.buyout.confirm=Purchase this item for: + +# Global Auction Management GUI +global.auction.manage.title=Global Auction Management +global.auction.manage.listings=Listings +global.auction.manage.create=Create +global.auction.manage.claims=Claims +global.auction.manage.sort=Sort +global.auction.manage.search=Search +global.auction.manage.page=Page +global.auction.manage.hint.listings=Left-click: open listing details +global.auction.manage.hint.listings2=Right-click: stop player | Shift-right: cancel to global +global.auction.manage.hint.stop=Right-click: stop player +global.auction.manage.hint.cancel=Shift-right-click: cancel to global +global.auction.manage.hint.claims=Left-click twice to claim global entries +global.auction.manage.details=Listing Details +global.auction.manage.selectListing=Left-click a listing to view details +global.auction.manage.seller=Seller Name +global.auction.manage.startPrice=Start Price +global.auction.manage.buyoutPrice=Buyout Price +global.auction.manage.durationHours=Duration Hours +global.auction.manage.stagedItem=Staged Item +global.auction.manage.create.submit=Create Fake Auction +global.auction.manage.hint.create=Use right-panel inventory to stage items +global.auction.manage.confirm=Click again to confirm +global.auction.manage.confirm.stop=Right-click again to confirm stop player +global.auction.manage.confirm.cancel=Shift-right-click again to confirm cancel to global +global.auction.manage.confirm.claim=Left-click again to confirm claim +global.auction.manage.error.noItem=Stage an item first +global.auction.manage.error.invalidStart=Enter a valid starting price +global.auction.manage.error.invalidBuyout=Buyout must be zero or >= start diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon_blue.png b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon_blue.png new file mode 100644 index 000000000..9f4c340c1 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon_blue.png differ