From a1461665d1625d3d8addebde0f312ab0f4cac3bb Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 02:30:39 -0500 Subject: [PATCH 01/21] Update api --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index d003d6d87..305a3778c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit d003d6d870db13d56fce223050a06faf15050eae +Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 From c647179c26a64ebe052b788f32fd07ab028d320b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 07:37:28 +0000 Subject: [PATCH 02/21] Update CNPC 1.11 Market Plan with design decisions - Add confirmed design decisions section based on discussion: - Outbid refunds go to claims (not direct balance) - Use existing CustomNPC+ permission system pattern - Currency is NOT profile slot bound (shared across slots) - Use existing gold coin (CustomItems.coinGold) for display - Stock reset timer shown as banner ABOVE trader GUI - Add Feature 4: Item Attributes & Restrictions - Untradeable, SlotBound, SoulBound item attributes - Auction house item blacklist system (by item, mod, NBT tag) - Add Feature 5: Admin Auction Management - Admin GUI for viewing/managing all listings - Force cancel, edit, end auctions early - Blacklist management GUI - Audit logging for admin actions - Update implementation phases (now 5 phases) - Update file summary with new files - Update testing strategy with new test cases - Update API/scripting extensions with item attribute methods --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 589 ++++++++++++++++++++++++++- 1 file changed, 567 insertions(+), 22 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index fb1c33158..62e744e27 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -10,15 +10,64 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f --- +## Design Decisions (Confirmed) + +### 1. Auction Refund System +**Decision:** When a player is outbid, the refund goes to **claims** (not direct to balance). +- Players can manage their bids from claims +- If auction ends while player is offline, they can claim their money later +- This keeps the flow consistent with item claims + +### 2. Permission System +**Decision:** Use the **existing CustomNPC+ permission system** used for Profiles. +- Pattern: `customnpcs.auction.slots.X` where X is the max slot count +- Implementation mirrors `ProfileController.allowSlotPermission()` approach +- Uses `CustomNpcsPermissions.hasCustomPermission(player, permissionString)` + +### 3. Virtual Currency Storage +**Decision:** Currency is **NOT profile slot bound** - all profile slots share the same currency pool per player. +- Stored in PlayerData outside of slot components +- Balance persists across profile slot switches +- Balance viewing system to be added later (for now, visible via GUI only) + +### 4. Currency Display Icon +**Decision:** Use the **existing gold coin** from the mod (`CustomItems.coinGold`). +- Already has textures and localization for all supported languages +- Available coin types: Wood, Stone, Bronze, Iron, Gold, Diamond, Emerald + +### 5. Trader Stock Reset Timer Display +**Decision:** Show "Stocks reset in X hours" as a **banner ABOVE the GUI**. +- Positioned at a higher Y level than the main GUI +- Not overlayed on the GUI content +- Displayed when opening the trader interface + +### 6. Item Trading Restrictions +**Decision:** Implement the following restrictions for auction listings: +- **Untradeable Attribute**: New item attribute that prevents auction listing +- **SlotBound Items**: Cannot be listed (needs to be implemented) +- **SoulBound Items**: Cannot be listed (needs to be implemented) +- **Auction House Blacklist**: Configurable list of items that cannot be auctioned + +### 7. Admin Auction Management +**Decision:** Create an **Auction Management system** with a Global Admin button. +- Admins can modify, remove, and edit auction listings in real-time +- Accessible via special admin GUI +- Permission-gated for server operators + +--- + ## Table of Contents -1. [Architecture Overview](#architecture-overview) -2. [Feature 1: Currency System](#feature-1-currency-system) -3. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) -4. [Feature 3: Auction House](#feature-3-auction-house) -5. [Implementation Order](#implementation-order) -6. [File Summary](#file-summary) -7. [Testing Strategy](#testing-strategy) +1. [Design Decisions](#design-decisions-confirmed) +2. [Architecture Overview](#architecture-overview) +3. [Feature 1: Currency System](#feature-1-currency-system) +4. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) +5. [Feature 3: Auction House](#feature-3-auction-house) +6. [Feature 4: Item Attributes & Restrictions](#feature-4-item-attributes--restrictions) +7. [Feature 5: Admin Auction Management](#feature-5-admin-auction-management) +8. [Implementation Order](#implementation-order) +9. [File Summary](#file-summary) +10. [Testing Strategy](#testing-strategy) --- @@ -1720,6 +1769,433 @@ public void onServerTick(TickEvent.ServerTickEvent event) { --- +## Feature 4: Item Attributes & Restrictions + +### 4.1 Overview + +Implement item attribute system to control which items can be traded/auctioned. Items with certain attributes will be blocked from the auction house. + +### 4.2 New Item Attributes + +#### 4.2.1 Untradeable Attribute + +A new NBT-based item attribute that marks items as untradeable. + +**File: `src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java`** + +```java +package kamkeel.npcs.controllers.data.attribute; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +public class ItemTradeAttribute { + public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; + public static final String TAG_SLOT_BOUND = "CNPC_SlotBound"; + public static final String TAG_SOUL_BOUND = "CNPC_SoulBound"; + + /** + * Check if item can be traded/auctioned + */ + public static boolean canTrade(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return true; + } + NBTTagCompound tag = item.getTagCompound(); + + // Check all trade-blocking attributes + if (tag.getBoolean(TAG_UNTRADEABLE)) return false; + if (tag.getBoolean(TAG_SLOT_BOUND)) return false; + if (tag.getBoolean(TAG_SOUL_BOUND)) return false; + + return true; + } + + /** + * Mark item as untradeable + */ + public static void setUntradeable(ItemStack item, boolean untradeable) { + if (item == null) return; + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + item.getTagCompound().setBoolean(TAG_UNTRADEABLE, untradeable); + } + + public static boolean isUntradeable(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); + } + + public static boolean isSlotBound(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + return item.getTagCompound().getBoolean(TAG_SLOT_BOUND); + } + + public static boolean isSoulBound(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + return item.getTagCompound().getBoolean(TAG_SOUL_BOUND); + } + + /** + * Get reason why item cannot be traded (for UI display) + */ + public static String getTradeBlockReason(ItemStack item) { + if (item == null || !item.hasTagCompound()) return null; + NBTTagCompound tag = item.getTagCompound(); + + if (tag.getBoolean(TAG_UNTRADEABLE)) return "gui.auction.untradeable"; + if (tag.getBoolean(TAG_SLOT_BOUND)) return "gui.auction.slotbound"; + if (tag.getBoolean(TAG_SOUL_BOUND)) return "gui.auction.soulbound"; + + return null; + } +} +``` + +### 4.3 Auction House Item Blacklist + +A configurable system to blacklist specific items from being auctioned. + +**File: `src/main/java/noppes/npcs/controllers/AuctionBlacklist.java`** + +```java +package noppes.npcs.controllers; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +public class AuctionBlacklist { + private static AuctionBlacklist instance; + + // Blacklist by item registry name (e.g., "minecraft:diamond_sword") + private Set blacklistedItems = new HashSet<>(); + + // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) + private Set blacklistedMods = new HashSet<>(); + + // Blacklist by NBT tag presence (e.g., items with specific custom data) + private Set blacklistedNBTTags = new HashSet<>(); + + public static AuctionBlacklist getInstance() { + if (instance == null) { + instance = new AuctionBlacklist(); + instance.load(); + } + return instance; + } + + /** + * Check if item is blacklisted + */ + public boolean isBlacklisted(ItemStack item) { + if (item == null) return false; + + // Check item registry name + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null && blacklistedItems.contains(itemName)) { + return true; + } + + // Check mod ID + if (itemName != null) { + String modId = itemName.split(":")[0]; + if (blacklistedMods.contains(modId)) { + return true; + } + } + + // Check NBT tags + if (item.hasTagCompound()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtTag : blacklistedNBTTags) { + if (tag.hasKey(nbtTag)) { + return true; + } + } + } + + return false; + } + + // Blacklist management methods + public void addItem(String itemName) { + blacklistedItems.add(itemName); + save(); + } + + public void removeItem(String itemName) { + blacklistedItems.remove(itemName); + save(); + } + + public void addMod(String modId) { + blacklistedMods.add(modId); + save(); + } + + public void removeMod(String modId) { + blacklistedMods.remove(modId); + save(); + } + + public void addNBTTag(String tag) { + blacklistedNBTTags.add(tag); + save(); + } + + public void removeNBTTag(String tag) { + blacklistedNBTTags.remove(tag); + save(); + } + + public Set getBlacklistedItems() { + return new HashSet<>(blacklistedItems); + } + + public Set getBlacklistedMods() { + return new HashSet<>(blacklistedMods); + } + + public Set getBlacklistedNBTTags() { + return new HashSet<>(blacklistedNBTTags); + } + + // Persistence + public void save() { + // Save to auction_blacklist.dat + } + + public void load() { + // Load from auction_blacklist.dat + } +} +``` + +### 4.4 Integration with Auction Listing + +Modify `AuctionController.createListing()` to check restrictions: + +```java +public AuctionListing createListing(EntityPlayer seller, ItemStack item, ...) { + // Check item attribute restrictions + if (!ItemTradeAttribute.canTrade(item)) { + String reason = ItemTradeAttribute.getTradeBlockReason(item); + // Send message to player with reason + return null; + } + + // Check blacklist + if (AuctionBlacklist.getInstance().isBlacklisted(item)) { + // Send "This item cannot be auctioned" message + return null; + } + + // ... rest of listing creation +} +``` + +--- + +## Feature 5: Admin Auction Management + +### 5.1 Overview + +A comprehensive admin system for managing the auction house in real-time. + +### 5.2 Admin Permissions + +**File: `src/main/java/noppes/npcs/CustomNpcsPermissions.java`** + +Add new permissions: + +```java +// Auction Administration +public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); +public static final Permission AUCTION_MANAGE = new Permission("customnpcs.auction.manage"); +public static final Permission AUCTION_BLACKLIST = new Permission("customnpcs.auction.blacklist"); +``` + +### 5.3 Admin GUI + +**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionAdmin.java`** + +Features: +- **View All Listings**: See all active, expired, and cancelled listings +- **Search/Filter**: Find listings by seller, item, status +- **Cancel Any Listing**: Force-cancel listings (returns item to seller) +- **Modify Listing**: Edit prices, duration (with warning) +- **End Auction Early**: Force-end with current highest bidder winning +- **Refund Bidders**: Manually refund specific bidders +- **View Blacklist**: Manage item blacklist +- **Audit Log**: View recent auction actions + +```java +public class GuiAuctionAdmin extends GuiNPCInterface { + private List allListings; + private int currentPage = 0; + private EnumAuctionStatus filterStatus = null; + private String searchQuery = ""; + + // Admin action buttons + private static final int BTN_CANCEL = 10; + private static final int BTN_END_EARLY = 11; + private static final int BTN_EDIT = 12; + private static final int BTN_REFUND = 13; + private static final int BTN_BLACKLIST = 14; + private static final int BTN_AUDIT = 15; + + @Override + public void initGui() { + // Tab buttons: All | Active | Sold | Expired | Cancelled + // Search bar + // Listing table with columns: ID, Seller, Item, Price, Bids, Status, Time + // Action buttons for selected listing + // Blacklist management button + } + + @Override + protected void actionPerformed(GuiButton button) { + switch (button.id) { + case BTN_CANCEL: + adminCancelListing(selectedListing); + break; + case BTN_END_EARLY: + adminEndAuction(selectedListing); + break; + case BTN_EDIT: + openEditDialog(selectedListing); + break; + case BTN_REFUND: + openRefundDialog(selectedListing); + break; + case BTN_BLACKLIST: + openBlacklistGui(); + break; + case BTN_AUDIT: + openAuditLog(); + break; + } + } +} +``` + +### 5.4 Admin Actions in AuctionController + +```java +// Admin-only methods +public boolean adminCancelListing(EntityPlayer admin, int listingId, String reason) { + if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { + return false; + } + + AuctionListing listing = listings.get(listingId); + if (listing == null) return false; + + // Refund current bidder if any + if (listing.highBidderUUID != null) { + addClaim(listing.highBidderUUID, listing.highBidderName, + null, listing.currentBid, "Admin cancelled auction: " + reason); + } + + // Return item to seller + addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, + "Listing cancelled by admin: " + reason); + + listing.status = EnumAuctionStatus.CANCELLED; + + // Log action + logAdminAction(admin, "CANCEL", listingId, reason); + + return true; +} + +public boolean adminEndAuction(EntityPlayer admin, int listingId) { + if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { + return false; + } + + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { + return false; + } + + // End as if time expired + listing.expirationTime = System.currentTimeMillis(); + processExpiredAuctions(); + + // Log action + logAdminAction(admin, "END_EARLY", listingId, ""); + + return true; +} + +public boolean adminEditListing(EntityPlayer admin, int listingId, + double newStartPrice, double newBuyout, + long newExpiration) { + if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { + return false; + } + + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { + return false; + } + + // Only allow if no bids placed + if (listing.highBidderUUID != null) { + return false; // Cannot edit listing with bids + } + + listing.startingPrice = newStartPrice; + listing.currentBid = newStartPrice; + listing.buyoutPrice = newBuyout; + listing.expirationTime = newExpiration; + + // Log action + logAdminAction(admin, "EDIT", listingId, + String.format("price=%.2f,buyout=%.2f", newStartPrice, newBuyout)); + + return true; +} + +// Audit logging +private void logAdminAction(EntityPlayer admin, String action, int listingId, String details) { + // Log to file: timestamp, admin name, action, listing id, details + LogWriter.info(String.format("[AUCTION ADMIN] %s performed %s on listing %d: %s", + admin.getCommandSenderName(), action, listingId, details)); +} +``` + +### 5.5 Blacklist Management GUI + +**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionBlacklist.java`** + +Features: +- Add item by holding it and clicking "Add" +- Add by typing item registry name +- Add entire mod by mod ID +- Add NBT tag pattern +- View current blacklist with remove buttons +- Test item against blacklist + +### 5.6 Access Points + +The Admin GUI can be accessed via: +1. **Global Menu**: New "Auction Management" button (requires permission) +2. **Auctioneer NPC**: Admin gear icon when editing NPC role (requires permission) +3. **Command**: `/cnpc auction admin` (requires OP or permission) + +--- + ## Implementation Order The features should be implemented in this order due to dependencies: @@ -1742,10 +2218,17 @@ The features should be implemented in this order due to dependencies: 6. Update network packets for stock synchronization 7. Test stock depletion and reset timers -### Phase 3: Auction House +### Phase 3: Item Attributes & Restrictions +1. Create `ItemTradeAttribute.java` with Untradeable, SlotBound, SoulBound tags +2. Create `AuctionBlacklist.java` controller +3. Add API methods to scripting interface for setting item attributes +4. Create blacklist persistence (load/save to file) +5. Test attribute blocking in listing creation + +### Phase 4: Auction House 1. Create auction enums (`EnumAuctionDuration`, `EnumAuctionStatus`, `EnumAuctionCategory`) 2. Create `AuctionListing.java` and `AuctionClaim` classes -3. Create `AuctionController.java` +3. Create `AuctionController.java` with item restriction checks 4. Update `EnumRoleType.java` to add Auctioneer 5. Create `RoleAuctioneer.java` 6. Update `DataAdvanced.java` for role instantiation @@ -1757,6 +2240,16 @@ The features should be implemented in this order due to dependencies: 12. Create auctioneer setup GUI 13. Comprehensive testing +### Phase 5: Admin Auction Management +1. Add auction admin permissions to `CustomNpcsPermissions.java` +2. Create `GuiAuctionAdmin.java` admin interface +3. Create `GuiAuctionBlacklist.java` blacklist management +4. Add admin methods to `AuctionController.java` +5. Add audit logging +6. Create admin network packets +7. Add command handler for `/cnpc auction admin` +8. Test admin operations + --- ## File Summary @@ -1767,9 +2260,11 @@ The features should be implemented in this order due to dependencies: |------|-------------| | `kamkeel/npcs/compat/VaultHelper.java` | VaultAPI integration | | `kamkeel/npcs/controllers/CurrencyController.java` | Currency management | -| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data | +| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data (NOT slot-bound) | | `noppes/npcs/controllers/data/TraderStock.java` | Trader stock configuration | | `noppes/npcs/constants/EnumStockReset.java` | Stock reset types | +| `kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java` | Untradeable/SlotBound/SoulBound item attributes | +| `noppes/npcs/controllers/AuctionBlacklist.java` | Auction item blacklist controller | | `noppes/npcs/controllers/AuctionController.java` | Auction house management | | `noppes/npcs/controllers/data/AuctionListing.java` | Auction listing data | | `noppes/npcs/constants/EnumAuctionDuration.java` | Auction duration options | @@ -1779,8 +2274,11 @@ The features should be implemented in this order due to dependencies: | `noppes/npcs/containers/ContainerAuction*.java` | Auction containers | | `noppes/npcs/client/gui/player/GuiAuction*.java` | Auction GUIs | | `noppes/npcs/client/gui/roles/GuiNpcAuctioneerSetup.java` | Auctioneer config | +| `noppes/npcs/client/gui/global/GuiAuctionAdmin.java` | Admin auction management | +| `noppes/npcs/client/gui/global/GuiAuctionBlacklist.java` | Blacklist management GUI | | `kamkeel/npcs/network/packets/player/AuctionActionPacket.java` | Auction actions | | `kamkeel/npcs/network/packets/data/AuctionSyncPacket.java` | Auction data sync | +| `kamkeel/npcs/network/packets/admin/AuctionAdminPacket.java` | Admin auction actions | ### Files to Modify @@ -1788,17 +2286,20 @@ The features should be implemented in this order due to dependencies: |------|---------| | `dependencies.gradle` | Add VaultAPI dependency | | `repositories.gradle` | Add Vault repository | -| `CustomNpcs.java` | Initialize VaultHelper, AuctionController | +| `CustomNpcs.java` | Initialize VaultHelper, AuctionController, AuctionBlacklist | +| `CustomNpcsPermissions.java` | Add auction admin permissions | | `EnumRoleType.java` | Add Auctioneer | -| `EnumGuiType.java` | Add auction GUI types | +| `EnumGuiType.java` | Add auction GUI types, admin GUIs | | `DataAdvanced.java` | Handle Auctioneer role | | `RoleTrader.java` | Add stock and currency support | | `ContainerNPCTrader.java` | Handle stock/currency transactions | +| `GuiNpcTrader.java` | Add stock reset timer banner above GUI | | `GuiNpcTraderSetup.java` | Add stock/currency UI | -| `PlayerData.java` | Add PlayerCurrencyData | +| `PlayerData.java` | Add PlayerCurrencyData (NOT in slot components - shared across slots) | | `CommonProxy.java` | Register auction containers | | `ServerTickHandler.java` | Add auction expiration check | -| `PacketHandler.java` | Register auction packets | +| `PacketHandler.java` | Register auction packets and admin packets | +| `CommandKamkeel.java` | Add `/cnpc auction admin` command | --- @@ -1806,29 +2307,41 @@ The features should be implemented in this order due to dependencies: ### Unit Testing - Currency operations (deposit, withdraw, balance) +- Currency shared across profile slots (not slot-bound) - Stock depletion and reset calculations - Auction bid validation - Auction expiration handling +- Item attribute checks (Untradeable, SlotBound, SoulBound) +- Blacklist matching (by item, mod, NBT tag) ### Integration Testing - VaultAPI integration on hybrid servers - Fallback to item currency when Vault unavailable - Stock persistence across server restarts - Auction persistence and recovery +- Profile slot switching with shared currency +- Outbid refunds going to claims correctly +- Admin actions affecting live auctions ### User Acceptance Testing - Complete trading flow with stock limits - Complete auction flow (list → bid → win → claim) -- Currency display formatting +- Currency display formatting with gold coin icon +- Stock reset timer banner display above trader GUI - GUI usability +- Admin management workflow ### Edge Cases - Negative currency amounts - Concurrent bids on same auction - Server crash during transaction -- Player offline when auction ends -- Maximum listings per player +- Player offline when auction ends (claim on next login) +- Maximum listings per player (permission-based) - Inventory full when claiming +- Trying to auction Untradeable/SlotBound/SoulBound items +- Trying to auction blacklisted items +- Admin cancelling auction with active bids +- Profile slot switch while having pending claims --- @@ -1843,29 +2356,61 @@ boolean withdraw(IPlayer player, double amount); boolean deposit(IPlayer player, double amount); String format(double amount); +// IItemStack extensions for trade attributes +boolean isUntradeable(); +void setUntradeable(boolean value); +boolean isSlotBound(); +void setSlotBound(boolean value); +boolean isSoulBound(); +void setSoulBound(boolean value); +boolean canTrade(); // Returns false if any blocking attribute is set +String getTradeBlockReason(); // Returns localization key for reason + // IAuction interface for scripts IAuctionListing createListing(IPlayer seller, IItemStack item, double startPrice, double buyout, int durationHours); boolean cancelListing(int listingId); List getPlayerListings(IPlayer player); +boolean isItemBlacklisted(IItemStack item); + +// IAuctionBlacklist interface for scripts (admin) +void addItemToBlacklist(String registryName); +void removeItemFromBlacklist(String registryName); +void addModToBlacklist(String modId); +void removeModFromBlacklist(String modId); +boolean isBlacklisted(IItemStack item); // Events onAuctionCreated(IAuctionListing listing); onAuctionBid(IAuctionListing listing, IPlayer bidder, double amount); +onAuctionOutbid(IAuctionListing listing, IPlayer previousBidder, double refundAmount); onAuctionSold(IAuctionListing listing, IPlayer buyer, double price); onAuctionExpired(IAuctionListing listing); +onAuctionCancelled(IAuctionListing listing, boolean byAdmin); onTraderPurchase(ITrader trader, IPlayer buyer, int slot, int stockRemaining); onTraderStockReset(ITrader trader); +onCurrencyTransaction(IPlayer player, double amount, boolean isDeposit); ``` --- ## Conclusion -This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The three features are designed to work together seamlessly: +This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The five features are designed to work together seamlessly: -1. **Currency System** provides the foundation for monetary transactions -2. **Trader Enhancements** modernize the existing trader with stock and currency support -3. **Auction House** creates a global marketplace leveraging the currency system +1. **Currency System** provides the foundation for monetary transactions (shared across profile slots) +2. **Trader Enhancements** modernize the existing trader with stock, currency support, and reset timer banners +3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, SlotBound, SoulBound attributes and blacklists +4. **Auction House** creates a global marketplace with claims-based refunds and permission-based slot limits +5. **Admin Management** provides comprehensive tools for server operators to manage the auction house in real-time -The implementation follows existing codebase patterns and can be developed incrementally, with each phase building on the previous one. +The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems) and can be developed incrementally, with each phase building on the previous one. + +### Key Design Decisions Summary +- Outbid refunds → Claims (not direct to balance) +- Currency → Shared across all profile slots per player +- Permissions → Uses existing CustomNPC+ permission pattern +- Display icon → Existing `CustomItems.coinGold` +- Stock reset timer → Banner displayed ABOVE the trader GUI +- Trade restrictions → Untradeable attribute + SlotBound/SoulBound + Blacklist system +- Admin tools → Full management GUI with audit logging From cdf30af20ca0587dfcfdadbe48ebea4009afa0cb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 07:49:44 +0000 Subject: [PATCH 03/21] Add auction configuration, timing rules, and finalize design decisions Design Decisions Added: - Modify Bid feature: auto-reclaim and re-bid from claims - Default 5 auction slots per player (configurable) - 30-day claim expiration (configurable) - Percentage-based minimum bid increment (configurable) - Snipe protection: extend TO 5 minutes when bid near end (configurable) - Auction durations: 2h, 12h, 24h, 48h (configurable) Item Restrictions Updated: - Reuse existing ProfileSlotRequirement (cnpc_profile_slot NBT tag) - Reuse existing SoulbindRequirement (cnpc_soulbind NBT tag) - Add new CNPC_Untradeable attribute Configuration Reference Added: - Full auction.cfg with all settings - Currency configuration - Trader stock configuration - Complete permission nodes table Updated conclusion with all finalized decisions. --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 202 +++++++++++++++++++++++---- 1 file changed, 173 insertions(+), 29 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index 62e744e27..cd9b80885 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -17,12 +17,14 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f - Players can manage their bids from claims - If auction ends while player is offline, they can claim their money later - This keeps the flow consistent with item claims +- **Modify Bid Feature**: Players can use a "Modify Bid" button that auto-reclaims their money and applies it to a higher bid amount ### 2. Permission System **Decision:** Use the **existing CustomNPC+ permission system** used for Profiles. - Pattern: `customnpcs.auction.slots.X` where X is the max slot count - Implementation mirrors `ProfileController.allowSlotPermission()` approach - Uses `CustomNpcsPermissions.hasCustomPermission(player, permissionString)` +- **Default**: 5 auction slots per player (configurable) ### 3. Virtual Currency Storage **Decision:** Currency is **NOT profile slot bound** - all profile slots share the same currency pool per player. @@ -43,9 +45,9 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f ### 6. Item Trading Restrictions **Decision:** Implement the following restrictions for auction listings: -- **Untradeable Attribute**: New item attribute that prevents auction listing -- **SlotBound Items**: Cannot be listed (needs to be implemented) -- **SoulBound Items**: Cannot be listed (needs to be implemented) +- **Untradeable Attribute**: New item attribute (`CNPC_Untradeable`) that prevents auction listing +- **ProfileSlot Bound Items**: Use existing `cnpc_profile_slot` NBT tag (from `ProfileSlotRequirement.java`) +- **Soulbound Items**: Use existing `cnpc_soulbind` NBT tag (from `SoulbindRequirement.java`) - **Auction House Blacklist**: Configurable list of items that cannot be auctioned ### 7. Admin Auction Management @@ -54,6 +56,15 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f - Accessible via special admin GUI - Permission-gated for server operators +### 8. Auction Timing & Bidding Rules +**Decision:** Configurable auction timing with snipe protection. +- **Claim Expiration**: 30 days by default (configurable) - claims auto-expire after this period +- **Bid Increment**: Percentage-based minimum increment (configurable, e.g., 5%) +- **Snipe Protection**: When a bid is placed near auction end, extend time TO 5 minutes remaining (never higher) + - Extension amount is configurable + - Example: If 2 minutes left and bid placed, extend to 5 minutes. If 10 minutes left, no extension. +- **Auction Durations**: Configurable default options (2h, 12h, 24h, 48h) + --- ## Table of Contents @@ -65,9 +76,10 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f 5. [Feature 3: Auction House](#feature-3-auction-house) 6. [Feature 4: Item Attributes & Restrictions](#feature-4-item-attributes--restrictions) 7. [Feature 5: Admin Auction Management](#feature-5-admin-auction-management) -8. [Implementation Order](#implementation-order) -9. [File Summary](#file-summary) -10. [Testing Strategy](#testing-strategy) +8. [Configuration Reference](#configuration-reference) +9. [Implementation Order](#implementation-order) +10. [File Summary](#file-summary) +11. [Testing Strategy](#testing-strategy) --- @@ -1775,9 +1787,16 @@ public void onServerTick(TickEvent.ServerTickEvent event) { Implement item attribute system to control which items can be traded/auctioned. Items with certain attributes will be blocked from the auction house. -### 4.2 New Item Attributes +### 4.2 Existing Item Requirements (Reuse) -#### 4.2.1 Untradeable Attribute +The codebase already has requirement types that we can leverage: + +| Requirement | NBT Key | File | Purpose | +|-------------|---------|------|---------| +| ProfileSlot Bound | `cnpc_profile_slot` | `ProfileSlotRequirement.java` | Item bound to specific profile slot | +| Soulbound | `cnpc_soulbind` | `SoulbindRequirement.java` | Item bound to specific player UUID | + +### 4.3 New Untradeable Attribute A new NBT-based item attribute that marks items as untradeable. @@ -1786,27 +1805,35 @@ A new NBT-based item attribute that marks items as untradeable. ```java package kamkeel.npcs.controllers.data.attribute; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; public class ItemTradeAttribute { + // New untradeable attribute public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; - public static final String TAG_SLOT_BOUND = "CNPC_SlotBound"; - public static final String TAG_SOUL_BOUND = "CNPC_SoulBound"; + + // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) + public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; + public static final String TAG_SOULBIND = "cnpc_soulbind"; /** * Check if item can be traded/auctioned */ - public static boolean canTrade(ItemStack item) { + public static boolean canTrade(ItemStack item, EntityPlayer player) { if (item == null || !item.hasTagCompound()) { return true; } NBTTagCompound tag = item.getTagCompound(); - // Check all trade-blocking attributes + // Check untradeable flag if (tag.getBoolean(TAG_UNTRADEABLE)) return false; - if (tag.getBoolean(TAG_SLOT_BOUND)) return false; - if (tag.getBoolean(TAG_SOUL_BOUND)) return false; + + // Check profile slot bound (item tied to specific profile slot) + if (tag.hasKey(TAG_PROFILE_SLOT)) return false; + + // Check soulbound (item tied to specific player) + if (tag.hasKey(TAG_SOULBIND)) return false; return true; } @@ -1819,7 +1846,11 @@ public class ItemTradeAttribute { if (!item.hasTagCompound()) { item.setTagCompound(new NBTTagCompound()); } - item.getTagCompound().setBoolean(TAG_UNTRADEABLE, untradeable); + if (untradeable) { + item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); + } else { + item.getTagCompound().removeTag(TAG_UNTRADEABLE); + } } public static boolean isUntradeable(ItemStack item) { @@ -1827,14 +1858,14 @@ public class ItemTradeAttribute { return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); } - public static boolean isSlotBound(ItemStack item) { + public static boolean isProfileSlotBound(ItemStack item) { if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().getBoolean(TAG_SLOT_BOUND); + return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); } - public static boolean isSoulBound(ItemStack item) { + public static boolean isSoulbound(ItemStack item) { if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().getBoolean(TAG_SOUL_BOUND); + return item.getTagCompound().hasKey(TAG_SOULBIND); } /** @@ -1845,15 +1876,15 @@ public class ItemTradeAttribute { NBTTagCompound tag = item.getTagCompound(); if (tag.getBoolean(TAG_UNTRADEABLE)) return "gui.auction.untradeable"; - if (tag.getBoolean(TAG_SLOT_BOUND)) return "gui.auction.slotbound"; - if (tag.getBoolean(TAG_SOUL_BOUND)) return "gui.auction.soulbound"; + if (tag.hasKey(TAG_PROFILE_SLOT)) return "gui.auction.profileslotbound"; + if (tag.hasKey(TAG_SOULBIND)) return "gui.auction.soulbound"; return null; } } ``` -### 4.3 Auction House Item Blacklist +### 4.4 Auction House Item Blacklist A configurable system to blacklist specific items from being auctioned. @@ -1980,7 +2011,7 @@ public class AuctionBlacklist { } ``` -### 4.4 Integration with Auction Listing +### 4.5 Integration with Auction Listing Modify `AuctionController.createListing()` to check restrictions: @@ -2196,6 +2227,115 @@ The Admin GUI can be accessed via: --- +## Configuration Reference + +All auction and market settings will be added to the configuration system. + +### Auction Configuration + +**File: `config/customnpcs/auction.cfg`** (or section in existing config) + +```properties +# =========================================== +# Auction House Configuration +# =========================================== + +# General Settings +# ---------------- +# Enable/disable the auction house system +B:AuctionEnabled=true + +# Default maximum auction listings per player (without permission override) +I:DefaultMaxListings=5 + +# Listing fee as percentage of starting price (0.0 = no fee) +D:ListingFeePercent=0.0 + +# Sales tax as percentage taken from final sale price (0.05 = 5%) +D:SalesTaxPercent=0.05 + +# Require an Auctioneer NPC to access auction house +B:RequireAuctioneerNPC=true + +# Timing Settings +# --------------- +# Available auction durations in hours (comma-separated) +S:AvailableDurations=2,12,24,48 + +# Default auction duration in hours +I:DefaultDurationHours=24 + +# Claim expiration in days (claims auto-expire after this period, 0 = never) +I:ClaimExpirationDays=30 + +# Bidding Rules +# ------------- +# Minimum bid increment as percentage (0.05 = 5% minimum increase) +D:MinBidIncrementPercent=0.05 + +# Snipe protection: extend auction TO this many minutes when bid placed near end +# Set to 0 to disable snipe protection +I:SnipeProtectionMinutes=5 + +# How many minutes before end triggers snipe protection +# (bids placed with less than this remaining will extend the auction) +I:SnipeProtectionThreshold=5 + +# Permissions +# ----------- +# Permission pattern for auction slot limits: customnpcs.auction.slots.X +# Players without any permission get DefaultMaxListings +# Example: Player with customnpcs.auction.slots.10 can have 10 listings +``` + +### Currency Configuration + +```properties +# =========================================== +# Currency System Configuration +# =========================================== + +# Use VaultAPI if available on Bukkit hybrid servers +B:UseVaultIfAvailable=true + +# Fallback currency item (registry name) when Vault unavailable +# Leave empty to disable fallback currency +S:FallbackCurrencyItem=customnpcs:npcCoinGold + +# Conversion ratio: 1 fallback item = X virtual currency +D:FallbackItemRatio=1.0 +``` + +### Trader Stock Configuration + +```properties +# =========================================== +# Trader Stock Configuration +# =========================================== + +# Enable stock system by default for new traders +B:EnableStockByDefault=false + +# Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) +S:DefaultResetType=NONE +``` + +### Permission Nodes + +| Permission | Description | Default | +|------------|-------------|---------| +| `customnpcs.auction.use` | Access auction house | true | +| `customnpcs.auction.sell` | Create auction listings | true | +| `customnpcs.auction.buy` | Bid and buyout auctions | true | +| `customnpcs.auction.slots.X` | Have X maximum listings | - | +| `customnpcs.auction.admin` | Access admin management | op | +| `customnpcs.auction.manage` | Modify/cancel any listing | op | +| `customnpcs.auction.blacklist` | Manage item blacklist | op | +| `customnpcs.auction.bypass.fee` | Bypass listing fees | op | +| `customnpcs.auction.bypass.tax` | Bypass sales tax | op | + +--- + ## Implementation Order The features should be implemented in this order due to dependencies: @@ -2400,17 +2540,21 @@ This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.1 1. **Currency System** provides the foundation for monetary transactions (shared across profile slots) 2. **Trader Enhancements** modernize the existing trader with stock, currency support, and reset timer banners -3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, SlotBound, SoulBound attributes and blacklists -4. **Auction House** creates a global marketplace with claims-based refunds and permission-based slot limits +3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, ProfileSlot Bound, and Soulbound attributes plus blacklists +4. **Auction House** creates a global marketplace with claims-based refunds, bid modification, and permission-based slot limits 5. **Admin Management** provides comprehensive tools for server operators to manage the auction house in real-time -The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems) and can be developed incrementally, with each phase building on the previous one. +The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems, existing requirement types) and can be developed incrementally, with each phase building on the previous one. ### Key Design Decisions Summary -- Outbid refunds → Claims (not direct to balance) +- Outbid refunds → Claims (not direct to balance) with **Modify Bid** feature - Currency → Shared across all profile slots per player -- Permissions → Uses existing CustomNPC+ permission pattern +- Permissions → Uses existing CustomNPC+ permission pattern (default: 5 slots) - Display icon → Existing `CustomItems.coinGold` - Stock reset timer → Banner displayed ABOVE the trader GUI -- Trade restrictions → Untradeable attribute + SlotBound/SoulBound + Blacklist system +- Trade restrictions → New Untradeable + existing ProfileSlotRequirement/SoulbindRequirement + Blacklist +- Claim expiration → 30 days default (configurable) +- Bid increment → Percentage-based minimum (configurable, e.g., 5%) +- Snipe protection → Extend TO 5 minutes when bid placed near end (configurable) +- Auction durations → 2h, 12h, 24h, 48h (configurable) - Admin tools → Full management GUI with audit logging From 4093bf9adf8c6a5ada5684b44d026c4b53f04cde Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 07:56:30 +0000 Subject: [PATCH 04/21] Move market configs to main.cfg with dedicated categories Configuration now uses the main config file with nested categories: - market (main category) - market.currency (currency system settings) - market.auction (auction house settings) - market.trader (trader stock settings) This keeps all market/economy settings in one place within the existing main configuration file. --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 133 +++++++++++++-------------- 1 file changed, 66 insertions(+), 67 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index cd9b80885..e9b4cd0ce 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -2229,95 +2229,94 @@ The Admin GUI can be accessed via: ## Configuration Reference -All auction and market settings will be added to the configuration system. +All auction and market settings will be added to the **main configuration file** under dedicated categories. -### Auction Configuration +### Location -**File: `config/customnpcs/auction.cfg`** (or section in existing config) +**File: `config/CustomNPCs/main.cfg`** (existing main config) + +New categories to add: +- `market` - General market/economy settings +- `market.currency` - Currency system settings +- `market.auction` - Auction house settings +- `market.trader` - Trader stock settings + +### Market Configuration (Main Category) ```properties # =========================================== -# Auction House Configuration +# Market & Economy Configuration # =========================================== +market { + # Enable the market/economy system + B:Enabled=true -# General Settings -# ---------------- -# Enable/disable the auction house system -B:AuctionEnabled=true + # =========================================== + # Currency Settings + # =========================================== + currency { + # Use VaultAPI if available on Bukkit hybrid servers + B:UseVaultIfAvailable=true -# Default maximum auction listings per player (without permission override) -I:DefaultMaxListings=5 + # Fallback currency item (registry name) when Vault unavailable + # Leave empty to disable fallback currency + S:FallbackCurrencyItem=customnpcs:npcCoinGold -# Listing fee as percentage of starting price (0.0 = no fee) -D:ListingFeePercent=0.0 + # Conversion ratio: 1 fallback item = X virtual currency + D:FallbackItemRatio=1.0 + } -# Sales tax as percentage taken from final sale price (0.05 = 5%) -D:SalesTaxPercent=0.05 + # =========================================== + # Auction House Settings + # =========================================== + auction { + # Enable/disable the auction house system + B:Enabled=true -# Require an Auctioneer NPC to access auction house -B:RequireAuctioneerNPC=true + # Default maximum auction listings per player (without permission override) + I:DefaultMaxListings=5 -# Timing Settings -# --------------- -# Available auction durations in hours (comma-separated) -S:AvailableDurations=2,12,24,48 + # Listing fee as percentage of starting price (0.0 = no fee) + D:ListingFeePercent=0.0 -# Default auction duration in hours -I:DefaultDurationHours=24 + # Sales tax as percentage taken from final sale price (0.05 = 5%) + D:SalesTaxPercent=0.05 -# Claim expiration in days (claims auto-expire after this period, 0 = never) -I:ClaimExpirationDays=30 + # Require an Auctioneer NPC to access auction house + B:RequireAuctioneerNPC=true -# Bidding Rules -# ------------- -# Minimum bid increment as percentage (0.05 = 5% minimum increase) -D:MinBidIncrementPercent=0.05 + # Available auction durations in hours (comma-separated) + S:AvailableDurations=2,12,24,48 -# Snipe protection: extend auction TO this many minutes when bid placed near end -# Set to 0 to disable snipe protection -I:SnipeProtectionMinutes=5 + # Default auction duration in hours + I:DefaultDurationHours=24 -# How many minutes before end triggers snipe protection -# (bids placed with less than this remaining will extend the auction) -I:SnipeProtectionThreshold=5 + # Claim expiration in days (claims auto-expire after this period, 0 = never) + I:ClaimExpirationDays=30 -# Permissions -# ----------- -# Permission pattern for auction slot limits: customnpcs.auction.slots.X -# Players without any permission get DefaultMaxListings -# Example: Player with customnpcs.auction.slots.10 can have 10 listings -``` + # Minimum bid increment as percentage (0.05 = 5% minimum increase) + D:MinBidIncrementPercent=0.05 -### Currency Configuration + # Snipe protection: extend auction TO this many minutes when bid placed near end + # Set to 0 to disable snipe protection + I:SnipeProtectionMinutes=5 -```properties -# =========================================== -# Currency System Configuration -# =========================================== + # How many minutes before end triggers snipe protection + # (bids placed with less than this remaining will extend the auction) + I:SnipeProtectionThreshold=5 + } -# Use VaultAPI if available on Bukkit hybrid servers -B:UseVaultIfAvailable=true + # =========================================== + # Trader Stock Settings + # =========================================== + trader { + # Enable stock system by default for new traders + B:EnableStockByDefault=false -# Fallback currency item (registry name) when Vault unavailable -# Leave empty to disable fallback currency -S:FallbackCurrencyItem=customnpcs:npcCoinGold - -# Conversion ratio: 1 fallback item = X virtual currency -D:FallbackItemRatio=1.0 -``` - -### Trader Stock Configuration - -```properties -# =========================================== -# Trader Stock Configuration -# =========================================== - -# Enable stock system by default for new traders -B:EnableStockByDefault=false - -# Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) -S:DefaultResetType=NONE + # Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) + S:DefaultResetType=NONE + } +} ``` ### Permission Nodes From 7e6b17c106afd719b0b0ee69a59e7c0c622e820c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:14:29 +0000 Subject: [PATCH 05/21] Implement Phase 1: Currency System foundation - Add ConfigMarket.java with separate market.cfg config file - Currency settings (name, starting balance, max balance, Vault integration) - Auction settings (durations, fees, snipe protection, claim expiration) - Trader settings (stock system defaults) - Add PlayerCurrencyData.java for balance storage - NOT profile slot bound - shared across all slots - Tracks balance, lifetime earned, and lifetime spent - Deposit/withdraw with overflow protection - Add VaultHelper.java for Bukkit hybrid server integration - Reflection-based VaultAPI integration - Falls back to internal currency when Vault unavailable - Supports getBalance, deposit, withdraw, transfer operations - Add CurrencyController.java for currency operations - Claims system for auction refunds and NPC payments - Claim expiration with configurable days - Persistence via currency_claims.dat - Update PlayerData.java to integrate currency - Separate loadCurrency/saveCurrency methods - Currency file stored separately from slot data - Add scripting API methods to ScriptPlayer.java - getCurrencyBalance(), addCurrency(), removeCurrency() - canAffordCurrency(), getCurrencyName(), formatCurrency() - hasPendingClaims(), getPendingClaimsTotal(), claimAllCurrency() --- src/main/java/noppes/npcs/CustomNpcs.java | 4 + .../java/noppes/npcs/config/ConfigMarket.java | 217 ++++++++++ .../noppes/npcs/config/LoadConfiguration.java | 3 + .../npcs/controllers/CurrencyController.java | 400 ++++++++++++++++++ .../noppes/npcs/controllers/VaultHelper.java | 272 ++++++++++++ .../controllers/data/PlayerCurrencyData.java | 131 ++++++ .../npcs/controllers/data/PlayerData.java | 66 +++ .../npcs/scripted/entity/ScriptPlayer.java | 118 ++++++ 8 files changed, 1211 insertions(+) create mode 100644 src/main/java/noppes/npcs/config/ConfigMarket.java create mode 100644 src/main/java/noppes/npcs/controllers/CurrencyController.java create mode 100644 src/main/java/noppes/npcs/controllers/VaultHelper.java create mode 100644 src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 4b5750c6a..422be3e61 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -58,6 +58,8 @@ import noppes.npcs.controllers.GlobalDataController; import noppes.npcs.controllers.LinkedItemController; import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.VaultHelper; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -304,6 +306,8 @@ public class CustomNpcs { //Loading items in the about to start event was corrupting items with a damage value @EventHandler public void started(FMLServerStartedEvent event) { + new VaultHelper(); + new CurrencyController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java new file mode 100644 index 000000000..9c5791929 --- /dev/null +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -0,0 +1,217 @@ +package noppes.npcs.config; + +import cpw.mods.fml.common.FMLLog; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.common.config.Property; +import org.apache.logging.log4j.Level; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; + +public class ConfigMarket { + public static Configuration config; + + public final static String MARKET = "Market"; + public final static String CURRENCY = "Market.Currency"; + public final static String AUCTION = "Market.Auction"; + public final static String TRADER = "Market.Trader"; + + // ========================================= + // Market General + // ========================================= + public static boolean MarketEnabled = true; + + // ========================================= + // Currency Settings + // ========================================= + public static boolean UseVaultIfAvailable = true; + public static String CurrencyName = "Gold"; + public static String FallbackCurrencyItem = "customnpcs:npcCoinGold"; + public static long FallbackItemRatio = 1; + public static long StartingBalance = 1000; + public static long MaxBalance = 999999999999L; + + // ========================================= + // Auction Settings + // ========================================= + public static boolean AuctionEnabled = true; + public static int DefaultMaxListings = 5; + public static double ListingFeePercent = 0.0; + public static double SalesTaxPercent = 0.05; + public static boolean RequireAuctioneerNPC = true; + public static String AvailableDurations = "2,12,24,48"; + public static int DefaultDurationHours = 24; + public static int ClaimExpirationDays = 30; + public static double MinBidIncrementPercent = 0.05; + public static int SnipeProtectionMinutes = 5; + public static int SnipeProtectionThreshold = 5; + + // ========================================= + // Trader Settings + // ========================================= + public static boolean EnableStockByDefault = false; + public static String DefaultResetType = "NONE"; + + public static void init(File configFile) { + config = new Configuration(configFile); + + try { + config.load(); + + // ========================================= + // Market General + // ========================================= + MarketEnabled = config.get(MARKET, "Enable Market System", true, + "Enable the market/economy system").getBoolean(true); + + // ========================================= + // Currency Settings + // ========================================= + config.setCategoryComment(CURRENCY, "Currency system settings for the virtual economy"); + + UseVaultIfAvailable = config.get(CURRENCY, "Use Vault If Available", true, + "Use VaultAPI if available on Bukkit hybrid servers").getBoolean(true); + + CurrencyName = config.get(CURRENCY, "Currency Name", "Gold", + "Display name for the currency").getString(); + + FallbackCurrencyItem = config.get(CURRENCY, "Fallback Currency Item", "customnpcs:npcCoinGold", + "Item registry name to use as currency when Vault is unavailable. Leave empty to disable.").getString(); + + FallbackItemRatio = config.get(CURRENCY, "Fallback Item Ratio", 1, + "Conversion ratio: 1 fallback item = X virtual currency").getInt(1); + + StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, + "Starting balance for new players (only used when Vault is not available)").getInt(1000); + + MaxBalance = config.get(CURRENCY, "Maximum Balance", 999999999999L, + "Maximum balance a player can have (only used when Vault is not available)").getLong(999999999999L); + + // ========================================= + // Auction Settings + // ========================================= + config.setCategoryComment(AUCTION, "Auction house settings"); + + AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, + "Enable/disable the auction house system").getBoolean(true); + + DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, + "Default maximum auction listings per player (without permission override)").getInt(5); + + ListingFeePercent = config.get(AUCTION, "Listing Fee Percent", 0.0, + "Listing fee as percentage of starting price (0.0 = no fee)").getDouble(0.0); + + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, + "Sales tax as percentage taken from final sale price (0.05 = 5%)").getDouble(0.05); + + RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, + "Require an Auctioneer NPC to access auction house").getBoolean(true); + + AvailableDurations = config.get(AUCTION, "Available Durations", "2,12,24,48", + "Available auction durations in hours (comma-separated)").getString(); + + DefaultDurationHours = config.get(AUCTION, "Default Duration Hours", 24, + "Default auction duration in hours").getInt(24); + + ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, + "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); + + MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, + "Minimum bid increment as percentage (0.05 = 5% minimum increase)").getDouble(0.05); + + SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 5, + "Extend auction TO this many minutes when bid placed near end. Set to 0 to disable.").getInt(5); + + SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, + "How many minutes before end triggers snipe protection").getInt(5); + + // ========================================= + // Trader Settings + // ========================================= + config.setCategoryComment(TRADER, "Trader stock system settings"); + + EnableStockByDefault = config.get(TRADER, "Enable Stock By Default", false, + "Enable stock system by default for new traders").getBoolean(false); + + DefaultResetType = config.get(TRADER, "Default Reset Type", "NONE", + "Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY)").getString(); + + // Set category order + config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList( + "Enable Market System" + ))); + + config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( + "Use Vault If Available", + "Currency Name", + "Fallback Currency Item", + "Fallback Item Ratio", + "Starting Balance", + "Maximum Balance" + ))); + + config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( + "Enable Auction House", + "Default Max Listings", + "Listing Fee Percent", + "Sales Tax Percent", + "Require Auctioneer NPC", + "Available Durations", + "Default Duration Hours", + "Claim Expiration Days", + "Min Bid Increment Percent", + "Snipe Protection Minutes", + "Snipe Protection Threshold" + ))); + + config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( + "Enable Stock By Default", + "Default Reset Type" + ))); + + // Validation + if (DefaultMaxListings < 1) { + DefaultMaxListings = 1; + } + if (ClaimExpirationDays < 0) { + ClaimExpirationDays = 0; + } + if (SnipeProtectionMinutes < 0) { + SnipeProtectionMinutes = 0; + } + if (SnipeProtectionThreshold < 0) { + SnipeProtectionThreshold = 0; + } + if (StartingBalance < 0) { + StartingBalance = 0; + } + if (MaxBalance < 1) { + MaxBalance = 1; + } + + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } + + /** + * Parse available durations from config string + */ + public static int[] getAvailableDurationsArray() { + String[] parts = AvailableDurations.split(","); + int[] durations = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + durations[i] = Integer.parseInt(parts[i].trim()); + } catch (NumberFormatException e) { + durations[i] = 24; // Default to 24 hours if parsing fails + } + } + return durations; + } +} diff --git a/src/main/java/noppes/npcs/config/LoadConfiguration.java b/src/main/java/noppes/npcs/config/LoadConfiguration.java index 19fad65c2..dfac43d90 100644 --- a/src/main/java/noppes/npcs/config/LoadConfiguration.java +++ b/src/main/java/noppes/npcs/config/LoadConfiguration.java @@ -9,6 +9,7 @@ public class LoadConfiguration { public static File debugConfigFile; public static File clientConfigFile; public static File experimentalConfigFile; + public static File marketConfigFile; public static void init(String configpath) { mainConfigFile = new File(configpath + "main.cfg"); @@ -17,6 +18,7 @@ public class LoadConfiguration { scriptConfigFile = new File(configpath + "script.cfg"); debugConfigFile = new File(configpath + "debug.cfg"); experimentalConfigFile = new File(configpath + "experimental.cfg"); + marketConfigFile = new File(configpath + "market.cfg"); ConfigMain.init(mainConfigFile); ConfigItem.init(itemConfigFile); @@ -24,5 +26,6 @@ public class LoadConfiguration { ConfigScript.init(scriptConfigFile); ConfigDebug.init(debugConfigFile); ConfigExperimental.init(experimentalConfigFile); + ConfigMarket.init(marketConfigFile); } } diff --git a/src/main/java/noppes/npcs/controllers/CurrencyController.java b/src/main/java/noppes/npcs/controllers/CurrencyController.java new file mode 100644 index 000000000..16d6c99f4 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/CurrencyController.java @@ -0,0 +1,400 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.PlayerData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.*; + +/** + * Controller for managing currency operations including: + * - Claims system (for auction refunds, NPC payments, etc.) + * - Transaction history + * - Currency administration + * + * Delegates actual balance operations to VaultHelper. + */ +public class CurrencyController { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static CurrencyController Instance; + + // Pending claims per player UUID - claims that can be collected + private Map> pendingClaims = new HashMap<>(); + + // File path for persistence + private String filePath = ""; + + public CurrencyController() { + Instance = this; + load(); + } + + /** + * Get player's current balance + */ + public long getBalance(EntityPlayer player) { + return VaultHelper.Instance.getBalance(player); + } + + /** + * Deposit currency directly to player's balance + */ + public boolean deposit(EntityPlayer player, long amount) { + return VaultHelper.Instance.deposit(player, amount); + } + + /** + * Withdraw currency from player's balance + */ + public boolean withdraw(EntityPlayer player, long amount) { + return VaultHelper.Instance.withdraw(player, amount); + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(EntityPlayer player, long amount) { + return VaultHelper.Instance.canAfford(player, amount); + } + + /** + * Transfer currency between players + */ + public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { + return VaultHelper.Instance.transfer(from, to, amount); + } + + /** + * Format amount for display + */ + public String formatAmount(long amount) { + return VaultHelper.Instance.formatAmount(amount); + } + + // ==================== CLAIMS SYSTEM ==================== + + /** + * Add a claim for a player (e.g., auction refund, NPC payment) + * Claims can be collected later by the player. + * + * @param playerUUID The player's UUID + * @param amount The amount to claim + * @param source Description of where this claim came from + * @param sourceId Optional ID of the source (e.g., auction ID) + */ + public void addClaim(UUID playerUUID, long amount, String source, String sourceId) { + if (amount <= 0) { + return; + } + + CurrencyClaim claim = new CurrencyClaim(); + claim.amount = amount; + claim.source = source; + claim.sourceId = sourceId; + claim.timestamp = System.currentTimeMillis(); + claim.expiresAt = claim.timestamp + (ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L); + + List claims = pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()); + claims.add(claim); + + save(); + logger.info("Added claim of {} for player {} from {}", amount, playerUUID, source); + } + + /** + * Get all pending claims for a player + */ + public List getPendingClaims(EntityPlayer player) { + return getPendingClaims(player.getUniqueID()); + } + + /** + * Get all pending claims for a player by UUID + */ + public List getPendingClaims(UUID playerUUID) { + List claims = pendingClaims.get(playerUUID); + if (claims == null) { + return new ArrayList<>(); + } + + // Filter out expired claims + long now = System.currentTimeMillis(); + claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); + + return new ArrayList<>(claims); + } + + /** + * Get total pending claim amount for a player + */ + public long getTotalPendingClaims(EntityPlayer player) { + return getTotalPendingClaims(player.getUniqueID()); + } + + /** + * Get total pending claim amount for a player by UUID + */ + public long getTotalPendingClaims(UUID playerUUID) { + List claims = getPendingClaims(playerUUID); + long total = 0; + for (CurrencyClaim claim : claims) { + total += claim.amount; + } + return total; + } + + /** + * Claim all pending claims for a player + * @return Amount successfully claimed + */ + public long claimAll(EntityPlayer player) { + List claims = getPendingClaims(player); + if (claims.isEmpty()) { + return 0; + } + + long totalClaimed = 0; + + for (CurrencyClaim claim : claims) { + if (deposit(player, claim.amount)) { + totalClaimed += claim.amount; + } + } + + // Clear claims after successful deposit + pendingClaims.remove(player.getUniqueID()); + save(); + + return totalClaimed; + } + + /** + * Claim a specific claim by index + * @return true if successful + */ + public boolean claimSingle(EntityPlayer player, int claimIndex) { + List claims = pendingClaims.get(player.getUniqueID()); + if (claims == null || claimIndex < 0 || claimIndex >= claims.size()) { + return false; + } + + CurrencyClaim claim = claims.get(claimIndex); + if (deposit(player, claim.amount)) { + claims.remove(claimIndex); + if (claims.isEmpty()) { + pendingClaims.remove(player.getUniqueID()); + } + save(); + return true; + } + + return false; + } + + /** + * Check if player has any pending claims + */ + public boolean hasPendingClaims(EntityPlayer player) { + return !getPendingClaims(player).isEmpty(); + } + + // ==================== PERSISTENCE ==================== + + public void load() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + filePath = saveDir.getAbsolutePath(); + pendingClaims.clear(); + + try { + File file = new File(saveDir, "currency_claims.dat"); + if (file.exists()) { + loadFromFile(file); + } + } catch (Exception e) { + logger.error("Error loading currency claims", e); + try { + File file = new File(saveDir, "currency_claims.dat_old"); + if (file.exists()) { + loadFromFile(file); + } + } catch (Exception ee) { + logger.error("Error loading backup currency claims", ee); + } + } + + // Clean expired claims + cleanExpiredClaims(); + } + + private void loadFromFile(File file) throws Exception { + FileInputStream fis = new FileInputStream(file); + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + fis.close(); + + NBTTagList claimsList = compound.getTagList("Claims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + NBTTagCompound playerCompound = claimsList.getCompoundTagAt(i); + UUID playerUUID = UUID.fromString(playerCompound.getString("UUID")); + + List playerClaims = new ArrayList<>(); + NBTTagList playerClaimsList = playerCompound.getTagList("PlayerClaims", 10); + for (int j = 0; j < playerClaimsList.tagCount(); j++) { + NBTTagCompound claimCompound = playerClaimsList.getCompoundTagAt(j); + CurrencyClaim claim = new CurrencyClaim(); + claim.readFromNBT(claimCompound); + playerClaims.add(claim); + } + + if (!playerClaims.isEmpty()) { + pendingClaims.put(playerUUID, playerClaims); + } + } + + logger.info("Loaded {} player claim records", pendingClaims.size()); + } + + public void save() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + try { + File file = new File(saveDir, "currency_claims.dat"); + File backup = new File(saveDir, "currency_claims.dat_old"); + + if (file.exists()) { + if (backup.exists()) { + backup.delete(); + } + file.renameTo(backup); + } + + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList claimsList = new NBTTagList(); + + for (Map.Entry> entry : pendingClaims.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + + NBTTagCompound playerCompound = new NBTTagCompound(); + playerCompound.setString("UUID", entry.getKey().toString()); + + NBTTagList playerClaimsList = new NBTTagList(); + for (CurrencyClaim claim : entry.getValue()) { + NBTTagCompound claimCompound = new NBTTagCompound(); + claim.writeToNBT(claimCompound); + playerClaimsList.appendTag(claimCompound); + } + playerCompound.setTag("PlayerClaims", playerClaimsList); + + claimsList.appendTag(playerCompound); + } + + compound.setTag("Claims", claimsList); + + file = new File(saveDir, "currency_claims.dat"); + FileOutputStream fos = new FileOutputStream(file); + CompressedStreamTools.writeCompressed(compound, fos); + fos.close(); + } catch (Exception e) { + logger.error("Error saving currency claims", e); + } + } + + /** + * Clean up expired claims + */ + public void cleanExpiredClaims() { + long now = System.currentTimeMillis(); + boolean changed = false; + + Iterator>> it = pendingClaims.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + List claims = entry.getValue(); + + int before = claims.size(); + claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); + + if (claims.size() != before) { + changed = true; + } + + if (claims.isEmpty()) { + it.remove(); + } + } + + if (changed) { + save(); + } + } + + // ==================== CLAIM DATA CLASS ==================== + + public static class CurrencyClaim { + public long amount; + public String source; // e.g., "Auction Refund", "NPC Payment", "Quest Reward" + public String sourceId; // e.g., auction ID + public long timestamp; // When the claim was created + public long expiresAt; // When the claim expires (0 = never) + + public void writeToNBT(NBTTagCompound compound) { + compound.setLong("Amount", amount); + compound.setString("Source", source != null ? source : ""); + compound.setString("SourceId", sourceId != null ? sourceId : ""); + compound.setLong("Timestamp", timestamp); + compound.setLong("ExpiresAt", expiresAt); + } + + public void readFromNBT(NBTTagCompound compound) { + amount = compound.getLong("Amount"); + source = compound.getString("Source"); + sourceId = compound.getString("SourceId"); + timestamp = compound.getLong("Timestamp"); + expiresAt = compound.getLong("ExpiresAt"); + } + + /** + * Get formatted time remaining until expiration + */ + public String getTimeRemaining() { + if (expiresAt <= 0) { + return "Never"; + } + + long remaining = expiresAt - System.currentTimeMillis(); + if (remaining <= 0) { + return "Expired"; + } + + long days = remaining / (24L * 60L * 60L * 1000L); + long hours = (remaining % (24L * 60L * 60L * 1000L)) / (60L * 60L * 1000L); + + if (days > 0) { + return days + " days, " + hours + " hours"; + } else if (hours > 0) { + return hours + " hours"; + } else { + long minutes = remaining / (60L * 1000L); + return minutes + " minutes"; + } + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java new file mode 100644 index 000000000..ae2c6f97a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/VaultHelper.java @@ -0,0 +1,272 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerDataController; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.Method; + +/** + * Helper class for VaultAPI integration on Bukkit hybrid servers. + * Uses reflection to integrate with Vault when available. + * Falls back to internal currency system when Vault is not present. + */ +public class VaultHelper { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static VaultHelper Instance; + + // Vault reflection references + private boolean vaultAvailable = false; + private Object economy = null; + private Method getBalance; + private Method depositPlayer; + private Method withdrawPlayer; + private Method hasAccount; + private Method createPlayerAccount; + private Method responseSuccess; + + // Bukkit references for getting offline player + private Class bukkitClass; + private Method getOfflinePlayer; + + public VaultHelper() { + Instance = this; + + if (!ConfigMarket.UseVaultIfAvailable) { + logger.info("Vault integration disabled by config"); + return; + } + + try { + // Check for Bukkit first + bukkitClass = Class.forName("org.bukkit.Bukkit"); + getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); + + // Check for Vault + Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); + + // Get the Economy service from Bukkit's ServicesManager + Method getServicesManager = bukkitClass.getMethod("getServicesManager"); + Object servicesManager = getServicesManager.invoke(null); + + Method getRegistration = servicesManager.getClass().getMethod("getRegistration", Class.class); + Object registration = getRegistration.invoke(servicesManager, vaultClass); + + if (registration != null) { + Method getProvider = registration.getClass().getMethod("getProvider"); + economy = getProvider.invoke(registration); + + if (economy != null) { + // Get Economy methods - using OfflinePlayer versions for broader compatibility + Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); + getBalance = economy.getClass().getMethod("getBalance", offlinePlayerClass); + depositPlayer = economy.getClass().getMethod("depositPlayer", offlinePlayerClass, double.class); + withdrawPlayer = economy.getClass().getMethod("withdrawPlayer", offlinePlayerClass, double.class); + hasAccount = economy.getClass().getMethod("hasAccount", offlinePlayerClass); + createPlayerAccount = economy.getClass().getMethod("createPlayerAccount", offlinePlayerClass); + + // EconomyResponse.transactionSuccess() method + Class economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); + responseSuccess = economyResponseClass.getField("transactionSuccess").getClass().getMethod("booleanValue"); + + vaultAvailable = true; + logger.info("Vault Economy integration enabled - using external economy"); + } + } + + if (!vaultAvailable) { + logger.info("Vault found but no Economy provider registered - using internal currency"); + } + } catch (ClassNotFoundException e) { + // Vault not present - use internal currency + logger.info("Vault not found - using internal currency system"); + } catch (Exception e) { + logger.warn("Error initializing Vault integration - using internal currency: " + e.getMessage()); + } + } + + /** + * Check if Vault economy is available and enabled + */ + public boolean isVaultEnabled() { + return vaultAvailable && ConfigMarket.UseVaultIfAvailable; + } + + /** + * Get player's balance + * Uses Vault if available, otherwise internal currency + */ + public long getBalance(EntityPlayer player) { + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + Double balance = (Double) getBalance.invoke(economy, offlinePlayer); + return balance.longValue(); + } catch (Exception e) { + logger.warn("Vault getBalance failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + return data.currencyData.getBalance(); + } + + /** + * Deposit currency to player's balance + * Uses Vault if available, otherwise internal currency + * @return true if successful + */ + public boolean deposit(EntityPlayer player, long amount) { + if (amount <= 0) { + return false; + } + + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + + // Ensure account exists + Boolean hasAcc = (Boolean) hasAccount.invoke(economy, offlinePlayer); + if (!hasAcc) { + createPlayerAccount.invoke(economy, offlinePlayer); + } + + Object response = depositPlayer.invoke(economy, offlinePlayer, (double) amount); + // Check if transaction succeeded by checking the transactionSuccess field + Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); + + if (success) { + // Also update internal tracking for statistics + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(getBalance(player)); + return true; + } + return false; + } catch (Exception e) { + logger.warn("Vault deposit failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + boolean success = data.currencyData.deposit(amount); + if (success) { + data.saveCurrency(); + } + return success; + } + + /** + * Withdraw currency from player's balance + * Uses Vault if available, otherwise internal currency + * @return true if successful + */ + public boolean withdraw(EntityPlayer player, long amount) { + if (amount <= 0) { + return false; + } + + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + + // Check if player has enough + Double currentBalance = (Double) getBalance.invoke(economy, offlinePlayer); + if (currentBalance < amount) { + return false; + } + + Object response = withdrawPlayer.invoke(economy, offlinePlayer, (double) amount); + Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); + + if (success) { + // Also update internal tracking for statistics + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(getBalance(player)); + return true; + } + return false; + } catch (Exception e) { + logger.warn("Vault withdraw failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + boolean success = data.currencyData.withdraw(amount); + if (success) { + data.saveCurrency(); + } + return success; + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(EntityPlayer player, long amount) { + return getBalance(player) >= amount; + } + + /** + * Transfer currency from one player to another + * @return true if successful + */ + public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { + if (amount <= 0) { + return false; + } + + if (!canAfford(from, amount)) { + return false; + } + + // Withdraw from sender first + if (!withdraw(from, amount)) { + return false; + } + + // Deposit to receiver + if (!deposit(to, amount)) { + // Rollback - deposit back to sender + deposit(from, amount); + return false; + } + + return true; + } + + /** + * Set player's balance directly (admin operation) + * Only works with internal currency - Vault manages its own balances + */ + public void setBalance(EntityPlayer player, long amount) { + if (isVaultEnabled()) { + logger.warn("Cannot set balance directly when using Vault - use Vault's economy commands"); + return; + } + + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(amount); + data.saveCurrency(); + } + + /** + * Format an amount for display with currency name + */ + public String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Get the currency name + */ + public String getCurrencyName() { + return ConfigMarket.CurrencyName; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java new file mode 100644 index 000000000..511321dbb --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -0,0 +1,131 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigMarket; + +/** + * Stores player currency balance. + * This data is NOT profile slot bound - all profile slots share the same currency pool. + * When Vault API is available, this serves as a cache/fallback. + */ +public class PlayerCurrencyData { + private long balance; + private long lifetimeEarned; + private long lifetimeSpent; + + public PlayerCurrencyData() { + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + } + + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("CurrencyBalance")) { + this.balance = compound.getLong("CurrencyBalance"); + } else { + this.balance = ConfigMarket.StartingBalance; + } + this.lifetimeEarned = compound.getLong("CurrencyLifetimeEarned"); + this.lifetimeSpent = compound.getLong("CurrencyLifetimeSpent"); + } + + public void writeToNBT(NBTTagCompound compound) { + compound.setLong("CurrencyBalance", this.balance); + compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); + compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); + } + + /** + * Get the current balance + */ + public long getBalance() { + return balance; + } + + /** + * Set the balance directly (use with caution) + */ + public void setBalance(long balance) { + if (balance < 0) { + balance = 0; + } + if (balance > ConfigMarket.MaxBalance) { + balance = ConfigMarket.MaxBalance; + } + this.balance = balance; + } + + /** + * Add currency to balance (deposit) + * @param amount Amount to add + * @return true if successful, false if would exceed max balance + */ + public boolean deposit(long amount) { + if (amount <= 0) { + return false; + } + long newBalance = this.balance + amount; + if (newBalance > ConfigMarket.MaxBalance) { + return false; + } + if (newBalance < this.balance) { + // Overflow protection + return false; + } + this.balance = newBalance; + this.lifetimeEarned += amount; + return true; + } + + /** + * Remove currency from balance (withdraw) + * @param amount Amount to remove + * @return true if successful, false if insufficient funds + */ + public boolean withdraw(long amount) { + if (amount <= 0) { + return false; + } + if (this.balance < amount) { + return false; + } + this.balance -= amount; + this.lifetimeSpent += amount; + return true; + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(long amount) { + return this.balance >= amount; + } + + /** + * Get lifetime earned currency + */ + public long getLifetimeEarned() { + return lifetimeEarned; + } + + /** + * Get lifetime spent currency + */ + public long getLifetimeSpent() { + return lifetimeSpent; + } + + /** + * Format the balance for display + */ + public String formatBalance() { + return formatAmount(this.balance); + } + + /** + * Format an amount for display + */ + public static String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 599f81ff1..25d4c81c8 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -59,6 +59,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public DataSkinOverlays skinOverlays = new DataSkinOverlays(this); public MagicData magicData = new MagicData(); + // Currency data - NOT slot-bound, shared across all profile slots + public PlayerCurrencyData currencyData = new PlayerCurrencyData(); + public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -443,6 +446,69 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { data = getNBT(); } setNBT(data); + + // Load currency data separately (not slot-bound) + loadCurrency(); + } + + /** + * Load currency data from separate file (shared across profile slots) + */ + public void loadCurrency() { + if (player == null) return; + + File saveDir = PlayerDataController.Instance.getSaveDir(); + String filename = player.getPersistentID().toString() + "_currency"; + + try { + File file = new File(saveDir, filename + ".json"); + if (file.exists()) { + NBTTagCompound compound = NBTJsonUtil.LoadFile(file); + currencyData.readFromNBT(compound); + return; + } + + // Try dat format + file = new File(saveDir, filename + ".dat"); + if (file.exists()) { + NBTTagCompound compound = NBTJsonUtil.loadNBTData(file); + currencyData.readFromNBT(compound); + return; + } + + // No currency file exists, use defaults (new player) + currencyData = new PlayerCurrencyData(); + } catch (Exception e) { + LogWriter.error("Error loading currency data for " + player.getCommandSenderName(), e); + currencyData = new PlayerCurrencyData(); + } + } + + /** + * Save currency data to separate file (shared across profile slots) + */ + public synchronized void saveCurrency() { + if (uuid == null || uuid.isEmpty()) return; + + final NBTTagCompound compound = new NBTTagCompound(); + currencyData.writeToNBT(compound); + + final String filename = uuid + "_currency.json"; + + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + File saveDir = PlayerDataController.Instance.getSaveDir(); + File file = new File(saveDir, filename + "_new"); + File file1 = new File(saveDir, filename); + NBTJsonUtil.SaveFile(file, compound); + if (file1.exists()) { + file1.delete(); + } + file.renameTo(file1); + } catch (Exception e) { + LogWriter.except(e); + } + }); } public static PlayerData get(EntityPlayer player) { diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 6206cfbac..5bfaba816 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,11 +49,13 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.PlayerQuestController; import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.data.Dialog; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -895,4 +897,120 @@ public class ScriptPlayer extends ScriptLivingBase } return (IPlayer[]) list.toArray(new IPlayer[list.size()]); } + + // ==================== CURRENCY METHODS ==================== + + /** + * Get the player's current currency balance. + * Uses Vault if available, otherwise uses internal currency system. + * @return The player's currency balance + * @since 1.11 + */ + public long getCurrencyBalance() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.getBalance(player); + } + + /** + * Add currency to the player's balance. + * @param amount The amount to add (must be positive) + * @return true if successful, false if failed (e.g., would exceed max balance) + * @since 1.11 + */ + public boolean addCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.deposit(player, amount); + } + + /** + * Remove currency from the player's balance. + * @param amount The amount to remove (must be positive) + * @return true if successful, false if insufficient funds + * @since 1.11 + */ + public boolean removeCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.withdraw(player, amount); + } + + /** + * Check if the player can afford a specific amount. + * @param amount The amount to check + * @return true if player has sufficient balance + * @since 1.11 + */ + public boolean canAffordCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.canAfford(player, amount); + } + + /** + * Get the configured currency name (e.g., "Gold"). + * @return The currency name + * @since 1.11 + */ + public String getCurrencyName() { + if (VaultHelper.Instance == null) { + return "Currency"; + } + return VaultHelper.Instance.getCurrencyName(); + } + + /** + * Format a currency amount for display (e.g., "1,000 Gold"). + * @param amount The amount to format + * @return Formatted string + * @since 1.11 + */ + public String formatCurrency(long amount) { + if (CurrencyController.Instance == null) { + return String.valueOf(amount); + } + return CurrencyController.Instance.formatAmount(amount); + } + + /** + * Check if the player has pending currency claims. + * Claims are created by auction refunds, NPC payments, etc. + * @return true if there are pending claims + * @since 1.11 + */ + public boolean hasPendingClaims() { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.hasPendingClaims(player); + } + + /** + * Get the total amount of pending currency claims. + * @return Total pending claim amount + * @since 1.11 + */ + public long getPendingClaimsTotal() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.getTotalPendingClaims(player); + } + + /** + * Claim all pending currency claims and add them to balance. + * @return The total amount claimed + * @since 1.11 + */ + public long claimAllCurrency() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.claimAll(player); + } } From 60b985d33d0a3cde23a0f058578450b13031a051 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:23:50 +0000 Subject: [PATCH 06/21] Fix currency storage to use main PlayerData file - Currency now stored in main player save file, not separate file - Currency excluded from getNBT/setNBT (slot-specific data) - Currency saved in save() after getNBT(), loaded before setNBT() - This ensures currency is preserved during profile slot changes - Removed loadCurrency() and saveCurrency() separate file methods - Updated VaultHelper to call save() instead of saveCurrency() --- .../noppes/npcs/controllers/VaultHelper.java | 6 +- .../npcs/controllers/data/PlayerData.java | 69 +++---------------- 2 files changed, 11 insertions(+), 64 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java index ae2c6f97a..f660002fc 100644 --- a/src/main/java/noppes/npcs/controllers/VaultHelper.java +++ b/src/main/java/noppes/npcs/controllers/VaultHelper.java @@ -157,7 +157,7 @@ public class VaultHelper { PlayerData data = PlayerDataController.Instance.getPlayerData(player); boolean success = data.currencyData.deposit(amount); if (success) { - data.saveCurrency(); + data.save(); } return success; } @@ -201,7 +201,7 @@ public class VaultHelper { PlayerData data = PlayerDataController.Instance.getPlayerData(player); boolean success = data.currencyData.withdraw(amount); if (success) { - data.saveCurrency(); + data.save(); } return success; } @@ -253,7 +253,7 @@ public class VaultHelper { PlayerData data = PlayerDataController.Instance.getPlayerData(player); data.currencyData.setBalance(amount); - data.saveCurrency(); + data.save(); } /** diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 25d4c81c8..08fdebdd0 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -409,6 +409,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); + // Add currency data to main save (NOT slot-bound, preserved across profile changes) + currencyData.writeToNBT(compound); + final String filename; if (ConfigMain.DatFormat) { filename = uuid + ".dat"; @@ -445,70 +448,14 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (data.hasNoTags()) { data = getNBT(); } - setNBT(data); - // Load currency data separately (not slot-bound) - loadCurrency(); - } - - /** - * Load currency data from separate file (shared across profile slots) - */ - public void loadCurrency() { - if (player == null) return; - - File saveDir = PlayerDataController.Instance.getSaveDir(); - String filename = player.getPersistentID().toString() + "_currency"; - - try { - File file = new File(saveDir, filename + ".json"); - if (file.exists()) { - NBTTagCompound compound = NBTJsonUtil.LoadFile(file); - currencyData.readFromNBT(compound); - return; - } - - // Try dat format - file = new File(saveDir, filename + ".dat"); - if (file.exists()) { - NBTTagCompound compound = NBTJsonUtil.loadNBTData(file); - currencyData.readFromNBT(compound); - return; - } - - // No currency file exists, use defaults (new player) - currencyData = new PlayerCurrencyData(); - } catch (Exception e) { - LogWriter.error("Error loading currency data for " + player.getCommandSenderName(), e); - currencyData = new PlayerCurrencyData(); + // Load currency BEFORE setNBT so we can preserve it + // Currency is stored in main file but NOT in slot data + if (data.hasKey("CurrencyBalance")) { + currencyData.readFromNBT(data); } - } - /** - * Save currency data to separate file (shared across profile slots) - */ - public synchronized void saveCurrency() { - if (uuid == null || uuid.isEmpty()) return; - - final NBTTagCompound compound = new NBTTagCompound(); - currencyData.writeToNBT(compound); - - final String filename = uuid + "_currency.json"; - - CustomNPCsThreader.customNPCThread.execute(() -> { - try { - File saveDir = PlayerDataController.Instance.getSaveDir(); - File file = new File(saveDir, filename + "_new"); - File file1 = new File(saveDir, filename); - NBTJsonUtil.SaveFile(file, compound); - if (file1.exists()) { - file1.delete(); - } - file.renameTo(file1); - } catch (Exception e) { - LogWriter.except(e); - } - }); + setNBT(data); } public static PlayerData get(EntityPlayer player) { From c9f77bed05fc77fd5fb3ad722b5a5dfc24c934fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:29:03 +0000 Subject: [PATCH 07/21] Implement Phase 2: Trader Enhancements with stock and currency system - Add EnumStockReset.java for stock reset timing types - NONE, MCDAILY, MCWEEKLY, MCCUSTOM (MC time) - RLDAILY, RLWEEKLY, RLCUSTOM (real time) - Helper methods for interval calculation and display formatting - Add TraderStock.java for stock management - Per-slot stock limits with -1 for unlimited - Per-player vs per-server stock tracking modes - Automatic reset timer with configurable intervals - PlayerTraderStock inner class for per-player tracking - NBT serialization for persistence - Modify RoleTrader.java to support stock and currency - Add TraderStock instance for stock management - Add useCurrency flag and slotPrices array for virtual currency - Auto-check for stock reset on interact() - Helper methods: hasStock(), getAvailableStock(), consumeStock() - Helper methods: getSlotPrice(), setSlotPrice(), hasSlotPrice() - getTimeUntilResetFormatted() for GUI display - Modify ContainerNPCTrader.java for transactions - Check stock availability before purchase - Handle virtual currency purchases via CurrencyController - Fall back to item-based currency when useCurrency disabled - Consume stock after successful purchase --- .../noppes/npcs/constants/EnumStockReset.java | 64 ++++ .../npcs/containers/ContainerNPCTrader.java | 37 ++- .../npcs/controllers/data/TraderStock.java | 313 ++++++++++++++++++ .../java/noppes/npcs/roles/RoleTrader.java | 126 +++++++ 4 files changed, 535 insertions(+), 5 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/EnumStockReset.java create mode 100644 src/main/java/noppes/npcs/controllers/data/TraderStock.java diff --git a/src/main/java/noppes/npcs/constants/EnumStockReset.java b/src/main/java/noppes/npcs/constants/EnumStockReset.java new file mode 100644 index 000000000..cdc56cb95 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumStockReset.java @@ -0,0 +1,64 @@ +package noppes.npcs.constants; + +/** + * Stock reset types for Trader NPCs. + * Mirrors EnumQuestRepeat pattern for consistency. + */ +public enum EnumStockReset { + NONE, // Never resets + MCDAILY, // Every 24000 MC ticks (1 MC day) + MCWEEKLY, // Every 168000 MC ticks (7 MC days) + MCCUSTOM, // Custom MC tick interval + RLDAILY, // Every 24 real hours + RLWEEKLY, // Every 7 real days + RLCUSTOM; // Custom real-time interval + + /** + * Get reset interval in milliseconds for real-time types + * or ticks for MC-time types + */ + public long getDefaultInterval() { + switch (this) { + case MCDAILY: + return 24000; // MC ticks + case MCWEEKLY: + return 168000; // MC ticks (7 days) + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + default: + return 0; + } + } + + /** + * Check if this reset type uses real-world time + */ + public boolean isRealTime() { + return this == RLDAILY || this == RLWEEKLY || this == RLCUSTOM; + } + + /** + * Check if this reset type uses Minecraft time + */ + public boolean isMinecraftTime() { + return this == MCDAILY || this == MCWEEKLY || this == MCCUSTOM; + } + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case NONE: return "Never"; + case MCDAILY: return "MC Daily"; + case MCWEEKLY: return "MC Weekly"; + case MCCUSTOM: return "MC Custom"; + case RLDAILY: return "Real Daily"; + case RLWEEKLY: return "Real Weekly"; + case RLCUSTOM: return "Real Custom"; + default: return "Unknown"; + } + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index 71dcd6fa6..936868152 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -4,6 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; @@ -61,17 +62,43 @@ public class ContainerNPCTrader extends ContainerNpcInterface { ItemStack item = slot.getStack(); if (!canGivePlayer(item, entityplayer)) return null; - if (!canBuy(i, entityplayer)) - return null; if (!isSlotEnabled(i, entityplayer)) return null; - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + + // Check stock availability + String playerName = entityplayer.getCommandSenderName(); + if (!role.hasStock(i, playerName, 1)) { + return null; // Out of stock + } + + // Handle currency-based purchase + if (role.hasSlotPrice(i)) { + long price = role.getSlotPrice(i); + if (CurrencyController.Instance == null) { + return null; // Currency system not available + } + if (!CurrencyController.Instance.canAfford(entityplayer, price)) { + return null; // Can't afford + } + // Withdraw currency + if (!CurrencyController.Instance.withdraw(entityplayer, price)) { + return null; // Withdrawal failed + } + } else { + // Item-based currency - existing logic + if (!canBuy(i, entityplayer)) + return null; + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + } + + // Consume stock + role.consumeStock(i, playerName, 1); + ItemStack soldItem = item.copy(); givePlayer(soldItem, entityplayer); role.addPurchase(i, entityplayer.getDisplayName()); return soldItem; - } public boolean isSlotEnabled(int slot, EntityPlayer player) { diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java new file mode 100644 index 000000000..808b06f36 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -0,0 +1,313 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.constants.EnumStockReset; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manages trader stock system including: + * - Per-slot stock limits + * - Per-player vs per-server stock tracking + * - Automatic stock reset timers + */ +public class TraderStock { + // Stock configuration + public boolean enableStock = false; + public boolean perPlayer = false; // false = per-server, true = per-player + + // Reset configuration (mirrors Quest cooldown system) + public EnumStockReset resetType = EnumStockReset.NONE; + public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types (in ticks or ms) + + // Stock quantities per slot (max stock, -1 = unlimited) + public int[] maxStock = new int[18]; + + // Current stock (per-server mode) + public int[] currentStock = new int[18]; + public long lastResetTime = 0; + + // Per-player stock tracking + public HashMap playerStock = new HashMap<>(); + + public TraderStock() { + for (int i = 0; i < 18; i++) { + maxStock[i] = -1; // -1 = unlimited + currentStock[i] = -1; + } + } + + /** + * Get available stock for a slot + * @param slot The slot index + * @param playerName The player name (for per-player mode) + * @return Available stock count, or Integer.MAX_VALUE if unlimited + */ + public int getAvailableStock(int slot, String playerName) { + if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { + return Integer.MAX_VALUE; // Unlimited + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.get(playerName); + if (pStock == null) { + return maxStock[slot]; // Not purchased yet + } + return pStock.getStock(slot, maxStock[slot]); + } + + return Math.max(0, currentStock[slot]); + } + + /** + * Check if stock is available for purchase + */ + public boolean hasStock(int slot, String playerName, int amount) { + return getAvailableStock(slot, playerName) >= amount; + } + + /** + * Consume stock when purchase is made + * @return true if successful, false if insufficient stock + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { + return true; // Unlimited, always succeeds + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.computeIfAbsent( + playerName, k -> new PlayerTraderStock()); + return pStock.consumeStock(slot, maxStock[slot], amount); + } + + if (currentStock[slot] >= amount) { + currentStock[slot] -= amount; + return true; + } + return false; + } + + /** + * Check if stock should reset based on current time + * @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type + */ + public boolean shouldReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return false; + } + + long elapsed = currentTime - lastResetTime; + long resetInterval = getResetInterval(); + + return elapsed >= resetInterval; + } + + /** + * Reset all stock to max values + * @param currentTime Current time to record as reset time + */ + public void resetStock(long currentTime) { + lastResetTime = currentTime; + + // Reset per-server stock + for (int i = 0; i < 18; i++) { + currentStock[i] = maxStock[i]; + } + + // Clear per-player stock + playerStock.clear(); + } + + /** + * Get reset interval based on reset type + * @return Interval in ticks (MC time) or milliseconds (real time) + */ + public long getResetInterval() { + switch (resetType) { + case MCDAILY: + return 24000; // 1 MC day in ticks + case MCWEEKLY: + return 168000; // 7 MC days in ticks + case MCCUSTOM: + return customResetTime; // Custom ticks + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + case RLCUSTOM: + return customResetTime; // Custom ms + default: + return Long.MAX_VALUE; + } + } + + /** + * Get time until next reset + * @param currentTime Current time + * @return Time remaining until reset, or -1 if no reset scheduled + */ + public long getTimeUntilReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return -1; + } + long elapsed = currentTime - lastResetTime; + long interval = getResetInterval(); + return Math.max(0, interval - elapsed); + } + + /** + * Format time until reset for display + * @param currentTime Current time + * @return Formatted string like "2 hours" or "1 day, 5 hours" + */ + public String getTimeUntilResetFormatted(long currentTime) { + long remaining = getTimeUntilReset(currentTime); + if (remaining < 0) { + return ""; + } + + if (resetType.isRealTime()) { + // Convert ms to readable format + long hours = remaining / 3600000L; + long days = hours / 24; + hours = hours % 24; + + if (days > 0) { + return days + " day" + (days != 1 ? "s" : "") + + (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); + } else if (hours > 0) { + return hours + " hour" + (hours != 1 ? "s" : ""); + } else { + long minutes = remaining / 60000L; + return minutes + " minute" + (minutes != 1 ? "s" : ""); + } + } else { + // Convert ticks to MC time format + long mcHours = remaining / 1000; // 1000 ticks = 1 MC hour + long mcDays = mcHours / 24; + mcHours = mcHours % 24; + + if (mcDays > 0) { + return mcDays + " MC day" + (mcDays != 1 ? "s" : "") + + (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); + } else { + return mcHours + " MC hour" + (mcHours != 1 ? "s" : ""); + } + } + } + + /** + * Set stock for a specific slot + */ + public void setMaxStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + maxStock[slot] = amount; + if (currentStock[slot] < 0 || currentStock[slot] > amount) { + currentStock[slot] = amount; + } + } + } + + // ==================== NBT Serialization ==================== + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("EnableStock", enableStock); + compound.setBoolean("PerPlayer", perPlayer); + compound.setInteger("ResetType", resetType.ordinal()); + compound.setLong("CustomResetTime", customResetTime); + compound.setIntArray("MaxStock", maxStock); + compound.setIntArray("CurrentStock", currentStock); + compound.setLong("LastResetTime", lastResetTime); + + // Save per-player stock + NBTTagList playerList = new NBTTagList(); + for (Map.Entry entry : playerStock.entrySet()) { + NBTTagCompound playerTag = new NBTTagCompound(); + playerTag.setString("Player", entry.getKey()); + entry.getValue().writeToNBT(playerTag); + playerList.appendTag(playerTag); + } + compound.setTag("PlayerStock", playerList); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + enableStock = compound.getBoolean("EnableStock"); + perPlayer = compound.getBoolean("PerPlayer"); + + int resetOrdinal = compound.getInteger("ResetType"); + if (resetOrdinal >= 0 && resetOrdinal < EnumStockReset.values().length) { + resetType = EnumStockReset.values()[resetOrdinal]; + } + + customResetTime = compound.getLong("CustomResetTime"); + + int[] loadedMax = compound.getIntArray("MaxStock"); + int[] loadedCurrent = compound.getIntArray("CurrentStock"); + + if (loadedMax != null && loadedMax.length == 18) { + maxStock = loadedMax; + } + if (loadedCurrent != null && loadedCurrent.length == 18) { + currentStock = loadedCurrent; + } + + lastResetTime = compound.getLong("LastResetTime"); + + // Load per-player stock + playerStock.clear(); + NBTTagList playerList = compound.getTagList("PlayerStock", 10); + for (int i = 0; i < playerList.tagCount(); i++) { + NBTTagCompound playerTag = playerList.getCompoundTagAt(i); + String playerName = playerTag.getString("Player"); + PlayerTraderStock pStock = new PlayerTraderStock(); + pStock.readFromNBT(playerTag); + playerStock.put(playerName, pStock); + } + } + + // ==================== Inner Class for Per-Player Stock ==================== + + public static class PlayerTraderStock { + public int[] purchasedAmounts = new int[18]; + public long lastPurchaseTime = 0; + + public PlayerTraderStock() { + for (int i = 0; i < 18; i++) { + purchasedAmounts[i] = 0; + } + } + + public int getStock(int slot, int maxStock) { + if (slot < 0 || slot >= 18) return 0; + return Math.max(0, maxStock - purchasedAmounts[slot]); + } + + public boolean consumeStock(int slot, int maxStock, int amount) { + if (slot < 0 || slot >= 18) return false; + if (getStock(slot, maxStock) >= amount) { + purchasedAmounts[slot] += amount; + lastPurchaseTime = System.currentTimeMillis(); + return true; + } + return false; + } + + public void writeToNBT(NBTTagCompound compound) { + compound.setIntArray("Purchased", purchasedAmounts); + compound.setLong("LastPurchase", lastPurchaseTime); + } + + public void readFromNBT(NBTTagCompound compound) { + int[] loaded = compound.getIntArray("Purchased"); + if (loaded != null && loaded.length == 18) { + purchasedAmounts = loaded; + } + lastPurchaseTime = compound.getLong("LastPurchase"); + } + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 5275c26c0..6215eef2f 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -4,11 +4,13 @@ import foxz.utils.Market; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; import noppes.npcs.NBTTags; import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.NoppesUtilServer; import noppes.npcs.NpcMiscInventory; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.data.TraderStock; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; @@ -30,6 +32,13 @@ public class RoleTrader extends RoleInterface { public HashMap playerPurchases; public HashMap playerDisableSlot; + // Stock system + public TraderStock stock = new TraderStock(); + + // Currency system + public boolean useCurrency = false; // Use virtual currency instead of items + public long[] slotPrices = new long[18]; // Currency prices per slot + public RoleTrader(EntityNPCInterface npc) { super(npc); inventoryCurrency = new NpcMiscInventory(36); @@ -59,6 +68,20 @@ public class RoleTrader extends RoleInterface { nbttagcompound.setIntArray("Purchases", purchases); nbttagcompound.setTag("PlayerPurchases", NBTTags.nbtStringIntegerArrayMap(playerPurchases)); } + + // Stock system + nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); + + // Currency system + nbttagcompound.setBoolean("UseCurrency", useCurrency); + NBTTagList priceList = new NBTTagList(); + for (long price : slotPrices) { + NBTTagCompound priceTag = new NBTTagCompound(); + priceTag.setLong("Price", price); + priceList.appendTag(priceTag); + } + nbttagcompound.setTag("SlotPrices", priceList); + return nbttagcompound; } @@ -94,10 +117,39 @@ public class RoleTrader extends RoleInterface { disableSlot = new int[18]; for (int i = 0; i < disableSlot.length; ++i) disableSlot[i] = 0; } + + // Stock system + if (nbttagcompound.hasKey("Stock")) { + stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); + } + + // Currency system + useCurrency = nbttagcompound.getBoolean("UseCurrency"); + if (nbttagcompound.hasKey("SlotPrices")) { + NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); + for (int i = 0; i < priceList.tagCount() && i < 18; i++) { + slotPrices[i] = priceList.getCompoundTagAt(i).getLong("Price"); + } + } } @Override public void interact(EntityPlayer player) { + // Check for stock reset before opening GUI + if (stock.enableStock) { + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + // Save to market if using shared market + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + } + } + npc.say(player, npc.advanced.getInteractLine()); try { Market.getMarket(this, marketName); @@ -138,4 +190,78 @@ public class RoleTrader extends RoleInterface { return map.get(name); } + // ==================== Stock System Methods ==================== + + /** + * Check if there is stock available for a slot + */ + public boolean hasStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled, always available + } + return stock.hasStock(slot, playerName, amount); + } + + /** + * Get available stock for a slot + */ + public int getAvailableStock(int slot, String playerName) { + if (!stock.enableStock) { + return Integer.MAX_VALUE; // Unlimited + } + return stock.getAvailableStock(slot, playerName); + } + + /** + * Consume stock after purchase + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled + } + boolean consumed = stock.consumeStock(slot, playerName, amount); + if (consumed && !marketName.isEmpty()) { + Market.save(this, marketName); + } + return consumed; + } + + /** + * Get time until stock reset formatted for display + */ + public String getTimeUntilResetFormatted() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return ""; + } + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + return stock.getTimeUntilResetFormatted(currentTime); + } + + // ==================== Currency System Methods ==================== + + /** + * Get the currency price for a slot + */ + public long getSlotPrice(int slot) { + if (slot < 0 || slot >= 18) return 0; + return slotPrices[slot]; + } + + /** + * Set the currency price for a slot + */ + public void setSlotPrice(int slot, long price) { + if (slot >= 0 && slot < 18) { + slotPrices[slot] = Math.max(0, price); + } + } + + /** + * Check if a slot has a currency price set + */ + public boolean hasSlotPrice(int slot) { + return useCurrency && slot >= 0 && slot < 18 && slotPrices[slot] > 0; + } } From dc9d15f3e3e0086535030bc961607368740c7bec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:34:45 +0000 Subject: [PATCH 08/21] Implement Phase 3: Item Attributes & Restrictions - Add ItemTradeAttribute.java for trade restriction utilities - TAG_UNTRADEABLE for items that cannot be traded - Check existing TAG_PROFILE_SLOT and TAG_SOULBIND tags - canTrade() checks all three restriction types - getTradeBlockReason() returns localization key for UI - Utility methods for setting/getting each restriction type - clearAllRestrictions() and getRestrictionSummary() helpers - Add AuctionBlacklist.java controller - Blacklist items by registry name (e.g., "minecraft:diamond_sword") - Blacklist entire mods by mod ID (e.g., "minecraft") - Blacklist items with specific NBT tags (supports dot notation) - isBlacklisted() and getBlacklistReason() for checking - Persistence via auction_blacklist.dat - Add/remove methods for each blacklist type - Initialize AuctionBlacklist in CustomNpcs.java - Add scripting API methods to ScriptItemStack.java - canTrade() - check if item can be traded - getTradeBlockReason() - get block reason - isUntradeable() / setUntradeable() - manage untradeable flag - isProfileSlotBound() / getProfileSlot() / setProfileSlotBound() - isSoulbound() / getSoulbindOwner() / removeSoulbind() - clearTradeRestrictions() - remove all restrictions - getRestrictionSummary() - human-readable summary --- .../data/attribute/ItemTradeAttribute.java | 308 +++++++++++++ src/main/java/noppes/npcs/CustomNpcs.java | 2 + .../npcs/controllers/AuctionBlacklist.java | 419 ++++++++++++++++++ .../npcs/scripted/item/ScriptItemStack.java | 114 +++++ 4 files changed, 843 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionBlacklist.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java b/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java new file mode 100644 index 000000000..180f41d02 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java @@ -0,0 +1,308 @@ +package kamkeel.npcs.controllers.data.attribute; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.data.PlayerData; + +import java.util.UUID; + +/** + * Utility class for managing item trade restrictions. + * Checks and sets attributes that control whether items can be traded/auctioned. + */ +public class ItemTradeAttribute { + + // New untradeable attribute - prevents all trading + public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; + + // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) + public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; + public static final String TAG_SOULBIND = "cnpc_soulbind"; + + // ==================== Trade Check Methods ==================== + + /** + * Check if item can be traded/auctioned by the given player. + * An item cannot be traded if: + * - It has the Untradeable flag + * - It is bound to a specific profile slot (ProfileSlot Bound) + * - It is soulbound to a different player + * + * @param item The item to check + * @param player The player trying to trade (null to check without player context) + * @return true if the item can be traded + */ + public static boolean canTrade(ItemStack item, EntityPlayer player) { + if (item == null) { + return true; + } + if (!item.hasTagCompound()) { + return true; + } + + NBTTagCompound tag = item.getTagCompound(); + + // Check untradeable flag - always blocks trading + if (tag.getBoolean(TAG_UNTRADEABLE)) { + return false; + } + + // Check profile slot bound - blocks trading as it's tied to a slot + if (tag.hasKey(TAG_PROFILE_SLOT)) { + return false; + } + + // Check soulbound - blocks trading as it's tied to a player + if (tag.hasKey(TAG_SOULBIND)) { + // If we have a player context, check if they're the owner + if (player != null) { + String soulbindUUID = tag.getString(TAG_SOULBIND); + if (soulbindUUID != null && !soulbindUUID.isEmpty()) { + // Even if bound to this player, still can't trade + return false; + } + } + return false; + } + + return true; + } + + /** + * Check if item can be traded (without player context) + */ + public static boolean canTrade(ItemStack item) { + return canTrade(item, null); + } + + /** + * Get the reason why an item cannot be traded. + * Returns a translation key for localization. + * + * @param item The item to check + * @return Translation key for the block reason, or null if tradeable + */ + public static String getTradeBlockReason(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return null; + } + + NBTTagCompound tag = item.getTagCompound(); + + if (tag.getBoolean(TAG_UNTRADEABLE)) { + return "gui.auction.untradeable"; + } + if (tag.hasKey(TAG_PROFILE_SLOT)) { + return "gui.auction.profileslotbound"; + } + if (tag.hasKey(TAG_SOULBIND)) { + return "gui.auction.soulbound"; + } + + return null; + } + + // ==================== Untradeable Methods ==================== + + /** + * Check if item is marked as untradeable + */ + public static boolean isUntradeable(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return false; + } + return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); + } + + /** + * Set the untradeable flag on an item + */ + public static void setUntradeable(ItemStack item, boolean untradeable) { + if (item == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + if (untradeable) { + item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); + } else { + item.getTagCompound().removeTag(TAG_UNTRADEABLE); + } + } + + // ==================== Profile Slot Bound Methods ==================== + + /** + * Check if item is bound to a profile slot + */ + public static boolean isProfileSlotBound(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return false; + } + return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); + } + + /** + * Get the profile slot an item is bound to + * @return The profile slot ID, or -1 if not bound + */ + public static int getProfileSlot(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return -1; + } + if (item.getTagCompound().hasKey(TAG_PROFILE_SLOT)) { + return item.getTagCompound().getInteger(TAG_PROFILE_SLOT); + } + return -1; + } + + /** + * Bind an item to a specific profile slot + */ + public static void setProfileSlotBound(ItemStack item, int slot) { + if (item == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + if (slot >= 0) { + item.getTagCompound().setInteger(TAG_PROFILE_SLOT, slot); + } else { + item.getTagCompound().removeTag(TAG_PROFILE_SLOT); + } + } + + /** + * Bind an item to the player's current profile slot + */ + public static void bindToCurrentSlot(ItemStack item, EntityPlayer player) { + if (item == null || player == null) { + return; + } + PlayerData data = PlayerData.get(player); + if (data != null) { + setProfileSlotBound(item, data.profileSlot); + } + } + + // ==================== Soulbind Methods ==================== + + /** + * Check if item is soulbound to any player + */ + public static boolean isSoulbound(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return false; + } + return item.getTagCompound().hasKey(TAG_SOULBIND); + } + + /** + * Check if item is soulbound to a specific player + */ + public static boolean isSoulboundTo(ItemStack item, EntityPlayer player) { + if (item == null || player == null || !item.hasTagCompound()) { + return false; + } + if (!item.getTagCompound().hasKey(TAG_SOULBIND)) { + return false; + } + String soulbindUUID = item.getTagCompound().getString(TAG_SOULBIND); + return player.getUniqueID().toString().equals(soulbindUUID); + } + + /** + * Get the UUID of the player the item is soulbound to + * @return The UUID string, or null if not soulbound + */ + public static String getSoulbindOwner(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return null; + } + if (item.getTagCompound().hasKey(TAG_SOULBIND)) { + return item.getTagCompound().getString(TAG_SOULBIND); + } + return null; + } + + /** + * Soulbind an item to a player + */ + public static void setSoulbound(ItemStack item, EntityPlayer player) { + if (item == null || player == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + item.getTagCompound().setString(TAG_SOULBIND, player.getUniqueID().toString()); + } + + /** + * Soulbind an item to a player by UUID + */ + public static void setSoulbound(ItemStack item, UUID playerUUID) { + if (item == null || playerUUID == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + item.getTagCompound().setString(TAG_SOULBIND, playerUUID.toString()); + } + + /** + * Remove soulbind from an item + */ + public static void removeSoulbind(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return; + } + item.getTagCompound().removeTag(TAG_SOULBIND); + } + + // ==================== Utility Methods ==================== + + /** + * Clear all trade restriction attributes from an item + */ + public static void clearAllRestrictions(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return; + } + NBTTagCompound tag = item.getTagCompound(); + tag.removeTag(TAG_UNTRADEABLE); + tag.removeTag(TAG_PROFILE_SLOT); + tag.removeTag(TAG_SOULBIND); + } + + /** + * Get a display string describing all trade restrictions on an item + * @return Human-readable string of restrictions, or empty if none + */ + public static String getRestrictionSummary(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return ""; + } + + StringBuilder summary = new StringBuilder(); + NBTTagCompound tag = item.getTagCompound(); + + if (tag.getBoolean(TAG_UNTRADEABLE)) { + summary.append("Untradeable"); + } + if (tag.hasKey(TAG_PROFILE_SLOT)) { + if (summary.length() > 0) summary.append(", "); + summary.append("Profile Slot Bound (Slot ").append(tag.getInteger(TAG_PROFILE_SLOT)).append(")"); + } + if (tag.hasKey(TAG_SOULBIND)) { + if (summary.length() > 0) summary.append(", "); + summary.append("Soulbound"); + } + + return summary.toString(); + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 422be3e61..dbdee9e67 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -60,6 +60,7 @@ import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.AuctionBlacklist; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -308,6 +309,7 @@ public class CustomNpcs { public void started(FMLServerStartedEvent event) { new VaultHelper(); new CurrencyController(); + new AuctionBlacklist(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java new file mode 100644 index 000000000..020bb0bcb --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java @@ -0,0 +1,419 @@ +package noppes.npcs.controllers; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import noppes.npcs.CustomNpcs; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashSet; +import java.util.Set; + +/** + * Controller for managing auction house item blacklisting. + * Allows administrators to block specific items, entire mods, + * or items with certain NBT tags from being listed. + */ +public class AuctionBlacklist { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static AuctionBlacklist Instance; + + // Blacklist by item registry name (e.g., "minecraft:diamond_sword") + private Set blacklistedItems = new HashSet<>(); + + // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) + private Set blacklistedMods = new HashSet<>(); + + // Blacklist by NBT tag presence (e.g., items with specific custom data) + private Set blacklistedNBTTags = new HashSet<>(); + + public AuctionBlacklist() { + Instance = this; + load(); + } + + // ==================== Blacklist Checking ==================== + + /** + * Check if an item is blacklisted from the auction house. + * + * @param item The item to check + * @return true if the item is blacklisted + */ + public boolean isBlacklisted(ItemStack item) { + if (item == null || item.getItem() == null) { + return false; + } + + // Check item registry name + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null) { + // Direct item blacklist + if (blacklistedItems.contains(itemName)) { + return true; + } + + // Also check with metadata for specific variants + String itemNameWithMeta = itemName + ":" + item.getItemDamage(); + if (blacklistedItems.contains(itemNameWithMeta)) { + return true; + } + + // Check mod ID + String modId = getModIdFromName(itemName); + if (modId != null && blacklistedMods.contains(modId)) { + return true; + } + } + + // Check NBT tags + if (item.hasTagCompound()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtTag : blacklistedNBTTags) { + if (hasNestedTag(tag, nbtTag)) { + return true; + } + } + } + + return false; + } + + /** + * Get the reason why an item is blacklisted + * @return Description of why blacklisted, or null if not blacklisted + */ + public String getBlacklistReason(ItemStack item) { + if (item == null || item.getItem() == null) { + return null; + } + + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null) { + if (blacklistedItems.contains(itemName)) { + return "Item is blacklisted: " + itemName; + } + + String itemNameWithMeta = itemName + ":" + item.getItemDamage(); + if (blacklistedItems.contains(itemNameWithMeta)) { + return "Item variant is blacklisted: " + itemNameWithMeta; + } + + String modId = getModIdFromName(itemName); + if (modId != null && blacklistedMods.contains(modId)) { + return "Mod is blacklisted: " + modId; + } + } + + if (item.hasTagCompound()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtTag : blacklistedNBTTags) { + if (hasNestedTag(tag, nbtTag)) { + return "Item has blacklisted NBT tag: " + nbtTag; + } + } + } + + return null; + } + + /** + * Extract mod ID from registry name (e.g., "minecraft" from "minecraft:stone") + */ + private String getModIdFromName(String registryName) { + if (registryName == null) { + return null; + } + int colonIndex = registryName.indexOf(':'); + if (colonIndex > 0) { + return registryName.substring(0, colonIndex); + } + return null; + } + + /** + * Check for nested NBT tag (supports dot notation like "display.Name") + */ + private boolean hasNestedTag(NBTTagCompound tag, String path) { + if (tag == null || path == null || path.isEmpty()) { + return false; + } + + String[] parts = path.split("\\."); + NBTTagCompound current = tag; + + for (int i = 0; i < parts.length - 1; i++) { + if (!current.hasKey(parts[i])) { + return false; + } + if (current.hasKey(parts[i], 10)) { // 10 = Compound tag + current = current.getCompoundTag(parts[i]); + } else { + return false; + } + } + + return current.hasKey(parts[parts.length - 1]); + } + + // ==================== Item Blacklist Management ==================== + + /** + * Add an item to the blacklist by registry name + * @param itemName Registry name (e.g., "minecraft:diamond_sword" or "minecraft:wool:14") + */ + public void addItem(String itemName) { + if (itemName != null && !itemName.isEmpty()) { + blacklistedItems.add(itemName); + save(); + logger.info("Added item to auction blacklist: " + itemName); + } + } + + /** + * Add an item to the blacklist from an ItemStack + */ + public void addItem(ItemStack item, boolean includeMeta) { + if (item == null || item.getItem() == null) { + return; + } + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null) { + if (includeMeta && item.getHasSubtypes()) { + addItem(itemName + ":" + item.getItemDamage()); + } else { + addItem(itemName); + } + } + } + + /** + * Remove an item from the blacklist + */ + public void removeItem(String itemName) { + if (blacklistedItems.remove(itemName)) { + save(); + logger.info("Removed item from auction blacklist: " + itemName); + } + } + + /** + * Check if a specific item is blacklisted + */ + public boolean isItemBlacklisted(String itemName) { + return blacklistedItems.contains(itemName); + } + + /** + * Get all blacklisted items + */ + public Set getBlacklistedItems() { + return new HashSet<>(blacklistedItems); + } + + // ==================== Mod Blacklist Management ==================== + + /** + * Add an entire mod to the blacklist + * @param modId The mod ID (e.g., "minecraft", "customnpcs") + */ + public void addMod(String modId) { + if (modId != null && !modId.isEmpty()) { + blacklistedMods.add(modId); + save(); + logger.info("Added mod to auction blacklist: " + modId); + } + } + + /** + * Remove a mod from the blacklist + */ + public void removeMod(String modId) { + if (blacklistedMods.remove(modId)) { + save(); + logger.info("Removed mod from auction blacklist: " + modId); + } + } + + /** + * Check if a mod is blacklisted + */ + public boolean isModBlacklisted(String modId) { + return blacklistedMods.contains(modId); + } + + /** + * Get all blacklisted mods + */ + public Set getBlacklistedMods() { + return new HashSet<>(blacklistedMods); + } + + // ==================== NBT Tag Blacklist Management ==================== + + /** + * Add an NBT tag to the blacklist + * Items containing this tag will be blocked + * @param tagPath The tag path (e.g., "display.Name" or "CustomData") + */ + public void addNBTTag(String tagPath) { + if (tagPath != null && !tagPath.isEmpty()) { + blacklistedNBTTags.add(tagPath); + save(); + logger.info("Added NBT tag to auction blacklist: " + tagPath); + } + } + + /** + * Remove an NBT tag from the blacklist + */ + public void removeNBTTag(String tagPath) { + if (blacklistedNBTTags.remove(tagPath)) { + save(); + logger.info("Removed NBT tag from auction blacklist: " + tagPath); + } + } + + /** + * Check if an NBT tag is blacklisted + */ + public boolean isNBTTagBlacklisted(String tagPath) { + return blacklistedNBTTags.contains(tagPath); + } + + /** + * Get all blacklisted NBT tags + */ + public Set getBlacklistedNBTTags() { + return new HashSet<>(blacklistedNBTTags); + } + + // ==================== Persistence ==================== + + public void save() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + try { + NBTTagCompound compound = new NBTTagCompound(); + + // Save blacklisted items + NBTTagList itemList = new NBTTagList(); + for (String item : blacklistedItems) { + itemList.appendTag(new NBTTagString(item)); + } + compound.setTag("Items", itemList); + + // Save blacklisted mods + NBTTagList modList = new NBTTagList(); + for (String mod : blacklistedMods) { + modList.appendTag(new NBTTagString(mod)); + } + compound.setTag("Mods", modList); + + // Save blacklisted NBT tags + NBTTagList nbtList = new NBTTagList(); + for (String nbt : blacklistedNBTTags) { + nbtList.appendTag(new NBTTagString(nbt)); + } + compound.setTag("NBTTags", nbtList); + + // Write to file + File file = new File(saveDir, "auction_blacklist.dat"); + File backup = new File(saveDir, "auction_blacklist.dat_old"); + + if (file.exists()) { + if (backup.exists()) { + backup.delete(); + } + file.renameTo(backup); + } + + file = new File(saveDir, "auction_blacklist.dat"); + FileOutputStream fos = new FileOutputStream(file); + CompressedStreamTools.writeCompressed(compound, fos); + fos.close(); + + } catch (Exception e) { + logger.error("Error saving auction blacklist", e); + } + } + + public void load() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + blacklistedItems.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + + try { + File file = new File(saveDir, "auction_blacklist.dat"); + if (!file.exists()) { + // Try backup + file = new File(saveDir, "auction_blacklist.dat_old"); + if (!file.exists()) { + return; + } + } + + FileInputStream fis = new FileInputStream(file); + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + fis.close(); + + // Load blacklisted items + NBTTagList itemList = compound.getTagList("Items", 8); // 8 = String tag + for (int i = 0; i < itemList.tagCount(); i++) { + blacklistedItems.add(itemList.getStringTagAt(i)); + } + + // Load blacklisted mods + NBTTagList modList = compound.getTagList("Mods", 8); + for (int i = 0; i < modList.tagCount(); i++) { + blacklistedMods.add(modList.getStringTagAt(i)); + } + + // Load blacklisted NBT tags + NBTTagList nbtList = compound.getTagList("NBTTags", 8); + for (int i = 0; i < nbtList.tagCount(); i++) { + blacklistedNBTTags.add(nbtList.getStringTagAt(i)); + } + + logger.info("Loaded auction blacklist: {} items, {} mods, {} NBT tags", + blacklistedItems.size(), blacklistedMods.size(), blacklistedNBTTags.size()); + + } catch (Exception e) { + logger.error("Error loading auction blacklist", e); + } + } + + /** + * Clear all blacklist entries + */ + public void clearAll() { + blacklistedItems.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + save(); + logger.info("Cleared all auction blacklist entries"); + } + + /** + * Get total number of blacklist entries + */ + public int getTotalEntries() { + return blacklistedItems.size() + blacklistedMods.size() + blacklistedNBTTags.size(); + } +} diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java index 6736369b9..a2588059f 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java @@ -1,6 +1,7 @@ package noppes.npcs.scripted.item; import com.google.common.collect.Multimap; +import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; import kamkeel.npcs.util.AttributeItemUtil; @@ -592,4 +593,117 @@ public class ScriptItemStack implements IItemStack { Set keys = reqTag.func_150296_c(); return keys.toArray(new String[keys.size()]); } + + // ==================== Trade Restriction Methods ==================== + + /** + * Check if this item can be traded or auctioned. + * An item cannot be traded if it is: + * - Marked as untradeable + * - Bound to a profile slot + * - Soulbound to a player + * @return true if the item can be traded + * @since 1.11 + */ + public boolean canTrade() { + return ItemTradeAttribute.canTrade(item); + } + + /** + * Get the reason why this item cannot be traded. + * @return Localization key for the block reason, or null if tradeable + * @since 1.11 + */ + public String getTradeBlockReason() { + return ItemTradeAttribute.getTradeBlockReason(item); + } + + /** + * Check if this item is marked as untradeable + * @return true if untradeable + * @since 1.11 + */ + public boolean isUntradeable() { + return ItemTradeAttribute.isUntradeable(item); + } + + /** + * Set whether this item is untradeable + * @param untradeable true to mark as untradeable + * @since 1.11 + */ + public void setUntradeable(boolean untradeable) { + ItemTradeAttribute.setUntradeable(item, untradeable); + } + + /** + * Check if this item is bound to a profile slot + * @return true if profile slot bound + * @since 1.11 + */ + public boolean isProfileSlotBound() { + return ItemTradeAttribute.isProfileSlotBound(item); + } + + /** + * Get the profile slot this item is bound to + * @return Profile slot ID, or -1 if not bound + * @since 1.11 + */ + public int getProfileSlot() { + return ItemTradeAttribute.getProfileSlot(item); + } + + /** + * Bind this item to a specific profile slot + * @param slot The profile slot ID, or -1 to unbind + * @since 1.11 + */ + public void setProfileSlotBound(int slot) { + ItemTradeAttribute.setProfileSlotBound(item, slot); + } + + /** + * Check if this item is soulbound to any player + * @return true if soulbound + * @since 1.11 + */ + public boolean isSoulbound() { + return ItemTradeAttribute.isSoulbound(item); + } + + /** + * Get the UUID of the player this item is soulbound to + * @return UUID string, or null if not soulbound + * @since 1.11 + */ + public String getSoulbindOwner() { + return ItemTradeAttribute.getSoulbindOwner(item); + } + + /** + * Remove the soulbind from this item + * @since 1.11 + */ + public void removeSoulbind() { + ItemTradeAttribute.removeSoulbind(item); + } + + /** + * Clear all trade restrictions from this item + * (untradeable, profile slot bound, soulbound) + * @since 1.11 + */ + public void clearTradeRestrictions() { + ItemTradeAttribute.clearAllRestrictions(item); + } + + /** + * Get a summary of all trade restrictions on this item + * @return Human-readable string of restrictions, or empty if none + * @since 1.11 + */ + public String getRestrictionSummary() { + return ItemTradeAttribute.getRestrictionSummary(item); + } } From c1dc58ae54397aedf0d4c5e134f126a86139be64 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:43:55 +0000 Subject: [PATCH 09/21] Implement Phase 4: Auction House core system - Add auction enums: - EnumAuctionDuration.java - configurable auction durations (SHORT/MEDIUM/LONG/VERY_LONG) - EnumAuctionStatus.java - listing statuses (ACTIVE/SOLD/EXPIRED/CANCELLED/CLAIMED) - EnumAuctionCategory.java - auto-categorization of items for browsing - Add AuctionListing.java data class: - Complete auction listing with seller/bidder tracking - Pricing with starting price, buyout price, current bid - Timing with snipe protection (extends TO X minutes, not by) - Item validation (checks trade restrictions and blacklist) - Previous bidder tracking for refund notifications - NBT serialization for persistence - Add AuctionController.java: - Global auction house management - Create/cancel/finalize listings - Bid placement with automatic outbid refunds via claims system - Buyout support with immediate sale - Claim system for sellers (proceeds) and buyers (items) - Search and filter by category/name - Periodic expiration processing - Persistence via auctions.dat - Add RoleAuctioneer.java NPC role: - Configurable tabs (listings, my listings, my bids, claims) - Configurable permissions (create, bid, buyout) - Custom welcome/disabled messages - Opens auction GUI on interaction - Update ConfigMarket.java: - Duration-specific settings (hours and fees) - Removed deprecated AvailableDurations string - Update EnumRoleType.java - add Auctioneer - Update EnumGuiType.java - add PlayerAuction, SetupAuctioneer - Update DataAdvanced.java - handle Auctioneer role creation - Update CustomNpcs.java - initialize AuctionController --- src/main/java/noppes/npcs/CustomNpcs.java | 2 + src/main/java/noppes/npcs/DataAdvanced.java | 3 + .../java/noppes/npcs/config/ConfigMarket.java | 74 +- .../npcs/constants/EnumAuctionCategory.java | 156 +++++ .../npcs/constants/EnumAuctionDuration.java | 68 ++ .../npcs/constants/EnumAuctionStatus.java | 54 ++ .../noppes/npcs/constants/EnumGuiType.java | 4 +- .../noppes/npcs/constants/EnumRoleType.java | 2 +- .../npcs/controllers/AuctionController.java | 662 ++++++++++++++++++ .../npcs/controllers/data/AuctionListing.java | 392 +++++++++++ .../noppes/npcs/roles/RoleAuctioneer.java | 134 ++++ 11 files changed, 1522 insertions(+), 29 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionCategory.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionDuration.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionStatus.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionController.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionListing.java create mode 100644 src/main/java/noppes/npcs/roles/RoleAuctioneer.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index dbdee9e67..c29fa4253 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -61,6 +61,7 @@ import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.AuctionBlacklist; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -310,6 +311,7 @@ public class CustomNpcs { new VaultHelper(); new CurrencyController(); new AuctionBlacklist(); + new AuctionController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/DataAdvanced.java b/src/main/java/noppes/npcs/DataAdvanced.java index 45ecfe4a8..f03714506 100644 --- a/src/main/java/noppes/npcs/DataAdvanced.java +++ b/src/main/java/noppes/npcs/DataAdvanced.java @@ -18,6 +18,7 @@ import noppes.npcs.roles.JobGuard; import noppes.npcs.roles.JobHealer; import noppes.npcs.roles.JobItemGiver; import noppes.npcs.roles.JobSpawner; +import noppes.npcs.roles.RoleAuctioneer; import noppes.npcs.roles.RoleBank; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; @@ -218,6 +219,8 @@ public class DataAdvanced { npc.roleInterface = new RoleCompanion(npc); else if (role == EnumRoleType.Mount && !(npc.roleInterface instanceof RoleMount)) npc.roleInterface = new RoleMount(npc); + else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) + npc.roleInterface = new RoleAuctioneer(npc); } public void setJob(int i) { diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 9c5791929..81eda2c3f 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -40,13 +40,23 @@ public class ConfigMarket { public static double ListingFeePercent = 0.0; public static double SalesTaxPercent = 0.05; public static boolean RequireAuctioneerNPC = true; - public static String AvailableDurations = "2,12,24,48"; - public static int DefaultDurationHours = 24; public static int ClaimExpirationDays = 30; public static double MinBidIncrementPercent = 0.05; public static int SnipeProtectionMinutes = 5; public static int SnipeProtectionThreshold = 5; + // Auction Duration Settings (in hours) + public static int AuctionDurationShort = 2; + public static int AuctionDurationMedium = 12; + public static int AuctionDurationLong = 24; + public static int AuctionDurationVeryLong = 48; + + // Auction Duration Fees (flat currency amount) + public static long AuctionFeeShort = 0; + public static long AuctionFeeMedium = 0; + public static long AuctionFeeLong = 0; + public static long AuctionFeeVeryLong = 0; + // ========================================= // Trader Settings // ========================================= @@ -108,12 +118,6 @@ public class ConfigMarket { RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, "Require an Auctioneer NPC to access auction house").getBoolean(true); - AvailableDurations = config.get(AUCTION, "Available Durations", "2,12,24,48", - "Available auction durations in hours (comma-separated)").getString(); - - DefaultDurationHours = config.get(AUCTION, "Default Duration Hours", 24, - "Default auction duration in hours").getInt(24); - ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); @@ -126,6 +130,32 @@ public class ConfigMarket { SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, "How many minutes before end triggers snipe protection").getInt(5); + // Duration settings + AuctionDurationShort = config.get(AUCTION, "Duration Short Hours", 2, + "Short auction duration in hours").getInt(2); + + AuctionDurationMedium = config.get(AUCTION, "Duration Medium Hours", 12, + "Medium auction duration in hours").getInt(12); + + AuctionDurationLong = config.get(AUCTION, "Duration Long Hours", 24, + "Long auction duration in hours").getInt(24); + + AuctionDurationVeryLong = config.get(AUCTION, "Duration Very Long Hours", 48, + "Very long auction duration in hours").getInt(48); + + // Duration fees + AuctionFeeShort = config.get(AUCTION, "Fee Short Duration", 0, + "Flat fee for short duration auctions").getInt(0); + + AuctionFeeMedium = config.get(AUCTION, "Fee Medium Duration", 0, + "Flat fee for medium duration auctions").getInt(0); + + AuctionFeeLong = config.get(AUCTION, "Fee Long Duration", 0, + "Flat fee for long duration auctions").getInt(0); + + AuctionFeeVeryLong = config.get(AUCTION, "Fee Very Long Duration", 0, + "Flat fee for very long duration auctions").getInt(0); + // ========================================= // Trader Settings // ========================================= @@ -157,12 +187,18 @@ public class ConfigMarket { "Listing Fee Percent", "Sales Tax Percent", "Require Auctioneer NPC", - "Available Durations", - "Default Duration Hours", "Claim Expiration Days", "Min Bid Increment Percent", "Snipe Protection Minutes", - "Snipe Protection Threshold" + "Snipe Protection Threshold", + "Duration Short Hours", + "Duration Medium Hours", + "Duration Long Hours", + "Duration Very Long Hours", + "Fee Short Duration", + "Fee Medium Duration", + "Fee Long Duration", + "Fee Very Long Duration" ))); config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( @@ -198,20 +234,4 @@ public class ConfigMarket { } } } - - /** - * Parse available durations from config string - */ - public static int[] getAvailableDurationsArray() { - String[] parts = AvailableDurations.split(","); - int[] durations = new int[parts.length]; - for (int i = 0; i < parts.length; i++) { - try { - durations[i] = Integer.parseInt(parts[i].trim()); - } catch (NumberFormatException e) { - durations[i] = 24; // Default to 24 hours if parsing fails - } - } - return durations; - } } diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java b/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java new file mode 100644 index 000000000..0906bb6ed --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java @@ -0,0 +1,156 @@ +package noppes.npcs.constants; + +import net.minecraft.init.Items; +import net.minecraft.item.Item; +import net.minecraft.item.ItemArmor; +import net.minecraft.item.ItemAxe; +import net.minecraft.item.ItemBlock; +import net.minecraft.item.ItemBow; +import net.minecraft.item.ItemEnchantedBook; +import net.minecraft.item.ItemFood; +import net.minecraft.item.ItemHoe; +import net.minecraft.item.ItemPickaxe; +import net.minecraft.item.ItemPotion; +import net.minecraft.item.ItemSpade; +import net.minecraft.item.ItemStack; +import net.minecraft.item.ItemSword; +import net.minecraft.item.ItemTool; + +/** + * Categories for auction house items. + * Used for filtering and browsing. + */ +public enum EnumAuctionCategory { + ALL, // Special: shows all categories + WEAPONS, // Swords, bows, etc. + ARMOR, // Helmets, chestplates, leggings, boots + TOOLS, // Pickaxes, axes, shovels, hoes + FOOD, // Edible items + POTIONS, // Potions and splash potions + BLOCKS, // Placeable blocks + MATERIALS, // Crafting materials (ingots, gems, etc.) + ENCHANTED, // Items with enchantments or enchanted books + MISC; // Everything else + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case ALL: return "All Items"; + case WEAPONS: return "Weapons"; + case ARMOR: return "Armor"; + case TOOLS: return "Tools"; + case FOOD: return "Food"; + case POTIONS: return "Potions"; + case BLOCKS: return "Blocks"; + case MATERIALS: return "Materials"; + case ENCHANTED: return "Enchanted"; + case MISC: return "Miscellaneous"; + default: return "Unknown"; + } + } + + /** + * Auto-categorize an item based on its type + */ + public static EnumAuctionCategory categorize(ItemStack itemStack) { + if (itemStack == null || itemStack.getItem() == null) { + return MISC; + } + + Item item = itemStack.getItem(); + + // Check for enchantments first (higher priority) + if (itemStack.isItemEnchanted() || item instanceof ItemEnchantedBook) { + return ENCHANTED; + } + + // Weapons + if (item instanceof ItemSword || item instanceof ItemBow) { + return WEAPONS; + } + + // Armor + if (item instanceof ItemArmor) { + return ARMOR; + } + + // Tools + if (item instanceof ItemTool || item instanceof ItemPickaxe || + item instanceof ItemAxe || item instanceof ItemSpade || + item instanceof ItemHoe) { + return TOOLS; + } + + // Food + if (item instanceof ItemFood) { + return FOOD; + } + + // Potions + if (item instanceof ItemPotion) { + return POTIONS; + } + + // Blocks + if (item instanceof ItemBlock) { + return BLOCKS; + } + + // Materials - common crafting items + if (isMaterial(item)) { + return MATERIALS; + } + + return MISC; + } + + /** + * Check if an item is a common crafting material + */ + private static boolean isMaterial(Item item) { + // Check common material items + return item == Items.iron_ingot || + item == Items.gold_ingot || + item == Items.diamond || + item == Items.emerald || + item == Items.coal || + item == Items.redstone || + item == Items.glowstone_dust || + item == Items.quartz || + item == Items.leather || + item == Items.string || + item == Items.feather || + item == Items.bone || + item == Items.gunpowder || + item == Items.slime_ball || + item == Items.ender_pearl || + item == Items.blaze_rod || + item == Items.ghast_tear || + item == Items.gold_nugget || + item == Items.nether_wart || + item == Items.spider_eye || + item == Items.magma_cream || + item == Items.nether_star; + } + + /** + * Get icon item for category display + */ + public ItemStack getIconItem() { + switch (this) { + case ALL: return new ItemStack(Items.compass); + case WEAPONS: return new ItemStack(Items.iron_sword); + case ARMOR: return new ItemStack(Items.iron_chestplate); + case TOOLS: return new ItemStack(Items.iron_pickaxe); + case FOOD: return new ItemStack(Items.bread); + case POTIONS: return new ItemStack(Items.potionitem); + case BLOCKS: return new ItemStack(Item.getItemFromBlock(net.minecraft.init.Blocks.grass)); + case MATERIALS: return new ItemStack(Items.iron_ingot); + case ENCHANTED: return new ItemStack(Items.enchanted_book); + case MISC: return new ItemStack(Items.chest_minecart); + default: return new ItemStack(Items.paper); + } + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java new file mode 100644 index 000000000..e18416f50 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java @@ -0,0 +1,68 @@ +package noppes.npcs.constants; + +import noppes.npcs.config.ConfigMarket; + +/** + * Duration options for auction listings. + * Values are configurable via ConfigMarket. + */ +public enum EnumAuctionDuration { + SHORT, // Default: 2 hours + MEDIUM, // Default: 12 hours + LONG, // Default: 24 hours + VERY_LONG; // Default: 48 hours + + /** + * Get duration in milliseconds + */ + public long getMillis() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionDurationShort * 60L * 60L * 1000L; + case MEDIUM: + return ConfigMarket.AuctionDurationMedium * 60L * 60L * 1000L; + case LONG: + return ConfigMarket.AuctionDurationLong * 60L * 60L * 1000L; + case VERY_LONG: + return ConfigMarket.AuctionDurationVeryLong * 60L * 60L * 1000L; + default: + return 24 * 60L * 60L * 1000L; + } + } + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionDurationShort + " Hours"; + case MEDIUM: + return ConfigMarket.AuctionDurationMedium + " Hours"; + case LONG: + return ConfigMarket.AuctionDurationLong + " Hours"; + case VERY_LONG: + return ConfigMarket.AuctionDurationVeryLong + " Hours"; + default: + return "Unknown"; + } + } + + /** + * Get listing fee for this duration + */ + public long getListingFee() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionFeeShort; + case MEDIUM: + return ConfigMarket.AuctionFeeMedium; + case LONG: + return ConfigMarket.AuctionFeeLong; + case VERY_LONG: + return ConfigMarket.AuctionFeeVeryLong; + default: + return 0; + } + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java new file mode 100644 index 000000000..9e614e496 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java @@ -0,0 +1,54 @@ +package noppes.npcs.constants; + +/** + * Status of an auction listing. + */ +public enum EnumAuctionStatus { + ACTIVE, // Currently listed and accepting bids + SOLD, // Successfully sold (buyout or auction end with bids) + EXPIRED, // Ended without any bids + CANCELLED, // Cancelled by seller (only allowed before first bid) + CLAIMED; // Item/money has been claimed by respective parties + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case ACTIVE: return "Active"; + case SOLD: return "Sold"; + case EXPIRED: return "Expired"; + case CANCELLED: return "Cancelled"; + case CLAIMED: return "Claimed"; + default: return "Unknown"; + } + } + + /** + * Get display color for GUI (MC color codes) + */ + public char getColorCode() { + switch (this) { + case ACTIVE: return 'a'; // Green + case SOLD: return '6'; // Gold + case EXPIRED: return '7'; // Gray + case CANCELLED: return 'c'; // Red + case CLAIMED: return '8'; // Dark gray + default: return 'f'; // White + } + } + + /** + * Check if listing can still receive bids + */ + public boolean canBid() { + return this == ACTIVE; + } + + /** + * Check if listing needs claiming (item or money) + */ + public boolean needsClaiming() { + return this == SOLD || this == EXPIRED || this == CANCELLED; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index d0919b9b1..adec6d51d 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -63,5 +63,7 @@ public enum EnumGuiType { ScriptItem, ScriptBlock, Paintbrush, - MagicBook; + MagicBook, + PlayerAuction, + SetupAuctioneer; } diff --git a/src/main/java/noppes/npcs/constants/EnumRoleType.java b/src/main/java/noppes/npcs/constants/EnumRoleType.java index 624658fb3..33f36941a 100644 --- a/src/main/java/noppes/npcs/constants/EnumRoleType.java +++ b/src/main/java/noppes/npcs/constants/EnumRoleType.java @@ -2,5 +2,5 @@ package noppes.npcs.constants; public enum EnumRoleType { - None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount + None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount, Auctioneer } diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java new file mode 100644 index 000000000..ec3b6de19 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -0,0 +1,662 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.data.AuctionListing; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Controller for the global auction house. + * Manages all auction listings, bidding, and claims. + */ +public class AuctionController { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static AuctionController Instance; + + // All auction listings by ID + private Map listings = new ConcurrentHashMap<>(); + + // Next available listing ID + private int nextId = 1; + + // Indices for fast lookup + private Map> listingsBySeller = new ConcurrentHashMap<>(); + private Map> listingsByBidder = new ConcurrentHashMap<>(); + + public AuctionController() { + Instance = this; + load(); + } + + // ==================== Listing Management ==================== + + /** + * Create a new auction listing + * @return The new listing, or null if creation failed + */ + public AuctionListing createListing(EntityPlayer seller, ItemStack item, + long startingPrice, long buyoutPrice, + EnumAuctionDuration duration) { + // Validate item + String validation = AuctionListing.validateItem(item); + if (validation != null) { + logger.warn("Auction listing validation failed: " + validation); + return null; + } + + // Check seller's listing limit + int currentListings = getActiveListingsCount(seller.getUniqueID()); + int maxListings = getMaxListings(seller); + if (currentListings >= maxListings) { + logger.info("Player {} has reached max listings ({}/{})", + seller.getCommandSenderName(), currentListings, maxListings); + return null; + } + + // Calculate and charge listing fee + AuctionListing listing = new AuctionListing( + seller.getUniqueID(), + seller.getCommandSenderName(), + item, + startingPrice, + buyoutPrice, + duration + ); + + long listingFee = listing.getListingFee(); + if (listingFee > 0) { + if (!CurrencyController.Instance.canAfford(seller, listingFee)) { + logger.info("Player {} cannot afford listing fee of {}", + seller.getCommandSenderName(), listingFee); + return null; + } + CurrencyController.Instance.withdraw(seller, listingFee); + } + + // Assign ID and store + listing.id = nextId++; + listings.put(listing.id, listing); + + // Update indices + addToSellerIndex(seller.getUniqueID(), listing.id); + + save(); + logger.info("Created auction listing {} by {} for {}", + listing.id, seller.getCommandSenderName(), + item.getDisplayName()); + + return listing; + } + + /** + * Get a listing by ID + */ + public AuctionListing getListing(int id) { + return listings.get(id); + } + + /** + * Get all active listings + */ + public List getActiveListings() { + processExpiredAuctions(); + return listings.values().stream() + .filter(AuctionListing::isActive) + .collect(Collectors.toList()); + } + + /** + * Get active listings by category + */ + public List getActiveListings(EnumAuctionCategory category) { + if (category == EnumAuctionCategory.ALL) { + return getActiveListings(); + } + return getActiveListings().stream() + .filter(l -> l.category == category) + .collect(Collectors.toList()); + } + + /** + * Search listings by item name + */ + public List searchListings(String query) { + String lowerQuery = query.toLowerCase(); + return getActiveListings().stream() + .filter(l -> l.item.getDisplayName().toLowerCase().contains(lowerQuery)) + .collect(Collectors.toList()); + } + + /** + * Get all listings by a seller (all statuses) + */ + public List getListingsBySeller(UUID sellerUUID) { + List ids = listingsBySeller.get(sellerUUID); + if (ids == null) { + return new ArrayList<>(); + } + return ids.stream() + .map(listings::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Get active listing count for a seller + */ + public int getActiveListingsCount(UUID sellerUUID) { + return (int) getListingsBySeller(sellerUUID).stream() + .filter(l -> l.status == EnumAuctionStatus.ACTIVE) + .count(); + } + + /** + * Get listings where player is the high bidder + */ + public List getListingsAsBidder(UUID bidderUUID) { + List ids = listingsByBidder.get(bidderUUID); + if (ids == null) { + return new ArrayList<>(); + } + return ids.stream() + .map(listings::get) + .filter(Objects::nonNull) + .filter(l -> bidderUUID.equals(l.highBidderUUID)) + .collect(Collectors.toList()); + } + + /** + * Get listings that need claiming by a player + * (items won, sales proceeds, cancelled/expired items) + */ + public List getClaimableListings(UUID playerUUID) { + List claimable = new ArrayList<>(); + + for (AuctionListing listing : listings.values()) { + // Seller can claim proceeds from sold items + if (listing.sellerUUID.equals(playerUUID) && + listing.status == EnumAuctionStatus.SOLD && + !listing.sellerClaimed) { + claimable.add(listing); + } + + // Seller can claim back cancelled/expired items + if (listing.sellerUUID.equals(playerUUID) && + (listing.status == EnumAuctionStatus.CANCELLED || + listing.status == EnumAuctionStatus.EXPIRED) && + !listing.sellerClaimed) { + claimable.add(listing); + } + + // Buyer can claim won items + if (listing.highBidderUUID != null && + listing.highBidderUUID.equals(playerUUID) && + listing.status == EnumAuctionStatus.SOLD && + !listing.buyerClaimed) { + claimable.add(listing); + } + } + + return claimable; + } + + // ==================== Bidding ==================== + + /** + * Place a bid on an auction + * @return true if bid was successful + */ + public boolean placeBid(int listingId, EntityPlayer bidder, long amount) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return false; + } + + // Check if bidder can afford + if (!CurrencyController.Instance.canAfford(bidder, amount)) { + return false; + } + + // Store previous bidder for refund + UUID previousBidder = listing.highBidderUUID; + long previousBidAmount = listing.currentBid; + + // Try to place bid + if (!listing.placeBid(bidder.getUniqueID(), bidder.getCommandSenderName(), amount)) { + return false; + } + + // Withdraw bid amount from new bidder + CurrencyController.Instance.withdraw(bidder, amount); + + // Refund previous bidder via claims system + if (previousBidder != null) { + CurrencyController.Instance.addClaim( + previousBidder, + previousBidAmount, + "Auction Outbid", + String.valueOf(listingId) + ); + } + + // Update bidder index + addToBidderIndex(bidder.getUniqueID(), listingId); + + save(); + logger.info("Bid placed on listing {} by {} for {}", + listingId, bidder.getCommandSenderName(), amount); + + return true; + } + + /** + * Execute buyout on an auction + * @return true if buyout was successful + */ + public boolean buyout(int listingId, EntityPlayer buyer) { + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.buyoutPrice <= 0) { + return false; + } + + // Check if buyer can afford + if (!CurrencyController.Instance.canAfford(buyer, listing.buyoutPrice)) { + return false; + } + + // Store previous bidder for refund + UUID previousBidder = listing.highBidderUUID; + long previousBidAmount = listing.currentBid; + + // Execute buyout + if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { + return false; + } + + // Withdraw buyout amount from buyer + CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); + + // Refund previous bidder via claims system + if (previousBidder != null) { + CurrencyController.Instance.addClaim( + previousBidder, + previousBidAmount, + "Auction Outbid (Buyout)", + String.valueOf(listingId) + ); + } + + // Update bidder index + addToBidderIndex(buyer.getUniqueID(), listingId); + + save(); + logger.info("Buyout executed on listing {} by {} for {}", + listingId, buyer.getCommandSenderName(), listing.buyoutPrice); + + return true; + } + + /** + * Cancel an auction listing + * @return true if cancellation was successful + */ + public boolean cancelListing(int listingId, EntityPlayer requester) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return false; + } + + if (!listing.cancel(requester.getUniqueID())) { + return false; + } + + save(); + logger.info("Listing {} cancelled by {}", + listingId, requester.getCommandSenderName()); + + return true; + } + + // ==================== Claiming ==================== + + /** + * Claim seller proceeds from a sold auction + * @return true if claim was successful + */ + public boolean claimSellerProceeds(int listingId, EntityPlayer seller) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return false; + } + + if (!listing.sellerUUID.equals(seller.getUniqueID())) { + return false; // Not the seller + } + + if (listing.status != EnumAuctionStatus.SOLD) { + return false; // Not sold + } + + if (listing.sellerClaimed) { + return false; // Already claimed + } + + // Deposit proceeds to seller + long proceeds = listing.getSellerProceeds(); + CurrencyController.Instance.deposit(seller, proceeds); + + listing.sellerClaimed = true; + checkAndMarkClaimed(listing); + save(); + + logger.info("Seller {} claimed {} from listing {}", + seller.getCommandSenderName(), proceeds, listingId); + + return true; + } + + /** + * Claim item from won auction + * @return The item if successful, null otherwise + */ + public ItemStack claimBuyerItem(int listingId, EntityPlayer buyer) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return null; + } + + if (listing.highBidderUUID == null || + !listing.highBidderUUID.equals(buyer.getUniqueID())) { + return null; // Not the buyer + } + + if (listing.status != EnumAuctionStatus.SOLD) { + return null; // Not sold + } + + if (listing.buyerClaimed) { + return null; // Already claimed + } + + listing.buyerClaimed = true; + checkAndMarkClaimed(listing); + save(); + + logger.info("Buyer {} claimed item from listing {}", + buyer.getCommandSenderName(), listingId); + + return listing.item.copy(); + } + + /** + * Claim item from cancelled or expired auction (seller reclaiming) + * @return The item if successful, null otherwise + */ + public ItemStack claimExpiredItem(int listingId, EntityPlayer seller) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return null; + } + + if (!listing.sellerUUID.equals(seller.getUniqueID())) { + return null; // Not the seller + } + + if (listing.status != EnumAuctionStatus.CANCELLED && + listing.status != EnumAuctionStatus.EXPIRED) { + return null; // Not claimable + } + + if (listing.sellerClaimed) { + return null; // Already claimed + } + + listing.sellerClaimed = true; + listing.status = EnumAuctionStatus.CLAIMED; + save(); + + logger.info("Seller {} reclaimed item from listing {}", + seller.getCommandSenderName(), listingId); + + return listing.item.copy(); + } + + /** + * Mark listing as fully claimed if both parties have claimed + */ + private void checkAndMarkClaimed(AuctionListing listing) { + if (listing.status == EnumAuctionStatus.SOLD) { + if (listing.sellerClaimed && listing.buyerClaimed) { + listing.status = EnumAuctionStatus.CLAIMED; + } + } + } + + // ==================== Expiration Processing ==================== + + /** + * Process all expired auctions + * Call this periodically (e.g., every minute) + */ + public void processExpiredAuctions() { + boolean changed = false; + + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { + listing.finalize(); + changed = true; + + // If sold, add proceeds to seller's claims + if (listing.status == EnumAuctionStatus.SOLD) { + // Proceeds will be claimed manually, no automatic deposit + logger.info("Auction {} ended with sale to {} for {}", + listing.id, listing.highBidderName, listing.currentBid); + } else { + logger.info("Auction {} expired without bids", listing.id); + } + } + } + + if (changed) { + save(); + } + } + + /** + * Clean up old claimed listings + * Call this periodically (e.g., daily) + */ + public void cleanupOldListings() { + long claimExpirationMs = ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L; + if (claimExpirationMs <= 0) { + return; // No expiration + } + + long cutoffTime = System.currentTimeMillis() - claimExpirationMs; + List toRemove = new ArrayList<>(); + + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.CLAIMED && + listing.expirationTime < cutoffTime) { + toRemove.add(listing.id); + } + } + + for (int id : toRemove) { + AuctionListing removed = listings.remove(id); + if (removed != null) { + removeFromSellerIndex(removed.sellerUUID, id); + if (removed.highBidderUUID != null) { + removeFromBidderIndex(removed.highBidderUUID, id); + } + } + } + + if (!toRemove.isEmpty()) { + save(); + logger.info("Cleaned up {} old auction listings", toRemove.size()); + } + } + + // ==================== Index Management ==================== + + private void addToSellerIndex(UUID seller, int listingId) { + listingsBySeller.computeIfAbsent(seller, k -> new ArrayList<>()).add(listingId); + } + + private void removeFromSellerIndex(UUID seller, int listingId) { + List ids = listingsBySeller.get(seller); + if (ids != null) { + ids.remove(Integer.valueOf(listingId)); + } + } + + private void addToBidderIndex(UUID bidder, int listingId) { + listingsByBidder.computeIfAbsent(bidder, k -> new ArrayList<>()).add(listingId); + } + + private void removeFromBidderIndex(UUID bidder, int listingId) { + List ids = listingsByBidder.get(bidder); + if (ids != null) { + ids.remove(Integer.valueOf(listingId)); + } + } + + private void rebuildIndices() { + listingsBySeller.clear(); + listingsByBidder.clear(); + + for (AuctionListing listing : listings.values()) { + addToSellerIndex(listing.sellerUUID, listing.id); + if (listing.highBidderUUID != null) { + addToBidderIndex(listing.highBidderUUID, listing.id); + } + } + } + + // ==================== Permission Helpers ==================== + + /** + * Get maximum listings allowed for a player + */ + public int getMaxListings(EntityPlayer player) { + // TODO: Check for permission-based overrides + // e.g., CustomNpcsPermissions.hasCustomPermission(player, "cnpc.auction.slots.10") + return ConfigMarket.DefaultMaxListings; + } + + // ==================== Persistence ==================== + + public void save() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + try { + NBTTagCompound compound = new NBTTagCompound(); + compound.setInteger("NextId", nextId); + + NBTTagList listingList = new NBTTagList(); + for (AuctionListing listing : listings.values()) { + listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + compound.setTag("Listings", listingList); + + File file = new File(saveDir, "auctions.dat"); + File backup = new File(saveDir, "auctions.dat_old"); + + if (file.exists()) { + if (backup.exists()) { + backup.delete(); + } + file.renameTo(backup); + } + + file = new File(saveDir, "auctions.dat"); + FileOutputStream fos = new FileOutputStream(file); + CompressedStreamTools.writeCompressed(compound, fos); + fos.close(); + + } catch (Exception e) { + logger.error("Error saving auctions", e); + } + } + + public void load() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + listings.clear(); + + try { + File file = new File(saveDir, "auctions.dat"); + if (!file.exists()) { + file = new File(saveDir, "auctions.dat_old"); + if (!file.exists()) { + return; + } + } + + FileInputStream fis = new FileInputStream(file); + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + fis.close(); + + nextId = compound.getInteger("NextId"); + if (nextId < 1) { + nextId = 1; + } + + NBTTagList listingList = compound.getTagList("Listings", 10); + for (int i = 0; i < listingList.tagCount(); i++) { + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(listingList.getCompoundTagAt(i)); + listings.put(listing.id, listing); + } + + rebuildIndices(); + + logger.info("Loaded {} auction listings", listings.size()); + + } catch (Exception e) { + logger.error("Error loading auctions", e); + } + } + + // ==================== Statistics ==================== + + /** + * Get total number of active listings + */ + public int getActiveListingCount() { + return (int) listings.values().stream() + .filter(l -> l.status == EnumAuctionStatus.ACTIVE) + .count(); + } + + /** + * Get total number of all listings + */ + public int getTotalListingCount() { + return listings.size(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java new file mode 100644 index 000000000..e17a08112 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -0,0 +1,392 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.AuctionBlacklist; + +import java.util.UUID; + +/** + * Represents a single auction listing. + * Contains all data about the item, seller, bidding, and timing. + */ +public class AuctionListing { + // Unique identifier + public int id; + + // Seller info + public UUID sellerUUID; + public String sellerName; + + // Item being sold + public ItemStack item; + + // Pricing + public long startingPrice; + public long buyoutPrice; // 0 = no buyout option + public long currentBid; + + // Current high bidder + public UUID highBidderUUID; + public String highBidderName; + + // Previous bidder (for refund notifications) + public UUID previousBidderUUID; + public String previousBidderName; + public long previousBidAmount; + + // Timing + public long listingTime; // When the auction was created + public long expirationTime; // When the auction ends + public EnumAuctionDuration duration; + + // Status + public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; + + // Category for filtering + public EnumAuctionCategory category = EnumAuctionCategory.MISC; + + // Claim tracking + public boolean sellerClaimed = false; // Has seller claimed their proceeds? + public boolean buyerClaimed = false; // Has buyer claimed their item? + + public AuctionListing() { + } + + /** + * Create a new auction listing + */ + public AuctionListing(UUID seller, String sellerName, ItemStack item, + long startPrice, long buyout, EnumAuctionDuration duration) { + this.sellerUUID = seller; + this.sellerName = sellerName; + this.item = item.copy(); + this.startingPrice = startPrice; + this.buyoutPrice = buyout; + this.currentBid = 0; // No bids yet + this.duration = duration; + this.listingTime = System.currentTimeMillis(); + this.expirationTime = listingTime + duration.getMillis(); + this.category = EnumAuctionCategory.categorize(item); + this.status = EnumAuctionStatus.ACTIVE; + } + + // ==================== Status Checks ==================== + + /** + * Check if auction has expired + */ + public boolean isExpired() { + return System.currentTimeMillis() >= expirationTime; + } + + /** + * Check if auction is still active + */ + public boolean isActive() { + return status == EnumAuctionStatus.ACTIVE && !isExpired(); + } + + /** + * Get time remaining in milliseconds + */ + public long getTimeRemaining() { + return Math.max(0, expirationTime - System.currentTimeMillis()); + } + + /** + * Get formatted time remaining + */ + public String getTimeRemainingFormatted() { + long remaining = getTimeRemaining(); + + if (remaining <= 0) { + return "Ended"; + } + + long hours = remaining / (60L * 60L * 1000L); + long minutes = (remaining % (60L * 60L * 1000L)) / (60L * 1000L); + + if (hours > 24) { + long days = hours / 24; + hours = hours % 24; + return days + "d " + hours + "h"; + } else if (hours > 0) { + return hours + "h " + minutes + "m"; + } else { + long seconds = (remaining % (60L * 1000L)) / 1000L; + return minutes + "m " + seconds + "s"; + } + } + + /** + * Check if auction has any bids + */ + public boolean hasBids() { + return highBidderUUID != null; + } + + // ==================== Bidding Methods ==================== + + /** + * Calculate minimum valid bid amount + */ + public long getMinimumBid() { + if (!hasBids()) { + return startingPrice; + } + // Minimum increment is configured percent or 1, whichever is greater + long minIncrement = Math.max(1, (long) (currentBid * ConfigMarket.MinBidIncrementPercent)); + return currentBid + minIncrement; + } + + /** + * Place a bid + * @return true if bid was successful + */ + public boolean placeBid(UUID bidder, String bidderName, long amount) { + if (status != EnumAuctionStatus.ACTIVE) { + return false; + } + if (isExpired()) { + return false; + } + if (amount < getMinimumBid()) { + return false; + } + if (bidder.equals(sellerUUID)) { + return false; // Can't bid on own item + } + + // Store previous bidder for refund notification + if (highBidderUUID != null) { + previousBidderUUID = highBidderUUID; + previousBidderName = highBidderName; + previousBidAmount = currentBid; + } + + // Update high bidder + highBidderUUID = bidder; + highBidderName = bidderName; + currentBid = amount; + + // Snipe protection - extend if bid placed in final minutes + if (ConfigMarket.SnipeProtectionMinutes > 0) { + long timeRemaining = getTimeRemaining(); + long thresholdMs = ConfigMarket.SnipeProtectionThreshold * 60L * 1000L; + + if (timeRemaining < thresholdMs) { + // Extend TO snipe protection time, not by it + long newExpiration = System.currentTimeMillis() + + (ConfigMarket.SnipeProtectionMinutes * 60L * 1000L); + // Only extend if it would actually extend the auction + if (newExpiration > expirationTime) { + expirationTime = newExpiration; + } + } + } + + return true; + } + + /** + * Execute buyout (instant purchase at buyout price) + * @return true if buyout was successful + */ + public boolean buyout(UUID buyer, String buyerName) { + if (status != EnumAuctionStatus.ACTIVE) { + return false; + } + if (isExpired()) { + return false; + } + if (buyoutPrice <= 0) { + return false; // No buyout available + } + if (buyer.equals(sellerUUID)) { + return false; // Can't buy own item + } + + // Store previous bidder for refund + if (highBidderUUID != null) { + previousBidderUUID = highBidderUUID; + previousBidderName = highBidderName; + previousBidAmount = currentBid; + } + + highBidderUUID = buyer; + highBidderName = buyerName; + currentBid = buyoutPrice; + status = EnumAuctionStatus.SOLD; + + return true; + } + + /** + * Cancel listing (seller only, before any bids) + * @return true if cancellation was successful + */ + public boolean cancel(UUID requester) { + if (!requester.equals(sellerUUID)) { + return false; // Only seller can cancel + } + if (hasBids()) { + return false; // Can't cancel with active bids + } + if (status != EnumAuctionStatus.ACTIVE) { + return false; + } + + status = EnumAuctionStatus.CANCELLED; + return true; + } + + /** + * Finalize auction when it expires + * Call this when auction timer runs out + */ + public void finalize() { + if (status != EnumAuctionStatus.ACTIVE) { + return; + } + + if (hasBids()) { + status = EnumAuctionStatus.SOLD; + } else { + status = EnumAuctionStatus.EXPIRED; + } + } + + // ==================== Validation ==================== + + /** + * Validate that the item can be listed + * @return null if valid, error message if invalid + */ + public static String validateItem(ItemStack item) { + if (item == null || item.getItem() == null) { + return "Invalid item"; + } + + // Check trade restrictions (Untradeable, ProfileSlot Bound, Soulbound) + if (!ItemTradeAttribute.canTrade(item)) { + String reason = ItemTradeAttribute.getTradeBlockReason(item); + return reason != null ? reason : "Item cannot be traded"; + } + + // Check blacklist + if (AuctionBlacklist.Instance != null && AuctionBlacklist.Instance.isBlacklisted(item)) { + return "Item is blacklisted from auction"; + } + + return null; // Valid + } + + // ==================== Financial Calculations ==================== + + /** + * Get listing fee for this auction + */ + public long getListingFee() { + long flatFee = duration.getListingFee(); + long percentFee = (long) (startingPrice * ConfigMarket.ListingFeePercent); + return flatFee + percentFee; + } + + /** + * Get seller's proceeds after sales tax + */ + public long getSellerProceeds() { + if (status != EnumAuctionStatus.SOLD) { + return 0; + } + long tax = (long) (currentBid * ConfigMarket.SalesTaxPercent); + return currentBid - tax; + } + + // ==================== NBT Serialization ==================== + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setInteger("Id", id); + compound.setString("SellerUUID", sellerUUID.toString()); + compound.setString("SellerName", sellerName); + compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); + compound.setLong("StartPrice", startingPrice); + compound.setLong("BuyoutPrice", buyoutPrice); + compound.setLong("CurrentBid", currentBid); + + if (highBidderUUID != null) { + compound.setString("BidderUUID", highBidderUUID.toString()); + compound.setString("BidderName", highBidderName); + } + + if (previousBidderUUID != null) { + compound.setString("PrevBidderUUID", previousBidderUUID.toString()); + compound.setString("PrevBidderName", previousBidderName); + compound.setLong("PrevBidAmount", previousBidAmount); + } + + compound.setLong("ListingTime", listingTime); + compound.setLong("ExpirationTime", expirationTime); + compound.setInteger("Duration", duration.ordinal()); + compound.setInteger("Status", status.ordinal()); + compound.setInteger("Category", category.ordinal()); + compound.setBoolean("SellerClaimed", sellerClaimed); + compound.setBoolean("BuyerClaimed", buyerClaimed); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getInteger("Id"); + sellerUUID = UUID.fromString(compound.getString("SellerUUID")); + sellerName = compound.getString("SellerName"); + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + startingPrice = compound.getLong("StartPrice"); + buyoutPrice = compound.getLong("BuyoutPrice"); + currentBid = compound.getLong("CurrentBid"); + + if (compound.hasKey("BidderUUID")) { + highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); + highBidderName = compound.getString("BidderName"); + } + + if (compound.hasKey("PrevBidderUUID")) { + previousBidderUUID = UUID.fromString(compound.getString("PrevBidderUUID")); + previousBidderName = compound.getString("PrevBidderName"); + previousBidAmount = compound.getLong("PrevBidAmount"); + } + + listingTime = compound.getLong("ListingTime"); + expirationTime = compound.getLong("ExpirationTime"); + + int durationOrdinal = compound.getInteger("Duration"); + if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { + duration = EnumAuctionDuration.values()[durationOrdinal]; + } else { + duration = EnumAuctionDuration.LONG; + } + + int statusOrdinal = compound.getInteger("Status"); + if (statusOrdinal >= 0 && statusOrdinal < EnumAuctionStatus.values().length) { + status = EnumAuctionStatus.values()[statusOrdinal]; + } else { + status = EnumAuctionStatus.ACTIVE; + } + + int categoryOrdinal = compound.getInteger("Category"); + if (categoryOrdinal >= 0 && categoryOrdinal < EnumAuctionCategory.values().length) { + category = EnumAuctionCategory.values()[categoryOrdinal]; + } else { + category = EnumAuctionCategory.MISC; + } + + sellerClaimed = compound.getBoolean("SellerClaimed"); + buyerClaimed = compound.getBoolean("BuyerClaimed"); + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java new file mode 100644 index 000000000..0c0f53d07 --- /dev/null +++ b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java @@ -0,0 +1,134 @@ +package noppes.npcs.roles; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * NPC role for Auctioneer - provides access to the global auction house. + */ +public class RoleAuctioneer extends RoleInterface { + + // Configuration + public boolean showActiveListings = true; // Show active listings tab + public boolean showMyListings = true; // Show player's listings tab + public boolean showMyBids = true; // Show player's bids tab + public boolean showClaims = true; // Show claimable items tab + public boolean allowCreatingListings = true; // Allow creating new listings + public boolean allowBidding = true; // Allow bidding on items + public boolean allowBuyout = true; // Allow buyout purchases + + // Custom greeting messages + public String welcomeMessage = ""; + public String noAuctionMessage = ""; + + public RoleAuctioneer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("ShowActiveListings", showActiveListings); + compound.setBoolean("ShowMyListings", showMyListings); + compound.setBoolean("ShowMyBids", showMyBids); + compound.setBoolean("ShowClaims", showClaims); + compound.setBoolean("AllowCreatingListings", allowCreatingListings); + compound.setBoolean("AllowBidding", allowBidding); + compound.setBoolean("AllowBuyout", allowBuyout); + compound.setString("WelcomeMessage", welcomeMessage); + compound.setString("NoAuctionMessage", noAuctionMessage); + return compound; + } + + @Override + public void readFromNBT(NBTTagCompound compound) { + showActiveListings = compound.hasKey("ShowActiveListings") ? + compound.getBoolean("ShowActiveListings") : true; + showMyListings = compound.hasKey("ShowMyListings") ? + compound.getBoolean("ShowMyListings") : true; + showMyBids = compound.hasKey("ShowMyBids") ? + compound.getBoolean("ShowMyBids") : true; + showClaims = compound.hasKey("ShowClaims") ? + compound.getBoolean("ShowClaims") : true; + allowCreatingListings = compound.hasKey("AllowCreatingListings") ? + compound.getBoolean("AllowCreatingListings") : true; + allowBidding = compound.hasKey("AllowBidding") ? + compound.getBoolean("AllowBidding") : true; + allowBuyout = compound.hasKey("AllowBuyout") ? + compound.getBoolean("AllowBuyout") : true; + welcomeMessage = compound.getString("WelcomeMessage"); + noAuctionMessage = compound.getString("NoAuctionMessage"); + } + + @Override + public void interact(EntityPlayer player) { + // Check if auction system is enabled + if (!ConfigMarket.AuctionEnabled) { + if (!noAuctionMessage.isEmpty()) { + npc.say(player, noAuctionMessage); + } else { + npc.say(player, "The auction house is currently closed."); + } + return; + } + + // Process expired auctions before opening GUI + if (AuctionController.Instance != null) { + AuctionController.Instance.processExpiredAuctions(); + } + + // Say welcome message + if (!welcomeMessage.isEmpty()) { + npc.say(player, welcomeMessage); + } else { + npc.say(player, npc.advanced.getInteractLine()); + } + + // Open auction GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); + } + + /** + * Check if player has claims waiting + */ + public boolean hasClaimsWaiting(EntityPlayer player) { + if (AuctionController.Instance == null) { + return false; + } + return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); + } + + /** + * Get number of active listings in the auction house + */ + public int getActiveListingCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getActiveListingCount(); + } + + /** + * Get player's current listing count + */ + public int getPlayerListingCount(EntityPlayer player) { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); + } + + /** + * Get player's maximum allowed listings + */ + public int getPlayerMaxListings(EntityPlayer player) { + if (AuctionController.Instance == null) { + return ConfigMarket.DefaultMaxListings; + } + return AuctionController.Instance.getMaxListings(player); + } +} From 57f9b706bff5278ae6e5c846c294e13f456e3564 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:57:13 +0000 Subject: [PATCH 10/21] Implement Phase 5: Network packets, permissions, and scripting API - Add auction permission nodes (use, list, bid, buyout, admin) - Create AuctionActionPacket for client-server communication - Supports: CreateListing, PlaceBid, Buyout, CancelListing, ClaimItem, ClaimProceeds - Includes permission checks and validation - Register packet in PacketHandler - Add auction scripting API methods to ScriptPlayer: - getAuctionListingCount() - getMaxAuctionListings() - hasAuctionClaims() - getAuctionClaimCount() - getActiveAuctionBidCount() --- .../kamkeel/npcs/network/PacketHandler.java | 4 + .../npcs/network/enums/EnumPlayerPacket.java | 3 + .../packets/player/AuctionActionPacket.java | 441 ++++++++++++++++++ .../noppes/npcs/CustomNpcsPermissions.java | 7 + .../npcs/scripted/entity/ScriptPlayer.java | 67 +++ 5 files changed, 522 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 745962aba..b22537070 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -49,6 +49,7 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; +import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; @@ -611,6 +612,9 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new PartyAcceptInvitePacket()); PLAYER_PACKET.registerPacket(new PartyIgnoreInvitePacket()); PLAYER_PACKET.registerPacket(new PartyLogToServerPacket()); + + // Auction Packets + PLAYER_PACKET.registerPacket(new AuctionActionPacket()); } public void registerChannels() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 0c1db3cd9..15c90fce5 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -33,4 +33,7 @@ public enum EnumPlayerPacket { ProfileGet, ProfileGetInfo, SyncRevisionInfo, + + // Auction + AuctionAction, } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java new file mode 100644 index 000000000..4aa56ab57 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -0,0 +1,441 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.ChatComponentText; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.IOException; + +/** + * Packet for all auction-related player actions. + * Client -> Server for actions, Server -> Client for responses. + */ +public class AuctionActionPacket extends AbstractPacket { + public static final String packetName = "Player|Auction"; + + private Action action; + private int listingId; + private long amount; + private long buyoutPrice; + private int durationOrdinal; + private NBTTagCompound itemData; + private String responseMessage; + private boolean success; + + public AuctionActionPacket() { + } + + private AuctionActionPacket(Action action) { + this.action = action; + } + + @Override + public boolean needsNPC() { + return true; + } + + // ==================== Client -> Server Actions ==================== + + /** + * Create a new auction listing + */ + @SideOnly(Side.CLIENT) + public static void CreateListing(ItemStack item, long startingPrice, long buyoutPrice, EnumAuctionDuration duration) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); + packet.amount = startingPrice; + packet.buyoutPrice = buyoutPrice; + packet.durationOrdinal = duration.ordinal(); + packet.itemData = item.writeToNBT(new NBTTagCompound()); + PacketClient.sendClient(packet); + } + + /** + * Place a bid on an auction + */ + @SideOnly(Side.CLIENT) + public static void PlaceBid(int listingId, long bidAmount) { + AuctionActionPacket packet = new AuctionActionPacket(Action.PlaceBid); + packet.listingId = listingId; + packet.amount = bidAmount; + PacketClient.sendClient(packet); + } + + /** + * Execute buyout on an auction + */ + @SideOnly(Side.CLIENT) + public static void Buyout(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Cancel own listing + */ + @SideOnly(Side.CLIENT) + public static void CancelListing(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CancelListing); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Claim item from won/expired/cancelled auction + */ + @SideOnly(Side.CLIENT) + public static void ClaimItem(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Claim proceeds from sold auction + */ + @SideOnly(Side.CLIENT) + public static void ClaimProceeds(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimProceeds); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + @Override + public Enum getType() { + return EnumPlayerPacket.AuctionAction; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeInt(action.ordinal()); + + switch (action) { + case CreateListing: + out.writeLong(amount); + out.writeLong(buyoutPrice); + out.writeInt(durationOrdinal); + String itemJson = NBTJsonUtil.Convert(itemData); + byte[] itemBytes = itemJson.getBytes("UTF-8"); + out.writeInt(itemBytes.length); + out.writeBytes(itemBytes); + break; + + case PlaceBid: + out.writeInt(listingId); + out.writeLong(amount); + break; + + case Buyout: + case CancelListing: + case ClaimItem: + case ClaimProceeds: + out.writeInt(listingId); + break; + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + Action requestedAction = Action.values()[in.readInt()]; + + if (!(player instanceof EntityPlayerMP)) { + return; + } + + EntityPlayerMP playerMP = (EntityPlayerMP) player; + + // Verify NPC is an Auctioneer + if (npc == null || npc.advanced.role != EnumRoleType.Auctioneer) { + return; + } + + // Check if auction system is enabled + if (!ConfigMarket.AuctionEnabled) { + sendResponse(playerMP, false, "Auction system is disabled"); + return; + } + + // Check controller exists + if (AuctionController.Instance == null) { + sendResponse(playerMP, false, "Auction controller not initialized"); + return; + } + + switch (requestedAction) { + case CreateListing: + handleCreateListing(in, playerMP); + break; + case PlaceBid: + handlePlaceBid(in, playerMP); + break; + case Buyout: + handleBuyout(in, playerMP); + break; + case CancelListing: + handleCancelListing(in, playerMP); + break; + case ClaimItem: + handleClaimItem(in, playerMP); + break; + case ClaimProceeds: + handleClaimProceeds(in, playerMP); + break; + } + } + + private void handleCreateListing(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_LIST)) { + sendResponse(player, false, "You don't have permission to create listings"); + return; + } + + long startingPrice = in.readLong(); + long buyoutPrice = in.readLong(); + int durationOrdinal = in.readInt(); + + int itemBytesLength = in.readInt(); + byte[] itemBytes = new byte[itemBytesLength]; + in.readBytes(itemBytes); + String itemJson = new String(itemBytes, "UTF-8"); + + try { + NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); + ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); + + if (item == null) { + sendResponse(player, false, "Invalid item"); + return; + } + + // Validate duration + EnumAuctionDuration duration; + if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { + duration = EnumAuctionDuration.values()[durationOrdinal]; + } else { + duration = EnumAuctionDuration.MEDIUM; + } + + // Validate prices + if (startingPrice < 1) { + sendResponse(player, false, "Starting price must be at least 1"); + return; + } + + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + sendResponse(player, false, "Buyout price must be higher than starting price"); + return; + } + + // Create the listing + AuctionListing listing = AuctionController.Instance.createListing( + player, item, startingPrice, buyoutPrice, duration); + + if (listing != null) { + // Remove item from player's inventory + // Note: The GUI should have already collected the item, this is validation + sendResponse(player, true, "Listing created successfully"); + } else { + sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); + } + } catch (Exception e) { + sendResponse(player, false, "Error creating listing: " + e.getMessage()); + } + } + + private void handlePlaceBid(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BID)) { + sendResponse(player, false, "You don't have permission to bid"); + return; + } + + int listingId = in.readInt(); + long bidAmount = in.readLong(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (!listing.isActive()) { + sendResponse(player, false, "This auction has ended"); + return; + } + + if (bidAmount < listing.getMinimumBid()) { + sendResponse(player, false, "Bid must be at least " + listing.getMinimumBid()); + return; + } + + if (!CurrencyController.Instance.canAfford(player, bidAmount)) { + sendResponse(player, false, "You cannot afford this bid"); + return; + } + + if (AuctionController.Instance.placeBid(listingId, player, bidAmount)) { + sendResponse(player, true, "Bid placed successfully"); + } else { + sendResponse(player, false, "Failed to place bid"); + } + } + + private void handleBuyout(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BUYOUT)) { + sendResponse(player, false, "You don't have permission to use buyout"); + return; + } + + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (!listing.isActive()) { + sendResponse(player, false, "This auction has ended"); + return; + } + + if (listing.buyoutPrice <= 0) { + sendResponse(player, false, "This listing has no buyout option"); + return; + } + + if (!CurrencyController.Instance.canAfford(player, listing.buyoutPrice)) { + sendResponse(player, false, "You cannot afford the buyout price"); + return; + } + + if (AuctionController.Instance.buyout(listingId, player)) { + sendResponse(player, true, "Buyout successful! Claim your item."); + } else { + sendResponse(player, false, "Failed to execute buyout"); + } + } + + private void handleCancelListing(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + // Admin can cancel any listing + boolean isAdmin = CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_ADMIN); + + if (!listing.sellerUUID.equals(player.getUniqueID()) && !isAdmin) { + sendResponse(player, false, "You can only cancel your own listings"); + return; + } + + if (listing.hasBids() && !isAdmin) { + sendResponse(player, false, "Cannot cancel listing with active bids"); + return; + } + + if (AuctionController.Instance.cancelListing(listingId, player)) { + sendResponse(player, true, "Listing cancelled. Claim your item."); + } else { + sendResponse(player, false, "Failed to cancel listing"); + } + } + + private void handleClaimItem(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + ItemStack claimed = null; + + // Try buyer claim first + if (listing.highBidderUUID != null && listing.highBidderUUID.equals(player.getUniqueID())) { + claimed = AuctionController.Instance.claimBuyerItem(listingId, player); + } + // Then try seller reclaim (for expired/cancelled) + else if (listing.sellerUUID.equals(player.getUniqueID())) { + claimed = AuctionController.Instance.claimExpiredItem(listingId, player); + } + + if (claimed != null) { + // Give item to player + if (!player.inventory.addItemStackToInventory(claimed)) { + // Drop if inventory full + player.entityDropItem(claimed, 0.5f); + } + sendResponse(player, true, "Item claimed"); + } else { + sendResponse(player, false, "Cannot claim item"); + } + } + + private void handleClaimProceeds(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (AuctionController.Instance.claimSellerProceeds(listingId, player)) { + long proceeds = listing.getSellerProceeds(); + sendResponse(player, true, "Claimed " + proceeds + " " + ConfigMarket.CurrencyName); + } else { + sendResponse(player, false, "Cannot claim proceeds"); + } + } + + /** + * Send a response message to the player + */ + private void sendResponse(EntityPlayerMP player, boolean success, String message) { + // Use chat message for now; can be extended to use GUI updates + if (message != null && !message.isEmpty()) { + player.addChatMessage(new ChatComponentText("[Auction] " + message)); + } + } + + private enum Action { + CreateListing, + PlaceBid, + Buyout, + CancelListing, + ClaimItem, + ClaimProceeds + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 3363c0351..1f09e466f 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -90,6 +90,13 @@ public class CustomNpcsPermissions { public static final Permission SOULSTONE_ALL = new Permission("customnpcs.soulstone.all"); + // Auction System + public static final Permission AUCTION_USE = new Permission("customnpcs.auction.use"); + public static final Permission AUCTION_LIST = new Permission("customnpcs.auction.list"); + public static final Permission AUCTION_BID = new Permission("customnpcs.auction.bid"); + public static final Permission AUCTION_BUYOUT = new Permission("customnpcs.auction.buyout"); + public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); + // Modify / Save Scripts public static final Permission SCRIPT = new Permission("customnpcs.script"); public static final Permission SCRIPT_NPC = new Permission("customnpcs.script.npc"); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 5bfaba816..9fc547260 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,6 +49,7 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; @@ -1013,4 +1014,70 @@ public class ScriptPlayer extends ScriptLivingBase } return CurrencyController.Instance.claimAll(player); } + + // ==================== AUCTION METHODS ==================== + + /** + * Get the number of active auction listings the player has. + * @return Number of active listings + * @since 1.11 + */ + public int getAuctionListingCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); + } + + /** + * Get the maximum number of auction listings the player is allowed. + * @return Maximum listing count + * @since 1.11 + */ + public int getMaxAuctionListings() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getMaxListings(player); + } + + /** + * Check if the player has auction items waiting to be claimed. + * This includes won items, expired listings, and sale proceeds. + * @return true if there are claimable items + * @since 1.11 + */ + public boolean hasAuctionClaims() { + if (AuctionController.Instance == null) { + return false; + } + return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); + } + + /** + * Get the number of auction items waiting to be claimed. + * @return Number of claimable items/listings + * @since 1.11 + */ + public int getAuctionClaimCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getClaimableListings(player.getUniqueID()).size(); + } + + /** + * Get the number of auctions where the player is the current high bidder. + * @return Number of active bids + * @since 1.11 + */ + public int getActiveAuctionBidCount() { + if (AuctionController.Instance == null) { + return 0; + } + return (int) AuctionController.Instance.getListingsAsBidder(player.getUniqueID()) + .stream() + .filter(l -> l.isActive()) + .count(); + } } From d09c6b0c81ff3557cc1cd5b16099c2a54273a9de Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 09:33:29 +0000 Subject: [PATCH 11/21] Add auction filtering and sorting system New files: - EnumAuctionSort: Sorting options (price, time, name, bids, etc.) - AuctionFilter: Search/filter criteria with builder pattern - AuctionDataPacket: Large packet for server->client listing data AuctionController changes: - getFilteredListings(): Main filtering/pagination method - sortListings(): Comprehensive sorting by various criteria - quickSearch(): Convenience method for simple searches - FilterResult: Pagination result container AuctionListing changes: - Added bidCount field with NBT persistence - Added creationTime alias methods AuctionActionPacket changes: - QueryListings: Filter-based search - QueryMyListings: Player's own listings - QueryMyBids: Player's active bids - QueryClaimable: Items ready to claim NoppesUtil changes: - Auction data storage and IAuctionData interface - Client-side data handling for GUI integration --- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumDataPacket.java | 3 + .../packets/data/large/AuctionDataPacket.java | 218 +++++++++++++++ .../packets/player/AuctionActionPacket.java | 127 ++++++++- .../java/noppes/npcs/client/NoppesUtil.java | 61 +++++ .../npcs/constants/EnumAuctionSort.java | 68 +++++ .../npcs/controllers/AuctionController.java | 167 ++++++++++++ .../npcs/controllers/data/AuctionFilter.java | 250 ++++++++++++++++++ .../npcs/controllers/data/AuctionListing.java | 18 ++ 9 files changed, 913 insertions(+), 1 deletion(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionSort.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionFilter.java diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index b22537070..fa8f99de4 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -36,6 +36,7 @@ import kamkeel.npcs.network.packets.data.gui.GuiWaypointPacket; import kamkeel.npcs.network.packets.data.gui.IsGuiOpenPacket; import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.network.packets.data.large.PartyDataPacket; +import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollListPacket; import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; @@ -551,6 +552,7 @@ public class PacketHandler { DATA_PACKET.registerPacket(new SyncEffectPacket()); DATA_PACKET.registerPacket(new GuiDataPacket()); DATA_PACKET.registerPacket(new PartyDataPacket()); + DATA_PACKET.registerPacket(new AuctionDataPacket()); } public void registerPlayerPackets() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 874677af9..2f8cf71bc 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -58,4 +58,7 @@ public enum EnumDataPacket { SWING_PLAYER_ARM, DISABLE_MOUSE_INPUT, LOGIN, + + // Auction + AUCTION_DATA, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java new file mode 100644 index 000000000..f7ca16dd7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java @@ -0,0 +1,218 @@ +package kamkeel.npcs.network.packets.data.large; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import kamkeel.npcs.network.LargeAbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; + +import java.io.IOException; +import java.util.List; + +/** + * Large packet for sending auction listing data from server to client. + * Used for initial load and search/filter queries. + */ +public final class AuctionDataPacket extends LargeAbstractPacket { + public static final String packetName = "Large|AuctionData"; + + private DataType dataType; + private List listings; + private int totalResults; + private int currentPage; + private int totalPages; + private long playerBalance; + + public AuctionDataPacket() { + } + + private AuctionDataPacket(DataType type, List listings, + int totalResults, int currentPage, int totalPages, + long playerBalance) { + this.dataType = type; + this.listings = listings; + this.totalResults = totalResults; + this.currentPage = currentPage; + this.totalPages = totalPages; + this.playerBalance = playerBalance; + } + + /** + * Send filtered listing results to client + */ + public static void sendFilteredListings(EntityPlayerMP player, AuctionFilter filter) { + if (AuctionController.Instance == null) { + return; + } + + AuctionController.FilterResult result = AuctionController.Instance.getFilteredListings(filter); + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.FILTERED_LISTINGS, + result.listings, + result.totalResults, + result.currentPage, + result.totalPages, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + /** + * Send player's own listings + */ + public static void sendMyListings(EntityPlayerMP player) { + if (AuctionController.Instance == null) { + return; + } + + List myListings = AuctionController.Instance + .getListingsBySeller(player.getUniqueID()); + + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.MY_LISTINGS, + myListings, + myListings.size(), + 0, + 1, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + /** + * Send player's active bids + */ + public static void sendMyBids(EntityPlayerMP player) { + if (AuctionController.Instance == null) { + return; + } + + List myBids = AuctionController.Instance + .getListingsAsBidder(player.getUniqueID()); + + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.MY_BIDS, + myBids, + myBids.size(), + 0, + 1, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + /** + * Send player's claimable items + */ + public static void sendClaimable(EntityPlayerMP player) { + if (AuctionController.Instance == null) { + return; + } + + List claimable = AuctionController.Instance + .getClaimableListings(player.getUniqueID()); + + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.CLAIMABLE, + claimable, + claimable.size(), + 0, + 1, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + @Override + public Enum getType() { + return EnumDataPacket.AUCTION_DATA; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + protected byte[] getData() throws IOException { + ByteBuf buffer = Unpooled.buffer(); + + buffer.writeInt(dataType.ordinal()); + buffer.writeInt(totalResults); + buffer.writeInt(currentPage); + buffer.writeInt(totalPages); + buffer.writeLong(playerBalance); + + // Write listings as NBT + NBTTagList listingList = new NBTTagList(); + if (listings != null) { + for (AuctionListing listing : listings) { + listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + } + + NBTTagCompound compound = new NBTTagCompound(); + compound.setTag("Listings", listingList); + ByteBufUtils.writeNBT(buffer, compound); + + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + return bytes; + } + + @Override + protected void handleCompleteData(ByteBuf data, EntityPlayer player) throws IOException { + // Client-side handling will be done in a client util class + // Similar to NoppesUtil.setScrollData() + int typeOrdinal = data.readInt(); + int totalResults = data.readInt(); + int currentPage = data.readInt(); + int totalPages = data.readInt(); + long playerBalance = data.readLong(); + + NBTTagCompound compound = ByteBufUtils.readNBT(data); + + DataType type = DataType.values()[typeOrdinal]; + + // Store data for client GUI to access + // This will be implemented in the client-side GUI handler + noppes.npcs.client.NoppesUtil.setAuctionData(type.ordinal(), compound, + totalResults, currentPage, totalPages, playerBalance); + } + + public enum DataType { + FILTERED_LISTINGS, + MY_LISTINGS, + MY_BIDS, + CLAIMABLE + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index 4aa56ab57..54bfaef50 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -19,8 +19,10 @@ import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.util.NBTJsonUtil; +import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; import java.io.IOException; @@ -37,6 +39,7 @@ public class AuctionActionPacket extends AbstractPacket { private long buyoutPrice; private int durationOrdinal; private NBTTagCompound itemData; + private NBTTagCompound filterData; // For query operations private String responseMessage; private boolean success; @@ -118,6 +121,43 @@ public class AuctionActionPacket extends AbstractPacket { PacketClient.sendClient(packet); } + /** + * Query filtered listings + */ + @SideOnly(Side.CLIENT) + public static void QueryListings(AuctionFilter filter) { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryListings); + packet.filterData = filter.writeToNBT(new NBTTagCompound()); + PacketClient.sendClient(packet); + } + + /** + * Request player's own listings + */ + @SideOnly(Side.CLIENT) + public static void QueryMyListings() { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyListings); + PacketClient.sendClient(packet); + } + + /** + * Request player's active bids + */ + @SideOnly(Side.CLIENT) + public static void QueryMyBids() { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyBids); + PacketClient.sendClient(packet); + } + + /** + * Request player's claimable items + */ + @SideOnly(Side.CLIENT) + public static void QueryClaimable() { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryClaimable); + PacketClient.sendClient(packet); + } + @Override public Enum getType() { return EnumPlayerPacket.AuctionAction; @@ -155,6 +195,19 @@ public class AuctionActionPacket extends AbstractPacket { case ClaimProceeds: out.writeInt(listingId); break; + + case QueryListings: + String filterJson = NBTJsonUtil.Convert(filterData); + byte[] filterBytes = filterJson.getBytes("UTF-8"); + out.writeInt(filterBytes.length); + out.writeBytes(filterBytes); + break; + + case QueryMyListings: + case QueryMyBids: + case QueryClaimable: + // No additional data needed + break; } } @@ -204,6 +257,18 @@ public class AuctionActionPacket extends AbstractPacket { case ClaimProceeds: handleClaimProceeds(in, playerMP); break; + case QueryListings: + handleQueryListings(in, playerMP); + break; + case QueryMyListings: + handleQueryMyListings(playerMP); + break; + case QueryMyBids: + handleQueryMyBids(playerMP); + break; + case QueryClaimable: + handleQueryClaimable(playerMP); + break; } } @@ -420,6 +485,62 @@ public class AuctionActionPacket extends AbstractPacket { } } + // ==================== Query Handlers ==================== + + private void handleQueryListings(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + int filterBytesLength = in.readInt(); + byte[] filterBytes = new byte[filterBytesLength]; + in.readBytes(filterBytes); + String filterJson = new String(filterBytes, "UTF-8"); + + try { + NBTTagCompound filterNBT = NBTJsonUtil.Convert(filterJson); + AuctionFilter filter = new AuctionFilter(); + filter.readFromNBT(filterNBT); + + // Send filtered listings to client + AuctionDataPacket.sendFilteredListings(player, filter); + } catch (Exception e) { + sendResponse(player, false, "Error parsing filter"); + } + } + + private void handleQueryMyListings(EntityPlayerMP player) { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + AuctionDataPacket.sendMyListings(player); + } + + private void handleQueryMyBids(EntityPlayerMP player) { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + AuctionDataPacket.sendMyBids(player); + } + + private void handleQueryClaimable(EntityPlayerMP player) { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + AuctionDataPacket.sendClaimable(player); + } + /** * Send a response message to the player */ @@ -436,6 +557,10 @@ public class AuctionActionPacket extends AbstractPacket { Buyout, CancelListing, ClaimItem, - ClaimProceeds + ClaimProceeds, + QueryListings, + QueryMyListings, + QueryMyBids, + QueryClaimable } } diff --git a/src/main/java/noppes/npcs/client/NoppesUtil.java b/src/main/java/noppes/npcs/client/NoppesUtil.java index 620142564..64b26fa5d 100644 --- a/src/main/java/noppes/npcs/client/NoppesUtil.java +++ b/src/main/java/noppes/npcs/client/NoppesUtil.java @@ -182,6 +182,67 @@ public class NoppesUtil { data = new HashMap(); } + // ==================== Auction Data ==================== + + // Static storage for auction data that the GUI can access + private static NBTTagCompound auctionListingData; + private static int auctionDataType; + private static int auctionTotalResults; + private static int auctionCurrentPage; + private static int auctionTotalPages; + private static long auctionPlayerBalance; + + public static void setAuctionData(int dataType, NBTTagCompound compound, + int totalResults, int currentPage, int totalPages, + long playerBalance) { + auctionDataType = dataType; + auctionListingData = compound; + auctionTotalResults = totalResults; + auctionCurrentPage = currentPage; + auctionTotalPages = totalPages; + auctionPlayerBalance = playerBalance; + + // Notify the current GUI if it's an auction GUI + GuiScreen gui = Minecraft.getMinecraft().currentScreen; + if (gui instanceof IAuctionData) { + ((IAuctionData) gui).onAuctionDataReceived(dataType, compound, + totalResults, currentPage, totalPages, playerBalance); + } + } + + public static NBTTagCompound getAuctionListingData() { + return auctionListingData; + } + + public static int getAuctionDataType() { + return auctionDataType; + } + + public static int getAuctionTotalResults() { + return auctionTotalResults; + } + + public static int getAuctionCurrentPage() { + return auctionCurrentPage; + } + + public static int getAuctionTotalPages() { + return auctionTotalPages; + } + + public static long getAuctionPlayerBalance() { + return auctionPlayerBalance; + } + + /** + * Interface for GUIs that receive auction data + */ + public interface IAuctionData { + void onAuctionDataReceived(int dataType, NBTTagCompound data, + int totalResults, int currentPage, int totalPages, + long playerBalance); + } + public static void guiQuestCompletion(EntityPlayer player, NBTTagCompound read) { Quest quest = new Quest(); quest.readNBT(read); diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java new file mode 100644 index 000000000..a94d1f1bf --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java @@ -0,0 +1,68 @@ +package noppes.npcs.constants; + +/** + * Sorting options for auction listings. + */ +public enum EnumAuctionSort { + /** Default - newest listings first */ + NEWEST_FIRST("Newest First"), + + /** Oldest listings first */ + OLDEST_FIRST("Oldest First"), + + /** Highest current price first */ + PRICE_HIGH_TO_LOW("Most Expensive"), + + /** Lowest current price first */ + PRICE_LOW_TO_HIGH("Least Expensive"), + + /** Buyout price high to low (listings without buyout at end) */ + BUYOUT_HIGH_TO_LOW("Buyout: High to Low"), + + /** Buyout price low to high (listings without buyout at end) */ + BUYOUT_LOW_TO_HIGH("Buyout: Low to High"), + + /** Ending soonest first */ + ENDING_SOON("Ending Soon"), + + /** Most time remaining first */ + MOST_TIME_LEFT("Most Time Left"), + + /** Alphabetical by item name A-Z */ + NAME_A_TO_Z("Name: A to Z"), + + /** Alphabetical by item name Z-A */ + NAME_Z_TO_A("Name: Z to A"), + + /** Most bids first */ + MOST_BIDS("Most Bids"), + + /** Least bids first */ + LEAST_BIDS("Least Bids"); + + private final String displayName; + + EnumAuctionSort(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Get next sort option (for cycling through in GUI) + */ + public EnumAuctionSort next() { + EnumAuctionSort[] values = values(); + return values[(this.ordinal() + 1) % values.length]; + } + + /** + * Get previous sort option + */ + public EnumAuctionSort previous() { + EnumAuctionSort[] values = values(); + return values[(this.ordinal() - 1 + values.length) % values.length]; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index ec3b6de19..3e0f44a21 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -9,7 +9,9 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -144,6 +146,171 @@ public class AuctionController { .collect(Collectors.toList()); } + // ==================== Filtering & Sorting ==================== + + /** + * Get filtered and sorted auction listings. + * This is the main method for GUI queries. + * @param filter The filter criteria + * @return FilterResult containing matching listings and metadata + */ + public FilterResult getFilteredListings(AuctionFilter filter) { + processExpiredAuctions(); + + // Get all active listings + List results = listings.values().stream() + .filter(AuctionListing::isActive) + .filter(filter::matches) + .collect(Collectors.toList()); + + // Sort the results + sortListings(results, filter.sortOrder); + + // Calculate pagination + int totalResults = results.size(); + int totalPages = (int) Math.ceil((double) totalResults / filter.pageSize); + int startIndex = filter.page * filter.pageSize; + int endIndex = Math.min(startIndex + filter.pageSize, totalResults); + + // Get the page slice + List pageResults; + if (startIndex >= totalResults) { + pageResults = new ArrayList<>(); + } else { + pageResults = results.subList(startIndex, endIndex); + } + + return new FilterResult(pageResults, totalResults, filter.page, totalPages); + } + + /** + * Sort listings according to the specified sort order + */ + public void sortListings(List listings, EnumAuctionSort sortOrder) { + if (listings == null || listings.isEmpty()) { + return; + } + + Comparator comparator; + + switch (sortOrder) { + case NEWEST_FIRST: + comparator = Comparator.comparingLong(l -> -l.listingTime); + break; + + case OLDEST_FIRST: + comparator = Comparator.comparingLong(l -> l.listingTime); + break; + + case PRICE_HIGH_TO_LOW: + comparator = Comparator.comparingLong((AuctionListing l) -> + -(l.currentBid > 0 ? l.currentBid : l.startingPrice)); + break; + + case PRICE_LOW_TO_HIGH: + comparator = Comparator.comparingLong(l -> + l.currentBid > 0 ? l.currentBid : l.startingPrice); + break; + + case BUYOUT_HIGH_TO_LOW: + // Listings with buyout first, sorted high to low + comparator = Comparator + .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) + .thenComparingLong(l -> -l.buyoutPrice); + break; + + case BUYOUT_LOW_TO_HIGH: + // Listings with buyout first, sorted low to high + comparator = Comparator + .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) + .thenComparingLong(l -> l.buyoutPrice); + break; + + case ENDING_SOON: + comparator = Comparator.comparingLong(l -> l.expirationTime); + break; + + case MOST_TIME_LEFT: + comparator = Comparator.comparingLong(l -> -l.expirationTime); + break; + + case NAME_A_TO_Z: + comparator = Comparator.comparing(l -> + l.item.getDisplayName().toLowerCase()); + break; + + case NAME_Z_TO_A: + comparator = Comparator.comparing((AuctionListing l) -> + l.item.getDisplayName().toLowerCase()).reversed(); + break; + + case MOST_BIDS: + comparator = Comparator.comparingInt(l -> -l.bidCount); + break; + + case LEAST_BIDS: + comparator = Comparator.comparingInt(l -> l.bidCount); + break; + + default: + comparator = Comparator.comparingLong(l -> l.expirationTime); + } + + listings.sort(comparator); + } + + /** + * Quick search with default sorting (ending soon) + */ + public List quickSearch(String searchText, EnumAuctionCategory category, int limit) { + AuctionFilter filter = new AuctionFilter() + .withSearch(searchText) + .withCategory(category) + .withSort(EnumAuctionSort.ENDING_SOON) + .withPage(0, limit); + + return getFilteredListings(filter).listings; + } + + /** + * Get listings sorted by a specific order + */ + public List getListingsSorted(EnumAuctionSort sortOrder) { + List results = new ArrayList<>(getActiveListings()); + sortListings(results, sortOrder); + return results; + } + + /** + * Result container for filtered queries + */ + public static class FilterResult { + public final List listings; + public final int totalResults; + public final int currentPage; + public final int totalPages; + + public FilterResult(List listings, int totalResults, + int currentPage, int totalPages) { + this.listings = listings; + this.totalResults = totalResults; + this.currentPage = currentPage; + this.totalPages = totalPages; + } + + public boolean hasNextPage() { + return currentPage < totalPages - 1; + } + + public boolean hasPreviousPage() { + return currentPage > 0; + } + + public boolean isEmpty() { + return listings == null || listings.isEmpty(); + } + } + /** * Get all listings by a seller (all statuses) */ diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java new file mode 100644 index 000000000..a4a3d1d5a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -0,0 +1,250 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionSort; + +/** + * Filter/search criteria for auction listings. + * Used to query the auction controller with specific criteria. + */ +public class AuctionFilter { + + /** Search text to match against item names */ + public String searchText = ""; + + /** Category to filter by (ALL for no category filter) */ + public EnumAuctionCategory category = EnumAuctionCategory.ALL; + + /** How to sort results */ + public EnumAuctionSort sortOrder = EnumAuctionSort.ENDING_SOON; + + /** Minimum current bid/price filter */ + public long minPrice = 0; + + /** Maximum current bid/price filter (0 = no max) */ + public long maxPrice = 0; + + /** Only show listings with buyout option */ + public boolean buyoutOnly = false; + + /** Only show listings without any bids yet */ + public boolean noBidsOnly = false; + + /** Page number for pagination (0-indexed) */ + public int page = 0; + + /** Items per page */ + public int pageSize = 20; + + /** Filter by specific seller UUID string (empty = all sellers) */ + public String sellerUUID = ""; + + /** Filter by specific seller name (partial match, empty = all) */ + public String sellerName = ""; + + public AuctionFilter() { + } + + /** + * Create a filter with search text + */ + public static AuctionFilter search(String text) { + AuctionFilter filter = new AuctionFilter(); + filter.searchText = text != null ? text : ""; + return filter; + } + + /** + * Create a filter with category + */ + public static AuctionFilter category(EnumAuctionCategory category) { + AuctionFilter filter = new AuctionFilter(); + filter.category = category; + return filter; + } + + /** + * Create a filter for a specific seller + */ + public static AuctionFilter seller(String sellerUUID) { + AuctionFilter filter = new AuctionFilter(); + filter.sellerUUID = sellerUUID != null ? sellerUUID : ""; + return filter; + } + + // Builder-style methods for chaining + + public AuctionFilter withSearch(String text) { + this.searchText = text != null ? text : ""; + return this; + } + + public AuctionFilter withCategory(EnumAuctionCategory category) { + this.category = category; + return this; + } + + public AuctionFilter withSort(EnumAuctionSort sort) { + this.sortOrder = sort; + return this; + } + + public AuctionFilter withPriceRange(long min, long max) { + this.minPrice = min; + this.maxPrice = max; + return this; + } + + public AuctionFilter withBuyoutOnly(boolean buyoutOnly) { + this.buyoutOnly = buyoutOnly; + return this; + } + + public AuctionFilter withNoBidsOnly(boolean noBidsOnly) { + this.noBidsOnly = noBidsOnly; + return this; + } + + public AuctionFilter withPage(int page, int pageSize) { + this.page = Math.max(0, page); + this.pageSize = Math.max(1, Math.min(100, pageSize)); + return this; + } + + public AuctionFilter withSeller(String sellerUUID, String sellerName) { + this.sellerUUID = sellerUUID != null ? sellerUUID : ""; + this.sellerName = sellerName != null ? sellerName : ""; + return this; + } + + /** + * Check if a listing matches this filter's criteria + */ + public boolean matches(AuctionListing listing) { + if (listing == null) { + return false; + } + + // Search text filter (case-insensitive) + if (!searchText.isEmpty()) { + String lowerSearch = searchText.toLowerCase(); + String itemName = listing.item.getDisplayName().toLowerCase(); + if (!itemName.contains(lowerSearch)) { + return false; + } + } + + // Category filter + if (category != EnumAuctionCategory.ALL && listing.category != category) { + return false; + } + + // Price range filter (uses current bid or starting price) + long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; + if (minPrice > 0 && price < minPrice) { + return false; + } + if (maxPrice > 0 && price > maxPrice) { + return false; + } + + // Buyout only filter + if (buyoutOnly && listing.buyoutPrice <= 0) { + return false; + } + + // No bids only filter + if (noBidsOnly && listing.hasBids()) { + return false; + } + + // Seller UUID filter + if (!sellerUUID.isEmpty() && !listing.sellerUUID.toString().equals(sellerUUID)) { + return false; + } + + // Seller name filter (partial match) + if (!sellerName.isEmpty()) { + String lowerName = sellerName.toLowerCase(); + if (!listing.sellerName.toLowerCase().contains(lowerName)) { + return false; + } + } + + return true; + } + + /** + * Reset all filters to defaults + */ + public void reset() { + searchText = ""; + category = EnumAuctionCategory.ALL; + sortOrder = EnumAuctionSort.ENDING_SOON; + minPrice = 0; + maxPrice = 0; + buyoutOnly = false; + noBidsOnly = false; + page = 0; + sellerUUID = ""; + sellerName = ""; + } + + /** + * Write filter to NBT for network transmission + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("SearchText", searchText); + compound.setInteger("Category", category.ordinal()); + compound.setInteger("SortOrder", sortOrder.ordinal()); + compound.setLong("MinPrice", minPrice); + compound.setLong("MaxPrice", maxPrice); + compound.setBoolean("BuyoutOnly", buyoutOnly); + compound.setBoolean("NoBidsOnly", noBidsOnly); + compound.setInteger("Page", page); + compound.setInteger("PageSize", pageSize); + compound.setString("SellerUUID", sellerUUID); + compound.setString("SellerName", sellerName); + return compound; + } + + /** + * Read filter from NBT + */ + public void readFromNBT(NBTTagCompound compound) { + searchText = compound.getString("SearchText"); + + int catOrdinal = compound.getInteger("Category"); + if (catOrdinal >= 0 && catOrdinal < EnumAuctionCategory.values().length) { + category = EnumAuctionCategory.values()[catOrdinal]; + } + + int sortOrdinal = compound.getInteger("SortOrder"); + if (sortOrdinal >= 0 && sortOrdinal < EnumAuctionSort.values().length) { + sortOrder = EnumAuctionSort.values()[sortOrdinal]; + } + + minPrice = compound.getLong("MinPrice"); + maxPrice = compound.getLong("MaxPrice"); + buyoutOnly = compound.getBoolean("BuyoutOnly"); + noBidsOnly = compound.getBoolean("NoBidsOnly"); + page = compound.getInteger("Page"); + pageSize = compound.getInteger("PageSize"); + if (pageSize <= 0) pageSize = 20; + sellerUUID = compound.getString("SellerUUID"); + sellerName = compound.getString("SellerName"); + } + + @Override + public String toString() { + return "AuctionFilter{" + + "search='" + searchText + '\'' + + ", category=" + category + + ", sort=" + sortOrder + + ", price=" + minPrice + "-" + maxPrice + + ", buyoutOnly=" + buyoutOnly + + ", page=" + page + + '}'; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index e17a08112..24c778439 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -55,9 +55,24 @@ public class AuctionListing { public boolean sellerClaimed = false; // Has seller claimed their proceeds? public boolean buyerClaimed = false; // Has buyer claimed their item? + // Bid tracking + public int bidCount = 0; // Number of bids placed + public AuctionListing() { } + /** + * Alias for listingTime (when the auction was created) + */ + public long getCreationTime() { + return listingTime; + } + + // For sorting compatibility + public long creationTime() { + return listingTime; + } + /** * Create a new auction listing */ @@ -174,6 +189,7 @@ public class AuctionListing { highBidderUUID = bidder; highBidderName = bidderName; currentBid = amount; + bidCount++; // Snipe protection - extend if bid placed in final minutes if (ConfigMarket.SnipeProtectionMinutes > 0) { @@ -338,6 +354,7 @@ public class AuctionListing { compound.setInteger("Category", category.ordinal()); compound.setBoolean("SellerClaimed", sellerClaimed); compound.setBoolean("BuyerClaimed", buyerClaimed); + compound.setInteger("BidCount", bidCount); return compound; } @@ -388,5 +405,6 @@ public class AuctionListing { sellerClaimed = compound.getBoolean("SellerClaimed"); buyerClaimed = compound.getBoolean("BuyerClaimed"); + bidCount = compound.getInteger("BidCount"); } } From a1de79053b3bd90e8b2412a493270ff8b944d93c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 10:09:40 +0000 Subject: [PATCH 12/21] Implement Phase 6: GUI components, notifications, and logging system Add auction GUI components: - GuiAuction: Main auction browse interface with search, categories, sorting, pagination, and live countdown timers - SubGuiAuctionBid: Bid placement dialog with quick bid buttons - SubGuiAuctionCreate: Listing creation form with duration options - SubGuiAuctionDetails: Item details view with 2x scaled rendering - ContainerAuction: Container class for auction GUI inventory Add notification system: - AuctionNotificationController: Handles real-time notifications - Supports outbid, won, sold, expired, cancelled events - Queues notifications for offline players - Sends pending notifications on player login Add auction logging: - AuctionLogger: Comprehensive logging to CNPC+ log system - Logs all auction events (create, bid, buyout, expire, claim) - Includes system startup and processing tick logs - Admin action tracking Integrate with existing systems: - Initialize AuctionNotificationController on server start - Hook player login for notification delivery - Add logging calls to all AuctionController methods --- src/main/java/noppes/npcs/CustomNpcs.java | 2 + .../java/noppes/npcs/ServerTickHandler.java | 6 + .../npcs/client/gui/player/GuiAuction.java | 489 ++++++++++++++++++ .../client/gui/player/SubGuiAuctionBid.java | 205 ++++++++ .../gui/player/SubGuiAuctionCreate.java | 171 ++++++ .../gui/player/SubGuiAuctionDetails.java | 193 +++++++ .../npcs/containers/ContainerAuction.java | 193 +++++++ .../npcs/controllers/AuctionController.java | 95 +++- .../npcs/controllers/AuctionLogger.java | 269 ++++++++++ .../AuctionNotificationController.java | 226 ++++++++ 10 files changed, 1845 insertions(+), 4 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuction.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java create mode 100644 src/main/java/noppes/npcs/containers/ContainerAuction.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionLogger.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionNotificationController.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index c29fa4253..79a4a1b40 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -62,6 +62,7 @@ import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.AuctionBlacklist; import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.AuctionNotificationController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -312,6 +313,7 @@ public class CustomNpcs { new CurrencyController(); new AuctionBlacklist(); new AuctionController(); + new AuctionNotificationController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 3648f00d7..204e34bb5 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,6 +10,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; +import noppes.npcs.controllers.AuctionNotificationController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; @@ -65,6 +66,11 @@ public class ServerTickHandler { ProfileController.Instance.login(player); SyncController.beginLogin(player); SyncController.syncEffects(player); + + // Send pending auction notifications + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.onPlayerLogin(player); + } } @SubscribeEvent diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java new file mode 100644 index 000000000..1fe089d40 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -0,0 +1,489 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.CustomNpcResourceListener; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.*; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.List; + +/** + * Main auction house browse GUI. + * Displays listings with search, filter, sort, and pagination. + */ +public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, + ICustomScrollListener, NoppesUtil.IAuctionData { + + private static final ResourceLocation background = new ResourceLocation("customnpcs", "textures/gui/auction.png"); + + // GUI Components + private GuiNpcTextField searchField; + private GuiCustomScroll listingScroll; + + // Current state + private AuctionFilter currentFilter = new AuctionFilter(); + private List listings = new ArrayList<>(); + private int totalResults = 0; + private int currentPage = 0; + private int totalPages = 0; + private long playerBalance = 0; + + // Tab state (0=Browse, 1=My Listings, 2=My Bids, 3=Claims, 4=Create) + private int currentTab = 0; + + // Selected listing for details + private int selectedListingIndex = -1; + + // Countdown update timer + private long lastCountdownUpdate = 0; + + public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { + super(npc, container); + this.title = "Auction House"; + this.xSize = 256; + this.ySize = 222; + this.closeOnEsc = true; + this.drawDefaultBackground = true; + + // Request initial data + AuctionActionPacket.QueryListings(currentFilter); + } + + @Override + public void initGui() { + super.initGui(); + + // Tab buttons at top + addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, "Browse")); + addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, "My Listings")); + addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, "My Bids")); + addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, "Claims")); + addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, "Sell")); + getTopButton(currentTab).setEnabled(false); + + // Search field + searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 120, 14, currentFilter.searchText); + searchField.setMaxStringLength(50); + addTextField(searchField); + + // Search button + addButton(new GuiNpcButton(11, guiLeft + 130, guiTop + 5, 30, 16, "Search")); + + // Category buttons (simplified row) + String[] categories = {"All", "Weapons", "Armor", "Tools", "Potions", "Blocks", "Other"}; + for (int i = 0; i < categories.length; i++) { + GuiNpcButton btn = new GuiNpcButton(20 + i, guiLeft + 8 + i * 35, guiTop + 24, 33, 14, categories[i]); + btn.setEnabled(currentFilter.category.ordinal() != getCategoryOrdinal(i)); + addButton(btn); + } + + // Sort dropdown button + addButton(new GuiNpcButton(30, guiLeft + 170, guiTop + 5, 78, 16, currentFilter.sortOrder.getDisplayName())); + + // Listing scroll area + listingScroll = new GuiCustomScroll(this, 0); + listingScroll.guiLeft = guiLeft + 8; + listingScroll.guiTop = guiTop + 42; + listingScroll.setSize(160, 130); + addScroll(listingScroll); + updateListingDisplay(); + + // Pagination buttons + addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, "< Prev")); + addLabel(new GuiNpcLabel(41, getPageLabel(), guiLeft + 90, guiTop + 180, CustomNpcResourceListener.DefaultTextColor)); + addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, "Next >")); + + // Action buttons (right side) + addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, "View Details")); + addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, "Place Bid")); + addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, "Buyout")); + addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, "Refresh")); + + // Balance display + addLabel(new GuiNpcLabel(60, "Balance: " + formatCurrency(playerBalance), + guiLeft + 175, guiTop + 155, 0x00AA00)); + + updateButtonStates(); + } + + private int getCategoryOrdinal(int buttonIndex) { + switch (buttonIndex) { + case 0: return EnumAuctionCategory.ALL.ordinal(); + case 1: return EnumAuctionCategory.WEAPONS.ordinal(); + case 2: return EnumAuctionCategory.ARMOR.ordinal(); + case 3: return EnumAuctionCategory.TOOLS.ordinal(); + case 4: return EnumAuctionCategory.POTIONS.ordinal(); + case 5: return EnumAuctionCategory.BLOCKS.ordinal(); + default: return EnumAuctionCategory.MISC.ordinal(); + } + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update countdowns every second + long now = System.currentTimeMillis(); + if (now - lastCountdownUpdate >= 1000) { + lastCountdownUpdate = now; + updateListingDisplay(); + } + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + int id = button.id; + + // Tab buttons + if (id >= 0 && id <= 4) { + switchTab(id); + } + // Search button + else if (id == 11) { + performSearch(); + } + // Category buttons + else if (id >= 20 && id <= 26) { + setCategory(getCategoryOrdinal(id - 20)); + } + // Sort button + else if (id == 30) { + cycleSortOrder(); + } + // Pagination + else if (id == 40) { + previousPage(); + } else if (id == 42) { + nextPage(); + } + // Actions + else if (id == 50) { + viewDetails(); + } else if (id == 51) { + placeBid(); + } else if (id == 52) { + buyout(); + } else if (id == 53) { + refresh(); + } + } + + private void switchTab(int tab) { + currentTab = tab; + currentPage = 0; + + switch (tab) { + case 0: // Browse + AuctionActionPacket.QueryListings(currentFilter); + break; + case 1: // My Listings + AuctionActionPacket.QueryMyListings(); + break; + case 2: // My Bids + AuctionActionPacket.QueryMyBids(); + break; + case 3: // Claims + AuctionActionPacket.QueryClaimable(); + break; + case 4: // Create - open sub GUI + setSubGui(new SubGuiAuctionCreate(this, (ContainerAuction) inventorySlots)); + break; + } + + initGui(); + } + + private void performSearch() { + currentFilter.searchText = searchField.getText(); + currentFilter.page = 0; + currentPage = 0; + AuctionActionPacket.QueryListings(currentFilter); + } + + private void setCategory(int categoryOrdinal) { + currentFilter.category = EnumAuctionCategory.values()[categoryOrdinal]; + currentFilter.page = 0; + currentPage = 0; + AuctionActionPacket.QueryListings(currentFilter); + initGui(); + } + + private void cycleSortOrder() { + currentFilter.sortOrder = currentFilter.sortOrder.next(); + currentFilter.page = 0; + currentPage = 0; + AuctionActionPacket.QueryListings(currentFilter); + initGui(); + } + + private void previousPage() { + if (currentPage > 0) { + currentPage--; + currentFilter.page = currentPage; + AuctionActionPacket.QueryListings(currentFilter); + } + } + + private void nextPage() { + if (currentPage < totalPages - 1) { + currentPage++; + currentFilter.page = currentPage; + AuctionActionPacket.QueryListings(currentFilter); + } + } + + private void viewDetails() { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + setSubGui(new SubGuiAuctionDetails(this, listing)); + } + } + + private void placeBid() { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + setSubGui(new SubGuiAuctionBid(this, listing, playerBalance)); + } + } + + private void buyout() { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + if (listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice) { + AuctionActionPacket.Buyout(listing.id); + refresh(); + } + } + } + + private void refresh() { + switch (currentTab) { + case 0: + AuctionActionPacket.QueryListings(currentFilter); + break; + case 1: + AuctionActionPacket.QueryMyListings(); + break; + case 2: + AuctionActionPacket.QueryMyBids(); + break; + case 3: + AuctionActionPacket.QueryClaimable(); + break; + } + } + + private void updateListingDisplay() { + if (listingScroll == null) return; + + List displayList = new ArrayList<>(); + for (AuctionListing listing : listings) { + String line = formatListingLine(listing); + displayList.add(line); + } + listingScroll.setList(displayList); + + if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { + listingScroll.selected = selectedListingIndex; + } + } + + private String formatListingLine(AuctionListing listing) { + String itemName = listing.item.getDisplayName(); + if (itemName.length() > 15) { + itemName = itemName.substring(0, 12) + "..."; + } + + long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; + String priceStr = formatCurrency(price); + + String timeStr = formatTimeRemaining(listing.getTimeRemaining()); + + return String.format("%s - %s (%s)", itemName, priceStr, timeStr); + } + + private String formatTimeRemaining(long millis) { + if (millis <= 0) return "Ended"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + if (days > 0) { + return days + "d " + (hours % 24) + "h"; + } else if (hours > 0) { + return hours + "h " + (minutes % 60) + "m"; + } else if (minutes > 0) { + return minutes + "m " + (seconds % 60) + "s"; + } else { + return seconds + "s"; + } + } + + private String formatCurrency(long amount) { + if (amount >= 1000000) { + return String.format("%.1fM", amount / 1000000.0); + } else if (amount >= 1000) { + return String.format("%.1fK", amount / 1000.0); + } + return String.valueOf(amount); + } + + private String getPageLabel() { + if (totalPages == 0) { + return "No results"; + } + return "Page " + (currentPage + 1) + " of " + totalPages; + } + + private void updateButtonStates() { + // Pagination buttons + if (getButton(40) != null) getButton(40).setEnabled(currentPage > 0); + if (getButton(42) != null) getButton(42).setEnabled(currentPage < totalPages - 1); + + // Action buttons based on selection + boolean hasSelection = selectedListingIndex >= 0 && selectedListingIndex < listings.size(); + if (getButton(50) != null) getButton(50).setEnabled(hasSelection); + if (getButton(51) != null) getButton(51).setEnabled(hasSelection); + + if (hasSelection && getButton(52) != null) { + AuctionListing listing = listings.get(selectedListingIndex); + getButton(52).setEnabled(listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice); + } else if (getButton(52) != null) { + getButton(52).setEnabled(false); + } + } + + @Override + protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { + drawDefaultBackground(); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + + // Draw background (using a standard size, can be customized) + mc.renderEngine.bindTexture(new ResourceLocation("customnpcs", "textures/gui/bgfilled.png")); + drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + + // Draw selected item preview + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + drawItemPreview(listing, guiLeft + 180, guiTop + 160); + } + + super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); + } + + private void drawItemPreview(AuctionListing listing, int x, int y) { + if (listing == null || listing.item == null) return; + + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glEnable(GL11.GL_LIGHTING); + + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); + + RenderHelper.disableStandardItemLighting(); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + // Title + String titleText = getTabTitle(); + fontRendererObj.drawString(titleText, (xSize - fontRendererObj.getStringWidth(titleText)) / 2, -8, + CustomNpcResourceListener.DefaultTextColor); + + // Results count + fontRendererObj.drawString(totalResults + " listings", 175, 42, 0x666666); + } + + private String getTabTitle() { + switch (currentTab) { + case 0: return "Browse Auctions"; + case 1: return "My Listings"; + case 2: return "My Active Bids"; + case 3: return "Claim Items"; + case 4: return "Create Listing"; + default: return "Auction House"; + } + } + + // ICustomScrollListener + @Override + public void scrollClicked(int scrollId, int index, boolean doubleClick, String selection) { + selectedListingIndex = index; + updateButtonStates(); + + if (doubleClick && selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + viewDetails(); + } + } + + @Override + public void scrollDoubleClicked(String selection, GuiCustomScroll scroll) { + // Handled above + } + + // IGuiData - Receive NBT data from server + @Override + public void setGuiData(NBTTagCompound compound) { + // This can be used for initial setup data + } + + // IAuctionData - Receive auction listing data + @Override + public void onAuctionDataReceived(int dataType, NBTTagCompound data, + int totalResults, int currentPage, int totalPages, + long playerBalance) { + this.totalResults = totalResults; + this.currentPage = currentPage; + this.totalPages = totalPages; + this.playerBalance = playerBalance; + + // Parse listings from NBT + listings.clear(); + NBTTagList listingList = data.getTagList("Listings", 10); + for (int i = 0; i < listingList.tagCount(); i++) { + NBTTagCompound listingNBT = listingList.getCompoundTagAt(i); + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(listingNBT); + listings.add(listing); + } + + selectedListingIndex = -1; + updateListingDisplay(); + updateButtonStates(); + + // Update labels + if (getLabel(41) != null) { + getLabel(41).label = getPageLabel(); + } + if (getLabel(60) != null) { + getLabel(60).label = "Balance: " + formatCurrency(playerBalance); + } + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java new file mode 100644 index 000000000..36572b14d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -0,0 +1,205 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.AuctionListing; + +/** + * Sub GUI for placing a bid on an auction. + */ +public class SubGuiAuctionBid extends SubGuiInterface { + private AuctionListing listing; + private long playerBalance; + private GuiNpcTextField bidField; + private long lastUpdate = 0; + + public SubGuiAuctionBid(GuiAuction parent, AuctionListing listing, long playerBalance) { + this.listing = listing; + this.playerBalance = playerBalance; + setBackground("menubg.png"); + xSize = 200; + ySize = 150; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + int labelColor = 0x404040; + + // Title + addLabel(new GuiNpcLabel(0, "Place Bid", guiLeft + 70, y, labelColor)); + y += 18; + + // Item name + addLabel(new GuiNpcLabel(1, "Item: " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); + y += 14; + + // Current price + long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; + addLabel(new GuiNpcLabel(2, "Current Price:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 100, y, 0x008800)); + y += 14; + + // Minimum bid + long minBid = listing.getMinimumBid(); + addLabel(new GuiNpcLabel(4, "Minimum Bid:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 100, y, 0x0066AA)); + y += 14; + + // Your balance + addLabel(new GuiNpcLabel(6, "Your Balance:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 100, y, + playerBalance >= minBid ? 0x008800 : 0xAA0000)); + y += 14; + + // Time remaining + addLabel(new GuiNpcLabel(8, "Time Left:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + getTimeColor(listing.getTimeRemaining()))); + y += 18; + + // Bid input + addLabel(new GuiNpcLabel(10, "Your Bid:", guiLeft + 15, y + 3, labelColor)); + bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); + bidField.setNumbersOnly(); + bidField.setMinMaxDefault(minBid, 999999999, minBid); + addTextField(bidField); + y += 24; + + // Quick bid buttons + addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, "Min Bid")); + addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, "+10%")); + addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, "+25%")); + y += 22; + + // Place bid button + GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, "Place Bid"); + bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); + addButton(bidButton); + + // Cancel button + addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, "Cancel")); + + // Error message area + if (playerBalance < minBid) { + addLabel(new GuiNpcLabel(40, "Insufficient funds!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + } else if (!listing.isActive()) { + addLabel(new GuiNpcLabel(40, "Auction has ended!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + } + } + + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + private String formatTimeRemaining(long millis) { + if (millis <= 0) return "Ended"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + + if (hours > 0) { + return String.format("%dh %dm", hours, minutes % 60); + } else if (minutes > 0) { + return String.format("%dm %ds", minutes, seconds % 60); + } else { + return String.format("%ds", seconds); + } + } + + private int getTimeColor(long millis) { + if (millis <= 0) return 0xAA0000; + if (millis < 5 * 60 * 1000) return 0xFF0000; + if (millis < 30 * 60 * 1000) return 0xFF6600; + return 0x008800; + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update time remaining every second + long now = System.currentTimeMillis(); + if (now - lastUpdate >= 1000) { + lastUpdate = now; + + GuiNpcLabel timeLabel = getLabel(9); + if (timeLabel != null) { + long remaining = listing.getTimeRemaining(); + timeLabel.label = formatTimeRemaining(remaining); + timeLabel.color = getTimeColor(remaining); + } + + // Disable bid button if auction ended + GuiNpcButton bidButton = getButton(30); + if (bidButton != null && !listing.isActive()) { + bidButton.setEnabled(false); + } + } + } + + @Override + public void actionPerformed(GuiButton button) { + int id = button.id; + + long minBid = listing.getMinimumBid(); + + if (id == 20) { + // Min bid + bidField.setText(String.valueOf(minBid)); + } else if (id == 21) { + // +10% + long newBid = (long) (minBid * 1.10); + if (newBid <= playerBalance) { + bidField.setText(String.valueOf(newBid)); + } + } else if (id == 22) { + // +25% + long newBid = (long) (minBid * 1.25); + if (newBid <= playerBalance) { + bidField.setText(String.valueOf(newBid)); + } + } else if (id == 30) { + // Place bid + placeBid(); + } else if (id == 31) { + // Cancel + close(); + } + } + + private void placeBid() { + long bidAmount = bidField.getInteger(); + long minBid = listing.getMinimumBid(); + + if (bidAmount < minBid) { + return; + } + + if (bidAmount > playerBalance) { + return; + } + + if (!listing.isActive()) { + return; + } + + // Send bid to server + AuctionActionPacket.PlaceBid(listing.id, bidAmount); + close(); + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java new file mode 100644 index 000000000..4fca552e0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -0,0 +1,171 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.item.ItemStack; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.containers.ContainerAuction; + +/** + * Sub GUI for creating a new auction listing. + */ +public class SubGuiAuctionCreate extends SubGuiInterface { + private ContainerAuction container; + + private GuiNpcTextField startingPriceField; + private GuiNpcTextField buyoutPriceField; + private int selectedDuration = 1; // Default to MEDIUM + + private static final String[] DURATION_NAMES = {"Short", "Medium", "Long", "Very Long"}; + + public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { + this.container = container; + setBackground("menubg.png"); + xSize = 220; + ySize = 180; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + // Title + addLabel(new GuiNpcLabel(0, "Create Auction Listing", guiLeft + 50, guiTop + 8, 0x404040)); + + // Item slot instructions + addLabel(new GuiNpcLabel(1, "Place item in slot below:", guiLeft + 20, guiTop + 28, 0x404040)); + + // Item info (shows when item is in slot) + ItemStack item = container.getListingItem(); + if (item != null) { + addLabel(new GuiNpcLabel(2, "Item: " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); + } else { + addLabel(new GuiNpcLabel(2, "(No item selected)", guiLeft + 20, guiTop + 50, 0x888888)); + } + + // Starting price + addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, guiTop + 70, 0x404040)); + startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); + startingPriceField.setNumbersOnly(); + startingPriceField.setMinMaxDefault(1, 999999999, 100); + addTextField(startingPriceField); + + // Buyout price (optional) + addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, guiTop + 90, 0x404040)); + buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); + buyoutPriceField.setNumbersOnly(); + buyoutPriceField.setMinMaxDefault(0, 999999999, 0); + addTextField(buyoutPriceField); + addLabel(new GuiNpcLabel(22, "(0 = no buyout)", guiLeft + 185, guiTop + 90, 0x888888)); + + // Duration selection + addLabel(new GuiNpcLabel(30, "Duration:", guiLeft + 20, guiTop + 112, 0x404040)); + addButton(new GuiNpcButton(31, guiLeft + 100, guiTop + 108, 100, 20, getDurationDisplay())); + + // Fee display + long fee = getListingFee(); + addLabel(new GuiNpcLabel(40, "Listing Fee: " + fee + " " + ConfigMarket.CurrencyName, + guiLeft + 20, guiTop + 135, fee > 0 ? 0xAA0000 : 0x008800)); + + // Create button + addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, "Create Listing")); + getButton(50).setEnabled(item != null); + + // Cancel button + addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, "Cancel")); + } + + private String getDurationDisplay() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return DURATION_NAMES[selectedDuration] + " (" + duration.getHours() + "h)"; + } + + private long getListingFee() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return duration.getFee(); + } + + @Override + public void actionPerformed(GuiButton button) { + int id = button.id; + + if (id == 31) { + // Cycle duration + selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; + initGui(); + } else if (id == 50) { + // Create listing + createListing(); + } else if (id == 51) { + // Cancel + close(); + } + } + + private void createListing() { + ItemStack item = container.getListingItem(); + if (item == null) { + return; + } + + long startingPrice = startingPriceField.getInteger(); + long buyoutPrice = buyoutPriceField.getInteger(); + + if (startingPrice < 1) { + startingPrice = 1; + } + + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + // Show error - buyout must be higher than starting + return; + } + + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + + // Send to server + AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); + + // Clear slot and close + container.clearListingSlot(); + close(); + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update item display if it changed + ItemStack item = container.getListingItem(); + GuiNpcLabel itemLabel = getLabel(2); + GuiNpcButton createButton = getButton(50); + + if (item != null) { + if (itemLabel != null) { + itemLabel.label = "Item: " + item.getDisplayName(); + itemLabel.color = 0x008800; + } + if (createButton != null) { + createButton.setEnabled(true); + } + } else { + if (itemLabel != null) { + itemLabel.label = "(No item selected)"; + itemLabel.color = 0x888888; + } + if (createButton != null) { + createButton.setEnabled(false); + } + } + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java new file mode 100644 index 000000000..7f226799a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -0,0 +1,193 @@ +package noppes.npcs.client.gui.player; + +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.RenderHelper; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.AuctionListing; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +/** + * Sub GUI for viewing auction listing details. + */ +public class SubGuiAuctionDetails extends SubGuiInterface { + private AuctionListing listing; + private long lastUpdate = 0; + + public SubGuiAuctionDetails(GuiAuction parent, AuctionListing listing) { + this.listing = listing; + setBackground("menubg.png"); + xSize = 240; + ySize = 200; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 8; + int labelColor = 0x404040; + int valueColor = 0x000000; + + // Title + addLabel(new GuiNpcLabel(0, "Auction Details", guiLeft + 80, y, labelColor)); + y += 16; + + // Item name + addLabel(new GuiNpcLabel(1, "Item:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 80, y, valueColor)); + y += 14; + + // Quantity + addLabel(new GuiNpcLabel(3, "Quantity:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 80, y, valueColor)); + y += 14; + + // Seller + addLabel(new GuiNpcLabel(5, "Seller:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 80, y, valueColor)); + y += 14; + + // Starting price + addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 100, y, valueColor)); + y += 14; + + // Current bid + if (listing.currentBid > 0) { + addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 100, y, 0x008800)); + y += 14; + + // High bidder + addLabel(new GuiNpcLabel(14, "High Bidder:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 100, y, valueColor)); + y += 14; + + // Bid count + addLabel(new GuiNpcLabel(16, "Total Bids:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 100, y, valueColor)); + y += 14; + } else { + addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(13, "No bids yet", guiLeft + 100, y, 0x888888)); + y += 14; + } + + // Buyout price + if (listing.buyoutPrice > 0) { + addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 100, y, 0xAA6600)); + y += 14; + } + + // Minimum bid + addLabel(new GuiNpcLabel(22, "Minimum Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 100, y, 0x0066AA)); + y += 14; + + // Time remaining (will be updated) + addLabel(new GuiNpcLabel(30, "Time Left:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + getTimeColor(listing.getTimeRemaining()))); + y += 20; + + // Close button + addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, "Close")); + } + + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + private String formatTimeRemaining(long millis) { + if (millis <= 0) return "Auction Ended"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + if (days > 0) { + return String.format("%dd %dh %dm", days, hours % 24, minutes % 60); + } else if (hours > 0) { + return String.format("%dh %dm %ds", hours, minutes % 60, seconds % 60); + } else if (minutes > 0) { + return String.format("%dm %ds", minutes, seconds % 60); + } else { + return String.format("%ds", seconds); + } + } + + private int getTimeColor(long millis) { + if (millis <= 0) return 0xAA0000; + if (millis < 5 * 60 * 1000) return 0xFF0000; // < 5 minutes - red + if (millis < 30 * 60 * 1000) return 0xFF6600; // < 30 minutes - orange + if (millis < 60 * 60 * 1000) return 0xAAAA00; // < 1 hour - yellow + return 0x008800; // green + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update time remaining every second + long now = System.currentTimeMillis(); + if (now - lastUpdate >= 1000) { + lastUpdate = now; + + GuiNpcLabel timeLabel = getLabel(31); + if (timeLabel != null) { + long remaining = listing.getTimeRemaining(); + timeLabel.label = formatTimeRemaining(remaining); + timeLabel.color = getTimeColor(remaining); + } + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw item icon + if (listing.item != null) { + int itemX = guiLeft + 200; + int itemY = guiTop + 30; + + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glScalef(2.0f, 2.0f, 2.0f); + + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, + listing.item, itemX / 2, itemY / 2); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, + listing.item, itemX / 2, itemY / 2); + + GL11.glScalef(0.5f, 0.5f, 0.5f); + RenderHelper.disableStandardItemLighting(); + + // Tooltip on hover + if (mouseX >= itemX && mouseX < itemX + 32 && mouseY >= itemY && mouseY < itemY + 32) { + renderToolTip(listing.item, mouseX, mouseY); + } + } + } + + @Override + public void actionPerformed(GuiButton button) { + if (button.id == 50) { + close(); + } + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java new file mode 100644 index 000000000..61e783d91 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuction.java @@ -0,0 +1,193 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.inventory.Container; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Container for auction house GUI. + * Handles player inventory and item slot for creating listings. + */ +public class ContainerAuction extends Container { + public EntityNPCInterface npc; + public EntityPlayer player; + + // Slot for item being listed + public SlotAuctionItem listingSlot; + private IInventory listingInventory; + + public ContainerAuction(EntityNPCInterface npc, EntityPlayer player) { + this.npc = npc; + this.player = player; + + // Create a single-slot inventory for the item being listed + listingInventory = new InventoryAuctionListing(); + listingSlot = new SlotAuctionItem(listingInventory, 0, 80, 35); + addSlotToContainer(listingSlot); + + // Player inventory slots (standard positioning) + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + addSlotToContainer(new Slot(player.inventory, col + row * 9 + 9, + 8 + col * 18, 140 + row * 18)); + } + } + + // Player hotbar + for (int col = 0; col < 9; col++) { + addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 198)); + } + } + + @Override + public boolean canInteractWith(EntityPlayer player) { + return npc != null && npc.isEntityAlive() && + player.getDistanceSqToEntity(npc) < 64.0D; + } + + @Override + public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { + ItemStack itemstack = null; + Slot slot = (Slot) inventorySlots.get(slotIndex); + + if (slot != null && slot.getHasStack()) { + ItemStack stackInSlot = slot.getStack(); + itemstack = stackInSlot.copy(); + + // If from listing slot, move to player inventory + if (slotIndex == 0) { + if (!mergeItemStack(stackInSlot, 1, 37, true)) { + return null; + } + } + // If from player inventory, move to listing slot + else { + if (!mergeItemStack(stackInSlot, 0, 1, false)) { + return null; + } + } + + if (stackInSlot.stackSize == 0) { + slot.putStack(null); + } else { + slot.onSlotChanged(); + } + } + + return itemstack; + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + + // Return any item in the listing slot to player + ItemStack stack = listingInventory.getStackInSlot(0); + if (stack != null) { + if (!player.inventory.addItemStackToInventory(stack)) { + player.entityDropItem(stack, 0.5f); + } + } + } + + /** + * Get the item currently in the listing slot + */ + public ItemStack getListingItem() { + return listingInventory.getStackInSlot(0); + } + + /** + * Clear the listing slot (after successful listing creation) + */ + public void clearListingSlot() { + listingInventory.setInventorySlotContents(0, null); + } + + /** + * Simple inventory for the listing slot + */ + private class InventoryAuctionListing implements IInventory { + private ItemStack item; + + @Override + public int getSizeInventory() { return 1; } + + @Override + public ItemStack getStackInSlot(int slot) { return item; } + + @Override + public ItemStack decrStackSize(int slot, int amount) { + if (item != null) { + ItemStack result; + if (item.stackSize <= amount) { + result = item; + item = null; + return result; + } else { + result = item.splitStack(amount); + if (item.stackSize == 0) { + item = null; + } + return result; + } + } + return null; + } + + @Override + public ItemStack getStackInSlotOnClosing(int slot) { + ItemStack result = item; + item = null; + return result; + } + + @Override + public void setInventorySlotContents(int slot, ItemStack stack) { + item = stack; + } + + @Override + public String getInventoryName() { return "AuctionListing"; } + + @Override + public boolean hasCustomInventoryName() { return false; } + + @Override + public int getInventoryStackLimit() { return 64; } + + @Override + public void markDirty() {} + + @Override + public boolean isUseableByPlayer(EntityPlayer player) { return true; } + + @Override + public void openInventory() {} + + @Override + public void closeInventory() {} + + @Override + public boolean isItemValidForSlot(int slot, ItemStack stack) { return true; } + } + + /** + * Custom slot for auction listings + */ + public class SlotAuctionItem extends Slot { + public SlotAuctionItem(IInventory inv, int index, int x, int y) { + super(inv, index, x, y); + } + + @Override + public boolean isItemValid(ItemStack stack) { + // Check if item can be traded (not untradeable, soulbound, etc.) + return kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute.canTrade(stack); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 3e0f44a21..76e5f0891 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -104,6 +104,9 @@ public class AuctionController { listing.id, seller.getCommandSenderName(), item.getDisplayName()); + // Log to CNPC+ auction history + AuctionLogger.logListingCreated(listing); + return listing; } @@ -401,8 +404,9 @@ public class AuctionController { return false; } - // Store previous bidder for refund + // Store previous bidder for refund and notification UUID previousBidder = listing.highBidderUUID; + String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; // Try to place bid @@ -413,7 +417,7 @@ public class AuctionController { // Withdraw bid amount from new bidder CurrencyController.Instance.withdraw(bidder, amount); - // Refund previous bidder via claims system + // Refund previous bidder via claims system and notify if (previousBidder != null) { CurrencyController.Instance.addClaim( previousBidder, @@ -421,6 +425,20 @@ public class AuctionController { "Auction Outbid", String.valueOf(listingId) ); + + // Notify previous bidder they were outbid + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyOutbid( + listing, previousBidder, previousBidderName, previousBidAmount, amount); + } + + // Log refund + AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); + } + + // Notify seller of new bid + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyNewBid(listing, bidder.getCommandSenderName(), amount); } // Update bidder index @@ -448,8 +466,9 @@ public class AuctionController { return false; } - // Store previous bidder for refund + // Store previous bidder for refund and notification UUID previousBidder = listing.highBidderUUID; + String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; // Execute buyout @@ -460,7 +479,7 @@ public class AuctionController { // Withdraw buyout amount from buyer CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); - // Refund previous bidder via claims system + // Refund previous bidder via claims system and notify if (previousBidder != null) { CurrencyController.Instance.addClaim( previousBidder, @@ -468,6 +487,15 @@ public class AuctionController { "Auction Outbid (Buyout)", String.valueOf(listingId) ); + + // Notify previous bidder they were outbid + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyOutbid( + listing, previousBidder, previousBidderName, previousBidAmount, listing.buyoutPrice); + } + + // Log refund + AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); } // Update bidder index @@ -477,6 +505,16 @@ public class AuctionController { logger.info("Buyout executed on listing {} by {} for {}", listingId, buyer.getCommandSenderName(), listing.buyoutPrice); + // Log buyout and send notifications + AuctionLogger.logBuyout(listing, buyer.getCommandSenderName()); + + if (AuctionNotificationController.Instance != null) { + // Notify buyer they won + AuctionNotificationController.Instance.notifyAuctionWon(listing); + // Notify seller their item sold + AuctionNotificationController.Instance.notifyAuctionSold(listing); + } + return true; } @@ -490,6 +528,8 @@ public class AuctionController { return false; } + boolean byAdmin = !listing.sellerUUID.equals(requester.getUniqueID()); + if (!listing.cancel(requester.getUniqueID())) { return false; } @@ -498,6 +538,12 @@ public class AuctionController { logger.info("Listing {} cancelled by {}", listingId, requester.getCommandSenderName()); + // Log and notify + AuctionLogger.logAuctionCancelled(listing, byAdmin); + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyAuctionCancelled(listing, byAdmin); + } + return true; } @@ -536,6 +582,9 @@ public class AuctionController { logger.info("Seller {} claimed {} from listing {}", seller.getCommandSenderName(), proceeds, listingId); + // Log the currency claim + AuctionLogger.logCurrencyClaimed(listing, seller.getCommandSenderName(), proceeds); + return true; } @@ -569,6 +618,9 @@ public class AuctionController { logger.info("Buyer {} claimed item from listing {}", buyer.getCommandSenderName(), listingId); + // Log the item claim + AuctionLogger.logItemClaimed(listing, buyer.getCommandSenderName(), false); + return listing.item.copy(); } @@ -602,6 +654,9 @@ public class AuctionController { logger.info("Seller {} reclaimed item from listing {}", seller.getCommandSenderName(), listingId); + // Log the item reclaim + AuctionLogger.logItemClaimed(listing, seller.getCommandSenderName(), true); + return listing.item.copy(); } @@ -624,6 +679,8 @@ public class AuctionController { */ public void processExpiredAuctions() { boolean changed = false; + int expiredCount = 0; + int soldCount = 0; for (AuctionListing listing : listings.values()) { if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { @@ -632,17 +689,32 @@ public class AuctionController { // If sold, add proceeds to seller's claims if (listing.status == EnumAuctionStatus.SOLD) { + soldCount++; // Proceeds will be claimed manually, no automatic deposit logger.info("Auction {} ended with sale to {} for {}", listing.id, listing.highBidderName, listing.currentBid); + + // Send notifications + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyAuctionWon(listing); + AuctionNotificationController.Instance.notifyAuctionSold(listing); + } } else { + expiredCount++; logger.info("Auction {} expired without bids", listing.id); + + // Notify seller about expiration + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyAuctionExpired(listing); + } } } } if (changed) { save(); + // Log processing tick if anything happened + AuctionLogger.logProcessingTick(expiredCount, soldCount); } } @@ -804,8 +876,23 @@ public class AuctionController { logger.info("Loaded {} auction listings", listings.size()); + // Log system startup + int activeCount = getActiveListingCount(); + int pendingClaims = 0; + for (AuctionListing listing : listings.values()) { + if (!listing.sellerClaimed || !listing.buyerClaimed) { + if (listing.status == EnumAuctionStatus.SOLD || + listing.status == EnumAuctionStatus.EXPIRED || + listing.status == EnumAuctionStatus.CANCELLED) { + pendingClaims++; + } + } + } + AuctionLogger.logSystemStartup(activeCount, pendingClaims); + } catch (Exception e) { logger.error("Error loading auctions", e); + AuctionLogger.logError("Loading auctions", e); } } diff --git a/src/main/java/noppes/npcs/controllers/AuctionLogger.java b/src/main/java/noppes/npcs/controllers/AuctionLogger.java new file mode 100644 index 000000000..4c5264319 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionLogger.java @@ -0,0 +1,269 @@ +package noppes.npcs.controllers; + +import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.data.AuctionListing; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Handles auction history logging to the CNPC+ logging system. + * All auction events are recorded for administrative review. + */ +public class AuctionLogger { + private static final String PREFIX = "[Auction] "; + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + /** + * Log when a new auction listing is created + */ + public static void logListingCreated(AuctionListing listing) { + String msg = String.format("%sLISTING_CREATED: ID=%d, Seller=%s (%s), Item=%s x%d, StartPrice=%d, Buyout=%s, Duration=%s", + PREFIX, + listing.id, + listing.sellerName, + listing.sellerUUID.toString(), + listing.item.getDisplayName(), + listing.item.stackSize, + listing.startingPrice, + listing.buyoutPrice > 0 ? String.valueOf(listing.buyoutPrice) : "None", + formatDuration(listing.expirationTime - listing.listingTime) + ); + LogWriter.info(msg); + } + + /** + * Log when a bid is placed + */ + public static void logBidPlaced(AuctionListing listing, String bidderName, long bidAmount) { + String msg = String.format("%sBID_PLACED: ID=%d, Item=%s, Bidder=%s, Amount=%d %s, Previous=%d %s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + bidderName, + bidAmount, + ConfigMarket.CurrencyName, + listing.currentBid, + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log when a player is outbid + */ + public static void logOutbid(AuctionListing listing, String outbidPlayerName, long previousBid, + String newBidderName, long newBid) { + String msg = String.format("%sOUTBID: ID=%d, Item=%s, OutbidPlayer=%s (had %d), NewBidder=%s (bid %d)", + PREFIX, + listing.id, + listing.item.getDisplayName(), + outbidPlayerName, + previousBid, + newBidderName, + newBid + ); + LogWriter.info(msg); + } + + /** + * Log when a buyout occurs + */ + public static void logBuyout(AuctionListing listing, String buyerName) { + String msg = String.format("%sBUYOUT: ID=%d, Item=%s, Buyer=%s, Price=%d %s, Seller=%s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + buyerName, + listing.buyoutPrice, + ConfigMarket.CurrencyName, + listing.sellerName + ); + LogWriter.info(msg); + } + + /** + * Log when an auction is won (ended with bids) + */ + public static void logAuctionWon(AuctionListing listing) { + String msg = String.format("%sAUCTION_WON: ID=%d, Item=%s, Winner=%s (%s), FinalPrice=%d %s, Seller=%s, SellerProceeds=%d %s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.highBidderName, + listing.highBidderUUID != null ? listing.highBidderUUID.toString() : "N/A", + listing.currentBid, + ConfigMarket.CurrencyName, + listing.sellerName, + listing.getSellerProceeds(), + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log when an auction sells (same as won, for notification controller compatibility) + */ + public static void logAuctionSold(AuctionListing listing) { + String msg = String.format("%sAUCTION_SOLD: ID=%d, Item=%s, Buyer=%s, SalePrice=%d %s, Seller=%s (%s), Proceeds=%d %s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.highBidderName, + listing.currentBid, + ConfigMarket.CurrencyName, + listing.sellerName, + listing.sellerUUID.toString(), + listing.getSellerProceeds(), + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log when an auction expires with no bids + */ + public static void logAuctionExpired(AuctionListing listing) { + String msg = String.format("%sAUCTION_EXPIRED: ID=%d, Item=%s, Seller=%s (%s), StartPrice=%d %s, Duration=%s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.sellerName, + listing.sellerUUID.toString(), + listing.startingPrice, + ConfigMarket.CurrencyName, + formatDuration(listing.expirationTime - listing.listingTime) + ); + LogWriter.info(msg); + } + + /** + * Log when an auction is cancelled + */ + public static void logAuctionCancelled(AuctionListing listing, boolean byAdmin) { + String msg = String.format("%sAUCTION_CANCELLED: ID=%d, Item=%s, Seller=%s, CancelledBy=%s, HadBids=%b", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.sellerName, + byAdmin ? "ADMIN" : "SELLER", + listing.currentBid > 0 + ); + LogWriter.info(msg); + } + + /** + * Log when an item is claimed + */ + public static void logItemClaimed(AuctionListing listing, String claimerName, boolean isSeller) { + String claimType = isSeller ? + (listing.status == EnumAuctionStatus.EXPIRED ? "RECLAIMED_EXPIRED" : "CLAIMED_PROCEEDS") : + "CLAIMED_WON_ITEM"; + + String msg = String.format("%s%s: ID=%d, Item=%s, Claimer=%s, Status=%s", + PREFIX, + claimType, + listing.id, + listing.item.getDisplayName(), + claimerName, + listing.status.name() + ); + LogWriter.info(msg); + } + + /** + * Log currency claimed from auction + */ + public static void logCurrencyClaimed(AuctionListing listing, String claimerName, long amount) { + String msg = String.format("%sCURRENCY_CLAIMED: ID=%d, Claimer=%s, Amount=%d %s", + PREFIX, + listing.id, + claimerName, + amount, + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log refund of bid to outbid player + */ + public static void logBidRefunded(AuctionListing listing, String playerName, long amount) { + String msg = String.format("%sBID_REFUNDED: ID=%d, Player=%s, Amount=%d %s", + PREFIX, + listing.id, + playerName, + amount, + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log admin actions on auctions + */ + public static void logAdminAction(String adminName, String action, AuctionListing listing) { + String msg = String.format("%sADMIN_ACTION: Admin=%s, Action=%s, ListingID=%d, Item=%s, Seller=%s", + PREFIX, + adminName, + action, + listing.id, + listing.item.getDisplayName(), + listing.sellerName + ); + LogWriter.info(msg); + } + + /** + * Log auction system startup + */ + public static void logSystemStartup(int activeListings, int pendingClaims) { + String msg = String.format("%sSYSTEM_STARTUP: ActiveListings=%d, PendingClaims=%d, Timestamp=%s", + PREFIX, + activeListings, + pendingClaims, + dateFormat.format(new Date()) + ); + LogWriter.info(msg); + } + + /** + * Log auction processing tick + */ + public static void logProcessingTick(int expired, int completed) { + if (expired > 0 || completed > 0) { + String msg = String.format("%sPROCESSING_TICK: ExpiredListings=%d, CompletedAuctions=%d", + PREFIX, + expired, + completed + ); + LogWriter.info(msg); + } + } + + /** + * Log errors in the auction system + */ + public static void logError(String context, Exception e) { + String msg = String.format("%sERROR: Context=%s, Message=%s", + PREFIX, + context, + e.getMessage() + ); + LogWriter.error(msg, e); + } + + /** + * Format duration in a human-readable format + */ + private static String formatDuration(long millis) { + long hours = millis / (1000 * 60 * 60); + if (hours >= 24) { + long days = hours / 24; + return days + "d " + (hours % 24) + "h"; + } + return hours + "h"; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java b/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java new file mode 100644 index 000000000..48da6d345 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java @@ -0,0 +1,226 @@ +package noppes.npcs.controllers; + +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.LogWriter; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.AuctionListing; + +import java.util.*; + +/** + * Handles auction notifications for players. + * Sends messages for outbid, won, sold, expired events. + */ +public class AuctionNotificationController { + public static AuctionNotificationController Instance; + + // Pending notifications for offline players (UUID -> List of notifications) + private Map> pendingNotifications = new HashMap<>(); + + public AuctionNotificationController() { + Instance = this; + } + + /** + * Notify a player they have been outbid + */ + public void notifyOutbid(AuctionListing listing, UUID outbidPlayerUUID, + String outbidPlayerName, long previousBid, long newBid) { + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + + "You have been outbid on " + EnumChatFormatting.WHITE + + listing.item.getDisplayName() + EnumChatFormatting.RED + + "! New bid: " + formatCurrency(newBid) + + " (Your bid: " + formatCurrency(previousBid) + " has been refunded to claims)"; + + sendOrQueueNotification(outbidPlayerUUID, NotificationType.OUTBID, message, listing.id); + + // Log the outbid event + AuctionLogger.logOutbid(listing, outbidPlayerName, previousBid, listing.highBidderName, newBid); + } + + /** + * Notify a player they won an auction + */ + public void notifyAuctionWon(AuctionListing listing) { + if (listing.highBidderUUID == null) return; + + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "Congratulations! You won the auction for " + EnumChatFormatting.WHITE + + listing.item.getDisplayName() + EnumChatFormatting.GREEN + + " for " + formatCurrency(listing.currentBid) + "! Visit an Auctioneer to claim your item."; + + sendOrQueueNotification(listing.highBidderUUID, NotificationType.WON, message, listing.id); + + // Log the win + AuctionLogger.logAuctionWon(listing); + } + + /** + * Notify seller their auction sold + */ + public void notifyAuctionSold(AuctionListing listing) { + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "Your " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.GREEN + " sold for " + formatCurrency(listing.currentBid) + + "! Visit an Auctioneer to claim " + formatCurrency(listing.getSellerProceeds()) + "."; + + sendOrQueueNotification(listing.sellerUUID, NotificationType.SOLD, message, listing.id); + + // Log the sale + AuctionLogger.logAuctionSold(listing); + } + + /** + * Notify seller their auction expired with no bids + */ + public void notifyAuctionExpired(AuctionListing listing) { + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + + "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.GRAY + " has expired with no bids. Visit an Auctioneer to reclaim your item."; + + sendOrQueueNotification(listing.sellerUUID, NotificationType.EXPIRED, message, listing.id); + + // Log the expiration + AuctionLogger.logAuctionExpired(listing); + } + + /** + * Notify player their auction was cancelled + */ + public void notifyAuctionCancelled(AuctionListing listing, boolean byAdmin) { + String message; + if (byAdmin) { + message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + + "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + " was cancelled by an administrator."; + } else { + message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + + "You cancelled your auction for " + EnumChatFormatting.WHITE + + listing.item.getDisplayName() + EnumChatFormatting.GRAY + "."; + } + + sendOrQueueNotification(listing.sellerUUID, NotificationType.CANCELLED, message, listing.id); + + // Log the cancellation + AuctionLogger.logAuctionCancelled(listing, byAdmin); + } + + /** + * Notify about a new bid placed + */ + public void notifyNewBid(AuctionListing listing, String bidderName, long bidAmount) { + // Notify seller about new bid + String sellerMessage = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.AQUA + + bidderName + " placed a bid of " + formatCurrency(bidAmount) + + " on your " + listing.item.getDisplayName() + "!"; + + sendOrQueueNotification(listing.sellerUUID, NotificationType.NEW_BID, sellerMessage, listing.id); + + // Log the bid + AuctionLogger.logBidPlaced(listing, bidderName, bidAmount); + } + + /** + * Send notification immediately if player online, otherwise queue for later + */ + private void sendOrQueueNotification(UUID playerUUID, NotificationType type, String message, int listingId) { + EntityPlayerMP player = getOnlinePlayer(playerUUID); + + if (player != null) { + player.addChatMessage(new ChatComponentText(message)); + } else { + // Queue for when player logs in + AuctionNotification notification = new AuctionNotification(type, message, listingId); + + if (!pendingNotifications.containsKey(playerUUID)) { + pendingNotifications.put(playerUUID, new ArrayList<>()); + } + pendingNotifications.get(playerUUID).add(notification); + + // Limit queue size per player + List queue = pendingNotifications.get(playerUUID); + while (queue.size() > 50) { + queue.remove(0); + } + } + } + + /** + * Called when a player logs in - send any pending notifications + */ + public void onPlayerLogin(EntityPlayerMP player) { + UUID uuid = player.getUniqueID(); + + if (pendingNotifications.containsKey(uuid)) { + List notifications = pendingNotifications.remove(uuid); + + if (!notifications.isEmpty()) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + + "You have " + notifications.size() + " auction notification(s):")); + + for (AuctionNotification notification : notifications) { + player.addChatMessage(new ChatComponentText(notification.message)); + } + } + } + + // Also check for claimable items + if (AuctionController.Instance != null) { + int claimableCount = AuctionController.Instance.getClaimableListings(uuid).size(); + if (claimableCount > 0) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "You have " + claimableCount + " item(s) waiting to be claimed!")); + } + } + } + + private EntityPlayerMP getOnlinePlayer(UUID uuid) { + if (noppes.npcs.CustomNpcs.getServer() == null) return null; + + for (Object obj : noppes.npcs.CustomNpcs.getServer().getConfigurationManager().playerEntityList) { + EntityPlayerMP player = (EntityPlayerMP) obj; + if (player.getUniqueID().equals(uuid)) { + return player; + } + } + return null; + } + + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Notification types for categorization + */ + public enum NotificationType { + OUTBID, + WON, + SOLD, + EXPIRED, + CANCELLED, + NEW_BID + } + + /** + * Stored notification for offline players + */ + private static class AuctionNotification { + public final NotificationType type; + public final String message; + public final int listingId; + public final long timestamp; + + public AuctionNotification(NotificationType type, String message, int listingId) { + this.type = type; + this.message = message; + this.listingId = listingId; + this.timestamp = System.currentTimeMillis(); + } + } +} From 740c2852e5d805fc6c4e54c353cd51f7aae1c3e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 10:26:56 +0000 Subject: [PATCH 13/21] Refactor auction system: consolidate controllers, add lang keys, remove categories - Consolidate AuctionLogger and AuctionNotificationController into AuctionController - Add inline logging and notification methods to AuctionController - Add comprehensive LANG keys for all auction GUI strings - Update EnumAuctionSort and EnumAuctionDuration to use lang keys - Remove category filtering system (EnumAuctionCategory deleted) - Update AuctionFilter to remove category references - Update GuiAuction and all sub-GUIs to use StatCollector.translateToLocal() - Update AuctionListing to remove category field and NBT serialization --- src/main/java/noppes/npcs/CustomNpcs.java | 2 - .../java/noppes/npcs/ServerTickHandler.java | 6 +- .../npcs/client/gui/player/GuiAuction.java | 109 ++--- .../client/gui/player/SubGuiAuctionBid.java | 31 +- .../gui/player/SubGuiAuctionCreate.java | 31 +- .../gui/player/SubGuiAuctionDetails.java | 31 +- .../npcs/constants/EnumAuctionCategory.java | 156 ------ .../npcs/constants/EnumAuctionDuration.java | 46 +- .../npcs/constants/EnumAuctionSort.java | 61 +-- .../npcs/controllers/AuctionController.java | 457 +++++++----------- .../npcs/controllers/AuctionLogger.java | 269 ----------- .../AuctionNotificationController.java | 226 --------- .../npcs/controllers/data/AuctionFilter.java | 31 -- .../npcs/controllers/data/AuctionListing.java | 13 - .../assets/customnpcs/lang/en_US.lang | 70 +++ 15 files changed, 374 insertions(+), 1165 deletions(-) delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionCategory.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionLogger.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionNotificationController.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 79a4a1b40..c29fa4253 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -62,7 +62,6 @@ import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.AuctionBlacklist; import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.AuctionNotificationController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -313,7 +312,6 @@ public class CustomNpcs { new CurrencyController(); new AuctionBlacklist(); new AuctionController(); - new AuctionNotificationController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 204e34bb5..fe846df19 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,7 +10,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; -import noppes.npcs.controllers.AuctionNotificationController; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; @@ -68,8 +68,8 @@ public class ServerTickHandler { SyncController.syncEffects(player); // Send pending auction notifications - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.onPlayerLogin(player); + if (AuctionController.Instance != null) { + AuctionController.Instance.onPlayerLogin(player); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 1fe089d40..118dde167 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -3,17 +3,13 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.util.*; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionCategory; -import noppes.npcs.constants.EnumAuctionSort; -import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.containers.ContainerAuction; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; @@ -26,13 +22,11 @@ import java.util.List; /** * Main auction house browse GUI. - * Displays listings with search, filter, sort, and pagination. + * Displays listings with search, sort, and pagination. */ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, ICustomScrollListener, NoppesUtil.IAuctionData { - private static final ResourceLocation background = new ResourceLocation("customnpcs", "textures/gui/auction.png"); - // GUI Components private GuiNpcTextField searchField; private GuiCustomScroll listingScroll; @@ -56,7 +50,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { super(npc, container); - this.title = "Auction House"; + this.title = ""; this.xSize = 256; this.ySize = 222; this.closeOnEsc = true; @@ -71,70 +65,50 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, super.initGui(); // Tab buttons at top - addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, "Browse")); - addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, "My Listings")); - addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, "My Bids")); - addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, "Claims")); - addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, "Sell")); + addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse"))); + addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, StatCollector.translateToLocal("auction.myListings"))); + addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, StatCollector.translateToLocal("auction.myBids"))); + addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, StatCollector.translateToLocal("auction.claims"))); + addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, StatCollector.translateToLocal("auction.sell"))); getTopButton(currentTab).setEnabled(false); // Search field - searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 120, 14, currentFilter.searchText); + searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 140, 14, currentFilter.searchText); searchField.setMaxStringLength(50); addTextField(searchField); // Search button - addButton(new GuiNpcButton(11, guiLeft + 130, guiTop + 5, 30, 16, "Search")); + addButton(new GuiNpcButton(11, guiLeft + 150, guiTop + 5, 40, 16, StatCollector.translateToLocal("auction.search"))); - // Category buttons (simplified row) - String[] categories = {"All", "Weapons", "Armor", "Tools", "Potions", "Blocks", "Other"}; - for (int i = 0; i < categories.length; i++) { - GuiNpcButton btn = new GuiNpcButton(20 + i, guiLeft + 8 + i * 35, guiTop + 24, 33, 14, categories[i]); - btn.setEnabled(currentFilter.category.ordinal() != getCategoryOrdinal(i)); - addButton(btn); - } - - // Sort dropdown button - addButton(new GuiNpcButton(30, guiLeft + 170, guiTop + 5, 78, 16, currentFilter.sortOrder.getDisplayName())); + // Sort button + addButton(new GuiNpcButton(30, guiLeft + 8, guiTop + 24, 120, 16, currentFilter.sortOrder.getDisplayName())); // Listing scroll area listingScroll = new GuiCustomScroll(this, 0); listingScroll.guiLeft = guiLeft + 8; - listingScroll.guiTop = guiTop + 42; - listingScroll.setSize(160, 130); + listingScroll.guiTop = guiTop + 44; + listingScroll.setSize(160, 128); addScroll(listingScroll); updateListingDisplay(); // Pagination buttons - addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, "< Prev")); + addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.prevPage"))); addLabel(new GuiNpcLabel(41, getPageLabel(), guiLeft + 90, guiTop + 180, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, "Next >")); + addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.nextPage"))); // Action buttons (right side) - addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, "View Details")); - addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, "Place Bid")); - addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, "Buyout")); - addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, "Refresh")); + addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, StatCollector.translateToLocal("auction.viewDetails"))); + addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, StatCollector.translateToLocal("auction.placeBid"))); + addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, StatCollector.translateToLocal("auction.buyout"))); + addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, StatCollector.translateToLocal("auction.refresh"))); // Balance display - addLabel(new GuiNpcLabel(60, "Balance: " + formatCurrency(playerBalance), + addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), guiLeft + 175, guiTop + 155, 0x00AA00)); updateButtonStates(); } - private int getCategoryOrdinal(int buttonIndex) { - switch (buttonIndex) { - case 0: return EnumAuctionCategory.ALL.ordinal(); - case 1: return EnumAuctionCategory.WEAPONS.ordinal(); - case 2: return EnumAuctionCategory.ARMOR.ordinal(); - case 3: return EnumAuctionCategory.TOOLS.ordinal(); - case 4: return EnumAuctionCategory.POTIONS.ordinal(); - case 5: return EnumAuctionCategory.BLOCKS.ordinal(); - default: return EnumAuctionCategory.MISC.ordinal(); - } - } - @Override public void updateScreen() { super.updateScreen(); @@ -160,10 +134,6 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, else if (id == 11) { performSearch(); } - // Category buttons - else if (id >= 20 && id <= 26) { - setCategory(getCategoryOrdinal(id - 20)); - } // Sort button else if (id == 30) { cycleSortOrder(); @@ -218,14 +188,6 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, AuctionActionPacket.QueryListings(currentFilter); } - private void setCategory(int categoryOrdinal) { - currentFilter.category = EnumAuctionCategory.values()[categoryOrdinal]; - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - initGui(); - } - private void cycleSortOrder() { currentFilter.sortOrder = currentFilter.sortOrder.next(); currentFilter.page = 0; @@ -321,7 +283,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } private String formatTimeRemaining(long millis) { - if (millis <= 0) return "Ended"; + if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); long seconds = millis / 1000; long minutes = seconds / 60; @@ -350,9 +312,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private String getPageLabel() { if (totalPages == 0) { - return "No results"; + return StatCollector.translateToLocal("auction.noResults"); } - return "Page " + (currentPage + 1) + " of " + totalPages; + return String.format(StatCollector.translateToLocal("auction.page"), currentPage + 1, totalPages); } private void updateButtonStates() { @@ -378,7 +340,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, drawDefaultBackground(); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - // Draw background (using a standard size, can be customized) + // Draw background mc.renderEngine.bindTexture(new ResourceLocation("customnpcs", "textures/gui/bgfilled.png")); drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); @@ -413,17 +375,18 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, CustomNpcResourceListener.DefaultTextColor); // Results count - fontRendererObj.drawString(totalResults + " listings", 175, 42, 0x666666); + String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); + fontRendererObj.drawString(listingsLabel, 175, 42, 0x666666); } private String getTabTitle() { switch (currentTab) { - case 0: return "Browse Auctions"; - case 1: return "My Listings"; - case 2: return "My Active Bids"; - case 3: return "Claim Items"; - case 4: return "Create Listing"; - default: return "Auction House"; + case 0: return StatCollector.translateToLocal("auction.browseTitle"); + case 1: return StatCollector.translateToLocal("auction.myListingsTitle"); + case 2: return StatCollector.translateToLocal("auction.myBidsTitle"); + case 3: return StatCollector.translateToLocal("auction.claimsTitle"); + case 4: return StatCollector.translateToLocal("auction.sellTitle"); + default: return StatCollector.translateToLocal("auction.title"); } } @@ -443,13 +406,13 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Handled above } - // IGuiData - Receive NBT data from server + // IGuiData @Override public void setGuiData(NBTTagCompound compound) { // This can be used for initial setup data } - // IAuctionData - Receive auction listing data + // IAuctionData @Override public void onAuctionDataReceived(int dataType, NBTTagCompound data, int totalResults, int currentPage, int totalPages, @@ -478,7 +441,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, getLabel(41).label = getPageLabel(); } if (getLabel(60) != null) { - getLabel(60).label = "Balance: " + formatCurrency(playerBalance); + getLabel(60).label = String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index 36572b14d..7f4665b40 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; @@ -35,39 +36,39 @@ public class SubGuiAuctionBid extends SubGuiInterface { int labelColor = 0x404040; // Title - addLabel(new GuiNpcLabel(0, "Place Bid", guiLeft + 70, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 70, y, labelColor)); y += 18; // Item name - addLabel(new GuiNpcLabel(1, "Item: " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item") + " " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); y += 14; // Current price long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - addLabel(new GuiNpcLabel(2, "Current Price:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.currentPrice"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 100, y, 0x008800)); y += 14; // Minimum bid long minBid = listing.getMinimumBid(); - addLabel(new GuiNpcLabel(4, "Minimum Bid:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 100, y, 0x0066AA)); y += 14; // Your balance - addLabel(new GuiNpcLabel(6, "Your Balance:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(6, StatCollector.translateToLocal("auction.yourBalance"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 100, y, playerBalance >= minBid ? 0x008800 : 0xAA0000)); y += 14; // Time remaining - addLabel(new GuiNpcLabel(8, "Time Left:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(8, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, getTimeColor(listing.getTimeRemaining()))); y += 18; // Bid input - addLabel(new GuiNpcLabel(10, "Your Bid:", guiLeft + 15, y + 3, labelColor)); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); bidField.setNumbersOnly(); bidField.setMinMaxDefault(minBid, 999999999, minBid); @@ -75,24 +76,24 @@ public class SubGuiAuctionBid extends SubGuiInterface { y += 24; // Quick bid buttons - addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, "Min Bid")); - addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, "+10%")); - addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, "+25%")); + addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, StatCollector.translateToLocal("auction.minBid"))); + addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus10"))); + addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus25"))); y += 22; // Place bid button - GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, "Place Bid"); + GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); addButton(bidButton); // Cancel button - addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, "Cancel")); + addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); // Error message area if (playerBalance < minBid) { - addLabel(new GuiNpcLabel(40, "Insufficient funds!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); } else if (!listing.isActive()) { - addLabel(new GuiNpcLabel(40, "Auction has ended!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); } } @@ -101,7 +102,7 @@ public class SubGuiAuctionBid extends SubGuiInterface { } private String formatTimeRemaining(long millis) { - if (millis <= 0) return "Ended"; + if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); long seconds = millis / 1000; long minutes = seconds / 60; diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index 4fca552e0..75329f5e8 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.item.ItemStack; +import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; @@ -21,8 +22,6 @@ public class SubGuiAuctionCreate extends SubGuiInterface { private GuiNpcTextField buyoutPriceField; private int selectedDuration = 1; // Default to MEDIUM - private static final String[] DURATION_NAMES = {"Short", "Medium", "Long", "Very Long"}; - public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { this.container = container; setBackground("menubg.png"); @@ -36,54 +35,54 @@ public class SubGuiAuctionCreate extends SubGuiInterface { super.initGui(); // Title - addLabel(new GuiNpcLabel(0, "Create Auction Listing", guiLeft + 50, guiTop + 8, 0x404040)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 50, guiTop + 8, 0x404040)); // Item slot instructions - addLabel(new GuiNpcLabel(1, "Place item in slot below:", guiLeft + 20, guiTop + 28, 0x404040)); + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.placeItemBelow"), guiLeft + 20, guiTop + 28, 0x404040)); // Item info (shows when item is in slot) ItemStack item = container.getListingItem(); if (item != null) { - addLabel(new GuiNpcLabel(2, "Item: " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); } else { - addLabel(new GuiNpcLabel(2, "(No item selected)", guiLeft + 20, guiTop + 50, 0x888888)); + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 20, guiTop + 50, 0x888888)); } // Starting price - addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, guiTop + 70, 0x404040)); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 70, 0x404040)); startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); startingPriceField.setNumbersOnly(); startingPriceField.setMinMaxDefault(1, 999999999, 100); addTextField(startingPriceField); // Buyout price (optional) - addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, guiTop + 90, 0x404040)); + addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 90, 0x404040)); buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); buyoutPriceField.setNumbersOnly(); buyoutPriceField.setMinMaxDefault(0, 999999999, 0); addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(22, "(0 = no buyout)", guiLeft + 185, guiTop + 90, 0x888888)); + addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 185, guiTop + 90, 0x888888)); // Duration selection - addLabel(new GuiNpcLabel(30, "Duration:", guiLeft + 20, guiTop + 112, 0x404040)); + addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 112, 0x404040)); addButton(new GuiNpcButton(31, guiLeft + 100, guiTop + 108, 100, 20, getDurationDisplay())); // Fee display long fee = getListingFee(); - addLabel(new GuiNpcLabel(40, "Listing Fee: " + fee + " " + ConfigMarket.CurrencyName, + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, guiLeft + 20, guiTop + 135, fee > 0 ? 0xAA0000 : 0x008800)); // Create button - addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, "Create Listing")); + addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, StatCollector.translateToLocal("auction.create"))); getButton(50).setEnabled(item != null); // Cancel button - addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, "Cancel")); + addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, StatCollector.translateToLocal("gui.cancel"))); } private String getDurationDisplay() { EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return DURATION_NAMES[selectedDuration] + " (" + duration.getHours() + "h)"; + return duration.getDisplayName(); } private long getListingFee() { @@ -147,7 +146,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { if (item != null) { if (itemLabel != null) { - itemLabel.label = "Item: " + item.getDisplayName(); + itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); itemLabel.color = 0x008800; } if (createButton != null) { @@ -155,7 +154,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { } } else { if (itemLabel != null) { - itemLabel.label = "(No item selected)"; + itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); itemLabel.color = 0x888888; } if (createButton != null) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java index 7f226799a..f18046922 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.player; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; @@ -34,70 +35,70 @@ public class SubGuiAuctionDetails extends SubGuiInterface { int valueColor = 0x000000; // Title - addLabel(new GuiNpcLabel(0, "Auction Details", guiLeft + 80, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 80, y, labelColor)); y += 16; // Item name - addLabel(new GuiNpcLabel(1, "Item:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 80, y, valueColor)); y += 14; // Quantity - addLabel(new GuiNpcLabel(3, "Quantity:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("auction.quantity"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 80, y, valueColor)); y += 14; // Seller - addLabel(new GuiNpcLabel(5, "Seller:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("auction.seller"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 80, y, valueColor)); y += 14; // Starting price - addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 100, y, valueColor)); y += 14; // Current bid if (listing.currentBid > 0) { - addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 100, y, 0x008800)); y += 14; // High bidder - addLabel(new GuiNpcLabel(14, "High Bidder:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(14, StatCollector.translateToLocal("auction.highBidder"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 100, y, valueColor)); y += 14; // Bid count - addLabel(new GuiNpcLabel(16, "Total Bids:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(16, StatCollector.translateToLocal("auction.totalBids"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 100, y, valueColor)); y += 14; } else { - addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, "No bids yet", guiLeft + 100, y, 0x888888)); + addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 100, y, 0x888888)); y += 14; } // Buyout price if (listing.buyoutPrice > 0) { - addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 100, y, 0xAA6600)); y += 14; } // Minimum bid - addLabel(new GuiNpcLabel(22, "Minimum Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 100, y, 0x0066AA)); y += 14; // Time remaining (will be updated) - addLabel(new GuiNpcLabel(30, "Time Left:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, getTimeColor(listing.getTimeRemaining()))); y += 20; // Close button - addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, "Close")); + addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); } private String formatCurrency(long amount) { @@ -105,7 +106,7 @@ public class SubGuiAuctionDetails extends SubGuiInterface { } private String formatTimeRemaining(long millis) { - if (millis <= 0) return "Auction Ended"; + if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); long seconds = millis / 1000; long minutes = seconds / 60; diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java b/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java deleted file mode 100644 index 0906bb6ed..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java +++ /dev/null @@ -1,156 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.init.Items; -import net.minecraft.item.Item; -import net.minecraft.item.ItemArmor; -import net.minecraft.item.ItemAxe; -import net.minecraft.item.ItemBlock; -import net.minecraft.item.ItemBow; -import net.minecraft.item.ItemEnchantedBook; -import net.minecraft.item.ItemFood; -import net.minecraft.item.ItemHoe; -import net.minecraft.item.ItemPickaxe; -import net.minecraft.item.ItemPotion; -import net.minecraft.item.ItemSpade; -import net.minecraft.item.ItemStack; -import net.minecraft.item.ItemSword; -import net.minecraft.item.ItemTool; - -/** - * Categories for auction house items. - * Used for filtering and browsing. - */ -public enum EnumAuctionCategory { - ALL, // Special: shows all categories - WEAPONS, // Swords, bows, etc. - ARMOR, // Helmets, chestplates, leggings, boots - TOOLS, // Pickaxes, axes, shovels, hoes - FOOD, // Edible items - POTIONS, // Potions and splash potions - BLOCKS, // Placeable blocks - MATERIALS, // Crafting materials (ingots, gems, etc.) - ENCHANTED, // Items with enchantments or enchanted books - MISC; // Everything else - - /** - * Get display name for GUI - */ - public String getDisplayName() { - switch (this) { - case ALL: return "All Items"; - case WEAPONS: return "Weapons"; - case ARMOR: return "Armor"; - case TOOLS: return "Tools"; - case FOOD: return "Food"; - case POTIONS: return "Potions"; - case BLOCKS: return "Blocks"; - case MATERIALS: return "Materials"; - case ENCHANTED: return "Enchanted"; - case MISC: return "Miscellaneous"; - default: return "Unknown"; - } - } - - /** - * Auto-categorize an item based on its type - */ - public static EnumAuctionCategory categorize(ItemStack itemStack) { - if (itemStack == null || itemStack.getItem() == null) { - return MISC; - } - - Item item = itemStack.getItem(); - - // Check for enchantments first (higher priority) - if (itemStack.isItemEnchanted() || item instanceof ItemEnchantedBook) { - return ENCHANTED; - } - - // Weapons - if (item instanceof ItemSword || item instanceof ItemBow) { - return WEAPONS; - } - - // Armor - if (item instanceof ItemArmor) { - return ARMOR; - } - - // Tools - if (item instanceof ItemTool || item instanceof ItemPickaxe || - item instanceof ItemAxe || item instanceof ItemSpade || - item instanceof ItemHoe) { - return TOOLS; - } - - // Food - if (item instanceof ItemFood) { - return FOOD; - } - - // Potions - if (item instanceof ItemPotion) { - return POTIONS; - } - - // Blocks - if (item instanceof ItemBlock) { - return BLOCKS; - } - - // Materials - common crafting items - if (isMaterial(item)) { - return MATERIALS; - } - - return MISC; - } - - /** - * Check if an item is a common crafting material - */ - private static boolean isMaterial(Item item) { - // Check common material items - return item == Items.iron_ingot || - item == Items.gold_ingot || - item == Items.diamond || - item == Items.emerald || - item == Items.coal || - item == Items.redstone || - item == Items.glowstone_dust || - item == Items.quartz || - item == Items.leather || - item == Items.string || - item == Items.feather || - item == Items.bone || - item == Items.gunpowder || - item == Items.slime_ball || - item == Items.ender_pearl || - item == Items.blaze_rod || - item == Items.ghast_tear || - item == Items.gold_nugget || - item == Items.nether_wart || - item == Items.spider_eye || - item == Items.magma_cream || - item == Items.nether_star; - } - - /** - * Get icon item for category display - */ - public ItemStack getIconItem() { - switch (this) { - case ALL: return new ItemStack(Items.compass); - case WEAPONS: return new ItemStack(Items.iron_sword); - case ARMOR: return new ItemStack(Items.iron_chestplate); - case TOOLS: return new ItemStack(Items.iron_pickaxe); - case FOOD: return new ItemStack(Items.bread); - case POTIONS: return new ItemStack(Items.potionitem); - case BLOCKS: return new ItemStack(Item.getItemFromBlock(net.minecraft.init.Blocks.grass)); - case MATERIALS: return new ItemStack(Items.iron_ingot); - case ENCHANTED: return new ItemStack(Items.enchanted_book); - case MISC: return new ItemStack(Items.chest_minecart); - default: return new ItemStack(Items.paper); - } - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java index e18416f50..c901d21ce 100644 --- a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java +++ b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java @@ -1,5 +1,6 @@ package noppes.npcs.constants; +import net.minecraft.util.StatCollector; import noppes.npcs.config.ConfigMarket; /** @@ -7,10 +8,16 @@ import noppes.npcs.config.ConfigMarket; * Values are configurable via ConfigMarket. */ public enum EnumAuctionDuration { - SHORT, // Default: 2 hours - MEDIUM, // Default: 12 hours - LONG, // Default: 24 hours - VERY_LONG; // Default: 48 hours + SHORT("auction.duration.short"), + MEDIUM("auction.duration.medium"), + LONG("auction.duration.long"), + VERY_LONG("auction.duration.veryLong"); + + private final String langKey; + + EnumAuctionDuration(String langKey) { + this.langKey = langKey; + } /** * Get duration in milliseconds @@ -30,22 +37,29 @@ public enum EnumAuctionDuration { } } + /** + * Get duration in hours + */ + public int getHours() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionDurationShort; + case MEDIUM: + return ConfigMarket.AuctionDurationMedium; + case LONG: + return ConfigMarket.AuctionDurationLong; + case VERY_LONG: + return ConfigMarket.AuctionDurationVeryLong; + default: + return 24; + } + } + /** * Get display name for GUI */ public String getDisplayName() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort + " Hours"; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium + " Hours"; - case LONG: - return ConfigMarket.AuctionDurationLong + " Hours"; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong + " Hours"; - default: - return "Unknown"; - } + return StatCollector.translateToLocal(langKey); } /** diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java index a94d1f1bf..75220861c 100644 --- a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java +++ b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java @@ -1,66 +1,37 @@ package noppes.npcs.constants; +import net.minecraft.util.StatCollector; + /** * Sorting options for auction listings. */ public enum EnumAuctionSort { - /** Default - newest listings first */ - NEWEST_FIRST("Newest First"), + NEWEST_FIRST("auction.sort.newest"), + OLDEST_FIRST("auction.sort.oldest"), + PRICE_HIGH_TO_LOW("auction.sort.priceHigh"), + PRICE_LOW_TO_HIGH("auction.sort.priceLow"), + ENDING_SOON("auction.sort.endingSoon"), + MOST_TIME_LEFT("auction.sort.mostTime"), + NAME_A_TO_Z("auction.sort.nameAZ"), + NAME_Z_TO_A("auction.sort.nameZA"), + MOST_BIDS("auction.sort.mostBids"), + LEAST_BIDS("auction.sort.leastBids"); - /** Oldest listings first */ - OLDEST_FIRST("Oldest First"), + private final String langKey; - /** Highest current price first */ - PRICE_HIGH_TO_LOW("Most Expensive"), - - /** Lowest current price first */ - PRICE_LOW_TO_HIGH("Least Expensive"), - - /** Buyout price high to low (listings without buyout at end) */ - BUYOUT_HIGH_TO_LOW("Buyout: High to Low"), - - /** Buyout price low to high (listings without buyout at end) */ - BUYOUT_LOW_TO_HIGH("Buyout: Low to High"), - - /** Ending soonest first */ - ENDING_SOON("Ending Soon"), - - /** Most time remaining first */ - MOST_TIME_LEFT("Most Time Left"), - - /** Alphabetical by item name A-Z */ - NAME_A_TO_Z("Name: A to Z"), - - /** Alphabetical by item name Z-A */ - NAME_Z_TO_A("Name: Z to A"), - - /** Most bids first */ - MOST_BIDS("Most Bids"), - - /** Least bids first */ - LEAST_BIDS("Least Bids"); - - private final String displayName; - - EnumAuctionSort(String displayName) { - this.displayName = displayName; + EnumAuctionSort(String langKey) { + this.langKey = langKey; } public String getDisplayName() { - return displayName; + return StatCollector.translateToLocal(langKey); } - /** - * Get next sort option (for cycling through in GUI) - */ public EnumAuctionSort next() { EnumAuctionSort[] values = values(); return values[(this.ordinal() + 1) % values.length]; } - /** - * Get previous sort option - */ public EnumAuctionSort previous() { EnumAuctionSort[] values = values(); return values[(this.ordinal() - 1 + values.length) % values.length]; diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 76e5f0891..b6bb41d10 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -1,20 +1,21 @@ package noppes.npcs.controllers; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import java.io.File; import java.io.FileInputStream; @@ -25,10 +26,10 @@ import java.util.stream.Collectors; /** * Controller for the global auction house. - * Manages all auction listings, bidding, and claims. + * Manages all auction listings, bidding, claims, notifications, and logging. */ public class AuctionController { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + private static final String LOG_PREFIX = "[Auction] "; public static AuctionController Instance; @@ -42,6 +43,9 @@ public class AuctionController { private Map> listingsBySeller = new ConcurrentHashMap<>(); private Map> listingsByBidder = new ConcurrentHashMap<>(); + // Pending notifications for offline players + private Map> pendingNotifications = new ConcurrentHashMap<>(); + public AuctionController() { Instance = this; load(); @@ -59,7 +63,7 @@ public class AuctionController { // Validate item String validation = AuctionListing.validateItem(item); if (validation != null) { - logger.warn("Auction listing validation failed: " + validation); + LogWriter.info(LOG_PREFIX + "Listing validation failed: " + validation); return null; } @@ -67,8 +71,6 @@ public class AuctionController { int currentListings = getActiveListingsCount(seller.getUniqueID()); int maxListings = getMaxListings(seller); if (currentListings >= maxListings) { - logger.info("Player {} has reached max listings ({}/{})", - seller.getCommandSenderName(), currentListings, maxListings); return null; } @@ -85,8 +87,6 @@ public class AuctionController { long listingFee = listing.getListingFee(); if (listingFee > 0) { if (!CurrencyController.Instance.canAfford(seller, listingFee)) { - logger.info("Player {} cannot afford listing fee of {}", - seller.getCommandSenderName(), listingFee); return null; } CurrencyController.Instance.withdraw(seller, listingFee); @@ -100,12 +100,11 @@ public class AuctionController { addToSellerIndex(seller.getUniqueID(), listing.id); save(); - logger.info("Created auction listing {} by {} for {}", - listing.id, seller.getCommandSenderName(), - item.getDisplayName()); - // Log to CNPC+ auction history - AuctionLogger.logListingCreated(listing); + // Log to CNPC+ + logEvent("LISTING_CREATED", String.format("ID=%d, Seller=%s, Item=%s x%d, StartPrice=%d, Buyout=%s", + listing.id, listing.sellerName, item.getDisplayName(), item.stackSize, + listing.startingPrice, listing.buyoutPrice > 0 ? listing.buyoutPrice : "None")); return listing; } @@ -127,18 +126,6 @@ public class AuctionController { .collect(Collectors.toList()); } - /** - * Get active listings by category - */ - public List getActiveListings(EnumAuctionCategory category) { - if (category == EnumAuctionCategory.ALL) { - return getActiveListings(); - } - return getActiveListings().stream() - .filter(l -> l.category == category) - .collect(Collectors.toList()); - } - /** * Search listings by item name */ @@ -153,7 +140,6 @@ public class AuctionController { /** * Get filtered and sorted auction listings. - * This is the main method for GUI queries. * @param filter The filter criteria * @return FilterResult containing matching listings and metadata */ @@ -171,7 +157,7 @@ public class AuctionController { // Calculate pagination int totalResults = results.size(); - int totalPages = (int) Math.ceil((double) totalResults / filter.pageSize); + int totalPages = Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); int startIndex = filter.page * filter.pageSize; int endIndex = Math.min(startIndex + filter.pageSize, totalResults); @@ -180,7 +166,7 @@ public class AuctionController { if (startIndex >= totalResults) { pageResults = new ArrayList<>(); } else { - pageResults = results.subList(startIndex, endIndex); + pageResults = new ArrayList<>(results.subList(startIndex, endIndex)); } return new FilterResult(pageResults, totalResults, filter.page, totalPages); @@ -200,61 +186,36 @@ public class AuctionController { case NEWEST_FIRST: comparator = Comparator.comparingLong(l -> -l.listingTime); break; - case OLDEST_FIRST: comparator = Comparator.comparingLong(l -> l.listingTime); break; - case PRICE_HIGH_TO_LOW: comparator = Comparator.comparingLong((AuctionListing l) -> -(l.currentBid > 0 ? l.currentBid : l.startingPrice)); break; - case PRICE_LOW_TO_HIGH: comparator = Comparator.comparingLong(l -> l.currentBid > 0 ? l.currentBid : l.startingPrice); break; - - case BUYOUT_HIGH_TO_LOW: - // Listings with buyout first, sorted high to low - comparator = Comparator - .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) - .thenComparingLong(l -> -l.buyoutPrice); - break; - - case BUYOUT_LOW_TO_HIGH: - // Listings with buyout first, sorted low to high - comparator = Comparator - .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) - .thenComparingLong(l -> l.buyoutPrice); - break; - case ENDING_SOON: comparator = Comparator.comparingLong(l -> l.expirationTime); break; - case MOST_TIME_LEFT: comparator = Comparator.comparingLong(l -> -l.expirationTime); break; - case NAME_A_TO_Z: - comparator = Comparator.comparing(l -> - l.item.getDisplayName().toLowerCase()); + comparator = Comparator.comparing(l -> l.item.getDisplayName().toLowerCase()); break; - case NAME_Z_TO_A: comparator = Comparator.comparing((AuctionListing l) -> l.item.getDisplayName().toLowerCase()).reversed(); break; - case MOST_BIDS: comparator = Comparator.comparingInt(l -> -l.bidCount); break; - case LEAST_BIDS: comparator = Comparator.comparingInt(l -> l.bidCount); break; - default: comparator = Comparator.comparingLong(l -> l.expirationTime); } @@ -262,28 +223,6 @@ public class AuctionController { listings.sort(comparator); } - /** - * Quick search with default sorting (ending soon) - */ - public List quickSearch(String searchText, EnumAuctionCategory category, int limit) { - AuctionFilter filter = new AuctionFilter() - .withSearch(searchText) - .withCategory(category) - .withSort(EnumAuctionSort.ENDING_SOON) - .withPage(0, limit); - - return getFilteredListings(filter).listings; - } - - /** - * Get listings sorted by a specific order - */ - public List getListingsSorted(EnumAuctionSort sortOrder) { - List results = new ArrayList<>(getActiveListings()); - sortListings(results, sortOrder); - return results; - } - /** * Result container for filtered queries */ @@ -315,7 +254,7 @@ public class AuctionController { } /** - * Get all listings by a seller (all statuses) + * Get all listings by a seller */ public List getListingsBySeller(UUID sellerUUID) { List ids = listingsBySeller.get(sellerUUID); @@ -354,7 +293,6 @@ public class AuctionController { /** * Get listings that need claiming by a player - * (items won, sales proceeds, cancelled/expired items) */ public List getClaimableListings(UUID playerUUID) { List claimable = new ArrayList<>(); @@ -391,7 +329,6 @@ public class AuctionController { /** * Place a bid on an auction - * @return true if bid was successful */ public boolean placeBid(int listingId, EntityPlayer bidder, long amount) { AuctionListing listing = listings.get(listingId); @@ -399,61 +336,51 @@ public class AuctionController { return false; } - // Check if bidder can afford if (!CurrencyController.Instance.canAfford(bidder, amount)) { return false; } - // Store previous bidder for refund and notification + // Store previous bidder for refund UUID previousBidder = listing.highBidderUUID; String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; - // Try to place bid if (!listing.placeBid(bidder.getUniqueID(), bidder.getCommandSenderName(), amount)) { return false; } - // Withdraw bid amount from new bidder CurrencyController.Instance.withdraw(bidder, amount); - // Refund previous bidder via claims system and notify + // Refund previous bidder if (previousBidder != null) { - CurrencyController.Instance.addClaim( - previousBidder, - previousBidAmount, - "Auction Outbid", - String.valueOf(listingId) - ); + CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, + "Auction Outbid", String.valueOf(listingId)); - // Notify previous bidder they were outbid - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyOutbid( - listing, previousBidder, previousBidderName, previousBidAmount, amount); - } + // Notify outbid player + notifyPlayer(previousBidder, EnumChatFormatting.RED + + "You have been outbid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + "! New bid: " + formatCurrency(amount)); - // Log refund - AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); + logEvent("OUTBID", String.format("ID=%d, OutbidPlayer=%s, PrevBid=%d, NewBid=%d", + listingId, previousBidderName, previousBidAmount, amount)); } - // Notify seller of new bid - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyNewBid(listing, bidder.getCommandSenderName(), amount); - } + // Notify seller + notifyPlayer(listing.sellerUUID, EnumChatFormatting.AQUA + + bidder.getCommandSenderName() + " placed a bid of " + formatCurrency(amount) + + " on your " + listing.item.getDisplayName() + "!"); - // Update bidder index addToBidderIndex(bidder.getUniqueID(), listingId); - save(); - logger.info("Bid placed on listing {} by {} for {}", - listingId, bidder.getCommandSenderName(), amount); + + logEvent("BID_PLACED", String.format("ID=%d, Bidder=%s, Amount=%d", + listingId, bidder.getCommandSenderName(), amount)); return true; } /** * Execute buyout on an auction - * @return true if buyout was successful */ public boolean buyout(int listingId, EntityPlayer buyer) { AuctionListing listing = listings.get(listingId); @@ -461,66 +388,50 @@ public class AuctionController { return false; } - // Check if buyer can afford if (!CurrencyController.Instance.canAfford(buyer, listing.buyoutPrice)) { return false; } - // Store previous bidder for refund and notification UUID previousBidder = listing.highBidderUUID; String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; - // Execute buyout if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { return false; } - // Withdraw buyout amount from buyer CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); - // Refund previous bidder via claims system and notify + // Refund previous bidder if (previousBidder != null) { - CurrencyController.Instance.addClaim( - previousBidder, - previousBidAmount, - "Auction Outbid (Buyout)", - String.valueOf(listingId) - ); + CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, + "Auction Outbid (Buyout)", String.valueOf(listingId)); - // Notify previous bidder they were outbid - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyOutbid( - listing, previousBidder, previousBidderName, previousBidAmount, listing.buyoutPrice); - } - - // Log refund - AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); + notifyPlayer(previousBidder, EnumChatFormatting.RED + + "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + " was outbid by a buyout. Your " + + formatCurrency(previousBidAmount) + " has been refunded."); } - // Update bidder index addToBidderIndex(buyer.getUniqueID(), listingId); - save(); - logger.info("Buyout executed on listing {} by {} for {}", - listingId, buyer.getCommandSenderName(), listing.buyoutPrice); - // Log buyout and send notifications - AuctionLogger.logBuyout(listing, buyer.getCommandSenderName()); + // Notify buyer and seller + notifyPlayer(buyer.getUniqueID(), EnumChatFormatting.GREEN + + "Buyout successful! Claim your " + listing.item.getDisplayName() + " from an Auctioneer."); - if (AuctionNotificationController.Instance != null) { - // Notify buyer they won - AuctionNotificationController.Instance.notifyAuctionWon(listing); - // Notify seller their item sold - AuctionNotificationController.Instance.notifyAuctionSold(listing); - } + notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + + "Your " + listing.item.getDisplayName() + " was bought out for " + + formatCurrency(listing.buyoutPrice) + "! Claim your proceeds."); + + logEvent("BUYOUT", String.format("ID=%d, Buyer=%s, Price=%d, Seller=%s", + listingId, buyer.getCommandSenderName(), listing.buyoutPrice, listing.sellerName)); return true; } /** * Cancel an auction listing - * @return true if cancellation was successful */ public boolean cancelListing(int listingId, EntityPlayer requester) { AuctionListing listing = listings.get(listingId); @@ -535,15 +446,16 @@ public class AuctionController { } save(); - logger.info("Listing {} cancelled by {}", - listingId, requester.getCommandSenderName()); - // Log and notify - AuctionLogger.logAuctionCancelled(listing, byAdmin); - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyAuctionCancelled(listing, byAdmin); + if (byAdmin) { + notifyPlayer(listing.sellerUUID, EnumChatFormatting.RED + + "Your auction for " + listing.item.getDisplayName() + + " was cancelled by an administrator."); } + logEvent("CANCELLED", String.format("ID=%d, CancelledBy=%s, ByAdmin=%b", + listingId, requester.getCommandSenderName(), byAdmin)); + return true; } @@ -551,27 +463,14 @@ public class AuctionController { /** * Claim seller proceeds from a sold auction - * @return true if claim was successful */ public boolean claimSellerProceeds(int listingId, EntityPlayer seller) { AuctionListing listing = listings.get(listingId); - if (listing == null) { + if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || + listing.status != EnumAuctionStatus.SOLD || listing.sellerClaimed) { return false; } - if (!listing.sellerUUID.equals(seller.getUniqueID())) { - return false; // Not the seller - } - - if (listing.status != EnumAuctionStatus.SOLD) { - return false; // Not sold - } - - if (listing.sellerClaimed) { - return false; // Already claimed - } - - // Deposit proceeds to seller long proceeds = listing.getSellerProceeds(); CurrencyController.Instance.deposit(seller, proceeds); @@ -579,95 +478,58 @@ public class AuctionController { checkAndMarkClaimed(listing); save(); - logger.info("Seller {} claimed {} from listing {}", - seller.getCommandSenderName(), proceeds, listingId); - - // Log the currency claim - AuctionLogger.logCurrencyClaimed(listing, seller.getCommandSenderName(), proceeds); + logEvent("PROCEEDS_CLAIMED", String.format("ID=%d, Seller=%s, Amount=%d", + listingId, seller.getCommandSenderName(), proceeds)); return true; } /** * Claim item from won auction - * @return The item if successful, null otherwise */ public ItemStack claimBuyerItem(int listingId, EntityPlayer buyer) { AuctionListing listing = listings.get(listingId); - if (listing == null) { + if (listing == null || listing.highBidderUUID == null || + !listing.highBidderUUID.equals(buyer.getUniqueID()) || + listing.status != EnumAuctionStatus.SOLD || listing.buyerClaimed) { return null; } - if (listing.highBidderUUID == null || - !listing.highBidderUUID.equals(buyer.getUniqueID())) { - return null; // Not the buyer - } - - if (listing.status != EnumAuctionStatus.SOLD) { - return null; // Not sold - } - - if (listing.buyerClaimed) { - return null; // Already claimed - } - listing.buyerClaimed = true; checkAndMarkClaimed(listing); save(); - logger.info("Buyer {} claimed item from listing {}", - buyer.getCommandSenderName(), listingId); - - // Log the item claim - AuctionLogger.logItemClaimed(listing, buyer.getCommandSenderName(), false); + logEvent("ITEM_CLAIMED", String.format("ID=%d, Buyer=%s, Item=%s", + listingId, buyer.getCommandSenderName(), listing.item.getDisplayName())); return listing.item.copy(); } /** - * Claim item from cancelled or expired auction (seller reclaiming) - * @return The item if successful, null otherwise + * Claim item from cancelled or expired auction */ public ItemStack claimExpiredItem(int listingId, EntityPlayer seller) { AuctionListing listing = listings.get(listingId); - if (listing == null) { + if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || + (listing.status != EnumAuctionStatus.CANCELLED && listing.status != EnumAuctionStatus.EXPIRED) || + listing.sellerClaimed) { return null; } - if (!listing.sellerUUID.equals(seller.getUniqueID())) { - return null; // Not the seller - } - - if (listing.status != EnumAuctionStatus.CANCELLED && - listing.status != EnumAuctionStatus.EXPIRED) { - return null; // Not claimable - } - - if (listing.sellerClaimed) { - return null; // Already claimed - } - listing.sellerClaimed = true; listing.status = EnumAuctionStatus.CLAIMED; save(); - logger.info("Seller {} reclaimed item from listing {}", - seller.getCommandSenderName(), listingId); - - // Log the item reclaim - AuctionLogger.logItemClaimed(listing, seller.getCommandSenderName(), true); + logEvent("ITEM_RECLAIMED", String.format("ID=%d, Seller=%s, Item=%s", + listingId, seller.getCommandSenderName(), listing.item.getDisplayName())); return listing.item.copy(); } - /** - * Mark listing as fully claimed if both parties have claimed - */ private void checkAndMarkClaimed(AuctionListing listing) { - if (listing.status == EnumAuctionStatus.SOLD) { - if (listing.sellerClaimed && listing.buyerClaimed) { - listing.status = EnumAuctionStatus.CLAIMED; - } + if (listing.status == EnumAuctionStatus.SOLD && + listing.sellerClaimed && listing.buyerClaimed) { + listing.status = EnumAuctionStatus.CLAIMED; } } @@ -675,58 +537,49 @@ public class AuctionController { /** * Process all expired auctions - * Call this periodically (e.g., every minute) */ public void processExpiredAuctions() { boolean changed = false; - int expiredCount = 0; - int soldCount = 0; for (AuctionListing listing : listings.values()) { if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { listing.finalize(); changed = true; - // If sold, add proceeds to seller's claims if (listing.status == EnumAuctionStatus.SOLD) { - soldCount++; - // Proceeds will be claimed manually, no automatic deposit - logger.info("Auction {} ended with sale to {} for {}", - listing.id, listing.highBidderName, listing.currentBid); + // Notify winner and seller + notifyPlayer(listing.highBidderUUID, EnumChatFormatting.GREEN + + "You won the auction for " + listing.item.getDisplayName() + + " for " + formatCurrency(listing.currentBid) + "! Claim your item."); - // Send notifications - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyAuctionWon(listing); - AuctionNotificationController.Instance.notifyAuctionSold(listing); - } + notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + + "Your " + listing.item.getDisplayName() + " sold for " + + formatCurrency(listing.currentBid) + "! Claim your proceeds."); + + logEvent("AUCTION_SOLD", String.format("ID=%d, Winner=%s, Price=%d, Seller=%s", + listing.id, listing.highBidderName, listing.currentBid, listing.sellerName)); } else { - expiredCount++; - logger.info("Auction {} expired without bids", listing.id); + notifyPlayer(listing.sellerUUID, EnumChatFormatting.GRAY + + "Your auction for " + listing.item.getDisplayName() + + " has expired with no bids. Claim your item."); - // Notify seller about expiration - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyAuctionExpired(listing); - } + logEvent("AUCTION_EXPIRED", String.format("ID=%d, Item=%s, Seller=%s", + listing.id, listing.item.getDisplayName(), listing.sellerName)); } } } if (changed) { save(); - // Log processing tick if anything happened - AuctionLogger.logProcessingTick(expiredCount, soldCount); } } /** * Clean up old claimed listings - * Call this periodically (e.g., daily) */ public void cleanupOldListings() { long claimExpirationMs = ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L; - if (claimExpirationMs <= 0) { - return; // No expiration - } + if (claimExpirationMs <= 0) return; long cutoffTime = System.currentTimeMillis() - claimExpirationMs; List toRemove = new ArrayList<>(); @@ -750,10 +603,71 @@ public class AuctionController { if (!toRemove.isEmpty()) { save(); - logger.info("Cleaned up {} old auction listings", toRemove.size()); + logEvent("CLEANUP", "Removed " + toRemove.size() + " old listings"); } } + // ==================== Notifications ==================== + + /** + * Send notification to player (or queue if offline) + */ + private void notifyPlayer(UUID playerUUID, String message) { + if (playerUUID == null) return; + + EntityPlayerMP player = getOnlinePlayer(playerUUID); + String fullMessage = EnumChatFormatting.YELLOW + "[Auction] " + message; + + if (player != null) { + player.addChatMessage(new ChatComponentText(fullMessage)); + } else { + pendingNotifications.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(fullMessage); + // Limit queue size + List queue = pendingNotifications.get(playerUUID); + while (queue.size() > 50) { + queue.remove(0); + } + } + } + + /** + * Send pending notifications on player login + */ + public void onPlayerLogin(EntityPlayerMP player) { + UUID uuid = player.getUniqueID(); + + List notifications = pendingNotifications.remove(uuid); + if (notifications != null && !notifications.isEmpty()) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + + "You have " + notifications.size() + " auction notification(s):")); + + for (String msg : notifications) { + player.addChatMessage(new ChatComponentText(msg)); + } + } + + // Check claimable items + int claimableCount = getClaimableListings(uuid).size(); + if (claimableCount > 0) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "You have " + claimableCount + " item(s) waiting to be claimed!")); + } + } + + private EntityPlayerMP getOnlinePlayer(UUID uuid) { + if (CustomNpcs.getServer() == null) return null; + + for (Object obj : CustomNpcs.getServer().getConfigurationManager().playerEntityList) { + EntityPlayerMP player = (EntityPlayerMP) obj; + if (player.getUniqueID().equals(uuid)) { + return player; + } + } + return null; + } + // ==================== Index Management ==================== private void addToSellerIndex(UUID seller, int listingId) { @@ -790,24 +704,25 @@ public class AuctionController { } } - // ==================== Permission Helpers ==================== + // ==================== Helpers ==================== - /** - * Get maximum listings allowed for a player - */ public int getMaxListings(EntityPlayer player) { - // TODO: Check for permission-based overrides - // e.g., CustomNpcsPermissions.hasCustomPermission(player, "cnpc.auction.slots.10") return ConfigMarket.DefaultMaxListings; } + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + private void logEvent(String event, String details) { + LogWriter.info(LOG_PREFIX + event + ": " + details); + } + // ==================== Persistence ==================== public void save() { File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } + if (saveDir == null) return; try { NBTTagCompound compound = new NBTTagCompound(); @@ -823,9 +738,7 @@ public class AuctionController { File backup = new File(saveDir, "auctions.dat_old"); if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } + if (backup.exists()) backup.delete(); file.renameTo(backup); } @@ -835,15 +748,13 @@ public class AuctionController { fos.close(); } catch (Exception e) { - logger.error("Error saving auctions", e); + LogWriter.error(LOG_PREFIX + "Error saving auctions", e); } } public void load() { File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } + if (saveDir == null) return; listings.clear(); @@ -851,9 +762,7 @@ public class AuctionController { File file = new File(saveDir, "auctions.dat"); if (!file.exists()) { file = new File(saveDir, "auctions.dat_old"); - if (!file.exists()) { - return; - } + if (!file.exists()) return; } FileInputStream fis = new FileInputStream(file); @@ -861,9 +770,7 @@ public class AuctionController { fis.close(); nextId = compound.getInteger("NextId"); - if (nextId < 1) { - nextId = 1; - } + if (nextId < 1) nextId = 1; NBTTagList listingList = compound.getTagList("Listings", 10); for (int i = 0; i < listingList.tagCount(); i++) { @@ -874,42 +781,22 @@ public class AuctionController { rebuildIndices(); - logger.info("Loaded {} auction listings", listings.size()); - - // Log system startup - int activeCount = getActiveListingCount(); - int pendingClaims = 0; - for (AuctionListing listing : listings.values()) { - if (!listing.sellerClaimed || !listing.buyerClaimed) { - if (listing.status == EnumAuctionStatus.SOLD || - listing.status == EnumAuctionStatus.EXPIRED || - listing.status == EnumAuctionStatus.CANCELLED) { - pendingClaims++; - } - } - } - AuctionLogger.logSystemStartup(activeCount, pendingClaims); + logEvent("SYSTEM_STARTUP", String.format("Loaded %d listings, %d active", + listings.size(), getActiveListingCount())); } catch (Exception e) { - logger.error("Error loading auctions", e); - AuctionLogger.logError("Loading auctions", e); + LogWriter.error(LOG_PREFIX + "Error loading auctions", e); } } // ==================== Statistics ==================== - /** - * Get total number of active listings - */ public int getActiveListingCount() { return (int) listings.values().stream() .filter(l -> l.status == EnumAuctionStatus.ACTIVE) .count(); } - /** - * Get total number of all listings - */ public int getTotalListingCount() { return listings.size(); } diff --git a/src/main/java/noppes/npcs/controllers/AuctionLogger.java b/src/main/java/noppes/npcs/controllers/AuctionLogger.java deleted file mode 100644 index 4c5264319..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionLogger.java +++ /dev/null @@ -1,269 +0,0 @@ -package noppes.npcs.controllers; - -import noppes.npcs.LogWriter; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionListing; - -import java.text.SimpleDateFormat; -import java.util.Date; - -/** - * Handles auction history logging to the CNPC+ logging system. - * All auction events are recorded for administrative review. - */ -public class AuctionLogger { - private static final String PREFIX = "[Auction] "; - private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - /** - * Log when a new auction listing is created - */ - public static void logListingCreated(AuctionListing listing) { - String msg = String.format("%sLISTING_CREATED: ID=%d, Seller=%s (%s), Item=%s x%d, StartPrice=%d, Buyout=%s, Duration=%s", - PREFIX, - listing.id, - listing.sellerName, - listing.sellerUUID.toString(), - listing.item.getDisplayName(), - listing.item.stackSize, - listing.startingPrice, - listing.buyoutPrice > 0 ? String.valueOf(listing.buyoutPrice) : "None", - formatDuration(listing.expirationTime - listing.listingTime) - ); - LogWriter.info(msg); - } - - /** - * Log when a bid is placed - */ - public static void logBidPlaced(AuctionListing listing, String bidderName, long bidAmount) { - String msg = String.format("%sBID_PLACED: ID=%d, Item=%s, Bidder=%s, Amount=%d %s, Previous=%d %s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - bidderName, - bidAmount, - ConfigMarket.CurrencyName, - listing.currentBid, - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log when a player is outbid - */ - public static void logOutbid(AuctionListing listing, String outbidPlayerName, long previousBid, - String newBidderName, long newBid) { - String msg = String.format("%sOUTBID: ID=%d, Item=%s, OutbidPlayer=%s (had %d), NewBidder=%s (bid %d)", - PREFIX, - listing.id, - listing.item.getDisplayName(), - outbidPlayerName, - previousBid, - newBidderName, - newBid - ); - LogWriter.info(msg); - } - - /** - * Log when a buyout occurs - */ - public static void logBuyout(AuctionListing listing, String buyerName) { - String msg = String.format("%sBUYOUT: ID=%d, Item=%s, Buyer=%s, Price=%d %s, Seller=%s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - buyerName, - listing.buyoutPrice, - ConfigMarket.CurrencyName, - listing.sellerName - ); - LogWriter.info(msg); - } - - /** - * Log when an auction is won (ended with bids) - */ - public static void logAuctionWon(AuctionListing listing) { - String msg = String.format("%sAUCTION_WON: ID=%d, Item=%s, Winner=%s (%s), FinalPrice=%d %s, Seller=%s, SellerProceeds=%d %s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.highBidderName, - listing.highBidderUUID != null ? listing.highBidderUUID.toString() : "N/A", - listing.currentBid, - ConfigMarket.CurrencyName, - listing.sellerName, - listing.getSellerProceeds(), - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log when an auction sells (same as won, for notification controller compatibility) - */ - public static void logAuctionSold(AuctionListing listing) { - String msg = String.format("%sAUCTION_SOLD: ID=%d, Item=%s, Buyer=%s, SalePrice=%d %s, Seller=%s (%s), Proceeds=%d %s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.highBidderName, - listing.currentBid, - ConfigMarket.CurrencyName, - listing.sellerName, - listing.sellerUUID.toString(), - listing.getSellerProceeds(), - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log when an auction expires with no bids - */ - public static void logAuctionExpired(AuctionListing listing) { - String msg = String.format("%sAUCTION_EXPIRED: ID=%d, Item=%s, Seller=%s (%s), StartPrice=%d %s, Duration=%s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.sellerName, - listing.sellerUUID.toString(), - listing.startingPrice, - ConfigMarket.CurrencyName, - formatDuration(listing.expirationTime - listing.listingTime) - ); - LogWriter.info(msg); - } - - /** - * Log when an auction is cancelled - */ - public static void logAuctionCancelled(AuctionListing listing, boolean byAdmin) { - String msg = String.format("%sAUCTION_CANCELLED: ID=%d, Item=%s, Seller=%s, CancelledBy=%s, HadBids=%b", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.sellerName, - byAdmin ? "ADMIN" : "SELLER", - listing.currentBid > 0 - ); - LogWriter.info(msg); - } - - /** - * Log when an item is claimed - */ - public static void logItemClaimed(AuctionListing listing, String claimerName, boolean isSeller) { - String claimType = isSeller ? - (listing.status == EnumAuctionStatus.EXPIRED ? "RECLAIMED_EXPIRED" : "CLAIMED_PROCEEDS") : - "CLAIMED_WON_ITEM"; - - String msg = String.format("%s%s: ID=%d, Item=%s, Claimer=%s, Status=%s", - PREFIX, - claimType, - listing.id, - listing.item.getDisplayName(), - claimerName, - listing.status.name() - ); - LogWriter.info(msg); - } - - /** - * Log currency claimed from auction - */ - public static void logCurrencyClaimed(AuctionListing listing, String claimerName, long amount) { - String msg = String.format("%sCURRENCY_CLAIMED: ID=%d, Claimer=%s, Amount=%d %s", - PREFIX, - listing.id, - claimerName, - amount, - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log refund of bid to outbid player - */ - public static void logBidRefunded(AuctionListing listing, String playerName, long amount) { - String msg = String.format("%sBID_REFUNDED: ID=%d, Player=%s, Amount=%d %s", - PREFIX, - listing.id, - playerName, - amount, - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log admin actions on auctions - */ - public static void logAdminAction(String adminName, String action, AuctionListing listing) { - String msg = String.format("%sADMIN_ACTION: Admin=%s, Action=%s, ListingID=%d, Item=%s, Seller=%s", - PREFIX, - adminName, - action, - listing.id, - listing.item.getDisplayName(), - listing.sellerName - ); - LogWriter.info(msg); - } - - /** - * Log auction system startup - */ - public static void logSystemStartup(int activeListings, int pendingClaims) { - String msg = String.format("%sSYSTEM_STARTUP: ActiveListings=%d, PendingClaims=%d, Timestamp=%s", - PREFIX, - activeListings, - pendingClaims, - dateFormat.format(new Date()) - ); - LogWriter.info(msg); - } - - /** - * Log auction processing tick - */ - public static void logProcessingTick(int expired, int completed) { - if (expired > 0 || completed > 0) { - String msg = String.format("%sPROCESSING_TICK: ExpiredListings=%d, CompletedAuctions=%d", - PREFIX, - expired, - completed - ); - LogWriter.info(msg); - } - } - - /** - * Log errors in the auction system - */ - public static void logError(String context, Exception e) { - String msg = String.format("%sERROR: Context=%s, Message=%s", - PREFIX, - context, - e.getMessage() - ); - LogWriter.error(msg, e); - } - - /** - * Format duration in a human-readable format - */ - private static String formatDuration(long millis) { - long hours = millis / (1000 * 60 * 60); - if (hours >= 24) { - long days = hours / 24; - return days + "d " + (hours % 24) + "h"; - } - return hours + "h"; - } -} diff --git a/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java b/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java deleted file mode 100644 index 48da6d345..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java +++ /dev/null @@ -1,226 +0,0 @@ -package noppes.npcs.controllers; - -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.LogWriter; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; - -import java.util.*; - -/** - * Handles auction notifications for players. - * Sends messages for outbid, won, sold, expired events. - */ -public class AuctionNotificationController { - public static AuctionNotificationController Instance; - - // Pending notifications for offline players (UUID -> List of notifications) - private Map> pendingNotifications = new HashMap<>(); - - public AuctionNotificationController() { - Instance = this; - } - - /** - * Notify a player they have been outbid - */ - public void notifyOutbid(AuctionListing listing, UUID outbidPlayerUUID, - String outbidPlayerName, long previousBid, long newBid) { - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + - "You have been outbid on " + EnumChatFormatting.WHITE + - listing.item.getDisplayName() + EnumChatFormatting.RED + - "! New bid: " + formatCurrency(newBid) + - " (Your bid: " + formatCurrency(previousBid) + " has been refunded to claims)"; - - sendOrQueueNotification(outbidPlayerUUID, NotificationType.OUTBID, message, listing.id); - - // Log the outbid event - AuctionLogger.logOutbid(listing, outbidPlayerName, previousBid, listing.highBidderName, newBid); - } - - /** - * Notify a player they won an auction - */ - public void notifyAuctionWon(AuctionListing listing) { - if (listing.highBidderUUID == null) return; - - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "Congratulations! You won the auction for " + EnumChatFormatting.WHITE + - listing.item.getDisplayName() + EnumChatFormatting.GREEN + - " for " + formatCurrency(listing.currentBid) + "! Visit an Auctioneer to claim your item."; - - sendOrQueueNotification(listing.highBidderUUID, NotificationType.WON, message, listing.id); - - // Log the win - AuctionLogger.logAuctionWon(listing); - } - - /** - * Notify seller their auction sold - */ - public void notifyAuctionSold(AuctionListing listing) { - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "Your " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.GREEN + " sold for " + formatCurrency(listing.currentBid) + - "! Visit an Auctioneer to claim " + formatCurrency(listing.getSellerProceeds()) + "."; - - sendOrQueueNotification(listing.sellerUUID, NotificationType.SOLD, message, listing.id); - - // Log the sale - AuctionLogger.logAuctionSold(listing); - } - - /** - * Notify seller their auction expired with no bids - */ - public void notifyAuctionExpired(AuctionListing listing) { - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + - "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.GRAY + " has expired with no bids. Visit an Auctioneer to reclaim your item."; - - sendOrQueueNotification(listing.sellerUUID, NotificationType.EXPIRED, message, listing.id); - - // Log the expiration - AuctionLogger.logAuctionExpired(listing); - } - - /** - * Notify player their auction was cancelled - */ - public void notifyAuctionCancelled(AuctionListing listing, boolean byAdmin) { - String message; - if (byAdmin) { - message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + - "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was cancelled by an administrator."; - } else { - message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + - "You cancelled your auction for " + EnumChatFormatting.WHITE + - listing.item.getDisplayName() + EnumChatFormatting.GRAY + "."; - } - - sendOrQueueNotification(listing.sellerUUID, NotificationType.CANCELLED, message, listing.id); - - // Log the cancellation - AuctionLogger.logAuctionCancelled(listing, byAdmin); - } - - /** - * Notify about a new bid placed - */ - public void notifyNewBid(AuctionListing listing, String bidderName, long bidAmount) { - // Notify seller about new bid - String sellerMessage = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.AQUA + - bidderName + " placed a bid of " + formatCurrency(bidAmount) + - " on your " + listing.item.getDisplayName() + "!"; - - sendOrQueueNotification(listing.sellerUUID, NotificationType.NEW_BID, sellerMessage, listing.id); - - // Log the bid - AuctionLogger.logBidPlaced(listing, bidderName, bidAmount); - } - - /** - * Send notification immediately if player online, otherwise queue for later - */ - private void sendOrQueueNotification(UUID playerUUID, NotificationType type, String message, int listingId) { - EntityPlayerMP player = getOnlinePlayer(playerUUID); - - if (player != null) { - player.addChatMessage(new ChatComponentText(message)); - } else { - // Queue for when player logs in - AuctionNotification notification = new AuctionNotification(type, message, listingId); - - if (!pendingNotifications.containsKey(playerUUID)) { - pendingNotifications.put(playerUUID, new ArrayList<>()); - } - pendingNotifications.get(playerUUID).add(notification); - - // Limit queue size per player - List queue = pendingNotifications.get(playerUUID); - while (queue.size() > 50) { - queue.remove(0); - } - } - } - - /** - * Called when a player logs in - send any pending notifications - */ - public void onPlayerLogin(EntityPlayerMP player) { - UUID uuid = player.getUniqueID(); - - if (pendingNotifications.containsKey(uuid)) { - List notifications = pendingNotifications.remove(uuid); - - if (!notifications.isEmpty()) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + - "You have " + notifications.size() + " auction notification(s):")); - - for (AuctionNotification notification : notifications) { - player.addChatMessage(new ChatComponentText(notification.message)); - } - } - } - - // Also check for claimable items - if (AuctionController.Instance != null) { - int claimableCount = AuctionController.Instance.getClaimableListings(uuid).size(); - if (claimableCount > 0) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "You have " + claimableCount + " item(s) waiting to be claimed!")); - } - } - } - - private EntityPlayerMP getOnlinePlayer(UUID uuid) { - if (noppes.npcs.CustomNpcs.getServer() == null) return null; - - for (Object obj : noppes.npcs.CustomNpcs.getServer().getConfigurationManager().playerEntityList) { - EntityPlayerMP player = (EntityPlayerMP) obj; - if (player.getUniqueID().equals(uuid)) { - return player; - } - } - return null; - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - /** - * Notification types for categorization - */ - public enum NotificationType { - OUTBID, - WON, - SOLD, - EXPIRED, - CANCELLED, - NEW_BID - } - - /** - * Stored notification for offline players - */ - private static class AuctionNotification { - public final NotificationType type; - public final String message; - public final int listingId; - public final long timestamp; - - public AuctionNotification(NotificationType type, String message, int listingId) { - this.type = type; - this.message = message; - this.listingId = listingId; - this.timestamp = System.currentTimeMillis(); - } - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java index a4a3d1d5a..70242179a 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -1,7 +1,6 @@ package noppes.npcs.controllers.data; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionSort; /** @@ -13,9 +12,6 @@ public class AuctionFilter { /** Search text to match against item names */ public String searchText = ""; - /** Category to filter by (ALL for no category filter) */ - public EnumAuctionCategory category = EnumAuctionCategory.ALL; - /** How to sort results */ public EnumAuctionSort sortOrder = EnumAuctionSort.ENDING_SOON; @@ -55,15 +51,6 @@ public class AuctionFilter { return filter; } - /** - * Create a filter with category - */ - public static AuctionFilter category(EnumAuctionCategory category) { - AuctionFilter filter = new AuctionFilter(); - filter.category = category; - return filter; - } - /** * Create a filter for a specific seller */ @@ -80,11 +67,6 @@ public class AuctionFilter { return this; } - public AuctionFilter withCategory(EnumAuctionCategory category) { - this.category = category; - return this; - } - public AuctionFilter withSort(EnumAuctionSort sort) { this.sortOrder = sort; return this; @@ -135,11 +117,6 @@ public class AuctionFilter { } } - // Category filter - if (category != EnumAuctionCategory.ALL && listing.category != category) { - return false; - } - // Price range filter (uses current bid or starting price) long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; if (minPrice > 0 && price < minPrice) { @@ -180,7 +157,6 @@ public class AuctionFilter { */ public void reset() { searchText = ""; - category = EnumAuctionCategory.ALL; sortOrder = EnumAuctionSort.ENDING_SOON; minPrice = 0; maxPrice = 0; @@ -196,7 +172,6 @@ public class AuctionFilter { */ public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("SearchText", searchText); - compound.setInteger("Category", category.ordinal()); compound.setInteger("SortOrder", sortOrder.ordinal()); compound.setLong("MinPrice", minPrice); compound.setLong("MaxPrice", maxPrice); @@ -215,11 +190,6 @@ public class AuctionFilter { public void readFromNBT(NBTTagCompound compound) { searchText = compound.getString("SearchText"); - int catOrdinal = compound.getInteger("Category"); - if (catOrdinal >= 0 && catOrdinal < EnumAuctionCategory.values().length) { - category = EnumAuctionCategory.values()[catOrdinal]; - } - int sortOrdinal = compound.getInteger("SortOrder"); if (sortOrdinal >= 0 && sortOrdinal < EnumAuctionSort.values().length) { sortOrder = EnumAuctionSort.values()[sortOrdinal]; @@ -240,7 +210,6 @@ public class AuctionFilter { public String toString() { return "AuctionFilter{" + "search='" + searchText + '\'' + - ", category=" + category + ", sort=" + sortOrder + ", price=" + minPrice + "-" + maxPrice + ", buyoutOnly=" + buyoutOnly + diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index 24c778439..5366e367b 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -4,7 +4,6 @@ import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.controllers.AuctionBlacklist; @@ -48,9 +47,6 @@ public class AuctionListing { // Status public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; - // Category for filtering - public EnumAuctionCategory category = EnumAuctionCategory.MISC; - // Claim tracking public boolean sellerClaimed = false; // Has seller claimed their proceeds? public boolean buyerClaimed = false; // Has buyer claimed their item? @@ -87,7 +83,6 @@ public class AuctionListing { this.duration = duration; this.listingTime = System.currentTimeMillis(); this.expirationTime = listingTime + duration.getMillis(); - this.category = EnumAuctionCategory.categorize(item); this.status = EnumAuctionStatus.ACTIVE; } @@ -351,7 +346,6 @@ public class AuctionListing { compound.setLong("ExpirationTime", expirationTime); compound.setInteger("Duration", duration.ordinal()); compound.setInteger("Status", status.ordinal()); - compound.setInteger("Category", category.ordinal()); compound.setBoolean("SellerClaimed", sellerClaimed); compound.setBoolean("BuyerClaimed", buyerClaimed); compound.setInteger("BidCount", bidCount); @@ -396,13 +390,6 @@ public class AuctionListing { status = EnumAuctionStatus.ACTIVE; } - int categoryOrdinal = compound.getInteger("Category"); - if (categoryOrdinal >= 0 && categoryOrdinal < EnumAuctionCategory.values().length) { - category = EnumAuctionCategory.values()[categoryOrdinal]; - } else { - category = EnumAuctionCategory.MISC; - } - sellerClaimed = compound.getBoolean("SellerClaimed"); buyerClaimed = compound.getBoolean("BuyerClaimed"); bidCount = compound.getInteger("BidCount"); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 466acfb99..88c10066c 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1437,3 +1437,73 @@ trail.heart=Heart trail.angryvillager=AngryVillager trail.happyvillager=HappyVillager trail.custom=Custom + +auction.title=Auction House +auction.browse=Browse +auction.myListings=My Listings +auction.myBids=My Bids +auction.claims=Claims +auction.sell=Sell +auction.search=Search +auction.refresh=Refresh +auction.viewDetails=View Details +auction.placeBid=Place Bid +auction.buyout=Buyout +auction.cancel=Cancel +auction.prevPage=< Prev +auction.nextPage=Next > +auction.noResults=No results +auction.page=Page %d of %d +auction.balance=Balance: %s +auction.listings=%d listings +auction.browseTitle=Browse Auctions +auction.myListingsTitle=My Listings +auction.myBidsTitle=My Active Bids +auction.claimsTitle=Claim Items +auction.sellTitle=Create Listing +auction.currentPrice=Current Price: +auction.minimumBid=Minimum Bid: +auction.yourBalance=Your Balance: +auction.timeLeft=Time Left: +auction.yourBid=Your Bid: +auction.minBid=Min Bid +auction.bidPlus10=+10% +auction.bidPlus25=+25% +auction.insufficientFunds=Insufficient funds! +auction.auctionEnded=Auction has ended! +auction.ended=Ended +auction.item=Item: +auction.seller=Seller: +auction.startingPrice=Starting Price: +auction.buyoutPrice=Buyout Price: +auction.noBuyout=None +auction.bidCount=Bid Count: +auction.highBidder=High Bidder: +auction.noBids=No bids +auction.claim=Claim +auction.createListing=Create Listing +auction.duration=Duration: +auction.listingFee=Listing Fee: +auction.create=Create +auction.sort.newest=Newest First +auction.sort.oldest=Oldest First +auction.sort.priceHigh=Most Expensive +auction.sort.priceLow=Least Expensive +auction.sort.endingSoon=Ending Soon +auction.sort.mostTime=Most Time Left +auction.sort.nameAZ=Name: A to Z +auction.sort.nameZA=Name: Z to A +auction.sort.mostBids=Most Bids +auction.sort.leastBids=Least Bids +auction.duration.short=Short (12h) +auction.duration.medium=Medium (24h) +auction.duration.long=Long (48h) +auction.duration.veryLong=Very Long (72h) +auction.placeItemBelow=Place item in slot below: +auction.noItemSelected=(No item selected) +auction.noBuyoutHint=(0 = no buyout) +auction.quantity=Quantity: +auction.totalBids=Total Bids: +auction.noBidsYet=No bids yet +auction.auctionDetails=Auction Details +auction.currentBid=Current Bid: From 77c8e418ebdc3bc3d7c65be74c13f2b766b65f9b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 10:29:57 +0000 Subject: [PATCH 14/21] Update market plan doc: convert to implementation status recap - Changed from planning document to status tracking document - Documented completed phases (2-6) - Documented not yet implemented phases (1, 7) - Documented removed features (categories, separate controllers) - Added file inventory and localization keys reference - Added design decisions summary - Added next steps section --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 2759 +++----------------------- 1 file changed, 262 insertions(+), 2497 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index e9b4cd0ce..4d62a7772 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -1,2559 +1,324 @@ -# CustomNPC+ 1.11 Market Update - Implementation Plan +# CustomNPC+ 1.11 Market Update - Implementation Status ## Overview -This document outlines the implementation plan for the CustomNPC+ 1.11 update, focusing on three major features: +This document tracks the implementation status of the CustomNPC+ 1.11 Market Update, which includes: -1. **Auction House** - A new NPC role allowing players to access a global auction system -2. **Currency System** - Built-in currency integration with VaultAPI for Bukkit hybrid servers -3. **Trader Enhancements** - Stock/Inventory system with per-player/per-server options and reset timers +1. **Auction House** - Global auction system with bidding, buyout, and claims +2. **Currency System** - Built-in currency with VaultAPI integration for Bukkit hybrid servers +3. **Trader Enhancements** - Stock/inventory system with reset timers --- -## Design Decisions (Confirmed) +## Implementation Status Summary -### 1. Auction Refund System -**Decision:** When a player is outbid, the refund goes to **claims** (not direct to balance). -- Players can manage their bids from claims -- If auction ends while player is offline, they can claim their money later -- This keeps the flow consistent with item claims -- **Modify Bid Feature**: Players can use a "Modify Bid" button that auto-reclaims their money and applies it to a higher bid amount - -### 2. Permission System -**Decision:** Use the **existing CustomNPC+ permission system** used for Profiles. -- Pattern: `customnpcs.auction.slots.X` where X is the max slot count -- Implementation mirrors `ProfileController.allowSlotPermission()` approach -- Uses `CustomNpcsPermissions.hasCustomPermission(player, permissionString)` -- **Default**: 5 auction slots per player (configurable) - -### 3. Virtual Currency Storage -**Decision:** Currency is **NOT profile slot bound** - all profile slots share the same currency pool per player. -- Stored in PlayerData outside of slot components -- Balance persists across profile slot switches -- Balance viewing system to be added later (for now, visible via GUI only) - -### 4. Currency Display Icon -**Decision:** Use the **existing gold coin** from the mod (`CustomItems.coinGold`). -- Already has textures and localization for all supported languages -- Available coin types: Wood, Stone, Bronze, Iron, Gold, Diamond, Emerald - -### 5. Trader Stock Reset Timer Display -**Decision:** Show "Stocks reset in X hours" as a **banner ABOVE the GUI**. -- Positioned at a higher Y level than the main GUI -- Not overlayed on the GUI content -- Displayed when opening the trader interface - -### 6. Item Trading Restrictions -**Decision:** Implement the following restrictions for auction listings: -- **Untradeable Attribute**: New item attribute (`CNPC_Untradeable`) that prevents auction listing -- **ProfileSlot Bound Items**: Use existing `cnpc_profile_slot` NBT tag (from `ProfileSlotRequirement.java`) -- **Soulbound Items**: Use existing `cnpc_soulbind` NBT tag (from `SoulbindRequirement.java`) -- **Auction House Blacklist**: Configurable list of items that cannot be auctioned - -### 7. Admin Auction Management -**Decision:** Create an **Auction Management system** with a Global Admin button. -- Admins can modify, remove, and edit auction listings in real-time -- Accessible via special admin GUI -- Permission-gated for server operators - -### 8. Auction Timing & Bidding Rules -**Decision:** Configurable auction timing with snipe protection. -- **Claim Expiration**: 30 days by default (configurable) - claims auto-expire after this period -- **Bid Increment**: Percentage-based minimum increment (configurable, e.g., 5%) -- **Snipe Protection**: When a bid is placed near auction end, extend time TO 5 minutes remaining (never higher) - - Extension amount is configurable - - Example: If 2 minutes left and bid placed, extend to 5 minutes. If 10 minutes left, no extension. -- **Auction Durations**: Configurable default options (2h, 12h, 24h, 48h) +| Phase | Feature | Status | +|-------|---------|--------| +| Phase 1 | Currency System Foundation | **NOT STARTED** | +| Phase 2 | Trader Enhancements | **COMPLETED** | +| Phase 3 | Item Attributes & Restrictions | **COMPLETED** | +| Phase 4 | Auction House Core | **COMPLETED** | +| Phase 5 | Network Packets & Permissions | **COMPLETED** | +| Phase 6 | Auction GUI & Notifications | **COMPLETED** | +| Phase 7 | Admin Management | **NOT STARTED** | --- -## Table of Contents +## Completed Features -1. [Design Decisions](#design-decisions-confirmed) -2. [Architecture Overview](#architecture-overview) -3. [Feature 1: Currency System](#feature-1-currency-system) -4. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) -5. [Feature 3: Auction House](#feature-3-auction-house) -6. [Feature 4: Item Attributes & Restrictions](#feature-4-item-attributes--restrictions) -7. [Feature 5: Admin Auction Management](#feature-5-admin-auction-management) -8. [Configuration Reference](#configuration-reference) -9. [Implementation Order](#implementation-order) -10. [File Summary](#file-summary) -11. [Testing Strategy](#testing-strategy) +### Phase 2: Trader Enhancements (COMPLETED) +- `EnumStockReset.java` - Stock reset timing options +- `TraderStock.java` - Stock tracking with per-player and per-server modes +- Modified `RoleTrader.java` with stock and currency support +- Modified `ContainerNPCTrader.java` for stock/currency transactions +- Stock persistence via NBT + +### Phase 3: Item Attributes & Restrictions (COMPLETED) +- `ItemTradeAttribute.java` - Untradeable, ProfileSlot Bound, Soulbound checks +- `AuctionBlacklist.java` - Item/mod/NBT blacklist system +- Integration with AuctionListing validation + +### Phase 4: Auction House Core (COMPLETED) +- `EnumAuctionDuration.java` - Configurable durations with lang keys +- `EnumAuctionStatus.java` - Auction state management +- `EnumAuctionSort.java` - Sort options with lang keys +- `AuctionListing.java` - Complete listing data structure with bidding logic +- `AuctionFilter.java` - Search/filter criteria (categories removed) +- `AuctionController.java` - Consolidated controller with: + - Listing creation/management + - Bidding and buyout logic + - Claim system for items and currency refunds + - Inline logging (via LogWriter) + - Inline player notifications (via chat messages) + - Pending notification queue for offline players + - Snipe protection + - Sales tax and listing fees +- `AuctionClaim.java` - Claim data for items/money +- `RoleAuctioneer.java` - Auctioneer NPC role +- `ContainerAuction.java` - Auction container +- `ConfigMarket.java` - All market configuration options + +### Phase 5: Network Packets & Permissions (COMPLETED) +- `AuctionActionPacket.java` - Client-to-server auction actions +- `AuctionDataPacket.java` - Server-to-client data sync +- Packet registration in `PacketHandler.java` +- Permission nodes added to `CustomNpcsPermissions.java` +- API methods added to `ICustomNpc.java` + +### Phase 6: Auction GUI & Notifications (COMPLETED) +- `GuiAuction.java` - Main auction browser with tabs +- `SubGuiAuctionCreate.java` - Create listing sub-GUI +- `SubGuiAuctionBid.java` - Place bid sub-GUI +- `SubGuiAuctionDetails.java` - View listing details sub-GUI +- All GUI strings use lang keys via `StatCollector.translateToLocal()` +- Localization keys added to `en_US.lang` --- -## Architecture Overview +## Removed/Changed Features -### Current Codebase Patterns +### Removed: Category System +Originally planned to have item categories (Weapons, Armor, Tools, etc.) for filtering. This was **removed** to simplify the initial implementation: +- Deleted `EnumAuctionCategory.java` +- Removed category field from `AuctionListing.java` +- Removed category filtering from `AuctionFilter.java` +- Removed category buttons from `GuiAuction.java` -The implementation will follow existing patterns found in the codebase: +**Rationale:** Categories can be added in a future update. Current implementation uses search-based filtering. -| Pattern | Used By | Will Be Applied To | -|---------|---------|-------------------| -| Singleton Controller | `BankController`, `QuestController` | `AuctionController`, `CurrencyController` | -| Role Interface | `RoleTrader`, `RoleBank` | `RoleAuctioneer` | -| Player Data Extension | `PlayerBankData`, `PlayerQuestData` | `PlayerAuctionData`, `PlayerCurrencyData` | -| Cooldown System | `Quest.repeat`, `JobItemGiver` | Trader Stock Reset | -| Optional Addon | `PixelmonHelper`, `DBCAddon` | `VaultHelper` | -| NBT Serialization | All data classes | All new data classes | -| Cache System | `Market.java`, `CacheHashMap` | Auction listings cache | +### Consolidated: Controller Architecture +Originally planned separate controllers: +- ~~`AuctionLogger.java`~~ - **CONSOLIDATED** into `AuctionController.logEvent()` +- ~~`AuctionNotificationController.java`~~ - **CONSOLIDATED** into `AuctionController.notifyPlayer()` -### Package Structure for New Features +**Rationale:** Reduces complexity, all auction logic in one place. -``` -noppes/npcs/ -├── controllers/ -│ ├── AuctionController.java # Manages global auction house -│ ├── CurrencyController.java # Manages currency settings -│ └── data/ -│ ├── AuctionListing.java # Single auction listing -│ ├── AuctionData.java # World auction data -│ ├── PlayerAuctionData.java # Per-player auction history -│ ├── PlayerCurrencyData.java # Per-player currency balance -│ └── TraderStock.java # Trader stock configuration -├── roles/ -│ └── RoleAuctioneer.java # Auctioneer NPC role -├── containers/ -│ ├── ContainerAuction*.java # Auction GUI containers -│ └── ContainerNPCTraderStock.java # Stock management container -├── client/gui/ -│ ├── player/ -│ │ └── GuiAuction*.java # Player-facing auction GUIs -│ └── roles/ -│ └── GuiNpcAuctioneerSetup.java # Auctioneer configuration GUI - -kamkeel/npcs/ -├── controllers/ -│ └── CurrencyController.java # Currency management -├── network/packets/ -│ ├── data/ -│ │ ├── AuctionSyncPacket.java # Sync auction data -│ │ └── CurrencySyncPacket.java # Sync currency data -│ └── player/ -│ ├── AuctionActionPacket.java # Auction actions (bid, buy, sell) -│ └── CurrencyActionPacket.java # Currency transactions -└── compat/ - └── VaultHelper.java # VaultAPI integration -``` +### Changed: Localization Approach +- All client GUI strings now use `StatCollector.translateToLocal()` with lang keys +- `EnumAuctionSort` uses lang keys for display names +- `EnumAuctionDuration` uses lang keys for display names --- -## Feature 1: Currency System - -### 1.1 Overview - -The currency system provides a unified economy layer that can: -- Use VaultAPI when available on Bukkit hybrid servers (Cauldron, Thermos, Mohist) -- Fall back to an internal item-based currency when VaultAPI is unavailable -- Be configured per-world or globally - -### 1.2 VaultAPI Integration - -#### 1.2.1 Dependency Setup - -**File: `dependencies.gradle`** -```gradle -// VaultAPI - compile-only for optional integration -compileOnly("net.milkbowl.vault:VaultAPI:1.4") { - transitive = false -} -``` - -**File: `repositories.gradle`** -```gradle -maven { - name = "vault-repo" - url = "https://nexus.hc.to/content/repositories/pub_releases/" -} -``` - -> **Note:** VaultAPI 1.4.1 works with 1.7.10 Bukkit hybrid servers. The dependency is compile-only so the mod works without Vault present. - -#### 1.2.2 VaultHelper Class - -**File: `src/main/java/kamkeel/npcs/compat/VaultHelper.java`** - -```java -package kamkeel.npcs.compat; - -import cpw.mods.fml.common.Loader; -import net.minecraft.entity.player.EntityPlayer; -import noppes.npcs.LogWriter; -import org.bukkit.Bukkit; -import org.bukkit.plugin.RegisteredServiceProvider; - -public class VaultHelper { - public static boolean Enabled = false; - public static boolean Available = false; - - // Cached via reflection - private static Object economy = null; - private static Class economyClass = null; - private static java.lang.reflect.Method getBalance; - private static java.lang.reflect.Method withdrawPlayer; - private static java.lang.reflect.Method depositPlayer; - private static java.lang.reflect.Method hasMethod; - private static java.lang.reflect.Method format; - - public static void load() { - // Check if we're on a Bukkit hybrid server - try { - Class.forName("org.bukkit.Bukkit"); - Available = true; - } catch (ClassNotFoundException e) { - Available = false; - return; - } - - // Try to hook into Vault - try { - Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); - economyClass = vaultClass; - - RegisteredServiceProvider rsp = Bukkit.getServicesManager() - .getRegistration(vaultClass); - - if (rsp != null) { - economy = rsp.getProvider(); - - // Cache reflection methods - getBalance = economyClass.getMethod("getBalance", String.class); - withdrawPlayer = economyClass.getMethod("withdrawPlayer", - String.class, double.class); - depositPlayer = economyClass.getMethod("depositPlayer", - String.class, double.class); - hasMethod = economyClass.getMethod("has", String.class, double.class); - format = economyClass.getMethod("format", double.class); - - Enabled = true; - LogWriter.info("VaultAPI Economy hooked successfully!"); - } - } catch (Exception e) { - LogWriter.info("VaultAPI not available: " + e.getMessage()); - Enabled = false; - } - } - - // Economy Methods - public static double getBalance(EntityPlayer player) { - if (!Enabled) return 0.0; - try { - return (Double) getBalance.invoke(economy, player.getCommandSenderName()); - } catch (Exception e) { - LogWriter.except(e); - return 0.0; - } - } - - public static boolean withdraw(EntityPlayer player, double amount) { - if (!Enabled) return false; - try { - Object response = withdrawPlayer.invoke(economy, - player.getCommandSenderName(), amount); - return (Boolean) response.getClass() - .getMethod("transactionSuccess").invoke(response); - } catch (Exception e) { - LogWriter.except(e); - return false; - } - } - - public static boolean deposit(EntityPlayer player, double amount) { - if (!Enabled) return false; - try { - Object response = depositPlayer.invoke(economy, - player.getCommandSenderName(), amount); - return (Boolean) response.getClass() - .getMethod("transactionSuccess").invoke(response); - } catch (Exception e) { - LogWriter.except(e); - return false; - } - } - - public static boolean has(EntityPlayer player, double amount) { - if (!Enabled) return false; - try { - return (Boolean) hasMethod.invoke(economy, - player.getCommandSenderName(), amount); - } catch (Exception e) { - LogWriter.except(e); - return false; - } - } - - public static String formatCurrency(double amount) { - if (!Enabled) return String.format("%.2f", amount); - try { - return (String) format.invoke(economy, amount); - } catch (Exception e) { - return String.format("%.2f", amount); - } - } -} -``` - -#### 1.2.3 Initialization Hook - -**File: `src/main/java/noppes/npcs/CustomNpcs.java`** - -Add to `load()` method after AddonManager: -```java -// Initialize VaultAPI integration (for Bukkit hybrid servers) -VaultHelper.load(); -``` - -### 1.3 Currency Controller - -**File: `src/main/java/kamkeel/npcs/controllers/CurrencyController.java`** - -```java -package kamkeel.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import kamkeel.npcs.compat.VaultHelper; -import noppes.npcs.NoppesUtilPlayer; - -public class CurrencyController { - private static CurrencyController instance; - - // Configuration - public boolean useVaultIfAvailable = true; - public ItemStack fallbackCurrency = null; // Item to use when Vault unavailable - public double itemToVaultRatio = 1.0; // 1 item = X vault currency - - public static CurrencyController getInstance() { - if (instance == null) { - instance = new CurrencyController(); - } - return instance; - } - - /** - * Check if Vault economy is being used - */ - public boolean isUsingVault() { - return useVaultIfAvailable && VaultHelper.Enabled; - } - - /** - * Get player's balance (Vault or item count) - */ - public double getBalance(EntityPlayer player) { - if (isUsingVault()) { - return VaultHelper.getBalance(player); - } - // Fall back to counting items - if (fallbackCurrency != null) { - return NoppesUtilPlayer.countItems(player, fallbackCurrency, false, false); - } - return 0.0; - } - - /** - * Check if player can afford amount - */ - public boolean canAfford(EntityPlayer player, double amount) { - if (isUsingVault()) { - return VaultHelper.has(player, amount); - } - return getBalance(player) >= amount; - } - - /** - * Withdraw currency from player - */ - public boolean withdraw(EntityPlayer player, double amount) { - if (isUsingVault()) { - return VaultHelper.withdraw(player, amount); - } - // Remove items - if (fallbackCurrency != null) { - ItemStack toRemove = fallbackCurrency.copy(); - toRemove.stackSize = (int) Math.ceil(amount / itemToVaultRatio); - return NoppesUtilPlayer.consumeItem(player, toRemove, false, false); - } - return false; - } - - /** - * Deposit currency to player - */ - public boolean deposit(EntityPlayer player, double amount) { - if (isUsingVault()) { - return VaultHelper.deposit(player, amount); - } - // Give items - if (fallbackCurrency != null) { - ItemStack toGive = fallbackCurrency.copy(); - toGive.stackSize = (int) Math.floor(amount / itemToVaultRatio); - return player.inventory.addItemStackToInventory(toGive); - } - return false; - } - - /** - * Format currency for display - */ - public String format(double amount) { - if (isUsingVault()) { - return VaultHelper.formatCurrency(amount); - } - return String.format("%.0f", amount); - } - - // NBT Persistence - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setBoolean("UseVault", useVaultIfAvailable); - compound.setDouble("ItemRatio", itemToVaultRatio); - if (fallbackCurrency != null) { - compound.setTag("FallbackItem", fallbackCurrency.writeToNBT(new NBTTagCompound())); - } - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - useVaultIfAvailable = compound.getBoolean("UseVault"); - itemToVaultRatio = compound.getDouble("ItemRatio"); - if (compound.hasKey("FallbackItem")) { - fallbackCurrency = ItemStack.loadItemStackFromNBT( - compound.getCompoundTag("FallbackItem")); - } - } -} -``` - -### 1.4 Player Currency Data - -**File: `src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java`** - -```java -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import java.util.HashMap; - -public class PlayerCurrencyData { - private final PlayerData parent; - - // Transaction history (for auditing/display) - public HashMap transactionHistory = new HashMap<>(); - public double totalEarned = 0.0; - public double totalSpent = 0.0; - - public PlayerCurrencyData(PlayerData parent) { - this.parent = parent; - } - - public void recordTransaction(double amount, boolean isEarning) { - long timestamp = System.currentTimeMillis(); - transactionHistory.put(timestamp, isEarning ? amount : -amount); - if (isEarning) { - totalEarned += amount; - } else { - totalSpent += amount; - } - // Limit history size - if (transactionHistory.size() > 100) { - // Remove oldest entries - Long oldest = transactionHistory.keySet().stream() - .min(Long::compare).orElse(null); - if (oldest != null) { - transactionHistory.remove(oldest); - } - } - } - - public void loadNBTData(NBTTagCompound compound) { - if (compound.hasKey("CurrencyData")) { - NBTTagCompound data = compound.getCompoundTag("CurrencyData"); - totalEarned = data.getDouble("TotalEarned"); - totalSpent = data.getDouble("TotalSpent"); - } - } - - public void saveNBTData(NBTTagCompound compound) { - NBTTagCompound data = new NBTTagCompound(); - data.setDouble("TotalEarned", totalEarned); - data.setDouble("TotalSpent", totalSpent); - compound.setTag("CurrencyData", data); - } -} -``` - -### 1.5 Configuration UI - -Add currency configuration to the global settings GUI: - -**File: `src/main/java/noppes/npcs/client/gui/global/GuiNPCManageCurrency.java`** - -This GUI will allow server operators to: -- Enable/disable Vault integration -- Set fallback currency item -- Configure item-to-Vault ratio -- View server economy statistics - ---- - -## Feature 2: Trader Enhancements - -### 2.1 Overview - -Enhance the existing `RoleTrader` with: -- **Stock System**: Limited inventory that depletes when players buy -- **Per-Player vs Per-Server Stock**: Configure whether stock is shared or individual -- **Stock Reset Timer**: Automatic restocking based on configurable cooldown (like quest cooldowns) -- **Currency Integration**: Use Vault currency alongside or instead of item-based trading - -### 2.2 Trader Stock Configuration - -#### 2.2.1 Stock Data Class - -**File: `src/main/java/noppes/npcs/controllers/data/TraderStock.java`** - -```java -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import java.util.HashMap; - -public class TraderStock { - // Stock configuration - public boolean enableStock = false; - public boolean perPlayer = false; // false = per-server, true = per-player - - // Reset configuration (mirrors Quest cooldown system) - public EnumStockReset resetType = EnumStockReset.NONE; - public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types - - // Stock quantities per slot (max stock) - public int[] maxStock = new int[18]; - - // Current stock (per-server mode) - public int[] currentStock = new int[18]; - public long lastResetTime = 0; - - // Per-player stock tracking - public HashMap playerStock = new HashMap<>(); - - public TraderStock() { - for (int i = 0; i < 18; i++) { - maxStock[i] = -1; // -1 = unlimited - currentStock[i] = -1; - } - } - - /** - * Get available stock for a slot - */ - public int getAvailableStock(int slot, String playerName) { - if (!enableStock || maxStock[slot] < 0) { - return Integer.MAX_VALUE; // Unlimited - } - - if (perPlayer) { - PlayerTraderStock pStock = playerStock.get(playerName); - if (pStock == null) { - return maxStock[slot]; // Not purchased yet - } - return pStock.getStock(slot, maxStock[slot]); - } - - return currentStock[slot]; - } - - /** - * Consume stock when purchase is made - */ - public boolean consumeStock(int slot, String playerName, int amount) { - if (!enableStock || maxStock[slot] < 0) { - return true; // Unlimited - } - - if (perPlayer) { - PlayerTraderStock pStock = playerStock.computeIfAbsent( - playerName, k -> new PlayerTraderStock()); - return pStock.consumeStock(slot, maxStock[slot], amount); - } - - if (currentStock[slot] >= amount) { - currentStock[slot] -= amount; - return true; - } - return false; - } - - /** - * Check if stock should reset - */ - public boolean shouldReset(long currentTime) { - if (resetType == EnumStockReset.NONE) { - return false; - } - - long elapsed = currentTime - lastResetTime; - long resetInterval = getResetInterval(); - - return elapsed >= resetInterval; - } - - /** - * Reset all stock to max values - */ - public void resetStock(long currentTime) { - lastResetTime = currentTime; - - for (int i = 0; i < 18; i++) { - currentStock[i] = maxStock[i]; - } - - // Clear per-player stock - playerStock.clear(); - } - - /** - * Get reset interval in milliseconds - */ - public long getResetInterval() { - switch (resetType) { - case MCDAILY: - return 24000 * 50; // 24000 ticks * 50ms - case MCWEEKLY: - return 168000 * 50; - case MCCUSTOM: - return customResetTime * 50; - case RLDAILY: - return 86400000L; // 24 hours in ms - case RLWEEKLY: - return 604800000L; // 7 days in ms - case RLCUSTOM: - return customResetTime; - default: - return Long.MAX_VALUE; - } - } - - /** - * Get time until next reset (for display) - */ - public long getTimeUntilReset(long currentTime) { - if (resetType == EnumStockReset.NONE) { - return -1; - } - long elapsed = currentTime - lastResetTime; - long interval = getResetInterval(); - return Math.max(0, interval - elapsed); - } - - // NBT Serialization - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setBoolean("EnableStock", enableStock); - compound.setBoolean("PerPlayer", perPlayer); - compound.setInteger("ResetType", resetType.ordinal()); - compound.setLong("CustomResetTime", customResetTime); - compound.setIntArray("MaxStock", maxStock); - compound.setIntArray("CurrentStock", currentStock); - compound.setLong("LastResetTime", lastResetTime); - - // Save per-player stock - NBTTagList playerList = new NBTTagList(); - for (Map.Entry entry : playerStock.entrySet()) { - NBTTagCompound playerTag = new NBTTagCompound(); - playerTag.setString("Player", entry.getKey()); - entry.getValue().writeToNBT(playerTag); - playerList.appendTag(playerTag); - } - compound.setTag("PlayerStock", playerList); - - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - enableStock = compound.getBoolean("EnableStock"); - perPlayer = compound.getBoolean("PerPlayer"); - resetType = EnumStockReset.values()[compound.getInteger("ResetType")]; - customResetTime = compound.getLong("CustomResetTime"); - maxStock = compound.getIntArray("MaxStock"); - currentStock = compound.getIntArray("CurrentStock"); - lastResetTime = compound.getLong("LastResetTime"); - - // Load per-player stock - NBTTagList playerList = compound.getTagList("PlayerStock", 10); - for (int i = 0; i < playerList.tagCount(); i++) { - NBTTagCompound playerTag = playerList.getCompoundTagAt(i); - String playerName = playerTag.getString("Player"); - PlayerTraderStock pStock = new PlayerTraderStock(); - pStock.readFromNBT(playerTag); - playerStock.put(playerName, pStock); - } - } -} - -// Inner class for per-player stock tracking -class PlayerTraderStock { - public int[] purchasedAmounts = new int[18]; - public long lastPurchaseTime = 0; - - public int getStock(int slot, int maxStock) { - return Math.max(0, maxStock - purchasedAmounts[slot]); - } - - public boolean consumeStock(int slot, int maxStock, int amount) { - if (getStock(slot, maxStock) >= amount) { - purchasedAmounts[slot] += amount; - lastPurchaseTime = System.currentTimeMillis(); - return true; - } - return false; - } - - public void writeToNBT(NBTTagCompound compound) { - compound.setIntArray("Purchased", purchasedAmounts); - compound.setLong("LastPurchase", lastPurchaseTime); - } - - public void readFromNBT(NBTTagCompound compound) { - purchasedAmounts = compound.getIntArray("Purchased"); - lastPurchaseTime = compound.getLong("LastPurchase"); - } -} -``` - -#### 2.2.2 Stock Reset Enum - -**File: `src/main/java/noppes/npcs/constants/EnumStockReset.java`** - -```java -package noppes.npcs.constants; - -public enum EnumStockReset { - NONE, // Never resets - MCDAILY, // Every 24000 MC ticks (1 MC day) - MCWEEKLY, // Every 168000 MC ticks (7 MC days) - MCCUSTOM, // Custom MC tick interval - RLDAILY, // Every 24 real hours - RLWEEKLY, // Every 7 real days - RLCUSTOM // Custom real-time interval -} -``` - -### 2.3 RoleTrader Modifications - -**File: `src/main/java/noppes/npcs/roles/RoleTrader.java`** - -Add the following to `RoleTrader`: - -```java -// Add new fields -public TraderStock stock = new TraderStock(); -public boolean useCurrency = false; // Use Vault/Currency system -public double[] slotPrices = new double[18]; // Currency prices per slot - -// Modify interact() to check stock reset -@Override -public void interact(EntityPlayer player) { - // Check for stock reset before opening GUI - if (stock.enableStock) { - long currentTime = stock.resetType.name().startsWith("RL") - ? System.currentTimeMillis() - : player.worldObj.getTotalWorldTime(); - - if (stock.shouldReset(currentTime)) { - stock.resetStock(currentTime); - // Save to market if using shared market - if (!marketName.isEmpty()) { - Market.save(this, marketName); - } - } - } - - npc.say(player, npc.advanced.getInteractLine()); - NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); -} - -// Modify writeToNBT -@Override -public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) { - // ... existing code ... - - // Add stock data - nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); - nbttagcompound.setBoolean("UseCurrency", useCurrency); - - // Save currency prices - NBTTagList priceList = new NBTTagList(); - for (double price : slotPrices) { - NBTTagCompound priceTag = new NBTTagCompound(); - priceTag.setDouble("Price", price); - priceList.appendTag(priceTag); - } - nbttagcompound.setTag("SlotPrices", priceList); - - return nbttagcompound; -} - -// Modify readFromNBT -@Override -public void readFromNBT(NBTTagCompound nbttagcompound) { - // ... existing code ... - - // Load stock data - if (nbttagcompound.hasKey("Stock")) { - stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); - } - useCurrency = nbttagcompound.getBoolean("UseCurrency"); - - // Load currency prices - if (nbttagcompound.hasKey("SlotPrices")) { - NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); - for (int i = 0; i < priceList.tagCount() && i < 18; i++) { - slotPrices[i] = priceList.getCompoundTagAt(i).getDouble("Price"); - } - } -} -``` - -### 2.4 ContainerNPCTrader Modifications - -**File: `src/main/java/noppes/npcs/containers/ContainerNPCTrader.java`** - -Modify `slotClick()` to handle stock and currency: - -```java -public ItemStack slotClick(int slotIndex, EntityPlayer player) { - RoleTrader role = (RoleTrader) npc.roleInterface; - String playerName = player.getCommandSenderName(); - - // ... existing validation ... - - // Check stock availability - if (role.stock.enableStock) { - int available = role.stock.getAvailableStock(slotIndex, playerName); - if (available <= 0) { - // Out of stock - send message to player - return null; - } - } - - // Handle currency-based purchase - if (role.useCurrency && role.slotPrices[slotIndex] > 0) { - double price = role.slotPrices[slotIndex]; - CurrencyController currency = CurrencyController.getInstance(); - - if (!currency.canAfford(player, price)) { - // Can't afford - send message - return null; - } - - // Withdraw currency - if (!currency.withdraw(player, price)) { - return null; - } - } else { - // ... existing item-based currency logic ... - } - - // Consume stock - if (role.stock.enableStock) { - role.stock.consumeStock(slotIndex, playerName, 1); - } - - // ... existing item delivery logic ... - - // Save if using market - if (!role.marketName.isEmpty()) { - Market.save(role, role.marketName); - } - - return soldItem; -} -``` - -### 2.5 Trader Setup GUI Additions - -**File: `src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java`** - -Add new configuration options: - -```java -// Add buttons/fields for: -// - "Enable Stock" toggle -// - "Per Player" / "Per Server" radio buttons -// - "Reset Type" dropdown (None, MC Daily, MC Weekly, etc.) -// - "Custom Reset Time" field -// - "Use Currency" toggle -// - Per-slot stock quantity inputs -// - Per-slot currency price inputs - -private void initStockGui() { - addButton(new GuiNpcButton(20, x, y, 80, 20, - "Enable Stock: " + (role.stock.enableStock ? "Yes" : "No"))); - - addButton(new GuiNpcButton(21, x, y + 22, 80, 20, - role.stock.perPlayer ? "Per Player" : "Per Server")); - - String[] resetTypes = {"None", "MC Daily", "MC Weekly", "MC Custom", - "RL Daily", "RL Weekly", "RL Custom"}; - addButton(new GuiNpcButton(22, x, y + 44, 80, 20, resetTypes, - role.stock.resetType.ordinal())); - - addButton(new GuiNpcButton(23, x, y + 66, 80, 20, - "Use Currency: " + (role.useCurrency ? "Yes" : "No"))); -} -``` - ---- - -## Feature 3: Auction House - -### 3.1 Overview - -The Auction House is a global trading system where players can: -- **List items for sale** at fixed prices or as auctions with bidding -- **Browse listings** by category, search, and filter -- **Bid on auctions** with automatic outbid notifications -- **Buy instantly** for buy-it-now prices -- **Manage their listings** (cancel, modify) -- **Collect proceeds and expired items** via mailbox or direct claim - -### 3.2 Auction Data Structures - -#### 3.2.1 AuctionListing - -**File: `src/main/java/noppes/npcs/controllers/data/AuctionListing.java`** - -```java -package noppes.npcs.controllers.data; - -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import java.util.UUID; - -public class AuctionListing { - public int id; - public UUID sellerUUID; - public String sellerName; - public ItemStack item; - - // Pricing - public double startingPrice; - public double buyoutPrice; // 0 = no buyout - public double currentBid; - public UUID highBidderUUID; - public String highBidderName; - - // Timing - public long listingTime; - public long expirationTime; - public EnumAuctionDuration duration; - - // Status - public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; - - // Category - public EnumAuctionCategory category = EnumAuctionCategory.MISC; - - public AuctionListing() {} - - public AuctionListing(UUID seller, String sellerName, ItemStack item, - double startPrice, double buyout, EnumAuctionDuration duration) { - this.sellerUUID = seller; - this.sellerName = sellerName; - this.item = item.copy(); - this.startingPrice = startPrice; - this.buyoutPrice = buyout; - this.currentBid = startPrice; - this.duration = duration; - this.listingTime = System.currentTimeMillis(); - this.expirationTime = listingTime + duration.getMillis(); - this.category = categorizeItem(item); - } - - /** - * Check if auction has expired - */ - public boolean isExpired() { - return System.currentTimeMillis() >= expirationTime; - } - - /** - * Get time remaining in milliseconds - */ - public long getTimeRemaining() { - return Math.max(0, expirationTime - System.currentTimeMillis()); - } - - /** - * Place a bid - */ - public boolean placeBid(UUID bidder, String bidderName, double amount) { - if (status != EnumAuctionStatus.ACTIVE) return false; - if (amount <= currentBid) return false; - if (bidder.equals(sellerUUID)) return false; // Can't bid on own item - - // Minimum bid increment (5% or 1, whichever is greater) - double minIncrement = Math.max(1, currentBid * 0.05); - if (amount < currentBid + minIncrement) return false; - - highBidderUUID = bidder; - highBidderName = bidderName; - currentBid = amount; - - return true; - } - - /** - * Execute buyout - */ - public boolean buyout(UUID buyer, String buyerName) { - if (status != EnumAuctionStatus.ACTIVE) return false; - if (buyoutPrice <= 0) return false; // No buyout available - if (buyer.equals(sellerUUID)) return false; - - highBidderUUID = buyer; - highBidderName = buyerName; - currentBid = buyoutPrice; - status = EnumAuctionStatus.SOLD; - - return true; - } - - /** - * Cancel listing (seller only, before any bids) - */ - public boolean cancel(UUID requester) { - if (!requester.equals(sellerUUID)) return false; - if (highBidderUUID != null) return false; // Has bids, can't cancel - - status = EnumAuctionStatus.CANCELLED; - return true; - } - - /** - * Auto-categorize item based on type - */ - private EnumAuctionCategory categorizeItem(ItemStack item) { - // Implement item categorization logic - // Check for weapons, armor, tools, food, blocks, etc. - return EnumAuctionCategory.MISC; - } - - // NBT Serialization - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("Id", id); - compound.setString("SellerUUID", sellerUUID.toString()); - compound.setString("SellerName", sellerName); - compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); - compound.setDouble("StartPrice", startingPrice); - compound.setDouble("BuyoutPrice", buyoutPrice); - compound.setDouble("CurrentBid", currentBid); - if (highBidderUUID != null) { - compound.setString("BidderUUID", highBidderUUID.toString()); - compound.setString("BidderName", highBidderName); - } - compound.setLong("ListingTime", listingTime); - compound.setLong("ExpirationTime", expirationTime); - compound.setInteger("Duration", duration.ordinal()); - compound.setInteger("Status", status.ordinal()); - compound.setInteger("Category", category.ordinal()); - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - id = compound.getInteger("Id"); - sellerUUID = UUID.fromString(compound.getString("SellerUUID")); - sellerName = compound.getString("SellerName"); - item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); - startingPrice = compound.getDouble("StartPrice"); - buyoutPrice = compound.getDouble("BuyoutPrice"); - currentBid = compound.getDouble("CurrentBid"); - if (compound.hasKey("BidderUUID")) { - highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); - highBidderName = compound.getString("BidderName"); - } - listingTime = compound.getLong("ListingTime"); - expirationTime = compound.getLong("ExpirationTime"); - duration = EnumAuctionDuration.values()[compound.getInteger("Duration")]; - status = EnumAuctionStatus.values()[compound.getInteger("Status")]; - category = EnumAuctionCategory.values()[compound.getInteger("Category")]; - } -} -``` - -#### 3.2.2 Supporting Enums - -**File: `src/main/java/noppes/npcs/constants/EnumAuctionDuration.java`** - -```java -package noppes.npcs.constants; - -public enum EnumAuctionDuration { - SHORT(2 * 60 * 60 * 1000L), // 2 hours - MEDIUM(12 * 60 * 60 * 1000L), // 12 hours - LONG(24 * 60 * 60 * 1000L), // 24 hours - VERY_LONG(48 * 60 * 60 * 1000L); // 48 hours - - private final long millis; - - EnumAuctionDuration(long millis) { - this.millis = millis; - } - - public long getMillis() { - return millis; - } - - public String getDisplayName() { - switch (this) { - case SHORT: return "2 Hours"; - case MEDIUM: return "12 Hours"; - case LONG: return "24 Hours"; - case VERY_LONG: return "48 Hours"; - default: return "Unknown"; - } - } -} -``` - -**File: `src/main/java/noppes/npcs/constants/EnumAuctionStatus.java`** - -```java -package noppes.npcs.constants; - -public enum EnumAuctionStatus { - ACTIVE, // Currently listed - SOLD, // Successfully sold (buyout or auction end with bids) - EXPIRED, // Ended without bids - CANCELLED, // Cancelled by seller - CLAIMED // Item/money has been claimed -} -``` - -**File: `src/main/java/noppes/npcs/constants/EnumAuctionCategory.java`** - -```java -package noppes.npcs.constants; - -public enum EnumAuctionCategory { - WEAPONS, - ARMOR, - TOOLS, - FOOD, - POTIONS, - BLOCKS, - MATERIALS, - ENCHANTED, - MISC -} -``` - -### 3.3 Auction Controller - -**File: `src/main/java/noppes/npcs/controllers/AuctionController.java`** - -```java -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.constants.*; - -import java.io.File; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public class AuctionController { - private static AuctionController instance; - - // All listings (thread-safe for async operations) - public ConcurrentHashMap listings = new ConcurrentHashMap<>(); - - // Index by seller for quick lookup - private Map> sellerIndex = new ConcurrentHashMap<>(); - - // Pending claims (seller -> items/money to claim) - public Map> pendingClaims = new ConcurrentHashMap<>(); - - private int nextId = 1; - private long lastSaveTime = 0; - private static final long SAVE_INTERVAL = 60000; // Save every minute - - // Configuration - public double listingFee = 0.0; // Fee to list item - public double saleTax = 0.05; // 5% tax on sales - public int maxListingsPerPlayer = 20; - public boolean requireAuctioneer = true; // Require Auctioneer NPC to access - - public static AuctionController getInstance() { - if (instance == null) { - instance = new AuctionController(); - instance.load(); - } - return instance; - } - - /** - * Create a new auction listing - */ - public AuctionListing createListing(EntityPlayer seller, ItemStack item, - double startPrice, double buyout, - EnumAuctionDuration duration) { - UUID sellerUUID = seller.getUniqueID(); - - // Check max listings - Set sellerListings = sellerIndex.get(sellerUUID); - if (sellerListings != null && sellerListings.size() >= maxListingsPerPlayer) { - return null; - } - - // Charge listing fee - if (listingFee > 0) { - CurrencyController currency = CurrencyController.getInstance(); - if (!currency.withdraw(seller, listingFee)) { - return null; - } - } - - // Create listing - AuctionListing listing = new AuctionListing( - sellerUUID, seller.getCommandSenderName(), - item, startPrice, buyout, duration - ); - listing.id = nextId++; - - // Add to indexes - listings.put(listing.id, listing); - sellerIndex.computeIfAbsent(sellerUUID, k -> new HashSet<>()).add(listing.id); - - // Remove item from seller inventory - item.stackSize = 0; - - scheduleSave(); - return listing; - } - - /** - * Place a bid on an auction - */ - public boolean placeBid(EntityPlayer bidder, int listingId, double amount) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.isExpired()) return false; - - CurrencyController currency = CurrencyController.getInstance(); - - // Check if bidder can afford - if (!currency.canAfford(bidder, amount)) return false; - - // Refund previous bidder - if (listing.highBidderUUID != null) { - addClaim(listing.highBidderUUID, listing.highBidderName, - null, listing.currentBid, "Outbid refund for: " + - listing.item.getDisplayName()); - } - - // Place bid - if (!listing.placeBid(bidder.getUniqueID(), - bidder.getCommandSenderName(), amount)) { - return false; - } - - // Hold currency from bidder - currency.withdraw(bidder, amount); - - scheduleSave(); - return true; - } - - /** - * Buyout an auction - */ - public boolean buyout(EntityPlayer buyer, int listingId) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.buyoutPrice <= 0) return false; - - CurrencyController currency = CurrencyController.getInstance(); - - // Check if buyer can afford - if (!currency.canAfford(buyer, listing.buyoutPrice)) return false; - - // Refund previous bidder if any - if (listing.highBidderUUID != null) { - addClaim(listing.highBidderUUID, listing.highBidderName, - null, listing.currentBid, "Buyout refund for: " + - listing.item.getDisplayName()); - } - - // Execute buyout - if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { - return false; - } - - // Withdraw from buyer - currency.withdraw(buyer, listing.buyoutPrice); - - // Calculate seller proceeds (minus tax) - double proceeds = listing.buyoutPrice * (1 - saleTax); - - // Add claims for both parties - addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, - "Sale of: " + listing.item.getDisplayName()); - addClaim(buyer.getUniqueID(), buyer.getCommandSenderName(), - listing.item, 0, "Purchased: " + listing.item.getDisplayName()); - - scheduleSave(); - return true; - } - - /** - * Cancel a listing (seller only) - */ - public boolean cancelListing(EntityPlayer player, int listingId) { - AuctionListing listing = listings.get(listingId); - if (listing == null) return false; - - if (!listing.cancel(player.getUniqueID())) return false; - - // Return item to seller - addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, - "Cancelled listing: " + listing.item.getDisplayName()); - - scheduleSave(); - return true; - } - - /** - * Process expired auctions (called periodically) - */ - public void processExpiredAuctions() { - long now = System.currentTimeMillis(); - - for (AuctionListing listing : listings.values()) { - if (listing.status != EnumAuctionStatus.ACTIVE) continue; - if (!listing.isExpired()) continue; - - if (listing.highBidderUUID != null) { - // Auction successful - has bids - listing.status = EnumAuctionStatus.SOLD; - - double proceeds = listing.currentBid * (1 - saleTax); - addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, - "Auction sold: " + listing.item.getDisplayName()); - addClaim(listing.highBidderUUID, listing.highBidderName, - listing.item, 0, "Won auction: " + listing.item.getDisplayName()); - } else { - // No bids - return to seller - listing.status = EnumAuctionStatus.EXPIRED; - addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, - "Expired listing: " + listing.item.getDisplayName()); - } - } - - scheduleSave(); - } - - /** - * Get active listings with optional filters - */ - public List getActiveListings(EnumAuctionCategory category, - String search, - int page, int pageSize) { - return listings.values().stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .filter(l -> !l.isExpired()) - .filter(l -> category == null || l.category == category) - .filter(l -> search == null || search.isEmpty() || - l.item.getDisplayName().toLowerCase() - .contains(search.toLowerCase())) - .sorted(Comparator.comparingLong(l -> l.expirationTime)) - .skip((long) page * pageSize) - .limit(pageSize) - .collect(Collectors.toList()); - } - - /** - * Get player's own listings - */ - public List getPlayerListings(UUID playerUUID) { - Set ids = sellerIndex.get(playerUUID); - if (ids == null) return Collections.emptyList(); - - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Get pending claims for a player - */ - public List getPendingClaims(UUID playerUUID) { - return pendingClaims.getOrDefault(playerUUID, Collections.emptyList()); - } - - /** - * Claim pending items/money - */ - public boolean claim(EntityPlayer player, int claimIndex) { - List claims = pendingClaims.get(player.getUniqueID()); - if (claims == null || claimIndex >= claims.size()) return false; - - AuctionClaim claim = claims.get(claimIndex); - - // Give item or money - if (claim.item != null) { - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { - return false; // Inventory full - } - } - if (claim.money > 0) { - CurrencyController.getInstance().deposit(player, claim.money); - } - - claims.remove(claimIndex); - scheduleSave(); - return true; - } - - private void addClaim(UUID playerUUID, String playerName, - ItemStack item, double money, String description) { - AuctionClaim claim = new AuctionClaim(item, money, description); - pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(claim); - } - - // Persistence - private void scheduleSave() { - long now = System.currentTimeMillis(); - if (now - lastSaveTime > SAVE_INTERVAL) { - save(); - lastSaveTime = now; - } - } - - public void save() { - try { - NBTTagCompound compound = new NBTTagCompound(); - compound.setInteger("NextId", nextId); - compound.setDouble("ListingFee", listingFee); - compound.setDouble("SaleTax", saleTax); - compound.setInteger("MaxListings", maxListingsPerPlayer); - - // Save listings - NBTTagList listingList = new NBTTagList(); - for (AuctionListing listing : listings.values()) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - compound.setTag("Listings", listingList); - - // Save claims - NBTTagList claimList = new NBTTagList(); - for (Map.Entry> entry : pendingClaims.entrySet()) { - NBTTagCompound playerClaims = new NBTTagCompound(); - playerClaims.setString("UUID", entry.getKey().toString()); - NBTTagList claims = new NBTTagList(); - for (AuctionClaim claim : entry.getValue()) { - claims.appendTag(claim.writeToNBT(new NBTTagCompound())); - } - playerClaims.setTag("Claims", claims); - claimList.appendTag(playerClaims); - } - compound.setTag("PendingClaims", claimList); - - File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); - CompressedStreamTools.writeCompressed(compound, - new java.io.FileOutputStream(file)); - - } catch (Exception e) { - LogWriter.except(e); - } - } - - public void load() { - try { - File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); - if (!file.exists()) return; - - NBTTagCompound compound = CompressedStreamTools.readCompressed( - new java.io.FileInputStream(file)); - - nextId = compound.getInteger("NextId"); - listingFee = compound.getDouble("ListingFee"); - saleTax = compound.getDouble("SaleTax"); - maxListingsPerPlayer = compound.getInteger("MaxListings"); - - // Load listings - NBTTagList listingList = compound.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingList.getCompoundTagAt(i)); - listings.put(listing.id, listing); - sellerIndex.computeIfAbsent(listing.sellerUUID, k -> new HashSet<>()) - .add(listing.id); - } - - // Load claims - NBTTagList claimList = compound.getTagList("PendingClaims", 10); - for (int i = 0; i < claimList.tagCount(); i++) { - NBTTagCompound playerClaims = claimList.getCompoundTagAt(i); - UUID playerUUID = UUID.fromString(playerClaims.getString("UUID")); - NBTTagList claims = playerClaims.getTagList("Claims", 10); - List claimsList = new ArrayList<>(); - for (int j = 0; j < claims.tagCount(); j++) { - AuctionClaim claim = new AuctionClaim(); - claim.readFromNBT(claims.getCompoundTagAt(j)); - claimsList.add(claim); - } - pendingClaims.put(playerUUID, claimsList); - } - - } catch (Exception e) { - LogWriter.except(e); - } - } -} - -// Claim data structure -class AuctionClaim { - public ItemStack item; - public double money; - public String description; - public long timestamp; - - public AuctionClaim() {} - - public AuctionClaim(ItemStack item, double money, String description) { - this.item = item != null ? item.copy() : null; - this.money = money; - this.description = description; - this.timestamp = System.currentTimeMillis(); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - if (item != null) { - compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); - } - compound.setDouble("Money", money); - compound.setString("Description", description); - compound.setLong("Timestamp", timestamp); - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Item")) { - item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); - } - money = compound.getDouble("Money"); - description = compound.getString("Description"); - timestamp = compound.getLong("Timestamp"); - } -} -``` - -### 3.4 Auctioneer Role - -**File: `src/main/java/noppes/npcs/roles/RoleAuctioneer.java`** - -```java -package noppes.npcs.roles; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NoppesUtilServer; -import noppes.npcs.constants.EnumGuiType; -import noppes.npcs.entity.EntityNPCInterface; - -public class RoleAuctioneer extends RoleInterface { - - // Configuration - public String auctionHouseName = "Global"; // For future multi-auction support - public boolean allowSelling = true; - public boolean allowBuying = true; - public boolean showExpiredListings = true; - - // Custom messages - public String welcomeMessage = ""; - public String sellDisabledMessage = "This auctioneer does not accept listings."; - public String buyDisabledMessage = "This auctioneer does not sell items."; - - public RoleAuctioneer(EntityNPCInterface npc) { - super(npc); - } - - @Override - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("AuctionHouse", auctionHouseName); - compound.setBoolean("AllowSelling", allowSelling); - compound.setBoolean("AllowBuying", allowBuying); - compound.setBoolean("ShowExpired", showExpiredListings); - compound.setString("WelcomeMsg", welcomeMessage); - compound.setString("SellDisabledMsg", sellDisabledMessage); - compound.setString("BuyDisabledMsg", buyDisabledMessage); - return compound; - } - - @Override - public void readFromNBT(NBTTagCompound compound) { - auctionHouseName = compound.getString("AuctionHouse"); - allowSelling = compound.getBoolean("AllowSelling"); - allowBuying = compound.getBoolean("AllowBuying"); - showExpiredListings = compound.getBoolean("ShowExpired"); - welcomeMessage = compound.getString("WelcomeMsg"); - sellDisabledMessage = compound.getString("SellDisabledMsg"); - buyDisabledMessage = compound.getString("BuyDisabledMsg"); - } - - @Override - public void interact(EntityPlayer player) { - // Show welcome message if set - if (!welcomeMessage.isEmpty()) { - npc.say(player, npc.advanced.getInteractLine()); - } - - // Open auction house GUI - NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); - } -} -``` - -### 3.5 Update EnumRoleType - -**File: `src/main/java/noppes/npcs/constants/EnumRoleType.java`** - -```java -public enum EnumRoleType { - None, - Trader, - Follower, - Bank, - Transporter, - Postman, - Companion, - Mount, - Auctioneer // Add this -} -``` - -### 3.6 Update DataAdvanced - -**File: `src/main/java/noppes/npcs/DataAdvanced.java`** - -Add to `setRole()` method: - -```java -else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) - npc.roleInterface = new RoleAuctioneer(npc); -``` - -### 3.7 Update EnumGuiType - -**File: `src/main/java/noppes/npcs/constants/EnumGuiType.java`** - -Add new GUI types: - -```java -// Auction House GUIs -PlayerAuction, // Main auction browse/buy interface -PlayerAuctionSell, // Create new listing -PlayerAuctionMyListings, // View own listings -PlayerAuctionClaims, // Claim items/money -AuctioneerSetup, // NPC configuration -ManageAuctions // Admin management -``` - -### 3.8 Auction GUI System - -The auction GUI system will consist of multiple interconnected screens: - -#### 3.8.1 Main Auction Browser - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionBrowse.java`** - -Features: -- Category filter tabs (Weapons, Armor, Tools, etc.) -- Search bar for item names -- Sort options (Time remaining, Price, Recently listed) -- Paginated listing display -- Quick buy/bid buttons - -#### 3.8.2 Create Listing GUI - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java`** - -Features: -- Item slot for the item to sell -- Starting price input -- Buyout price input (optional) -- Duration selector -- Listing fee display -- Confirm button - -#### 3.8.3 My Listings GUI - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionMyListings.java`** - -Features: -- List of player's active listings -- Current bid status -- Time remaining -- Cancel button (if no bids) - -#### 3.8.4 Claims GUI - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionClaims.java`** - -Features: -- List of pending claims (items won, money from sales, refunds) -- Claim button for each item -- Claim all button - -### 3.9 Network Packets - -#### 3.9.1 Auction Action Packet - -**File: `src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java`** - -```java -package kamkeel.npcs.network.packets.player; - -public class AuctionActionPacket extends AbstractPacket { - - public enum Action { - CREATE_LISTING, - PLACE_BID, - BUYOUT, - CANCEL, - CLAIM, - CLAIM_ALL, - BROWSE, - MY_LISTINGS - } - - private Action action; - private int listingId; - private double amount; - private NBTTagCompound itemData; - private int duration; - private double buyoutPrice; - private int category; - private String search; - private int page; - - // Implement sendData/receiveData... -} -``` - -#### 3.9.2 Auction Sync Packet - -**File: `src/main/java/kamkeel/npcs/network/packets/data/AuctionSyncPacket.java`** - -Used to send auction listing data to clients for GUI display. - -### 3.10 Periodic Tasks - -Add to server tick handler to process expired auctions: - -**File: `src/main/java/noppes/npcs/ServerTickHandler.java`** - -```java -private int auctionCheckCounter = 0; - -@SubscribeEvent -public void onServerTick(TickEvent.ServerTickEvent event) { - // ... existing code ... - - // Check expired auctions every 5 minutes (6000 ticks) - if (++auctionCheckCounter >= 6000) { - auctionCheckCounter = 0; - AuctionController.getInstance().processExpiredAuctions(); - } -} -``` - ---- - -## Feature 4: Item Attributes & Restrictions - -### 4.1 Overview - -Implement item attribute system to control which items can be traded/auctioned. Items with certain attributes will be blocked from the auction house. - -### 4.2 Existing Item Requirements (Reuse) - -The codebase already has requirement types that we can leverage: - -| Requirement | NBT Key | File | Purpose | -|-------------|---------|------|---------| -| ProfileSlot Bound | `cnpc_profile_slot` | `ProfileSlotRequirement.java` | Item bound to specific profile slot | -| Soulbound | `cnpc_soulbind` | `SoulbindRequirement.java` | Item bound to specific player UUID | - -### 4.3 New Untradeable Attribute - -A new NBT-based item attribute that marks items as untradeable. - -**File: `src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java`** - -```java -package kamkeel.npcs.controllers.data.attribute; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; - -public class ItemTradeAttribute { - // New untradeable attribute - public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; - - // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) - public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; - public static final String TAG_SOULBIND = "cnpc_soulbind"; - - /** - * Check if item can be traded/auctioned - */ - public static boolean canTrade(ItemStack item, EntityPlayer player) { - if (item == null || !item.hasTagCompound()) { - return true; - } - NBTTagCompound tag = item.getTagCompound(); - - // Check untradeable flag - if (tag.getBoolean(TAG_UNTRADEABLE)) return false; - - // Check profile slot bound (item tied to specific profile slot) - if (tag.hasKey(TAG_PROFILE_SLOT)) return false; - - // Check soulbound (item tied to specific player) - if (tag.hasKey(TAG_SOULBIND)) return false; - - return true; - } - - /** - * Mark item as untradeable - */ - public static void setUntradeable(ItemStack item, boolean untradeable) { - if (item == null) return; - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (untradeable) { - item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); - } else { - item.getTagCompound().removeTag(TAG_UNTRADEABLE); - } - } - - public static boolean isUntradeable(ItemStack item) { - if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); - } - - public static boolean isProfileSlotBound(ItemStack item) { - if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); - } - - public static boolean isSoulbound(ItemStack item) { - if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().hasKey(TAG_SOULBIND); - } - - /** - * Get reason why item cannot be traded (for UI display) - */ - public static String getTradeBlockReason(ItemStack item) { - if (item == null || !item.hasTagCompound()) return null; - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) return "gui.auction.untradeable"; - if (tag.hasKey(TAG_PROFILE_SLOT)) return "gui.auction.profileslotbound"; - if (tag.hasKey(TAG_SOULBIND)) return "gui.auction.soulbound"; - - return null; - } -} -``` - -### 4.4 Auction House Item Blacklist - -A configurable system to blacklist specific items from being auctioned. - -**File: `src/main/java/noppes/npcs/controllers/AuctionBlacklist.java`** - -```java -package noppes.npcs.controllers; - -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; - -import java.io.File; -import java.util.HashSet; -import java.util.Set; - -public class AuctionBlacklist { - private static AuctionBlacklist instance; - - // Blacklist by item registry name (e.g., "minecraft:diamond_sword") - private Set blacklistedItems = new HashSet<>(); - - // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) - private Set blacklistedMods = new HashSet<>(); - - // Blacklist by NBT tag presence (e.g., items with specific custom data) - private Set blacklistedNBTTags = new HashSet<>(); - - public static AuctionBlacklist getInstance() { - if (instance == null) { - instance = new AuctionBlacklist(); - instance.load(); - } - return instance; - } - - /** - * Check if item is blacklisted - */ - public boolean isBlacklisted(ItemStack item) { - if (item == null) return false; - - // Check item registry name - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null && blacklistedItems.contains(itemName)) { - return true; - } - - // Check mod ID - if (itemName != null) { - String modId = itemName.split(":")[0]; - if (blacklistedMods.contains(modId)) { - return true; - } - } - - // Check NBT tags - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (tag.hasKey(nbtTag)) { - return true; - } - } - } - - return false; - } - - // Blacklist management methods - public void addItem(String itemName) { - blacklistedItems.add(itemName); - save(); - } - - public void removeItem(String itemName) { - blacklistedItems.remove(itemName); - save(); - } - - public void addMod(String modId) { - blacklistedMods.add(modId); - save(); - } - - public void removeMod(String modId) { - blacklistedMods.remove(modId); - save(); - } - - public void addNBTTag(String tag) { - blacklistedNBTTags.add(tag); - save(); - } - - public void removeNBTTag(String tag) { - blacklistedNBTTags.remove(tag); - save(); - } - - public Set getBlacklistedItems() { - return new HashSet<>(blacklistedItems); - } - - public Set getBlacklistedMods() { - return new HashSet<>(blacklistedMods); - } - - public Set getBlacklistedNBTTags() { - return new HashSet<>(blacklistedNBTTags); - } - - // Persistence - public void save() { - // Save to auction_blacklist.dat - } - - public void load() { - // Load from auction_blacklist.dat - } -} -``` - -### 4.5 Integration with Auction Listing - -Modify `AuctionController.createListing()` to check restrictions: - -```java -public AuctionListing createListing(EntityPlayer seller, ItemStack item, ...) { - // Check item attribute restrictions - if (!ItemTradeAttribute.canTrade(item)) { - String reason = ItemTradeAttribute.getTradeBlockReason(item); - // Send message to player with reason - return null; - } - - // Check blacklist - if (AuctionBlacklist.getInstance().isBlacklisted(item)) { - // Send "This item cannot be auctioned" message - return null; - } - - // ... rest of listing creation -} -``` - ---- - -## Feature 5: Admin Auction Management - -### 5.1 Overview - -A comprehensive admin system for managing the auction house in real-time. - -### 5.2 Admin Permissions - -**File: `src/main/java/noppes/npcs/CustomNpcsPermissions.java`** - -Add new permissions: - -```java -// Auction Administration -public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); -public static final Permission AUCTION_MANAGE = new Permission("customnpcs.auction.manage"); -public static final Permission AUCTION_BLACKLIST = new Permission("customnpcs.auction.blacklist"); -``` - -### 5.3 Admin GUI - -**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionAdmin.java`** - -Features: -- **View All Listings**: See all active, expired, and cancelled listings -- **Search/Filter**: Find listings by seller, item, status -- **Cancel Any Listing**: Force-cancel listings (returns item to seller) -- **Modify Listing**: Edit prices, duration (with warning) -- **End Auction Early**: Force-end with current highest bidder winning -- **Refund Bidders**: Manually refund specific bidders -- **View Blacklist**: Manage item blacklist -- **Audit Log**: View recent auction actions - -```java -public class GuiAuctionAdmin extends GuiNPCInterface { - private List allListings; - private int currentPage = 0; - private EnumAuctionStatus filterStatus = null; - private String searchQuery = ""; - - // Admin action buttons - private static final int BTN_CANCEL = 10; - private static final int BTN_END_EARLY = 11; - private static final int BTN_EDIT = 12; - private static final int BTN_REFUND = 13; - private static final int BTN_BLACKLIST = 14; - private static final int BTN_AUDIT = 15; - - @Override - public void initGui() { - // Tab buttons: All | Active | Sold | Expired | Cancelled - // Search bar - // Listing table with columns: ID, Seller, Item, Price, Bids, Status, Time - // Action buttons for selected listing - // Blacklist management button - } - - @Override - protected void actionPerformed(GuiButton button) { - switch (button.id) { - case BTN_CANCEL: - adminCancelListing(selectedListing); - break; - case BTN_END_EARLY: - adminEndAuction(selectedListing); - break; - case BTN_EDIT: - openEditDialog(selectedListing); - break; - case BTN_REFUND: - openRefundDialog(selectedListing); - break; - case BTN_BLACKLIST: - openBlacklistGui(); - break; - case BTN_AUDIT: - openAuditLog(); - break; - } - } -} -``` - -### 5.4 Admin Actions in AuctionController - -```java -// Admin-only methods -public boolean adminCancelListing(EntityPlayer admin, int listingId, String reason) { - if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { - return false; - } - - AuctionListing listing = listings.get(listingId); - if (listing == null) return false; - - // Refund current bidder if any - if (listing.highBidderUUID != null) { - addClaim(listing.highBidderUUID, listing.highBidderName, - null, listing.currentBid, "Admin cancelled auction: " + reason); - } - - // Return item to seller - addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, - "Listing cancelled by admin: " + reason); - - listing.status = EnumAuctionStatus.CANCELLED; - - // Log action - logAdminAction(admin, "CANCEL", listingId, reason); - - return true; -} - -public boolean adminEndAuction(EntityPlayer admin, int listingId) { - if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { - return false; - } - - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { - return false; - } - - // End as if time expired - listing.expirationTime = System.currentTimeMillis(); - processExpiredAuctions(); - - // Log action - logAdminAction(admin, "END_EARLY", listingId, ""); - - return true; -} - -public boolean adminEditListing(EntityPlayer admin, int listingId, - double newStartPrice, double newBuyout, - long newExpiration) { - if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { - return false; - } - - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { - return false; - } - - // Only allow if no bids placed - if (listing.highBidderUUID != null) { - return false; // Cannot edit listing with bids - } - - listing.startingPrice = newStartPrice; - listing.currentBid = newStartPrice; - listing.buyoutPrice = newBuyout; - listing.expirationTime = newExpiration; - - // Log action - logAdminAction(admin, "EDIT", listingId, - String.format("price=%.2f,buyout=%.2f", newStartPrice, newBuyout)); - - return true; -} - -// Audit logging -private void logAdminAction(EntityPlayer admin, String action, int listingId, String details) { - // Log to file: timestamp, admin name, action, listing id, details - LogWriter.info(String.format("[AUCTION ADMIN] %s performed %s on listing %d: %s", - admin.getCommandSenderName(), action, listingId, details)); -} -``` - -### 5.5 Blacklist Management GUI - -**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionBlacklist.java`** - -Features: -- Add item by holding it and clicking "Add" -- Add by typing item registry name -- Add entire mod by mod ID -- Add NBT tag pattern -- View current blacklist with remove buttons -- Test item against blacklist - -### 5.6 Access Points - -The Admin GUI can be accessed via: -1. **Global Menu**: New "Auction Management" button (requires permission) -2. **Auctioneer NPC**: Admin gear icon when editing NPC role (requires permission) -3. **Command**: `/cnpc auction admin` (requires OP or permission) +## Not Yet Implemented + +### Phase 1: Currency System (NOT STARTED) +Requires implementation: +- [ ] `VaultHelper.java` - VaultAPI integration via reflection +- [ ] `CurrencyController.java` - Currency management +- [ ] `PlayerCurrencyData.java` - Per-player currency (shared across profile slots) +- [ ] Vault dependency in build files +- [ ] Currency configuration GUI + +**Note:** Currently auctions use `long` for prices but the actual currency deduction/deposit is not connected to any economy system. + +### Phase 7: Admin Management (NOT STARTED) +Requires implementation: +- [ ] `GuiAuctionAdmin.java` - Admin auction management GUI +- [ ] `GuiAuctionBlacklist.java` - Blacklist management GUI +- [ ] Admin methods in `AuctionController.java` (adminCancelListing, adminEditListing, etc.) +- [ ] Audit logging for admin actions +- [ ] `/cnpc auction admin` command + +### Additional Missing Features +- [ ] Auctioneer NPC setup GUI (`GuiNpcAuctioneerSetup.java`) +- [ ] Trader stock reset timer banner display +- [ ] Comprehensive testing +- [ ] Player balance display (requires Currency System) --- ## Configuration Reference -All auction and market settings will be added to the **main configuration file** under dedicated categories. +All settings are in `ConfigMarket.java`: -### Location +```java +// Currency +public static String CurrencyName = "Gold"; -**File: `config/CustomNPCs/main.cfg`** (existing main config) +// Auction Durations (in hours) +public static int AuctionDurationShort = 12; +public static int AuctionDurationMedium = 24; +public static int AuctionDurationLong = 48; +public static int AuctionDurationVeryLong = 72; -New categories to add: -- `market` - General market/economy settings -- `market.currency` - Currency system settings -- `market.auction` - Auction house settings -- `market.trader` - Trader stock settings +// Auction Fees (per duration) +public static long AuctionFeeShort = 10; +public static long AuctionFeeMedium = 25; +public static long AuctionFeeLong = 50; +public static long AuctionFeeVeryLong = 100; -### Market Configuration (Main Category) +// Listing Configuration +public static int DefaultMaxListings = 5; +public static double ListingFeePercent = 0.0; +public static double SalesTaxPercent = 0.05; +public static double MinBidIncrementPercent = 0.05; +public static int ClaimExpirationDays = 30; +public static boolean RequireAuctioneerNPC = true; -```properties -# =========================================== -# Market & Economy Configuration -# =========================================== -market { - # Enable the market/economy system - B:Enabled=true - - # =========================================== - # Currency Settings - # =========================================== - currency { - # Use VaultAPI if available on Bukkit hybrid servers - B:UseVaultIfAvailable=true - - # Fallback currency item (registry name) when Vault unavailable - # Leave empty to disable fallback currency - S:FallbackCurrencyItem=customnpcs:npcCoinGold - - # Conversion ratio: 1 fallback item = X virtual currency - D:FallbackItemRatio=1.0 - } - - # =========================================== - # Auction House Settings - # =========================================== - auction { - # Enable/disable the auction house system - B:Enabled=true - - # Default maximum auction listings per player (without permission override) - I:DefaultMaxListings=5 - - # Listing fee as percentage of starting price (0.0 = no fee) - D:ListingFeePercent=0.0 - - # Sales tax as percentage taken from final sale price (0.05 = 5%) - D:SalesTaxPercent=0.05 - - # Require an Auctioneer NPC to access auction house - B:RequireAuctioneerNPC=true - - # Available auction durations in hours (comma-separated) - S:AvailableDurations=2,12,24,48 - - # Default auction duration in hours - I:DefaultDurationHours=24 - - # Claim expiration in days (claims auto-expire after this period, 0 = never) - I:ClaimExpirationDays=30 - - # Minimum bid increment as percentage (0.05 = 5% minimum increase) - D:MinBidIncrementPercent=0.05 - - # Snipe protection: extend auction TO this many minutes when bid placed near end - # Set to 0 to disable snipe protection - I:SnipeProtectionMinutes=5 - - # How many minutes before end triggers snipe protection - # (bids placed with less than this remaining will extend the auction) - I:SnipeProtectionThreshold=5 - } - - # =========================================== - # Trader Stock Settings - # =========================================== - trader { - # Enable stock system by default for new traders - B:EnableStockByDefault=false - - # Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) - S:DefaultResetType=NONE - } -} +// Snipe Protection +public static int SnipeProtectionMinutes = 5; +public static int SnipeProtectionThreshold = 5; ``` -### Permission Nodes - -| Permission | Description | Default | -|------------|-------------|---------| -| `customnpcs.auction.use` | Access auction house | true | -| `customnpcs.auction.sell` | Create auction listings | true | -| `customnpcs.auction.buy` | Bid and buyout auctions | true | -| `customnpcs.auction.slots.X` | Have X maximum listings | - | -| `customnpcs.auction.admin` | Access admin management | op | -| `customnpcs.auction.manage` | Modify/cancel any listing | op | -| `customnpcs.auction.blacklist` | Manage item blacklist | op | -| `customnpcs.auction.bypass.fee` | Bypass listing fees | op | -| `customnpcs.auction.bypass.tax` | Bypass sales tax | op | - --- -## Implementation Order +## File Inventory -The features should be implemented in this order due to dependencies: +### New Files Created -### Phase 1: Currency System (Foundation) -1. Add VaultAPI dependency to build files -2. Create `VaultHelper.java` with reflection-based integration -3. Create `CurrencyController.java` -4. Add initialization hook in `CustomNpcs.java` -5. Create `PlayerCurrencyData.java` and integrate with `PlayerData` -6. Create currency configuration GUI -7. Test with and without Vault present +| File | Purpose | +|------|---------| +| `constants/EnumAuctionDuration.java` | Auction duration options | +| `constants/EnumAuctionSort.java` | Sort options with lang keys | +| `constants/EnumAuctionStatus.java` | Auction state enum | +| `constants/EnumStockReset.java` | Trader stock reset types | +| `controllers/AuctionController.java` | Main auction controller | +| `controllers/AuctionBlacklist.java` | Item blacklist system | +| `controllers/data/AuctionListing.java` | Auction listing data | +| `controllers/data/AuctionFilter.java` | Search/filter criteria | +| `controllers/data/AuctionClaim.java` | Claim data structure | +| `controllers/data/TraderStock.java` | Trader stock system | +| `controllers/data/attribute/ItemTradeAttribute.java` | Trade restriction attributes | +| `roles/RoleAuctioneer.java` | Auctioneer NPC role | +| `containers/ContainerAuction.java` | Auction GUI container | +| `client/gui/player/GuiAuction.java` | Main auction GUI | +| `client/gui/player/SubGuiAuctionCreate.java` | Create listing GUI | +| `client/gui/player/SubGuiAuctionBid.java` | Place bid GUI | +| `client/gui/player/SubGuiAuctionDetails.java` | Listing details GUI | +| `config/ConfigMarket.java` | Market configuration | +| `network/packets/player/AuctionActionPacket.java` | Auction action packet | +| `network/packets/data/AuctionDataPacket.java` | Auction data sync packet | -### Phase 2: Trader Enhancements -1. Create `EnumStockReset.java` -2. Create `TraderStock.java` -3. Modify `RoleTrader.java` to add stock and currency fields -4. Modify `ContainerNPCTrader.java` to handle stock/currency -5. Update `GuiNpcTraderSetup.java` with new options -6. Update network packets for stock synchronization -7. Test stock depletion and reset timers +### Files Deleted +| File | Reason | +|------|--------| +| ~~`constants/EnumAuctionCategory.java`~~ | Categories removed | +| ~~`controllers/AuctionLogger.java`~~ | Consolidated into AuctionController | +| ~~`controllers/AuctionNotificationController.java`~~ | Consolidated into AuctionController | -### Phase 3: Item Attributes & Restrictions -1. Create `ItemTradeAttribute.java` with Untradeable, SlotBound, SoulBound tags -2. Create `AuctionBlacklist.java` controller -3. Add API methods to scripting interface for setting item attributes -4. Create blacklist persistence (load/save to file) -5. Test attribute blocking in listing creation - -### Phase 4: Auction House -1. Create auction enums (`EnumAuctionDuration`, `EnumAuctionStatus`, `EnumAuctionCategory`) -2. Create `AuctionListing.java` and `AuctionClaim` classes -3. Create `AuctionController.java` with item restriction checks -4. Update `EnumRoleType.java` to add Auctioneer -5. Create `RoleAuctioneer.java` -6. Update `DataAdvanced.java` for role instantiation -7. Update `EnumGuiType.java` with auction GUIs -8. Create auction containers -9. Create auction GUI classes -10. Create network packets -11. Add periodic task for expired auctions -12. Create auctioneer setup GUI -13. Comprehensive testing - -### Phase 5: Admin Auction Management -1. Add auction admin permissions to `CustomNpcsPermissions.java` -2. Create `GuiAuctionAdmin.java` admin interface -3. Create `GuiAuctionBlacklist.java` blacklist management -4. Add admin methods to `AuctionController.java` -5. Add audit logging -6. Create admin network packets -7. Add command handler for `/cnpc auction admin` -8. Test admin operations - ---- - -## File Summary - -### New Files to Create - -| File | Description | -|------|-------------| -| `kamkeel/npcs/compat/VaultHelper.java` | VaultAPI integration | -| `kamkeel/npcs/controllers/CurrencyController.java` | Currency management | -| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data (NOT slot-bound) | -| `noppes/npcs/controllers/data/TraderStock.java` | Trader stock configuration | -| `noppes/npcs/constants/EnumStockReset.java` | Stock reset types | -| `kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java` | Untradeable/SlotBound/SoulBound item attributes | -| `noppes/npcs/controllers/AuctionBlacklist.java` | Auction item blacklist controller | -| `noppes/npcs/controllers/AuctionController.java` | Auction house management | -| `noppes/npcs/controllers/data/AuctionListing.java` | Auction listing data | -| `noppes/npcs/constants/EnumAuctionDuration.java` | Auction duration options | -| `noppes/npcs/constants/EnumAuctionStatus.java` | Auction status states | -| `noppes/npcs/constants/EnumAuctionCategory.java` | Item categories | -| `noppes/npcs/roles/RoleAuctioneer.java` | Auctioneer NPC role | -| `noppes/npcs/containers/ContainerAuction*.java` | Auction containers | -| `noppes/npcs/client/gui/player/GuiAuction*.java` | Auction GUIs | -| `noppes/npcs/client/gui/roles/GuiNpcAuctioneerSetup.java` | Auctioneer config | -| `noppes/npcs/client/gui/global/GuiAuctionAdmin.java` | Admin auction management | -| `noppes/npcs/client/gui/global/GuiAuctionBlacklist.java` | Blacklist management GUI | -| `kamkeel/npcs/network/packets/player/AuctionActionPacket.java` | Auction actions | -| `kamkeel/npcs/network/packets/data/AuctionSyncPacket.java` | Auction data sync | -| `kamkeel/npcs/network/packets/admin/AuctionAdminPacket.java` | Admin auction actions | - -### Files to Modify +### Files Modified | File | Changes | |------|---------| -| `dependencies.gradle` | Add VaultAPI dependency | -| `repositories.gradle` | Add Vault repository | -| `CustomNpcs.java` | Initialize VaultHelper, AuctionController, AuctionBlacklist | -| `CustomNpcsPermissions.java` | Add auction admin permissions | -| `EnumRoleType.java` | Add Auctioneer | -| `EnumGuiType.java` | Add auction GUI types, admin GUIs | -| `DataAdvanced.java` | Handle Auctioneer role | -| `RoleTrader.java` | Add stock and currency support | -| `ContainerNPCTrader.java` | Handle stock/currency transactions | -| `GuiNpcTrader.java` | Add stock reset timer banner above GUI | -| `GuiNpcTraderSetup.java` | Add stock/currency UI | -| `PlayerData.java` | Add PlayerCurrencyData (NOT in slot components - shared across slots) | -| `CommonProxy.java` | Register auction containers | -| `ServerTickHandler.java` | Add auction expiration check | -| `PacketHandler.java` | Register auction packets and admin packets | -| `CommandKamkeel.java` | Add `/cnpc auction admin` command | +| `CustomNpcs.java` | Initialize AuctionController, AuctionBlacklist | +| `ServerTickHandler.java` | Call AuctionController.onPlayerLogin() | +| `constants/EnumRoleType.java` | Added Auctioneer | +| `constants/EnumGuiType.java` | Added auction GUI types | +| `DataAdvanced.java` | Handle Auctioneer role creation | +| `roles/RoleTrader.java` | Added stock system | +| `containers/ContainerNPCTrader.java` | Stock handling | +| `CustomNpcsPermissions.java` | Auction permission nodes | +| `api/ICustomNpc.java` | Auction API methods | +| `PacketHandler.java` | Register auction packets | +| `en_US.lang` | All auction lang keys | --- -## Testing Strategy +## Localization Keys Added -### Unit Testing -- Currency operations (deposit, withdraw, balance) -- Currency shared across profile slots (not slot-bound) -- Stock depletion and reset calculations -- Auction bid validation -- Auction expiration handling -- Item attribute checks (Untradeable, SlotBound, SoulBound) -- Blacklist matching (by item, mod, NBT tag) +```properties +# Main GUI +auction.title=Auction House +auction.browse=Browse +auction.myListings=My Listings +auction.myBids=My Bids +auction.claims=Claims +auction.sell=Sell +auction.search=Search +auction.refresh=Refresh +auction.viewDetails=View Details +auction.placeBid=Place Bid +auction.buyout=Buyout +auction.cancel=Cancel +auction.prevPage=< Prev +auction.nextPage=Next > +auction.noResults=No results +auction.page=Page %d of %d +auction.balance=Balance: %s +auction.listings=%d listings -### Integration Testing -- VaultAPI integration on hybrid servers -- Fallback to item currency when Vault unavailable -- Stock persistence across server restarts -- Auction persistence and recovery -- Profile slot switching with shared currency -- Outbid refunds going to claims correctly -- Admin actions affecting live auctions +# Tab Titles +auction.browseTitle=Browse Auctions +auction.myListingsTitle=My Listings +auction.myBidsTitle=My Active Bids +auction.claimsTitle=Claim Items +auction.sellTitle=Create Listing -### User Acceptance Testing -- Complete trading flow with stock limits -- Complete auction flow (list → bid → win → claim) -- Currency display formatting with gold coin icon -- Stock reset timer banner display above trader GUI -- GUI usability -- Admin management workflow +# Listing Details +auction.item=Item: +auction.seller=Seller: +auction.startingPrice=Starting Price: +auction.buyoutPrice=Buyout Price: +auction.currentPrice=Current Price: +auction.currentBid=Current Bid: +auction.minimumBid=Minimum Bid: +auction.highBidder=High Bidder: +auction.totalBids=Total Bids: +auction.quantity=Quantity: +auction.timeLeft=Time Left: +auction.yourBid=Your Bid: +auction.yourBalance=Your Balance: +auction.ended=Ended +auction.noBidsYet=No bids yet -### Edge Cases -- Negative currency amounts -- Concurrent bids on same auction -- Server crash during transaction -- Player offline when auction ends (claim on next login) -- Maximum listings per player (permission-based) -- Inventory full when claiming -- Trying to auction Untradeable/SlotBound/SoulBound items -- Trying to auction blacklisted items -- Admin cancelling auction with active bids -- Profile slot switch while having pending claims +# Create Listing +auction.createListing=Create Listing +auction.placeItemBelow=Place item in slot below: +auction.noItemSelected=(No item selected) +auction.noBuyoutHint=(0 = no buyout) +auction.duration=Duration: +auction.listingFee=Listing Fee: +auction.create=Create ---- +# Bid GUI +auction.minBid=Min Bid +auction.bidPlus10=+10% +auction.bidPlus25=+25% +auction.insufficientFunds=Insufficient funds! +auction.auctionEnded=Auction has ended! +auction.auctionDetails=Auction Details -## API/Scripting Extensions +# Sort Options +auction.sort.newest=Newest First +auction.sort.oldest=Oldest First +auction.sort.priceHigh=Most Expensive +auction.sort.priceLow=Least Expensive +auction.sort.endingSoon=Ending Soon +auction.sort.mostTime=Most Time Left +auction.sort.nameAZ=Name: A to Z +auction.sort.nameZA=Name: Z to A +auction.sort.mostBids=Most Bids +auction.sort.leastBids=Least Bids -Consider adding these scripting hooks: - -```java -// ICurrency interface for scripts -double getBalance(IPlayer player); -boolean withdraw(IPlayer player, double amount); -boolean deposit(IPlayer player, double amount); -String format(double amount); - -// IItemStack extensions for trade attributes -boolean isUntradeable(); -void setUntradeable(boolean value); -boolean isSlotBound(); -void setSlotBound(boolean value); -boolean isSoulBound(); -void setSoulBound(boolean value); -boolean canTrade(); // Returns false if any blocking attribute is set -String getTradeBlockReason(); // Returns localization key for reason - -// IAuction interface for scripts -IAuctionListing createListing(IPlayer seller, IItemStack item, - double startPrice, double buyout, int durationHours); -boolean cancelListing(int listingId); -List getPlayerListings(IPlayer player); -boolean isItemBlacklisted(IItemStack item); - -// IAuctionBlacklist interface for scripts (admin) -void addItemToBlacklist(String registryName); -void removeItemFromBlacklist(String registryName); -void addModToBlacklist(String modId); -void removeModFromBlacklist(String modId); -boolean isBlacklisted(IItemStack item); - -// Events -onAuctionCreated(IAuctionListing listing); -onAuctionBid(IAuctionListing listing, IPlayer bidder, double amount); -onAuctionOutbid(IAuctionListing listing, IPlayer previousBidder, double refundAmount); -onAuctionSold(IAuctionListing listing, IPlayer buyer, double price); -onAuctionExpired(IAuctionListing listing); -onAuctionCancelled(IAuctionListing listing, boolean byAdmin); -onTraderPurchase(ITrader trader, IPlayer buyer, int slot, int stockRemaining); -onTraderStockReset(ITrader trader); -onCurrencyTransaction(IPlayer player, double amount, boolean isDeposit); +# Duration Options +auction.duration.short=Short (12h) +auction.duration.medium=Medium (24h) +auction.duration.long=Long (48h) +auction.duration.veryLong=Very Long (72h) ``` --- -## Conclusion +## Design Decisions (Confirmed) -This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The five features are designed to work together seamlessly: +1. **Outbid Refunds** → Go to claims (not direct to balance) +2. **Currency** → Shared across all profile slots per player (NOT slot-bound) +3. **Permissions** → Uses existing CustomNPC+ permission pattern (default: 5 slots) +4. **Categories** → Removed for initial implementation (can add later) +5. **Controller Architecture** → Single consolidated AuctionController +6. **Localization** → All client strings use StatCollector with lang keys +7. **StatCollector Usage** → Safe - only called from client-side GUI code +8. **Claim Expiration** → 30 days default (configurable) +9. **Snipe Protection** → Extend TO 5 minutes when bid placed near end +10. **Logging** → Inline in AuctionController using LogWriter -1. **Currency System** provides the foundation for monetary transactions (shared across profile slots) -2. **Trader Enhancements** modernize the existing trader with stock, currency support, and reset timer banners -3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, ProfileSlot Bound, and Soulbound attributes plus blacklists -4. **Auction House** creates a global marketplace with claims-based refunds, bid modification, and permission-based slot limits -5. **Admin Management** provides comprehensive tools for server operators to manage the auction house in real-time +--- -The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems, existing requirement types) and can be developed incrementally, with each phase building on the previous one. +## Next Steps -### Key Design Decisions Summary -- Outbid refunds → Claims (not direct to balance) with **Modify Bid** feature -- Currency → Shared across all profile slots per player -- Permissions → Uses existing CustomNPC+ permission pattern (default: 5 slots) -- Display icon → Existing `CustomItems.coinGold` -- Stock reset timer → Banner displayed ABOVE the trader GUI -- Trade restrictions → New Untradeable + existing ProfileSlotRequirement/SoulbindRequirement + Blacklist -- Claim expiration → 30 days default (configurable) -- Bid increment → Percentage-based minimum (configurable, e.g., 5%) -- Snipe protection → Extend TO 5 minutes when bid placed near end (configurable) -- Auction durations → 2h, 12h, 24h, 48h (configurable) -- Admin tools → Full management GUI with audit logging +1. **Currency System** - Implement VaultHelper and CurrencyController to connect actual economy +2. **Admin Management** - Implement admin GUI and commands +3. **Testing** - Comprehensive testing of all auction flows +4. **Auctioneer Setup GUI** - Create NPC configuration interface +5. **Trader Banner** - Add stock reset timer display above trader GUI From d657bab73c707590122253f58a8dce73a0e48c15 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 15:21:11 -0500 Subject: [PATCH 15/21] Harden auction display and trader stock init --- .../packets/player/AuctionActionPacket.java | 27 ++++++++--- .../npcs/client/gui/player/GuiAuction.java | 32 +++++++++---- .../client/gui/player/SubGuiAuctionBid.java | 21 +++++++-- .../gui/player/SubGuiAuctionCreate.java | 18 +++++-- .../gui/player/SubGuiAuctionDetails.java | 6 +++ .../java/noppes/npcs/config/ConfigMarket.java | 12 ++++- .../npcs/controllers/AuctionController.java | 47 +++++++++++++++---- .../noppes/npcs/controllers/VaultHelper.java | 2 +- .../npcs/controllers/data/AuctionFilter.java | 11 ++++- .../npcs/controllers/data/TraderStock.java | 6 +++ .../noppes/npcs/roles/RoleAuctioneer.java | 7 +-- 11 files changed, 148 insertions(+), 41 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index 54bfaef50..a63312d80 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -17,6 +17,7 @@ import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.containers.ContainerAuction; import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.data.AuctionFilter; @@ -290,10 +291,23 @@ public class AuctionActionPacket extends AbstractPacket { try { NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); - ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); + ItemStack requestedItem = ItemStack.loadItemStackFromNBT(itemNBT); - if (item == null) { - sendResponse(player, false, "Invalid item"); + if (!(player.openContainer instanceof ContainerAuction)) { + sendResponse(player, false, "Open the auction listing screen to create a listing"); + return; + } + + ContainerAuction container = (ContainerAuction) player.openContainer; + ItemStack slotItem = container.getListingItem(); + + if (requestedItem == null || slotItem == null) { + sendResponse(player, false, "Place an item in the listing slot"); + return; + } + + if (!ItemStack.areItemStacksEqual(slotItem, requestedItem) || slotItem.stackSize != requestedItem.stackSize) { + sendResponse(player, false, "Listing item mismatch"); return; } @@ -318,11 +332,10 @@ public class AuctionActionPacket extends AbstractPacket { // Create the listing AuctionListing listing = AuctionController.Instance.createListing( - player, item, startingPrice, buyoutPrice, duration); + player, slotItem, startingPrice, buyoutPrice, duration); if (listing != null) { - // Remove item from player's inventory - // Note: The GUI should have already collected the item, this is validation + container.clearListingSlot(); sendResponse(player, true, "Listing created successfully"); } else { sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); @@ -429,7 +442,7 @@ public class AuctionActionPacket extends AbstractPacket { return; } - if (AuctionController.Instance.cancelListing(listingId, player)) { + if (AuctionController.Instance.cancelListing(listingId, player, isAdmin)) { sendResponse(player, true, "Listing cancelled. Claim your item."); } else { sendResponse(player, false, "Failed to cancel listing"); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 118dde167..507368c7d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -215,6 +215,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private void viewDetails() { if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { AuctionListing listing = listings.get(selectedListingIndex); + if (listing.item == null) { + return; + } setSubGui(new SubGuiAuctionDetails(this, listing)); } } @@ -222,6 +225,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private void placeBid() { if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { AuctionListing listing = listings.get(selectedListingIndex); + if (listing.item == null) { + return; + } setSubGui(new SubGuiAuctionBid(this, listing, playerBalance)); } } @@ -261,7 +267,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, String line = formatListingLine(listing); displayList.add(line); } - listingScroll.setList(displayList); + listingScroll.setList(displayList, true, false); if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { listingScroll.selected = selectedListingIndex; @@ -269,7 +275,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } private String formatListingLine(AuctionListing listing) { - String itemName = listing.item.getDisplayName(); + String itemName = listing.item == null + ? "Unknown" + : listing.item.getDisplayName(); if (itemName.length() > 15) { itemName = itemName.substring(0, 12) + "..."; } @@ -324,12 +332,17 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Action buttons based on selection boolean hasSelection = selectedListingIndex >= 0 && selectedListingIndex < listings.size(); + if (hasSelection && listings.get(selectedListingIndex).item == null) { + hasSelection = false; + } if (getButton(50) != null) getButton(50).setEnabled(hasSelection); if (getButton(51) != null) getButton(51).setEnabled(hasSelection); if (hasSelection && getButton(52) != null) { AuctionListing listing = listings.get(selectedListingIndex); - getButton(52).setEnabled(listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice); + getButton(52).setEnabled(listing.item != null && + listing.buyoutPrice > 0 && + playerBalance >= listing.buyoutPrice); } else if (getButton(52) != null) { getButton(52).setEnabled(false); } @@ -392,18 +405,17 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // ICustomScrollListener @Override - public void scrollClicked(int scrollId, int index, boolean doubleClick, String selection) { + public void customScrollClicked(int mouseX, int mouseY, int mouseButton, GuiCustomScroll scroll) { + int index = scroll.selected; selectedListingIndex = index; updateButtonStates(); - - if (doubleClick && selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - viewDetails(); - } } @Override - public void scrollDoubleClicked(String selection, GuiCustomScroll scroll) { - // Handled above + public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + viewDetails(); + } } // IGuiData diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index 7f4665b40..fbf62fc1d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -35,6 +35,12 @@ public class SubGuiAuctionBid extends SubGuiInterface { int y = guiTop + 10; int labelColor = 0x404040; + if (listing.item == null) { + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 40, y, 0xAA0000)); + addButton(new GuiNpcButton(31, guiLeft + 60, guiTop + 110, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + return; + } + // Title addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 70, y, labelColor)); y += 18; @@ -70,8 +76,9 @@ public class SubGuiAuctionBid extends SubGuiInterface { // Bid input addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); - bidField.setNumbersOnly(); - bidField.setMinMaxDefault(minBid, 999999999, minBid); + bidField.setIntegersOnly(); + int minBidInt = (int) Math.min(Integer.MAX_VALUE, Math.max(1L, minBid)); + bidField.setMinMaxDefault(minBidInt, Integer.MAX_VALUE, minBidInt); addTextField(bidField); y += 24; @@ -179,7 +186,7 @@ public class SubGuiAuctionBid extends SubGuiInterface { } private void placeBid() { - long bidAmount = bidField.getInteger(); + long bidAmount = parseBid(bidField); long minBid = listing.getMinimumBid(); if (bidAmount < minBid) { @@ -199,6 +206,14 @@ public class SubGuiAuctionBid extends SubGuiInterface { close(); } + private long parseBid(GuiNpcTextField field) { + try { + return Long.parseLong(field.getText()); + } catch (NumberFormatException e) { + return 0L; + } + } + @Override public void save() { // Nothing to save diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index 75329f5e8..d95dc229a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -51,14 +51,14 @@ public class SubGuiAuctionCreate extends SubGuiInterface { // Starting price addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 70, 0x404040)); startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); - startingPriceField.setNumbersOnly(); + startingPriceField.setIntegersOnly(); startingPriceField.setMinMaxDefault(1, 999999999, 100); addTextField(startingPriceField); // Buyout price (optional) addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 90, 0x404040)); buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); - buyoutPriceField.setNumbersOnly(); + buyoutPriceField.setIntegersOnly(); buyoutPriceField.setMinMaxDefault(0, 999999999, 0); addTextField(buyoutPriceField); addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 185, guiTop + 90, 0x888888)); @@ -87,7 +87,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { private long getListingFee() { EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getFee(); + return duration.getListingFee(); } @Override @@ -113,8 +113,8 @@ public class SubGuiAuctionCreate extends SubGuiInterface { return; } - long startingPrice = startingPriceField.getInteger(); - long buyoutPrice = buyoutPriceField.getInteger(); + long startingPrice = parsePrice(startingPriceField, 1); + long buyoutPrice = parsePrice(buyoutPriceField, 0); if (startingPrice < 1) { startingPrice = 1; @@ -135,6 +135,14 @@ public class SubGuiAuctionCreate extends SubGuiInterface { close(); } + private long parsePrice(GuiNpcTextField field, long defaultValue) { + try { + return Long.parseLong(field.getText()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + @Override public void updateScreen() { super.updateScreen(); diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java index f18046922..4054d04b1 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -34,6 +34,12 @@ public class SubGuiAuctionDetails extends SubGuiInterface { int labelColor = 0x404040; int valueColor = 0x000000; + if (listing.item == null) { + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); + addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); + return; + } + // Title addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 80, y, labelColor)); y += 16; diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 81eda2c3f..f0f019290 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -95,8 +95,16 @@ public class ConfigMarket { StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, "Starting balance for new players (only used when Vault is not available)").getInt(1000); - MaxBalance = config.get(CURRENCY, "Maximum Balance", 999999999999L, - "Maximum balance a player can have (only used when Vault is not available)").getLong(999999999999L); + Property maxBalanceProp = config.get(CURRENCY, "Maximum Balance", String.valueOf(MaxBalance), + "Maximum balance a player can have (only used when Vault is not available)"); + try { + MaxBalance = Long.parseLong(maxBalanceProp.getString()); + } catch (NumberFormatException e) { + MaxBalance = 999999999999L; + } + if (MaxBalance < 0) { + MaxBalance = 0; + } // ========================================= // Auction Settings diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index b6bb41d10..8333fd01a 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -132,6 +132,7 @@ public class AuctionController { public List searchListings(String query) { String lowerQuery = query.toLowerCase(); return getActiveListings().stream() + .filter(l -> l.item != null) .filter(l -> l.item.getDisplayName().toLowerCase().contains(lowerQuery)) .collect(Collectors.toList()); } @@ -157,8 +158,14 @@ public class AuctionController { // Calculate pagination int totalResults = results.size(); - int totalPages = Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); - int startIndex = filter.page * filter.pageSize; + int totalPages = totalResults == 0 + ? 0 + : Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); + int currentPage = Math.max(0, filter.page); + if (totalPages > 0 && currentPage >= totalPages) { + currentPage = totalPages - 1; + } + int startIndex = currentPage * filter.pageSize; int endIndex = Math.min(startIndex + filter.pageSize, totalResults); // Get the page slice @@ -169,7 +176,7 @@ public class AuctionController { pageResults = new ArrayList<>(results.subList(startIndex, endIndex)); } - return new FilterResult(pageResults, totalResults, filter.page, totalPages); + return new FilterResult(pageResults, totalResults, currentPage, totalPages); } /** @@ -204,11 +211,11 @@ public class AuctionController { comparator = Comparator.comparingLong(l -> -l.expirationTime); break; case NAME_A_TO_Z: - comparator = Comparator.comparing(l -> l.item.getDisplayName().toLowerCase()); + comparator = Comparator.comparing(l -> getListingName(l).toLowerCase()); break; case NAME_Z_TO_A: comparator = Comparator.comparing((AuctionListing l) -> - l.item.getDisplayName().toLowerCase()).reversed(); + getListingName(l).toLowerCase()).reversed(); break; case MOST_BIDS: comparator = Comparator.comparingInt(l -> -l.bidCount); @@ -287,6 +294,7 @@ public class AuctionController { return ids.stream() .map(listings::get) .filter(Objects::nonNull) + .filter(AuctionListing::isActive) .filter(l -> bidderUUID.equals(l.highBidderUUID)) .collect(Collectors.toList()); } @@ -433,17 +441,33 @@ public class AuctionController { /** * Cancel an auction listing */ - public boolean cancelListing(int listingId, EntityPlayer requester) { + public boolean cancelListing(int listingId, EntityPlayer requester, boolean forceCancel) { AuctionListing listing = listings.get(listingId); if (listing == null) { return false; } boolean byAdmin = !listing.sellerUUID.equals(requester.getUniqueID()); - - if (!listing.cancel(requester.getUniqueID())) { + if (byAdmin && !forceCancel) { return false; } + if (!forceCancel && listing.hasBids()) { + return false; + } + if (listing.status != EnumAuctionStatus.ACTIVE) { + return false; + } + + if (forceCancel && listing.hasBids() && listing.highBidderUUID != null) { + CurrencyController.Instance.addClaim(listing.highBidderUUID, listing.currentBid, + "Auction Cancelled", String.valueOf(listingId)); + notifyPlayer(listing.highBidderUUID, EnumChatFormatting.RED + + "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + " was cancelled. Your " + + formatCurrency(listing.currentBid) + " has been refunded."); + } + + listing.status = EnumAuctionStatus.CANCELLED; save(); @@ -718,6 +742,13 @@ public class AuctionController { LogWriter.info(LOG_PREFIX + event + ": " + details); } + private String getListingName(AuctionListing listing) { + if (listing == null || listing.item == null) { + return ""; + } + return listing.item.getDisplayName(); + } + // ==================== Persistence ==================== public void save() { diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java index f660002fc..54b9235ef 100644 --- a/src/main/java/noppes/npcs/controllers/VaultHelper.java +++ b/src/main/java/noppes/npcs/controllers/VaultHelper.java @@ -4,7 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.CustomNpcs; import noppes.npcs.config.ConfigMarket; import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.controllers.data.PlayerDataController; +import noppes.npcs.controllers.PlayerDataController; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java index 70242179a..176896fae 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -107,6 +107,9 @@ public class AuctionFilter { if (listing == null) { return false; } + if (listing.item == null) { + return false; + } // Search text filter (case-insensitive) if (!searchText.isEmpty()) { @@ -199,9 +202,13 @@ public class AuctionFilter { maxPrice = compound.getLong("MaxPrice"); buyoutOnly = compound.getBoolean("BuyoutOnly"); noBidsOnly = compound.getBoolean("NoBidsOnly"); - page = compound.getInteger("Page"); + page = Math.max(0, compound.getInteger("Page")); pageSize = compound.getInteger("PageSize"); - if (pageSize <= 0) pageSize = 20; + if (pageSize <= 0) { + pageSize = 20; + } else { + pageSize = Math.min(100, pageSize); + } sellerUUID = compound.getString("SellerUUID"); sellerName = compound.getString("SellerName"); } diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index 808b06f36..f3c5f4db9 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -58,6 +58,9 @@ public class TraderStock { return pStock.getStock(slot, maxStock[slot]); } + if (currentStock[slot] < 0) { + return maxStock[slot]; + } return Math.max(0, currentStock[slot]); } @@ -83,6 +86,9 @@ public class TraderStock { return pStock.consumeStock(slot, maxStock[slot], amount); } + if (currentStock[slot] < 0) { + currentStock[slot] = maxStock[slot]; + } if (currentStock[slot] >= amount) { currentStock[slot] -= amount; return true; diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java index 0c0f53d07..4d8febaf1 100644 --- a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java +++ b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java @@ -6,6 +6,7 @@ import noppes.npcs.NoppesUtilServer; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.Line; import noppes.npcs.entity.EntityNPCInterface; /** @@ -69,9 +70,9 @@ public class RoleAuctioneer extends RoleInterface { // Check if auction system is enabled if (!ConfigMarket.AuctionEnabled) { if (!noAuctionMessage.isEmpty()) { - npc.say(player, noAuctionMessage); + npc.say(player, new Line(noAuctionMessage)); } else { - npc.say(player, "The auction house is currently closed."); + npc.say(player, new Line("The auction house is currently closed.")); } return; } @@ -83,7 +84,7 @@ public class RoleAuctioneer extends RoleInterface { // Say welcome message if (!welcomeMessage.isEmpty()) { - npc.say(player, welcomeMessage); + npc.say(player, new Line(welcomeMessage)); } else { npc.say(player, npc.advanced.getInteractLine()); } From e7f6177be89084345123db0a5d1810fc072c8c9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 20:36:40 +0000 Subject: [PATCH 16/21] Add Auctioneer role to NPC GUI and connect auction system - Add Auctioneer to role dropdown in GuiNpcAdvanced - Create GuiNpcAuctioneer setup GUI for configuring: - Tab visibility (browse, my listings, my bids, claims) - Player actions (create, bid, buyout) - Custom welcome/disabled messages - Add PlayerAuction container handler in CommonProxy - Add PlayerAuction GUI handler in ClientProxy - Add all role.auctioneer lang keys to en_US.lang --- src/main/java/noppes/npcs/CommonProxy.java | 4 + .../java/noppes/npcs/client/ClientProxy.java | 5 + .../client/gui/mainmenu/GuiNpcAdvanced.java | 5 +- .../client/gui/roles/GuiNpcAuctioneer.java | 144 ++++++++++++++++++ .../assets/customnpcs/lang/en_US.lang | 13 ++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index 58f931ab8..a73c179e4 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -15,6 +15,7 @@ import noppes.npcs.api.IWorld; import noppes.npcs.blocks.tiles.TileNpcContainer; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; +import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -131,6 +132,9 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.CompanionInv) return new ContainerNPCCompanion(npc, player); + if (gui == EnumGuiType.PlayerAuction) + return new ContainerAuction(npc, player); + return null; } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 7cc3d7b76..cbe2d3da7 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -104,6 +104,7 @@ import noppes.npcs.client.gui.player.GuiCrate; import noppes.npcs.client.gui.player.GuiMailbox; import noppes.npcs.client.gui.player.GuiMailmanWrite; import noppes.npcs.client.gui.player.GuiNPCBankChest; +import noppes.npcs.client.gui.player.GuiAuction; import noppes.npcs.client.gui.player.GuiNPCTrader; import noppes.npcs.client.gui.player.GuiNpcAnvil; import noppes.npcs.client.gui.player.GuiNpcCarpentryBench; @@ -177,6 +178,7 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.config.StringCache; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; +import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -568,6 +570,9 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.MagicBook) return new GuiNpcMagicBook(); + else if (gui == EnumGuiType.PlayerAuction) + return new GuiAuction(npc, (ContainerAuction) container); + else if (gui == EnumGuiType.GlobalRemote) return new GuiNPCGlobalMainMenu(null); diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 6452145a6..8c6248e40 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -25,6 +25,7 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; import noppes.npcs.client.gui.roles.GuiNpcSpawner; +import noppes.npcs.client.gui.roles.GuiNpcAuctioneer; import noppes.npcs.client.gui.roles.GuiNpcMount; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; @@ -48,7 +49,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { super.initGui(); int y = guiTop + 10; this.addButton(new GuiNpcButton(3, guiLeft + 85 + 160, y, 52, 20, "selectServer.edit")); - this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount"}, npc.advanced.role.ordinal())); + this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount", "role.auctioneer"}, npc.advanced.role.ordinal())); getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); this.addButton(new GuiNpcButton(4, guiLeft + 85 + 160, y += 22, 52, 20, "selectServer.edit")); @@ -151,6 +152,8 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { displayGuiScreen(new GuiNpcCompanion(npc)); else if (npc.advanced.role == EnumRoleType.Mount) displayGuiScreen(new GuiNpcMount(npc)); + else if (npc.advanced.role == EnumRoleType.Auctioneer) + displayGuiScreen(new GuiNpcAuctioneer(npc)); } else if (compound.hasKey("JobData")) { if (npc.jobInterface != null) npc.jobInterface.readFromNBT(compound); diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java new file mode 100644 index 000000000..d5fb8e962 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java @@ -0,0 +1,144 @@ +package noppes.npcs.client.gui.roles; + +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.role.RoleSavePacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.gui.util.GuiNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.roles.RoleAuctioneer; + +public class GuiNpcAuctioneer extends GuiNPCInterface2 implements ITextfieldListener { + + private final RoleAuctioneer role; + + public GuiNpcAuctioneer(EntityNPCInterface npc) { + super(npc); + this.role = npc.roleInterface instanceof RoleAuctioneer ? (RoleAuctioneer) npc.roleInterface : null; + } + + @Override + public void initGui() { + super.initGui(); + if (role == null) { + close(); + return; + } + + int y = guiTop + 10; + + // Left column - Tab visibility + addLabel(new GuiNpcLabel(0, "role.auctioneer.tabs", guiLeft + 10, y, 0x404040)); + y += 18; + + addLabel(new GuiNpcLabel(1, "role.auctioneer.showBrowse", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(1, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showActiveListings ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(2, "role.auctioneer.showMyListings", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(2, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyListings ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(3, "role.auctioneer.showMyBids", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(3, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyBids ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(4, "role.auctioneer.showClaims", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(4, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showClaims ? 1 : 0)); + y += 30; + + // Actions + addLabel(new GuiNpcLabel(10, "role.auctioneer.actions", guiLeft + 10, y, 0x404040)); + y += 18; + + addLabel(new GuiNpcLabel(11, "role.auctioneer.allowCreate", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(11, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowCreatingListings ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(12, "role.auctioneer.allowBid", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(12, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBidding ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(13, "role.auctioneer.allowBuyout", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(13, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBuyout ? 1 : 0)); + + // Right column - Messages + int rightX = guiLeft + 210; + int rightY = guiTop + 10; + + addLabel(new GuiNpcLabel(20, "role.auctioneer.messages", rightX, rightY, 0x404040)); + rightY += 18; + + addLabel(new GuiNpcLabel(21, "role.auctioneer.welcome", rightX, rightY + 5, 0x404040)); + rightY += 18; + GuiNpcTextField welcomeField = new GuiNpcTextField(21, this, rightX, rightY, 180, 20, role.welcomeMessage); + welcomeField.setMaxStringLength(200); + addTextField(welcomeField); + rightY += 28; + + addLabel(new GuiNpcLabel(22, "role.auctioneer.noAuction", rightX, rightY + 5, 0x404040)); + rightY += 18; + GuiNpcTextField noAuctionField = new GuiNpcTextField(22, this, rightX, rightY, 180, 20, role.noAuctionMessage); + noAuctionField.setMaxStringLength(200); + addTextField(noAuctionField); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (role == null) return; + + if (guibutton instanceof GuiNpcButton) { + GuiNpcButton button = (GuiNpcButton) guibutton; + boolean value = button.getValue() == 1; + + switch (button.id) { + case 1: + role.showActiveListings = value; + break; + case 2: + role.showMyListings = value; + break; + case 3: + role.showMyBids = value; + break; + case 4: + role.showClaims = value; + break; + case 11: + role.allowCreatingListings = value; + break; + case 12: + role.allowBidding = value; + break; + case 13: + role.allowBuyout = value; + break; + } + } + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (role == null) return; + + switch (textfield.id) { + case 21: + role.welcomeMessage = textfield.getText(); + break; + case 22: + role.noAuctionMessage = textfield.getText(); + break; + } + } + + @Override + public void save() { + if (role != null) { + PacketClient.sendClient(new RoleSavePacket(role.writeToNBT(new NBTTagCompound()))); + } + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 88c10066c..efec9bd2f 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -429,6 +429,19 @@ role.mount.hover=Hover role.mount.ascendspeed=Ascend Speed role.mount.descendspeed=Descend Speed role.mount.walkspeed=Walk Speed: %s +role.auctioneer=Auctioneer +role.auctioneer.tabs=Tab Visibility +role.auctioneer.showBrowse=Show Browse +role.auctioneer.showMyListings=Show My Listings +role.auctioneer.showMyBids=Show My Bids +role.auctioneer.showClaims=Show Claims +role.auctioneer.actions=Player Actions +role.auctioneer.allowCreate=Allow Create +role.auctioneer.allowBid=Allow Bid +role.auctioneer.allowBuyout=Allow Buyout +role.auctioneer.messages=Messages +role.auctioneer.welcome=Welcome Message +role.auctioneer.noAuction=Auction Disabled Message trader.insufficient=Insufficient Currency trader.sufficient=Sufficient Currency From bc7e768d5fbf14205075020c52423b433cd6872e Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 15:59:01 -0500 Subject: [PATCH 17/21] Show auction inventory only on create tab --- .../npcs/client/gui/player/GuiAuction.java | 274 +++++++++++++++--- .../client/gui/player/SubGuiAuctionBid.java | 34 +-- .../gui/player/SubGuiAuctionCreate.java | 26 +- .../gui/player/SubGuiAuctionDetails.java | 38 +-- .../npcs/containers/ContainerAuction.java | 6 +- 5 files changed, 284 insertions(+), 94 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 507368c7d..0a93e98ed 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -5,12 +5,15 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.util.*; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.entity.EntityNPCInterface; @@ -27,8 +30,25 @@ import java.util.List; public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, ICustomScrollListener, NoppesUtil.IAuctionData { + private static final ResourceLocation BG_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/standardbg.png"); + private static final int LEFT_PANEL_X = 8; + private static final int LEFT_PANEL_WIDTH = 190; + private static final int RIGHT_PANEL_X = 210; + private static final int ACTION_BUTTON_WIDTH = 98; + private static final int LISTING_SCROLL_HEIGHT = 100; + private static final int LISTING_SCROLL_WIDTH = 190; + private static final int PAGE_BUTTON_WIDTH = 60; + private static final int SEARCH_FIELD_WIDTH = 140; + private static final int SEARCH_BUTTON_WIDTH = 46; + private static final int HIDDEN_SLOT_POSITION = -1000; + private static final int INVENTORY_START_X = 8; + private static final int INVENTORY_START_Y = 166; + private static final int HOTBAR_Y_OFFSET = 58; + // GUI Components private GuiNpcTextField searchField; + private GuiNpcTextField startingPriceField; + private GuiNpcTextField buyoutPriceField; private GuiCustomScroll listingScroll; // Current state @@ -41,6 +61,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Tab state (0=Browse, 1=My Listings, 2=My Bids, 3=Claims, 4=Create) private int currentTab = 0; + private int selectedDuration = 1; // Selected listing for details private int selectedListingIndex = -1; @@ -51,8 +72,8 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { super(npc, container); this.title = ""; - this.xSize = 256; - this.ySize = 222; + this.xSize = 319; + this.ySize = 238; this.closeOnEsc = true; this.drawDefaultBackground = true; @@ -64,47 +85,66 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, public void initGui() { super.initGui(); + int leftX = guiLeft + LEFT_PANEL_X; + int rightX = guiLeft + RIGHT_PANEL_X; + int actionY = guiTop + 44; + boolean isCreateTab = currentTab == 4; + + updateListingSlotPosition(isCreateTab); + // Tab buttons at top - addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse"))); - addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, StatCollector.translateToLocal("auction.myListings"))); - addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, StatCollector.translateToLocal("auction.myBids"))); - addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, StatCollector.translateToLocal("auction.claims"))); - addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, StatCollector.translateToLocal("auction.sell"))); + GuiMenuTopButton browseButton = new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse")); + addTopButton(browseButton); + GuiMenuTopButton myListingsButton = new GuiMenuTopButton(1, browseButton, StatCollector.translateToLocal("auction.myListings")); + addTopButton(myListingsButton); + GuiMenuTopButton myBidsButton = new GuiMenuTopButton(2, myListingsButton, StatCollector.translateToLocal("auction.myBids")); + addTopButton(myBidsButton); + GuiMenuTopButton claimsButton = new GuiMenuTopButton(3, myBidsButton, StatCollector.translateToLocal("auction.claims")); + addTopButton(claimsButton); + GuiMenuTopButton sellButton = new GuiMenuTopButton(4, claimsButton, StatCollector.translateToLocal("auction.sell")); + addTopButton(sellButton); getTopButton(currentTab).setEnabled(false); - // Search field - searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 140, 14, currentFilter.searchText); - searchField.setMaxStringLength(50); - addTextField(searchField); + if (isCreateTab) { + addCreateControls(leftX, rightX); + } else { + // Search field + searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); + searchField.setMaxStringLength(50); + addTextField(searchField); - // Search button - addButton(new GuiNpcButton(11, guiLeft + 150, guiTop + 5, 40, 16, StatCollector.translateToLocal("auction.search"))); + // Search button + addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.search"))); - // Sort button - addButton(new GuiNpcButton(30, guiLeft + 8, guiTop + 24, 120, 16, currentFilter.sortOrder.getDisplayName())); + // Sort button + addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); - // Listing scroll area - listingScroll = new GuiCustomScroll(this, 0); - listingScroll.guiLeft = guiLeft + 8; - listingScroll.guiTop = guiTop + 44; - listingScroll.setSize(160, 128); - addScroll(listingScroll); - updateListingDisplay(); + // Listing scroll area + listingScroll = new GuiCustomScroll(this, 0); + listingScroll.guiLeft = leftX; + listingScroll.guiTop = guiTop + 44; + listingScroll.setSize(LISTING_SCROLL_WIDTH, LISTING_SCROLL_HEIGHT); + addScroll(listingScroll); + updateListingDisplay(); - // Pagination buttons - addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.prevPage"))); - addLabel(new GuiNpcLabel(41, getPageLabel(), guiLeft + 90, guiTop + 180, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.nextPage"))); + // Pagination buttons + addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.prevPage"))); + addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); + addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.nextPage"))); - // Action buttons (right side) - addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, StatCollector.translateToLocal("auction.viewDetails"))); - addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, StatCollector.translateToLocal("auction.placeBid"))); - addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, StatCollector.translateToLocal("auction.buyout"))); - addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, StatCollector.translateToLocal("auction.refresh"))); + // Action buttons (right side) + addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.viewDetails"))); + addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.placeBid"))); + addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.buyout"))); + addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.refresh"))); - // Balance display - addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), - guiLeft + 175, guiTop + 155, 0x00AA00)); + // Balance display + addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), + rightX, guiTop + 170, 0x00AA00)); + } updateButtonStates(); } @@ -119,6 +159,29 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, lastCountdownUpdate = now; updateListingDisplay(); } + + if (currentTab == 4 && inventorySlots instanceof ContainerAuction) { + ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); + GuiNpcLabel itemLabel = getLabel(102); + GuiNpcButton createButton = getButton(71); + if (item != null) { + if (itemLabel != null) { + itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); + itemLabel.color = 0x008800; + } + if (createButton != null) { + createButton.setEnabled(true); + } + } else { + if (itemLabel != null) { + itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); + itemLabel.color = 0x888888; + } + if (createButton != null) { + createButton.setEnabled(false); + } + } + } } @Override @@ -153,6 +216,13 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, buyout(); } else if (id == 53) { refresh(); + } else if (id == 70) { + selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; + initGui(); + } else if (id == 71) { + createListing(); + } else if (id == 72) { + switchTab(0); } } @@ -173,8 +243,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, case 3: // Claims AuctionActionPacket.QueryClaimable(); break; - case 4: // Create - open sub GUI - setSubGui(new SubGuiAuctionCreate(this, (ContainerAuction) inventorySlots)); + case 4: break; } @@ -348,19 +417,139 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } } + private void updateListingSlotPosition(boolean showInventory) { + if (!(inventorySlots instanceof ContainerAuction)) { + return; + } + + ContainerAuction container = (ContainerAuction) inventorySlots; + if (showInventory) { + container.listingSlot.xDisplayPosition = RIGHT_PANEL_X + 41; + container.listingSlot.yDisplayPosition = 64; + + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int slotIndex = 1 + col + row * 9; + container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; + container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + row * 18; + } + } + + for (int col = 0; col < 9; col++) { + int slotIndex = 28 + col; + container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; + container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; + } + } else { + container.listingSlot.xDisplayPosition = HIDDEN_SLOT_POSITION; + container.listingSlot.yDisplayPosition = HIDDEN_SLOT_POSITION; + + for (int slotIndex = 1; slotIndex < container.inventorySlots.size(); slotIndex++) { + container.inventorySlots.get(slotIndex).xDisplayPosition = HIDDEN_SLOT_POSITION; + container.inventorySlots.get(slotIndex).yDisplayPosition = HIDDEN_SLOT_POSITION; + } + } + } + + private void addCreateControls(int leftX, int rightX) { + addLabel(new GuiNpcLabel(100, StatCollector.translateToLocal("auction.createListing"), leftX, guiTop + 8, 0x404040)); + addLabel(new GuiNpcLabel(101, StatCollector.translateToLocal("auction.placeItemBelow"), rightX, guiTop + 32, 0x404040)); + + ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); + if (item != null) { + addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), + rightX, guiTop + 86, 0x008800)); + } else { + addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.noItemSelected"), rightX, guiTop + 86, 0x888888)); + } + + addLabel(new GuiNpcLabel(110, StatCollector.translateToLocal("auction.startingPrice"), leftX, guiTop + 36, 0x404040)); + startingPriceField = new GuiNpcTextField(111, this, leftX + 120, guiTop + 34, 90, 14, "100"); + startingPriceField.setIntegersOnly(); + startingPriceField.setMinMaxDefault(1, 999999999, 100); + addTextField(startingPriceField); + + addLabel(new GuiNpcLabel(120, StatCollector.translateToLocal("auction.buyoutPrice"), leftX, guiTop + 58, 0x404040)); + buyoutPriceField = new GuiNpcTextField(121, this, leftX + 120, guiTop + 56, 90, 14, "0"); + buyoutPriceField.setIntegersOnly(); + buyoutPriceField.setMinMaxDefault(0, 999999999, 0); + addTextField(buyoutPriceField); + addLabel(new GuiNpcLabel(122, StatCollector.translateToLocal("auction.noBuyoutHint"), leftX + 214, guiTop + 58, 0x888888)); + + addLabel(new GuiNpcLabel(130, StatCollector.translateToLocal("auction.duration"), leftX, guiTop + 80, 0x404040)); + addButton(new GuiNpcButton(70, leftX + 120, guiTop + 76, 110, 20, getDurationDisplay())); + + long fee = getListingFee(); + addLabel(new GuiNpcLabel(140, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, + leftX, guiTop + 104, fee > 0 ? 0xAA0000 : 0x008800)); + + addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(72, leftX + 100, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.cancel"))); + + if (getButton(71) != null) { + getButton(71).setEnabled(item != null); + } + } + + private String getDurationDisplay() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return duration.getDisplayName(); + } + + private long getListingFee() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return duration.getListingFee(); + } + + private void createListing() { + ContainerAuction container = (ContainerAuction) inventorySlots; + ItemStack item = container.getListingItem(); + if (item == null) { + return; + } + + long startingPrice = parsePrice(startingPriceField, 1); + long buyoutPrice = parsePrice(buyoutPriceField, 0); + + if (startingPrice < 1) { + startingPrice = 1; + } + + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return; + } + + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); + container.clearListingSlot(); + switchTab(0); + } + + private long parsePrice(GuiNpcTextField field, long defaultValue) { + if (field == null) { + return defaultValue; + } + try { + return Long.parseLong(field.getText()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + @Override protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { drawDefaultBackground(); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); // Draw background - mc.renderEngine.bindTexture(new ResourceLocation("customnpcs", "textures/gui/bgfilled.png")); - drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + mc.renderEngine.bindTexture(BG_TEXTURE); + drawTexturedModalRect(guiLeft, guiTop, 0, 0, 252, ySize); + drawTexturedModalRect(guiLeft + 252, guiTop, 188, 0, xSize - 252, ySize); // Draw selected item preview if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { AuctionListing listing = listings.get(selectedListingIndex); - drawItemPreview(listing, guiLeft + 180, guiTop + 160); + drawItemPreview(listing, guiLeft + RIGHT_PANEL_X + 41, guiTop + 192); } super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); @@ -387,9 +576,10 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, fontRendererObj.drawString(titleText, (xSize - fontRendererObj.getStringWidth(titleText)) / 2, -8, CustomNpcResourceListener.DefaultTextColor); - // Results count - String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); - fontRendererObj.drawString(listingsLabel, 175, 42, 0x666666); + if (currentTab != 4) { + String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); + fontRendererObj.drawString(listingsLabel, LEFT_PANEL_X, 36, 0x666666); + } } private String getTabTitle() { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index fbf62fc1d..99f1e00f1 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -23,8 +23,8 @@ public class SubGuiAuctionBid extends SubGuiInterface { this.listing = listing; this.playerBalance = playerBalance; setBackground("menubg.png"); - xSize = 200; - ySize = 150; + xSize = 240; + ySize = 165; closeOnEsc = true; } @@ -36,13 +36,13 @@ public class SubGuiAuctionBid extends SubGuiInterface { int labelColor = 0x404040; if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 40, y, 0xAA0000)); - addButton(new GuiNpcButton(31, guiLeft + 60, guiTop + 110, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); + addButton(new GuiNpcButton(31, guiLeft + 80, guiTop + 125, 80, 20, StatCollector.translateToLocal("gui.cancel"))); return; } // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 70, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 90, y, labelColor)); y += 18; // Item name @@ -52,30 +52,30 @@ public class SubGuiAuctionBid extends SubGuiInterface { // Current price long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.currentPrice"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 100, y, 0x008800)); + addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 120, y, 0x008800)); y += 14; // Minimum bid long minBid = listing.getMinimumBid(); addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 100, y, 0x0066AA)); + addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 120, y, 0x0066AA)); y += 14; // Your balance addLabel(new GuiNpcLabel(6, StatCollector.translateToLocal("auction.yourBalance"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 100, y, + addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 120, y, playerBalance >= minBid ? 0x008800 : 0xAA0000)); y += 14; // Time remaining addLabel(new GuiNpcLabel(8, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, getTimeColor(listing.getTimeRemaining()))); y += 18; // Bid input addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); - bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); + bidField = new GuiNpcTextField(11, this, guiLeft + 100, y, 110, 16, String.valueOf(minBid)); bidField.setIntegersOnly(); int minBidInt = (int) Math.min(Integer.MAX_VALUE, Math.max(1L, minBid)); bidField.setMinMaxDefault(minBidInt, Integer.MAX_VALUE, minBidInt); @@ -83,24 +83,24 @@ public class SubGuiAuctionBid extends SubGuiInterface { y += 24; // Quick bid buttons - addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, StatCollector.translateToLocal("auction.minBid"))); - addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus10"))); - addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus25"))); + addButton(new GuiNpcButton(20, guiLeft + 15, y, 60, 16, StatCollector.translateToLocal("auction.minBid"))); + addButton(new GuiNpcButton(21, guiLeft + 79, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus10"))); + addButton(new GuiNpcButton(22, guiLeft + 143, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus25"))); y += 22; // Place bid button - GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); + GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 35, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); addButton(bidButton); // Cancel button - addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + addButton(new GuiNpcButton(31, guiLeft + 125, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); // Error message area if (playerBalance < minBid) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); } else if (!listing.isActive()) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index d95dc229a..6d7f0a756 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -25,8 +25,8 @@ public class SubGuiAuctionCreate extends SubGuiInterface { public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { this.container = container; setBackground("menubg.png"); - xSize = 220; - ySize = 180; + xSize = 260; + ySize = 190; closeOnEsc = true; } @@ -35,7 +35,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { super.initGui(); // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 50, guiTop + 8, 0x404040)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 80, guiTop + 8, 0x404040)); // Item slot instructions addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.placeItemBelow"), guiLeft + 20, guiTop + 28, 0x404040)); @@ -49,35 +49,35 @@ public class SubGuiAuctionCreate extends SubGuiInterface { } // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 70, 0x404040)); - startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 74, 0x404040)); + startingPriceField = new GuiNpcTextField(11, this, guiLeft + 120, guiTop + 72, 90, 14, "100"); startingPriceField.setIntegersOnly(); startingPriceField.setMinMaxDefault(1, 999999999, 100); addTextField(startingPriceField); // Buyout price (optional) - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 90, 0x404040)); - buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); + addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 96, 0x404040)); + buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 120, guiTop + 94, 90, 14, "0"); buyoutPriceField.setIntegersOnly(); buyoutPriceField.setMinMaxDefault(0, 999999999, 0); addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 185, guiTop + 90, 0x888888)); + addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 215, guiTop + 96, 0x888888)); // Duration selection - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 112, 0x404040)); - addButton(new GuiNpcButton(31, guiLeft + 100, guiTop + 108, 100, 20, getDurationDisplay())); + addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 118, 0x404040)); + addButton(new GuiNpcButton(31, guiLeft + 120, guiTop + 114, 110, 20, getDurationDisplay())); // Fee display long fee = getListingFee(); addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - guiLeft + 20, guiTop + 135, fee > 0 ? 0xAA0000 : 0x008800)); + guiLeft + 20, guiTop + 142, fee > 0 ? 0xAA0000 : 0x008800)); // Create button - addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("auction.create"))); getButton(50).setEnabled(item != null); // Cancel button - addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + addButton(new GuiNpcButton(51, guiLeft + 140, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.cancel"))); } private String getDurationDisplay() { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java index 4054d04b1..e4131fe94 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -21,8 +21,8 @@ public class SubGuiAuctionDetails extends SubGuiInterface { public SubGuiAuctionDetails(GuiAuction parent, AuctionListing listing) { this.listing = listing; setBackground("menubg.png"); - xSize = 240; - ySize = 200; + xSize = 280; + ySize = 210; closeOnEsc = true; } @@ -35,76 +35,76 @@ public class SubGuiAuctionDetails extends SubGuiInterface { int valueColor = 0x000000; if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); - addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 80, y, 0xAA0000)); + addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); return; } // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 80, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 100, y, labelColor)); y += 16; // Item name addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 80, y, valueColor)); + addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 100, y, valueColor)); y += 14; // Quantity addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("auction.quantity"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 80, y, valueColor)); + addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 100, y, valueColor)); y += 14; // Seller addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("auction.seller"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 80, y, valueColor)); + addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 100, y, valueColor)); y += 14; // Starting price addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 100, y, valueColor)); + addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 120, y, valueColor)); y += 14; // Current bid if (listing.currentBid > 0) { addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 100, y, 0x008800)); + addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 120, y, 0x008800)); y += 14; // High bidder addLabel(new GuiNpcLabel(14, StatCollector.translateToLocal("auction.highBidder"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 100, y, valueColor)); + addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 120, y, valueColor)); y += 14; // Bid count addLabel(new GuiNpcLabel(16, StatCollector.translateToLocal("auction.totalBids"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 100, y, valueColor)); + addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 120, y, valueColor)); y += 14; } else { addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 100, y, 0x888888)); + addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 120, y, 0x888888)); y += 14; } // Buyout price if (listing.buyoutPrice > 0) { addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 100, y, 0xAA6600)); + addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 120, y, 0xAA6600)); y += 14; } // Minimum bid addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 100, y, 0x0066AA)); + addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 120, y, 0x0066AA)); y += 14; // Time remaining (will be updated) addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, getTimeColor(listing.getTimeRemaining()))); y += 20; // Close button - addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); + addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); } private String formatCurrency(long amount) { @@ -162,8 +162,8 @@ public class SubGuiAuctionDetails extends SubGuiInterface { // Draw item icon if (listing.item != null) { - int itemX = guiLeft + 200; - int itemY = guiTop + 30; + int itemX = guiLeft + 230; + int itemY = guiTop + 32; RenderHelper.enableGUIStandardItemLighting(); GL11.glEnable(GL12.GL_RESCALE_NORMAL); diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java index 61e783d91..2efd2b1b5 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuction.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuction.java @@ -26,20 +26,20 @@ public class ContainerAuction extends Container { // Create a single-slot inventory for the item being listed listingInventory = new InventoryAuctionListing(); - listingSlot = new SlotAuctionItem(listingInventory, 0, 80, 35); + listingSlot = new SlotAuctionItem(listingInventory, 0, 251, 146); addSlotToContainer(listingSlot); // Player inventory slots (standard positioning) for (int row = 0; row < 3; row++) { for (int col = 0; col < 9; col++) { addSlotToContainer(new Slot(player.inventory, col + row * 9 + 9, - 8 + col * 18, 140 + row * 18)); + 8 + col * 18, 166 + row * 18)); } } // Player hotbar for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 198)); + addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 224)); } } From db14662e90cd90196fe39ec80da6ead1fd1e63aa Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 20:10:58 -0500 Subject: [PATCH 18/21] Fix auction GUI tab actions --- .../npcs/client/gui/player/GuiAuction.java | 273 +++++++++++++++--- 1 file changed, 230 insertions(+), 43 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 0a93e98ed..8dcbcfcbe 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.inventory.Slot; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.item.ItemStack; @@ -14,6 +15,7 @@ import noppes.npcs.client.gui.util.*; import noppes.npcs.config.ConfigMarket; import noppes.npcs.containers.ContainerAuction; import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.entity.EntityNPCInterface; @@ -22,6 +24,7 @@ import org.lwjgl.opengl.GL12; import java.util.ArrayList; import java.util.List; +import java.util.UUID; /** * Main auction house browse GUI. @@ -69,6 +72,12 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Countdown update timer private long lastCountdownUpdate = 0; + private enum ActionMode { + BID, + CANCEL, + CLAIM + } + public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { super(npc, container); this.title = ""; @@ -89,6 +98,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, int rightX = guiLeft + RIGHT_PANEL_X; int actionY = guiTop + 44; boolean isCreateTab = currentTab == 4; + boolean isBrowseTab = currentTab == 0; updateListingSlotPosition(isCreateTab); @@ -108,17 +118,19 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, if (isCreateTab) { addCreateControls(leftX, rightX); } else { - // Search field - searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); - searchField.setMaxStringLength(50); - addTextField(searchField); + if (isBrowseTab) { + // Search field + searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); + searchField.setMaxStringLength(50); + addTextField(searchField); - // Search button - addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.search"))); + // Search button + addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.search"))); - // Sort button - addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); + // Sort button + addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); + } // Listing scroll area listingScroll = new GuiCustomScroll(this, 0); @@ -128,18 +140,17 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, addScroll(listingScroll); updateListingDisplay(); - // Pagination buttons - addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.prevPage"))); - addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.nextPage"))); + if (isBrowseTab) { + // Pagination buttons + addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.prevPage"))); + addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); + addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.nextPage"))); + } // Action buttons (right side) - addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.viewDetails"))); - addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.placeBid"))); - addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.buyout"))); - addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.refresh"))); + addActionButtons(rightX, actionY); // Balance display addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), @@ -195,25 +206,42 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } // Search button else if (id == 11) { - performSearch(); + if (currentTab == 0) { + performSearch(); + } } // Sort button else if (id == 30) { - cycleSortOrder(); + if (currentTab == 0) { + cycleSortOrder(); + } } // Pagination else if (id == 40) { - previousPage(); + if (currentTab == 0) { + previousPage(); + } } else if (id == 42) { - nextPage(); + if (currentTab == 0) { + nextPage(); + } } // Actions else if (id == 50) { viewDetails(); } else if (id == 51) { - placeBid(); + ActionMode mode = getActionMode(); + if (mode == ActionMode.BID) { + placeBid(); + } else if (mode == ActionMode.CANCEL) { + cancelListing(); + } else if (mode == ActionMode.CLAIM) { + claimListing(); + } } else if (id == 52) { - buyout(); + if (getActionMode() == ActionMode.BID) { + buyout(); + } } else if (id == 53) { refresh(); } else if (id == 70) { @@ -229,6 +257,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private void switchTab(int tab) { currentTab = tab; currentPage = 0; + selectedListingIndex = -1; switch (tab) { case 0: // Browse @@ -340,6 +369,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { listingScroll.selected = selectedListingIndex; + } else { + selectedListingIndex = -1; + listingScroll.selected = -1; } } @@ -395,25 +427,34 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } private void updateButtonStates() { + ActionMode mode = getActionMode(); + AuctionListing listing = getSelectedListing(); + boolean hasSelection = listing != null; + // Pagination buttons if (getButton(40) != null) getButton(40).setEnabled(currentPage > 0); if (getButton(42) != null) getButton(42).setEnabled(currentPage < totalPages - 1); // Action buttons based on selection - boolean hasSelection = selectedListingIndex >= 0 && selectedListingIndex < listings.size(); - if (hasSelection && listings.get(selectedListingIndex).item == null) { - hasSelection = false; - } if (getButton(50) != null) getButton(50).setEnabled(hasSelection); - if (getButton(51) != null) getButton(51).setEnabled(hasSelection); + if (getButton(51) != null) { + if (!hasSelection) { + getButton(51).setEnabled(false); + } else if (mode == ActionMode.BID) { + getButton(51).setEnabled(canPlaceBid(listing)); + } else if (mode == ActionMode.CANCEL) { + getButton(51).setEnabled(canCancelListing(listing)); + } else { + getButton(51).setEnabled(getClaimAction(listing) != ClaimAction.NONE); + } + } - if (hasSelection && getButton(52) != null) { - AuctionListing listing = listings.get(selectedListingIndex); - getButton(52).setEnabled(listing.item != null && - listing.buyoutPrice > 0 && - playerBalance >= listing.buyoutPrice); - } else if (getButton(52) != null) { - getButton(52).setEnabled(false); + if (getButton(52) != null) { + if (!hasSelection || mode != ActionMode.BID) { + getButton(52).setEnabled(false); + } else { + getButton(52).setEnabled(canBuyout(listing)); + } } } @@ -430,23 +471,26 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, for (int row = 0; row < 3; row++) { for (int col = 0; col < 9; col++) { int slotIndex = 1 + col + row * 9; - container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; - container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + row * 18; + Slot slot = (Slot) container.inventorySlots.get(slotIndex); + slot.xDisplayPosition = INVENTORY_START_X + col * 18; + slot.yDisplayPosition = INVENTORY_START_Y + row * 18; } } for (int col = 0; col < 9; col++) { int slotIndex = 28 + col; - container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; - container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; + Slot slot = (Slot) container.inventorySlots.get(slotIndex); + slot.xDisplayPosition = INVENTORY_START_X + col * 18; + slot.yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; } } else { container.listingSlot.xDisplayPosition = HIDDEN_SLOT_POSITION; container.listingSlot.yDisplayPosition = HIDDEN_SLOT_POSITION; for (int slotIndex = 1; slotIndex < container.inventorySlots.size(); slotIndex++) { - container.inventorySlots.get(slotIndex).xDisplayPosition = HIDDEN_SLOT_POSITION; - container.inventorySlots.get(slotIndex).yDisplayPosition = HIDDEN_SLOT_POSITION; + Slot slot = (Slot) container.inventorySlots.get(slotIndex); + slot.xDisplayPosition = HIDDEN_SLOT_POSITION; + slot.yDisplayPosition = HIDDEN_SLOT_POSITION; } } } @@ -525,6 +569,149 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, switchTab(0); } + private void cancelListing() { + AuctionListing listing = getSelectedListing(); + if (!canCancelListing(listing)) { + return; + } + + AuctionActionPacket.CancelListing(listing.id); + refresh(); + } + + private void claimListing() { + AuctionListing listing = getSelectedListing(); + if (listing == null) { + return; + } + + ClaimAction action = getClaimAction(listing); + if (action == ClaimAction.ITEM) { + AuctionActionPacket.ClaimItem(listing.id); + } else if (action == ClaimAction.PROCEEDS) { + AuctionActionPacket.ClaimProceeds(listing.id); + } else { + return; + } + + refresh(); + } + + private ActionMode getActionMode() { + if (currentTab == 1) { + return ActionMode.CANCEL; + } + if (currentTab == 3) { + return ActionMode.CLAIM; + } + return ActionMode.BID; + } + + private String getPrimaryActionLabel(ActionMode mode) { + if (mode == ActionMode.CANCEL) { + return StatCollector.translateToLocal("auction.cancel"); + } + if (mode == ActionMode.CLAIM) { + return StatCollector.translateToLocal("auction.claim"); + } + return StatCollector.translateToLocal("auction.placeBid"); + } + + private void addActionButtons(int rightX, int actionY) { + ActionMode mode = getActionMode(); + addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.viewDetails"))); + addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, + getPrimaryActionLabel(mode))); + if (mode == ActionMode.BID) { + addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.buyout"))); + addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.refresh"))); + } else { + addButton(new GuiNpcButton(53, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.refresh"))); + } + } + + private AuctionListing getSelectedListing() { + if (selectedListingIndex < 0 || selectedListingIndex >= listings.size()) { + return null; + } + AuctionListing listing = listings.get(selectedListingIndex); + if (listing == null || listing.item == null) { + return null; + } + return listing; + } + + private boolean canPlaceBid(AuctionListing listing) { + if (listing == null || !listing.isActive()) { + return false; + } + if (isPlayerSeller(listing)) { + return false; + } + return playerBalance >= listing.getMinimumBid(); + } + + private boolean canBuyout(AuctionListing listing) { + if (listing == null || !listing.isActive()) { + return false; + } + if (listing.buyoutPrice <= 0) { + return false; + } + if (isPlayerSeller(listing)) { + return false; + } + return playerBalance >= listing.buyoutPrice; + } + + private boolean canCancelListing(AuctionListing listing) { + if (listing == null || listing.item == null) { + return false; + } + return listing.status == EnumAuctionStatus.ACTIVE && !listing.hasBids(); + } + + private boolean isPlayerSeller(AuctionListing listing) { + if (mc == null || mc.thePlayer == null || listing == null || listing.sellerUUID == null) { + return false; + } + UUID playerId = mc.thePlayer.getUniqueID(); + return listing.sellerUUID.equals(playerId); + } + + private enum ClaimAction { + ITEM, + PROCEEDS, + NONE + } + + private ClaimAction getClaimAction(AuctionListing listing) { + if (listing == null || mc == null || mc.thePlayer == null) { + return ClaimAction.NONE; + } + + UUID playerId = mc.thePlayer.getUniqueID(); + if (listing.sellerUUID != null && listing.sellerUUID.equals(playerId) && !listing.sellerClaimed) { + if (listing.status == EnumAuctionStatus.SOLD) { + return ClaimAction.PROCEEDS; + } + if (listing.status == EnumAuctionStatus.CANCELLED || listing.status == EnumAuctionStatus.EXPIRED) { + return ClaimAction.ITEM; + } + } + + if (listing.highBidderUUID != null && listing.highBidderUUID.equals(playerId) + && listing.status == EnumAuctionStatus.SOLD && !listing.buyerClaimed) { + return ClaimAction.ITEM; + } + + return ClaimAction.NONE; + } + private long parsePrice(GuiNpcTextField field, long defaultValue) { if (field == null) { return defaultValue; From 384b61832d2d0d4b5f5281c13f46d1fa202f849f Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 19 Jan 2026 16:29:24 -0500 Subject: [PATCH 19/21] Hitbox Fixes --- src/main/java/noppes/npcs/entity/EntityNPCInterface.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 702d27cd0..c30f15f57 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -1303,6 +1303,11 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.setRevengeTarget(null); this.deathTime = 0; //fleeingTick = 0; + + // Restore hitbox before moving entity to prevent bounding box validation issues + currentAnimation = EnumAnimation.NONE; + updateHitbox(); + if (ais.returnToStart && !hasOwner() && !this.isRemote()) setLocationAndAngles(getStartXPos(), getStartYPos(), getStartZPos(), rotationYaw, rotationPitch); killedtime = 0; @@ -1316,8 +1321,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } moveEntityWithHeading(0, 0); distanceWalkedModified = 0; - currentAnimation = EnumAnimation.NONE; - updateHitbox(); updateAI = true; ais.movingPos = 0; if (getOwner() != null) { From 692b9852371441e08fcdf9f01512ef701103e3a8 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 20 Jan 2026 23:50:42 -0500 Subject: [PATCH 20/21] Cleanup for Langs + Abilities --- .../gui/advanced/SubGuiAbilityConfig.java | 13 ++++---- .../advanced/ability/SubGuiAbilityBeam.java | 2 +- .../advanced/ability/SubGuiAbilityCharge.java | 4 +-- .../advanced/ability/SubGuiAbilityCutter.java | 4 +-- .../advanced/ability/SubGuiAbilityDash.java | 2 +- .../advanced/ability/SubGuiAbilityHeal.java | 2 +- .../ability/SubGuiAbilityHeavyHit.java | 2 +- .../advanced/ability/SubGuiAbilityOrb.java | 6 ++-- .../ability/SubGuiAbilityProjectile.java | 4 +-- .../ability/SubGuiAbilityShockwave.java | 2 +- .../advanced/ability/SubGuiAbilitySlam.java | 2 +- .../ability/SubGuiAbilityTeleport.java | 2 +- .../advanced/ability/SubGuiAbilityTrap.java | 2 +- .../advanced/ability/SubGuiAbilityVortex.java | 2 +- .../npcs/client/gui/player/GuiAuction.java | 2 +- .../gui/player/SubGuiAuctionCreate.java | 2 +- .../gui/select/GuiAnimationSelection.java | 2 +- .../assets/customnpcs/lang/en_US.lang | 30 ++++--------------- 18 files changed, 33 insertions(+), 52 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 6d12693bb..c88c18095 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -144,20 +144,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ═══════════════════════════════════════════════════════════════════════ // TOP TABS using GuiMenuTopButton // ═══════════════════════════════════════════════════════════════════════ - GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "ability.tab.general"); + GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); GuiMenuTopButton lastTab = generalTab; if (hasTypeSettings) { - GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "ability.tab.type"); + GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); typeTab.active = (activeTab == TAB_TYPE); addTopButton(typeTab); lastTab = typeTab; } - GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "ability.tab.target"); + GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "script.target"); targetTab.active = (activeTab == TAB_TARGET); addTopButton(targetTab); lastTab = targetTab; @@ -435,7 +435,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ───────────────────────────────────────────────────────────────────── // SOUNDS SECTION // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(30, "ability.sounds", col1LabelX, y)); + addLabel(new GuiNpcLabel(30, "advanced.sounds", col1LabelX, y)); y += 14; // Wind Up Sound @@ -455,7 +455,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ───────────────────────────────────────────────────────────────────── // ANIMATIONS SECTION // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(33, "ability.animations", col1LabelX, y)); + addLabel(new GuiNpcLabel(33, "menu.animations", col1LabelX, y)); y += 14; // Wind Up Animation @@ -486,7 +486,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Telegraph Type (separate column) String typeKey = ability.getTelegraphType().name().toLowerCase(); addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5)); - addLabel(new GuiNpcLabel(43, "ability.telegraph." + typeKey, guiLeft + 280, y + 5)); + String telegraphLangKey = typeKey.equals("none") ? "gui.none" : "telegraph." + typeKey; + addLabel(new GuiNpcLabel(43, telegraphLangKey, guiLeft + 280, y + 5)); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java index e951e5fbd..fb3e6b074 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java @@ -28,7 +28,7 @@ public class SubGuiAbilityBeam extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Damage Interval - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java index a7b19b05c..d3ff5fd81 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java @@ -27,10 +27,10 @@ public class SubGuiAbilityCharge extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, charge.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, charge.getChargeSpeed())); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index b825789a8..d917282f2 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -28,10 +28,10 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Range - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, cutter.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.range", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "gui.range", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, cutter.getRange())); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java index 2d0d5c33a..7ae0ea1f5 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java @@ -28,7 +28,7 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Dash Mode - addLabel(new GuiNpcLabel(100, "ability.dashMode", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); String[] dashModes = {"Aggressive", "Defensive"}; addButton(new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java index 365460672..30ccb739e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -40,7 +40,7 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius())); - addLabel(new GuiNpcLabel(103, "ability.instant", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5)); addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java index c045833b9..5c0d99c48 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java @@ -27,7 +27,7 @@ public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Potion Time - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage())); addLabel(new GuiNpcLabel(101, "ability.potionTime", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 5c82c42f8..fd75700c0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -28,16 +28,16 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed())); y += 24; // Row 2: Orb Size + Max Distance - addLabel(new GuiNpcLabel(102, "ability.orbSize", labelX, y + 5)); + addLabel(new GuiNpcLabel(102, "stats.size", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize())); addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java index cbde41b7f..88d7bbf00 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java @@ -28,10 +28,10 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, projectile.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, projectile.getSpeed())); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java index 9230e242c..38ab22158 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java @@ -27,7 +27,7 @@ public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Push Radius - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, shockwave.getDamage())); addLabel(new GuiNpcLabel(101, "ability.pushRadius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java index 2fdb0d7fb..7f8c7194e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java @@ -27,7 +27,7 @@ public class SubGuiAbilitySlam extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Radius - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, slam.getDamage())); addLabel(new GuiNpcLabel(101, "ability.radius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java index c4e9154e5..5570d944f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -71,7 +71,7 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { } // Damage + Damage Radius - addLabel(new GuiNpcLabel(106, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(106, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(106, fieldX, y, 50, teleport.getDamage())); addLabel(new GuiNpcLabel(107, "ability.dmgRadius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index 8c8d92bb4..88529fe68 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -38,7 +38,7 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { y += 24; // Row 2: Damage + Damage Radius - addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, trap.getDamage())); addLabel(new GuiNpcLabel(103, "ability.dmgRadius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java index 6978abce7..a1abbd661 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java @@ -37,7 +37,7 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { y += 24; // Row 2: Damage + Knockback - addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, vortex.getDamage())); addLabel(new GuiNpcLabel(103, "ability.knockback", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 8dcbcfcbe..30e857e7a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -527,7 +527,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, addLabel(new GuiNpcLabel(140, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, leftX, guiTop + 104, fee > 0 ? 0xAA0000 : 0x008800)); - addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.create"))); addButton(new GuiNpcButton(72, leftX + 100, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.cancel"))); if (getButton(71) != null) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index 6d7f0a756..01c650bbd 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -73,7 +73,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { guiLeft + 20, guiTop + 142, fee > 0 ? 0xAA0000 : 0x008800)); // Create button - addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.create"))); getButton(50).setEnabled(item != null); // Cancel button diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java index c0fbb0295..882f5817b 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -50,7 +50,7 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr public void initGui() { super.initGui(); - this.addLabel(new GuiNpcLabel(0, "gui.animations", guiLeft + 8, guiTop + 4)); + this.addLabel(new GuiNpcLabel(0, "menu.animations", guiLeft + 8, guiTop + 4)); this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear")); this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done")); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 66837c543..7f7314d1c 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1477,14 +1477,12 @@ ability.target.self=Self ability.target.aoe_self=AOE Self ability.target.aoe_target=AOE Target -ability.telegraph.none=None -ability.telegraph.circle=Circle -ability.telegraph.ring=Ring -ability.telegraph.line=Line -ability.telegraph.cone=Cone -ability.telegraph.point=Point +telegraph.circle=Circle +telegraph.ring=Ring +telegraph.line=Line +telegraph.cone=Cone +telegraph.point=Point -ability.tab.general=General ability.tab.timing=Timing ability.tab.effects=Effects ability.tab.telegraph=Telegraph @@ -1497,7 +1495,6 @@ ability.windUpSound=Wind Up Sound ability.activeSound=Active Sound ability.windUpAnim=Wind Up Anim ability.activeAnim=Active Anim -gui.animations=Animations ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit @@ -1516,15 +1513,11 @@ ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap # Ability GUI - Type Tab -ability.tab.type=Type # Common Type Settings -ability.damage=Damage ability.radius=Radius ability.knockback=Knockback ability.knockbackUp=KB Up -ability.speed=Speed -ability.range=Range ability.maxDist=Max Dist ability.minDist=Min Dist ability.distance=Distance @@ -1551,13 +1544,10 @@ ability.hitRadius=Hit Radius # Cutter Settings ability.arcAngle=Arc Angle ability.sweepWaves=Waves -ability.waveInterval=Interval ability.rotSpeed=Rot Speed ability.piercing=Piercing # Orb Settings -ability.orbSpeed=Speed -ability.orbSize=Size ability.homing=Homing ability.explosive=Explosive @@ -1569,7 +1559,6 @@ ability.aoe=AOE ability.maxTargets=Max Targets # Dash Settings -ability.dashMode=Mode ability.dash.aggressive=Aggressive ability.dash.defensive=Defensive ability.mode=Mode @@ -1609,7 +1598,6 @@ ability.counterAnim=Counter Anim ability.healAmount=Amount ability.healPercent=Percent ability.healRadius=Radius -ability.instant=Instant ability.healSelf=Heal Self ability.healAllies=Heal Allies @@ -1620,9 +1608,6 @@ ability.shape=Shape ability.placement=Placement ability.affectsCaster=Hit Caster ability.ignoreInvuln=Ign Invuln -ability.shape.circle=Circle -ability.shape.ring=Ring -ability.shape.cone=Cone ability.place.caster=At Caster ability.place.target=At Target ability.place.followCaster=Follow Caster @@ -1633,7 +1618,6 @@ ability.place.ahead=Ahead ability.triggerRadius=Trig Rad ability.armTime=Arm Time ability.maxTriggers=Max Trig -ability.visible=Visible # Ability Save/Load ability.save.confirm=Save ability preset @@ -1663,11 +1647,8 @@ ability.shockwaveFalloff=Damage/knockback drop off with distance # Ability Config GUI ability.typeId=Type ID -ability.tab.target=Target ability.conditions=Conditions ability.noTypeSettings=No type-specific settings -ability.sounds=Sounds -ability.animations=Animations # Conditions condition.edit=Edit Condition @@ -1726,7 +1707,6 @@ auction.claim=Claim auction.createListing=Create Listing auction.duration=Duration: auction.listingFee=Listing Fee: -auction.create=Create auction.sort.newest=Newest First auction.sort.oldest=Oldest First auction.sort.priceHigh=Most Expensive From d0dc8a4578c25c5d0792981b8a0496d3c4521b6f Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 12:38:56 -0500 Subject: [PATCH 21/21] Trader System + Stocks Implemented - Removed Auction System Temporarily for Full Rebase --- 1.11_Market_Update.md | 834 +++++++++++++++++ docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 324 ------- src/api | 2 +- src/main/java/foxz/utils/Market.java | 3 - .../data/attribute/ItemTradeAttribute.java | 308 ------- .../kamkeel/npcs/network/PacketHandler.java | 10 +- .../npcs/network/enums/EnumDataPacket.java | 3 - .../npcs/network/enums/EnumPlayerPacket.java | 1 + .../packets/data/large/AuctionDataPacket.java | 218 ----- .../packets/player/AuctionActionPacket.java | 579 ------------ .../network/packets/player/GetTraderData.java | 77 ++ src/main/java/noppes/npcs/CommonProxy.java | 4 - src/main/java/noppes/npcs/CustomNpcs.java | 8 - .../noppes/npcs/CustomNpcsPermissions.java | 7 - src/main/java/noppes/npcs/DataAdvanced.java | 3 - .../java/noppes/npcs/NoppesUtilServer.java | 1 + .../java/noppes/npcs/ServerTickHandler.java | 6 - .../java/noppes/npcs/client/ClientProxy.java | 5 - .../java/noppes/npcs/client/NoppesUtil.java | 61 -- .../client/gui/mainmenu/GuiNpcAdvanced.java | 3 - .../npcs/client/gui/player/GuiAuction.java | 841 ------------------ .../npcs/client/gui/player/GuiNPCTrader.java | 309 +++++-- .../client/gui/player/SubGuiAuctionBid.java | 221 ----- .../gui/player/SubGuiAuctionCreate.java | 178 ---- .../gui/player/SubGuiAuctionDetails.java | 200 ----- .../client/gui/roles/GuiNpcAuctioneer.java | 144 --- .../client/gui/roles/GuiNpcTraderSetup.java | 86 +- .../roles/SubGuiNpcTraderCooldownReset.java | 52 ++ .../gui/roles/SubGuiNpcTraderSettings.java | 66 ++ .../gui/roles/SubGuiNpcTraderStock.java | 159 ++++ .../gui/roles/SubGuiNpcTraderStockReset.java | 55 ++ .../gui/roles/SubGuiNpcTraderStockSlots.java | 76 ++ .../gui/util/GuiContainerNPCInterface.java | 9 + .../gui/util/GuiContainerNPCInterface2.java | 4 +- .../npcs/client/gui/util/GuiNPCInterface.java | 15 +- .../java/noppes/npcs/config/ConfigMarket.java | 173 +--- .../npcs/constants/EnumAuctionDuration.java | 82 -- .../npcs/constants/EnumAuctionSort.java | 39 - .../npcs/constants/EnumAuctionStatus.java | 54 -- .../noppes/npcs/constants/EnumGuiType.java | 4 +- .../noppes/npcs/constants/EnumRoleType.java | 2 +- .../noppes/npcs/constants/EnumStockReset.java | 45 +- .../npcs/containers/ContainerAuction.java | 193 ---- .../npcs/containers/ContainerNPCTrader.java | 108 ++- .../containers/ContainerNPCTraderSetup.java | 14 +- .../npcs/controllers/AuctionBlacklist.java | 419 --------- .../npcs/controllers/AuctionController.java | 834 ----------------- .../npcs/controllers/CurrencyController.java | 400 --------- .../npcs/controllers/MarketRegistry.java | 214 +++++ .../noppes/npcs/controllers/VaultHelper.java | 272 ------ .../npcs/controllers/data/AuctionFilter.java | 226 ----- .../npcs/controllers/data/AuctionListing.java | 397 --------- .../controllers/data/PlayerCurrencyData.java | 105 ++- .../npcs/controllers/data/PlayerData.java | 16 +- .../npcs/controllers/data/TraderStock.java | 50 +- .../noppes/npcs/roles/RoleAuctioneer.java | 135 --- .../java/noppes/npcs/roles/RoleTrader.java | 282 +++++- .../npcs/scripted/entity/ScriptPlayer.java | 194 +--- .../npcs/scripted/item/ScriptItemStack.java | 114 --- .../npcs/scripted/roles/ScriptRoleTrader.java | 130 ++- .../assets/customnpcs/lang/en_US.lang | 118 +-- 61 files changed, 2595 insertions(+), 6897 deletions(-) create mode 100644 1.11_Market_Update.md delete mode 100644 docs/CNPC_1.11_MARKET_UPDATE_PLAN.md delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java delete mode 100644 src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java delete mode 100644 src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuction.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java delete mode 100644 src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionDuration.java delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionSort.java delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionStatus.java delete mode 100644 src/main/java/noppes/npcs/containers/ContainerAuction.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionBlacklist.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionController.java delete mode 100644 src/main/java/noppes/npcs/controllers/CurrencyController.java create mode 100644 src/main/java/noppes/npcs/controllers/MarketRegistry.java delete mode 100644 src/main/java/noppes/npcs/controllers/VaultHelper.java delete mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionFilter.java delete mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionListing.java delete mode 100644 src/main/java/noppes/npcs/roles/RoleAuctioneer.java diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md new file mode 100644 index 000000000..304965950 --- /dev/null +++ b/1.11_Market_Update.md @@ -0,0 +1,834 @@ +# CustomNPC+ 1.11 Market Update - Implementation Plan + +## Overview + +The 1.11 Market Update introduces a comprehensive economy system for CustomNPC+: + +1. **Currency System** - Built-in CNPC+ currency with optional Vault integration +2. **Trader Enhancements** - Stock system, currency-based trades, reset timers +3. **Auction House** - Global auction system with bidding, buyout, and claims + +--- + +## 1. Currency System + +### Status: IMPLEMENTED + +The currency system provides a unified economy for CNPC+ features. + +### Features +- **Built-in CNPC+ Currency** - Stored in PlayerData, persists across profile slots +- **Vault Integration** - Optional, when enabled all operations use Vault instead +- **Scripting API** - Full access via `IPlayer` methods + +### Key Files +| File | Purpose | +|------|---------| +| `PlayerCurrencyData.java` | Per-player currency storage, Vault delegation | +| `IPlayerCurrencyData.java` | API interface for currency operations | +| `ConfigMarket.java` | Currency configuration (UseVault, CurrencyName, StartingBalance, MaxBalance) | +| `VaultUtil.java` | Vault API integration via reflection | + +### Configuration (ConfigMarket.java) +``` +Market.Currency: + - Use Vault: false + - Currency Name: "Coins" + - Starting Balance: 0 + - Max Balance: 2147483647 +``` + +### Scripting API (IPlayer) +```javascript +player.getCurrencyBalance() // Get balance +player.setCurrencyBalance(amount) // Set balance +player.depositCurrency(amount) // Add currency (returns success) +player.withdrawCurrency(amount) // Remove currency (returns success) +player.canAffordCurrency(amount) // Check if can afford +player.isUsingVaultCurrency() // Check if Vault is active +player.getFormattedCurrencyBalance() // Get formatted string +``` + +--- + +## 2. Trader Enhancements + +### Status: COMPLETE + +--- + +### 2.1 Stock System - IMPLEMENTED + +Traders can have limited stock that resets on configurable schedules. + +#### Stock Modes +| Mode | Description | Status | +|------|-------------|--------| +| **Global** | All players share the same stock pool | ✅ Working | +| **Per-Player** | Each player has their own stock counts | ✅ Working | + +#### Stock Reset Types (EnumStockReset) +| Type | Description | Status | +|------|-------------|--------| +| `NONE` | Stock never resets | ✅ Working | +| `MCDAILY` | Resets every MC day (24000 ticks) | ✅ Working | +| `MCWEEKLY` | Resets every 7 MC days (168000 ticks) | ✅ Working | +| `MCCUSTOM` | Custom MC tick interval | ✅ Working | +| `RLDAILY` | Resets every 24 real-life hours | ✅ Working | +| `RLWEEKLY` | Resets every 7 real-life days | ✅ Working | +| `RLCUSTOM` | Custom real-time interval | ✅ Working | + +#### Stock UI Features - IMPLEMENTED +| Feature | Status | Notes | +|---------|--------|-------| +| Stock count per item | ✅ | Shows to right of output slot | +| Out of stock indicator | ✅ | Dark overlay + red count | +| Reset timer countdown | ✅ | Real-time client-side countdown in top-right | +| "Reopen Trader" message | ✅ | Shows when countdown reaches 0 | +| Admin stock configuration | ✅ | SubGuiNpcTraderStock | +| Per-slot stock limits | ✅ | SubGuiNpcTraderStockSlots | +| Reset stock button | ✅ | With confirmation dialog | +| Reset cooldown button | ✅ | With confirmation dialog | + +#### Key Files +| File | Purpose | +|------|---------| +| `TraderStock.java` | Core stock data class with per-player/global modes | +| `TraderStock.PlayerTraderStock` | Inner class for per-player tracking | +| `EnumStockReset.java` | Reset type enum with intervals | +| `SubGuiNpcTraderStock.java` | Main stock configuration GUI | +| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limit editor | +| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | +| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | + +--- + +### 2.2 Trader Currency Support - IMPLEMENTED + +Trades can require BOTH items AND currency (additive costs). + +#### Trade Entry Structure +Each trade slot can have: +- **Sold Item** - What the player receives ✅ +- **Currency 1** - First item cost (existing) ✅ +- **Currency 2** - Second item cost (existing) ✅ +- **Currency Cost** - CNPC+ Currency / Vault cost ✅ NEW + +#### Example Trades +| Sold Item | Item Cost | Currency Cost | +|-----------|-----------|---------------| +| Diamond | 2 Emeralds | $0 | +| Diamond Sword | 5 Diamonds | $100 | +| Special Armor | None | $500 | +| Enchanted Book | 1 Book + 10 Lapis | $50 | + +#### Implementation Details +- Currency cost stored as `long[] currencyCost[18]` in RoleTrader +- Default to 0 (no currency cost) for legacy compatibility ✅ +- Check both item AND currency requirements before purchase ✅ +- Withdraw currency on successful purchase ✅ +- NBT serialization for persistence ✅ + +--- + +### 2.3 Player Trader GUI - IMPLEMENTED + +#### Layout (256px wide) +- 3 columns × 6 rows = 18 trade slots +- Column width: 80px +- Per slot: [Currency1][Currency2] = [Output] +- Player inventory positioned below + +#### Display Features +| Element | Position | Status | +|---------|----------|--------| +| "Trader" label | Top-left | ✅ | +| Reset countdown | Top-right | ✅ Real-time ticking | +| Stock count | Right of each trade | ✅ Green/Red | +| Player balance | Bottom-right | ✅ | +| Status message | Bottom-left | ✅ On hover | + +#### Status Messages (on hover) +| Condition | Message | Color | +|-----------|---------|-------| +| Out of stock | "Out of Stock ($COST)" | Red | +| Can't afford balance | "Insufficient Balance ($COST)" | Red | +| Missing item currency | "Unavailable ($COST)" | Red | +| Slot disabled | "Slot Disabled ($COST)" | Orange | +| Can purchase | "Available ($COST)" | Green | + +Cost suffix only appears if trade has currency cost > 0. + +--- + +### 2.4 Multi-Player Sync - IMPLEMENTED + +#### MarketRegistry System +Central registry for linked market synchronization across all NPCs sharing the same market. + +| Component | Purpose | +|-----------|---------| +| `MarketRegistry.java` | Central registry tracking traders and viewers per market | +| `marketTraders` | `Map>` - All traders using each market | +| `marketViewers` | `Map>` - All players viewing each market | + +#### Viewer Tracking System +| Feature | Status | Notes | +|---------|--------|-------| +| Register viewer on GUI open | ✅ | For shared stock mode (linked or local) | +| Unregister on GUI close | ✅ | Via onContainerClosed | +| Sync after purchase | ✅ | All viewers updated market-wide | +| Per-player mode optimization | ✅ | Only sync to purchasing player | +| Disconnected player cleanup | ✅ | Removed before sync | +| NPC unload cleanup | ✅ | Via delete() → onUnload() | + +#### Sync Behavior by Mode +| Mode | Linked Market | Behavior | +|------|---------------|----------| +| Shared Stock | Yes | All viewers across ALL NPCs with same market see update immediately | +| Shared Stock | No | All viewers of THIS NPC see update immediately | +| Per-Player Stock | Yes/No | Only purchasing player receives update | + +#### Data Flow +``` +Purchase on NPC1 (market="shop1") + ↓ +consumeStock() → Market.save() → MarketRegistry.syncMarket("shop1") + ↓ +┌──────────────────────────────────────────────┐ +│ 1. Update ALL traders' stock data │ +│ (NPC1, NPC2, NPC3... all with "shop1") │ +│ 2. Sync ALL viewers across ALL those NPCs │ +└──────────────────────────────────────────────┘ +``` + +--- + +### 2.5 Key Files Modified/Created + +| File | Changes | +|------|---------| +| `MarketRegistry.java` | NEW - Central registry for linked market sync | +| `RoleTrader.java` | currencyCost[], viewer tracking (local + registry), syncAllViewers(), resetCooldown(), delete() cleanup | +| `ContainerNPCTrader.java` | Layout constants, currency/stock validation, viewer registration | +| `GuiNPCTrader.java` | Client-side countdown, stock display, status messages | +| `GetTraderData.java` | Network packet for requesting trader data (IGuiData pattern) | +| `TraderStock.java` | Stock management with per-player/global modes, thread-safe methods | +| `EnumStockReset.java` | Reset type definitions with lang key support | +| `SubGuiNpcTraderStock.java` | Stock configuration dialog, custom time validation | +| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limits | +| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | +| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | +| `GuiNPCInterface.java` | Fixed SubGui background tiling for smaller widths | +| `Market.java` | Fixed save logic for linked markets | +| `IRoleTrader.java` | Extended API with stock manipulation methods | +| `ScriptRoleTrader.java` | Implemented new API methods | + +--- + +### 2.6 Resolved Issues + +All previously identified issues have been fixed: + +#### Critical Issues - FIXED +| Issue | Fix | +|-------|-----| +| Market load overwrites reset | Reordered `interact()` to load market BEFORE stock reset check | +| recordHistory flag inverted | Removed inverted early return in `Market.save()` | +| API returns wrong item | Fixed `getSellOption()` to return `inventorySold` instead of `inventoryCurrency` | + +#### Medium Priority - FIXED +| Issue | Fix | +|-------|-----| +| Thread safety | Added `synchronized` to `getAvailableStock()`, `consumeStock()`, `resetStock()` | +| Per-player cleanup | `playerStock.clear()` called on stock reset | +| Linked market sync | Implemented `MarketRegistry` for market-wide sync | +| Custom time validation | Added min validation (1 tick MC, 1000ms RL) in `SubGuiNpcTraderStock` | + +#### Low Priority - FIXED +| Issue | Fix | +|-------|-----| +| lastPurchaseTime unused | Removed from `PlayerTraderStock` | +| Currency format | Verified working correctly for all ranges | +| Hardcoded reset types | Now uses `EnumStockReset.getDisplayNames()` with lang keys | + +--- + +### 2.7 Scripting API - IMPLEMENTED + +Full API available via `IRoleTrader` interface: + +```javascript +// Stock Configuration +trader.isStockEnabled() +trader.setStockEnabled(boolean) +trader.isPerPlayerStock() +trader.setPerPlayerStock(boolean) +trader.getStockResetType() // Returns int (0-6 for EnumStockReset) +trader.setStockResetType(int) +trader.getCustomResetTime() +trader.setCustomResetTime(long) + +// Stock Limits +trader.getMaxStock(slot) // Max stock for slot (-1 = unlimited) +trader.setMaxStock(slot, amount) + +// Stock Queries +trader.getAvailableStock(slot) // Global mode +trader.getAvailableStock(slot, player) // Per-player mode +trader.getCurrentStock(slot) // Current stock (global) +trader.getPlayerPurchased(slot, player) // Amount purchased by player + +// Stock Manipulation +trader.setCurrentStock(slot, amount) // Set current stock (global) +trader.setPlayerPurchased(slot, player, amount) // Set player's purchased amount +trader.resetStock() // Reset all stock to max +trader.resetCooldown() // Reset cooldown timer + +// Time Queries +trader.getLastResetTime() // Timestamp of last reset +trader.getTimeUntilReset() // Time remaining until next reset + +// Currency Cost System +trader.getCurrencyCost(slot) +trader.setCurrencyCost(slot, amount) +trader.hasCurrencyCost(slot) +``` + +--- + +## 3. Auction House System + +### Status: NOT STARTED (Fresh implementation) + +### 3.1 Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ AuctionController │ +│ - Manages all auction data │ +│ - Handles listing/bidding/buyout logic │ +│ - Processes claims │ +│ - Saves/loads auction data │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ AuctionNotification │ +│ - Tracks pending notifications per player │ +│ - Sends notifications on login │ +│ - Types: Won, Outbid, Sold, Expired, Claimed │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 GUI Structure + +``` +GuiAuctionHouse (Base GUI - opened via Auctioneer NPC) + │ + ├── GuiAuction (Browse/Search auctions) + │ - Paginated item display (ContainerAuction) + │ - Next/Back page buttons + │ - Filter button → opens GuiAuctionFilter + │ - My Auctions button → opens GuiAuctionClaims + │ - Sell button → opens GuiAuctionSell + │ - Click item → SubGuiAuctionDetails + │ + ├── GuiAuctionSell (Create new listing) + │ - Item slot for item to sell + │ - Starting price input + │ - Buyout price input (0 = no buyout) + │ - Listing fee display + │ - Create button + │ + ├── GuiAuctionClaims (My Auctions / Claims) + │ - Active listings tab + │ - Pending claims tab (items + currency to collect) + │ - Cancel listing button + │ - Claim all button + │ + └── GuiAuctionFilter (Filter settings) + - Search text + - Sort options (EnumAuctionSort) + - Price range (min/max) + - Show my listings only toggle +``` + +### 3.3 Admin GUI + +``` +GuiManageAuction (Admin tool - like ManageMagic, ManageQuests) + │ + ├── Active Auctions Tab + │ - View all auctions + │ - Cancel any auction (with reason) + │ - View auction details + │ + ├── Blacklist Tab (Stub for future) + │ - Add/remove blacklisted items + │ - Add/remove blacklisted mods + │ + └── Settings Tab + - View current config values + - Quick reference for config file +``` + +### 3.4 Data Structures + +#### AuctionListing +```java +public class AuctionListing { + int id; // Unique listing ID + UUID sellerUUID; // Seller's UUID + String sellerName; // Seller's display name + ItemStack item; // Item being sold + long startingPrice; // Minimum starting bid + long buyoutPrice; // Instant buy price (0 = no buyout) + long currentBid; // Current highest bid + UUID highBidderUUID; // Current high bidder (null if no bids) + String highBidderName; // High bidder display name + int bidCount; // Number of bids placed + long createdTime; // When listing was created + long endTime; // When auction ends + EnumAuctionStatus status; // ACTIVE, ENDED, CANCELLED, CLAIMED +} +``` + +#### AuctionClaim +```java +public class AuctionClaim { + int id; // Unique claim ID + UUID playerUUID; // Player who can claim + int listingId; // Related auction listing + EnumClaimType type; // ITEM, CURRENCY, REFUND + ItemStack item; // Item to claim (if type = ITEM) + long currency; // Currency to claim (if type = CURRENCY/REFUND) + long createdTime; // When claim was created + boolean claimed; // Whether claimed +} +``` + +#### AuctionFilter +```java +public class AuctionFilter { + String searchText; // Item name search + EnumAuctionSort sortBy; // Sort order + long minPrice; // Minimum price filter + long maxPrice; // Maximum price filter (0 = no max) + boolean myListingsOnly; // Show only my listings +} +``` + +#### Enums +```java +public enum EnumAuctionStatus { + ACTIVE, // Auction is live + ENDED, // Auction ended (winner determined) + CANCELLED, // Cancelled by seller or admin + CLAIMED // All claims processed +} + +public enum EnumAuctionSort { + NEWEST, // Most recently listed + ENDING_SOON, // Ending soonest + PRICE_LOW, // Lowest current price + PRICE_HIGH, // Highest current price + MOST_BIDS // Most bid activity +} + +public enum EnumClaimType { + ITEM, // Winner claims item + CURRENCY, // Seller claims sale currency + REFUND // Outbid player claims refund +} +``` + +### 3.5 AuctionController + +Main controller handling all auction logic. + +#### Core Methods +```java +// Listing Management +int createListing(EntityPlayer seller, ItemStack item, long startingPrice, long buyoutPrice) +void cancelListing(int listingId, EntityPlayer player, boolean isAdmin) +List getActiveListings(AuctionFilter filter, int page, int pageSize) +AuctionListing getListing(int listingId) +int getPlayerListingCount(UUID playerUUID) + +// Bidding +boolean placeBid(int listingId, EntityPlayer bidder, long bidAmount) +boolean buyout(int listingId, EntityPlayer buyer) + +// Claims +List getPlayerClaims(UUID playerUUID) +boolean claimItem(int claimId, EntityPlayer player) +boolean claimCurrency(int claimId, EntityPlayer player) +void claimAll(EntityPlayer player) + +// Lifecycle +void processEndedAuctions() // Called on tick, creates claims for ended auctions +void onPlayerLogin(EntityPlayer player) // Send pending notifications + +// Persistence +void save() +void load() +``` + +#### Auction Flow + +**Creating a Listing:** +1. Validate item not blacklisted +2. Check player has listing slots available +3. Deduct listing fee from player +4. Remove item from player inventory +5. Create AuctionListing with ACTIVE status +6. Save + +**Placing a Bid:** +1. Validate auction is ACTIVE +2. Validate bid > current bid + minimum increment +3. Validate bidder has enough currency +4. Deduct bid amount from bidder +5. If previous bidder exists, create REFUND claim for them +6. Update listing with new bid/bidder +7. Apply snipe protection if needed +8. Save + +**Buyout:** +1. Validate auction is ACTIVE and has buyout price +2. Validate buyer has enough currency +3. Deduct buyout amount from buyer +4. If previous bidder exists, create REFUND claim +5. Create ITEM claim for buyer +6. Create CURRENCY claim for seller (minus sales tax) +7. Set status to ENDED +8. Save + +**Auction Ends (no buyout):** +1. If bids exist: + - Create ITEM claim for high bidder + - Create CURRENCY claim for seller (minus sales tax) +2. If no bids: + - Create ITEM claim for seller (item returned) +3. Set status to ENDED +4. Save + +### 3.6 Snipe Protection + +When a bid is placed with less than `SnipeProtectionMinutes` remaining: +- Extend the auction end time TO `SnipeProtectionMinutes` from now +- Never extend beyond `SnipeProtectionMinutes` +- Default: 2 minutes + +Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 + +### 3.7 Notifications + +#### AuctionNotification Class +```java +public class AuctionNotification { + UUID playerUUID; + EnumNotificationType type; + int listingId; + String message; + long timestamp; +} + +public enum EnumNotificationType { + AUCTION_WON, // You won an auction + AUCTION_OUTBID, // You were outbid + AUCTION_SOLD, // Your item sold + AUCTION_EXPIRED, // Your auction expired (no bids) + CLAIM_READY // You have items/currency to claim +} +``` + +Notifications are queued and sent when player logs in. + +### 3.8 Permissions + +| Permission | Default | Description | +|------------|---------|-------------| +| `customnpcs.auction.use` | true | Can use auction house | +| `customnpcs.auction.sell` | true | Can create listings | +| `customnpcs.auction.slots.X` | 5 | Max listing slots (X = number) | +| `customnpcs.auction.admin` | op | Admin auction management | + +--- + +## 4. Configuration + +### ConfigMarket.java Structure + +```java +// ========================================= +// Market General +// ========================================= +public static boolean MarketEnabled = true; + +// ========================================= +// Currency (Market.Currency) +// ========================================= +public static boolean UseVault = false; +public static String CurrencyName = "Coins"; +public static long StartingBalance = 0; +public static long MaxBalance = Long.MAX_VALUE; + +// ========================================= +// Trader (Market.Trader) +// ========================================= +public static boolean EnableStockByDefault = false; +public static String DefaultResetType = "NONE"; + +// ========================================= +// Auction (Market.Auction) +// ========================================= +public static boolean AuctionEnabled = true; +public static int AuctionDuration = 24; // Hours +public static long ListingFee = 10; // Flat fee to create listing +public static double SalesTaxPercent = 0.05; // 5% tax on sales +public static int DefaultMaxListings = 5; // Per player +public static int SnipeProtectionMinutes = 2; // Snipe protection timer +public static int ClaimExpirationDays = 30; // Days before unclaimed items deleted +public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase +``` + +--- + +## 5. File Structure + +### New Files to Create + +``` +constants/ + EnumAuctionStatus.java + EnumAuctionSort.java + EnumClaimType.java + EnumNotificationType.java + +controllers/ + AuctionController.java + AuctionNotification.java + +controllers/data/ + AuctionListing.java + AuctionClaim.java + AuctionFilter.java + +roles/ + RoleAuctioneer.java + +containers/ + ContainerAuction.java + +client/gui/player/ + GuiAuctionHouse.java (Base GUI) + GuiAuction.java (Browse auctions) + GuiAuctionSell.java (Create listing) + GuiAuctionClaims.java (My auctions/claims) + GuiAuctionFilter.java (Filter settings) + SubGuiAuctionDetails.java (View listing details) + +client/gui/global/ + GuiManageAuction.java (Admin management) + +network/packets/ + AuctionDataPacket.java (Server → Client data sync) + AuctionActionPacket.java (Client → Server actions) +``` + +### Files to Modify + +``` +CustomNpcs.java - Initialize AuctionController +ServerTickHandler.java - Process ended auctions, login notifications +EnumRoleType.java - Add Auctioneer +EnumGuiType.java - Add auction GUI types +DataAdvanced.java - Handle Auctioneer role +PacketHandler.java - Register auction packets +CustomNpcsPermissions.java - Auction permissions +ConfigMarket.java - Add Auction section +RoleTrader.java - Add currency cost per trade +ContainerNPCTrader.java - Currency + stock integration +en_US.lang - All auction lang keys +``` + +--- + +## 6. Data Storage + +### Auction Data File +Location: `customnpcs/auction.dat` + +Structure: +``` +NBTTagCompound root + ├── NextListingId: int + ├── NextClaimId: int + ├── Listings: NBTTagList + │ └── [AuctionListing NBT...] + ├── Claims: NBTTagList + │ └── [AuctionClaim NBT...] + └── PendingNotifications: NBTTagList + └── [AuctionNotification NBT...] +``` + +--- + +## 7. Lang Keys + +```properties +# Auction House +auction.title=Auction House +auction.browse=Browse +auction.sell=Sell +auction.claims=My Auctions +auction.filter=Filter + +# Browse +auction.nextPage=Next > +auction.prevPage=< Prev +auction.page=Page %d of %d +auction.noResults=No auctions found +auction.refresh=Refresh + +# Listing Display +auction.seller=Seller: %s +auction.currentBid=Current Bid: %s +auction.buyout=Buyout: %s +auction.timeLeft=Time Left: %s +auction.bids=%d bids +auction.noBids=No bids + +# Create Listing +auction.sell.title=Create Listing +auction.sell.startingPrice=Starting Price +auction.sell.buyoutPrice=Buyout Price +auction.sell.noBuyout=(0 = no buyout) +auction.sell.fee=Listing Fee: %s +auction.sell.create=Create Listing +auction.sell.noItem=Place an item to sell + +# Claims +auction.claims.title=My Auctions +auction.claims.active=Active Listings +auction.claims.pending=Pending Claims +auction.claims.claimAll=Claim All +auction.claims.cancel=Cancel Listing +auction.claims.empty=No pending claims + +# Filter +auction.filter.title=Filter Auctions +auction.filter.search=Search +auction.filter.minPrice=Min Price +auction.filter.maxPrice=Max Price +auction.filter.myOnly=My Listings Only +auction.filter.apply=Apply + +# Sort Options +auction.sort.newest=Newest +auction.sort.endingSoon=Ending Soon +auction.sort.priceLow=Price: Low to High +auction.sort.priceHigh=Price: High to Low +auction.sort.mostBids=Most Bids + +# Notifications +auction.notify.won=You won the auction for %s! +auction.notify.outbid=You were outbid on %s! +auction.notify.sold=Your %s sold for %s! +auction.notify.expired=Your auction for %s expired with no bids. +auction.notify.claim=You have items to claim at the Auction House. + +# Errors +auction.error.notEnoughCurrency=Not enough currency! +auction.error.auctionEnded=This auction has ended. +auction.error.bidTooLow=Bid must be at least %s. +auction.error.maxListings=You have reached your maximum listings. +auction.error.blacklisted=This item cannot be auctioned. +auction.error.noAuctioneer=You must visit an Auctioneer NPC. + +# Trader Currency +trader.currencyCost=Currency Cost +trader.outOfStock=Out of Stock +trader.stock=Stock: %d +``` + +--- + +## 8. Implementation Order + +### Phase 1: Trader Currency Integration - COMPLETE ✅ +1. ~~Add `currencyCost` field to RoleTrader trades~~ +2. ~~Update ContainerNPCTrader to check/deduct currency~~ +3. ~~Update trader setup GUI with currency input~~ +4. ~~Test with existing item trades (backward compatibility)~~ + +### Phase 2: Trader Stock UI - COMPLETE ✅ +1. ~~Add stock display to player trader GUI~~ +2. ~~Show "Out of Stock" indicator~~ +3. ~~Verify reset timers working~~ +4. ~~Real-time client-side countdown~~ +5. ~~Status messages with costs~~ +6. ~~Reset cooldown button with confirmation~~ + +### Phase 2.5: Trader Sync System - COMPLETE ✅ +1. ~~Implement MarketRegistry for linked market sync~~ +2. ~~Viewer tracking (market-wide and local)~~ +3. ~~NPC unload cleanup~~ +4. ~~Thread safety for stock operations~~ + +### Phase 3: Auction Core +1. Create enums (EnumAuctionStatus, EnumAuctionSort, etc.) +2. Create data classes (AuctionListing, AuctionClaim, AuctionFilter) +3. Create AuctionController with core logic +4. Create RoleAuctioneer +5. Add to CustomNpcs initialization + +### Phase 4: Auction GUI +1. Create GuiAuctionHouse base +2. Create GuiAuction (browse) +3. Create GuiAuctionSell +4. Create GuiAuctionClaims +5. Create GuiAuctionFilter +6. Create SubGuiAuctionDetails +7. Create ContainerAuction + +### Phase 5: Auction Network +1. Create AuctionDataPacket +2. Create AuctionActionPacket +3. Register packets + +### Phase 6: Admin & Polish +1. Create GuiManageAuction +2. Add permissions +3. Add notifications system +4. Add lang keys +5. Testing + +--- + +## 9. Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Claims System | Everything to claims | Prevents offline handling issues | +| Auction Duration | Single config value | Simplicity over flexibility | +| Snipe Protection | Reset to X minutes | Fair bidding, prevents last-second sniping | +| Listing Fees | Flat fee only | Simple, predictable | +| Sales Tax | Percentage of sale | Scales with item value | +| Data Storage | Single .dat file | Consistent with quests/dialogs | +| Blacklist | Stub for now | Complex feature for later | +| Currency per Trade | Additive with items | Backward compatible, flexible | + +--- + +## 10. Future Considerations (Not in 1.11) + +- Category-based filtering +- Advanced blacklist system (items, mods, NBT patterns) +- Auction history/statistics +- Command-based auction access +- Auction house tax sink +- Reserve prices +- Private auctions diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md deleted file mode 100644 index 4d62a7772..000000000 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ /dev/null @@ -1,324 +0,0 @@ -# CustomNPC+ 1.11 Market Update - Implementation Status - -## Overview - -This document tracks the implementation status of the CustomNPC+ 1.11 Market Update, which includes: - -1. **Auction House** - Global auction system with bidding, buyout, and claims -2. **Currency System** - Built-in currency with VaultAPI integration for Bukkit hybrid servers -3. **Trader Enhancements** - Stock/inventory system with reset timers - ---- - -## Implementation Status Summary - -| Phase | Feature | Status | -|-------|---------|--------| -| Phase 1 | Currency System Foundation | **NOT STARTED** | -| Phase 2 | Trader Enhancements | **COMPLETED** | -| Phase 3 | Item Attributes & Restrictions | **COMPLETED** | -| Phase 4 | Auction House Core | **COMPLETED** | -| Phase 5 | Network Packets & Permissions | **COMPLETED** | -| Phase 6 | Auction GUI & Notifications | **COMPLETED** | -| Phase 7 | Admin Management | **NOT STARTED** | - ---- - -## Completed Features - -### Phase 2: Trader Enhancements (COMPLETED) -- `EnumStockReset.java` - Stock reset timing options -- `TraderStock.java` - Stock tracking with per-player and per-server modes -- Modified `RoleTrader.java` with stock and currency support -- Modified `ContainerNPCTrader.java` for stock/currency transactions -- Stock persistence via NBT - -### Phase 3: Item Attributes & Restrictions (COMPLETED) -- `ItemTradeAttribute.java` - Untradeable, ProfileSlot Bound, Soulbound checks -- `AuctionBlacklist.java` - Item/mod/NBT blacklist system -- Integration with AuctionListing validation - -### Phase 4: Auction House Core (COMPLETED) -- `EnumAuctionDuration.java` - Configurable durations with lang keys -- `EnumAuctionStatus.java` - Auction state management -- `EnumAuctionSort.java` - Sort options with lang keys -- `AuctionListing.java` - Complete listing data structure with bidding logic -- `AuctionFilter.java` - Search/filter criteria (categories removed) -- `AuctionController.java` - Consolidated controller with: - - Listing creation/management - - Bidding and buyout logic - - Claim system for items and currency refunds - - Inline logging (via LogWriter) - - Inline player notifications (via chat messages) - - Pending notification queue for offline players - - Snipe protection - - Sales tax and listing fees -- `AuctionClaim.java` - Claim data for items/money -- `RoleAuctioneer.java` - Auctioneer NPC role -- `ContainerAuction.java` - Auction container -- `ConfigMarket.java` - All market configuration options - -### Phase 5: Network Packets & Permissions (COMPLETED) -- `AuctionActionPacket.java` - Client-to-server auction actions -- `AuctionDataPacket.java` - Server-to-client data sync -- Packet registration in `PacketHandler.java` -- Permission nodes added to `CustomNpcsPermissions.java` -- API methods added to `ICustomNpc.java` - -### Phase 6: Auction GUI & Notifications (COMPLETED) -- `GuiAuction.java` - Main auction browser with tabs -- `SubGuiAuctionCreate.java` - Create listing sub-GUI -- `SubGuiAuctionBid.java` - Place bid sub-GUI -- `SubGuiAuctionDetails.java` - View listing details sub-GUI -- All GUI strings use lang keys via `StatCollector.translateToLocal()` -- Localization keys added to `en_US.lang` - ---- - -## Removed/Changed Features - -### Removed: Category System -Originally planned to have item categories (Weapons, Armor, Tools, etc.) for filtering. This was **removed** to simplify the initial implementation: -- Deleted `EnumAuctionCategory.java` -- Removed category field from `AuctionListing.java` -- Removed category filtering from `AuctionFilter.java` -- Removed category buttons from `GuiAuction.java` - -**Rationale:** Categories can be added in a future update. Current implementation uses search-based filtering. - -### Consolidated: Controller Architecture -Originally planned separate controllers: -- ~~`AuctionLogger.java`~~ - **CONSOLIDATED** into `AuctionController.logEvent()` -- ~~`AuctionNotificationController.java`~~ - **CONSOLIDATED** into `AuctionController.notifyPlayer()` - -**Rationale:** Reduces complexity, all auction logic in one place. - -### Changed: Localization Approach -- All client GUI strings now use `StatCollector.translateToLocal()` with lang keys -- `EnumAuctionSort` uses lang keys for display names -- `EnumAuctionDuration` uses lang keys for display names - ---- - -## Not Yet Implemented - -### Phase 1: Currency System (NOT STARTED) -Requires implementation: -- [ ] `VaultHelper.java` - VaultAPI integration via reflection -- [ ] `CurrencyController.java` - Currency management -- [ ] `PlayerCurrencyData.java` - Per-player currency (shared across profile slots) -- [ ] Vault dependency in build files -- [ ] Currency configuration GUI - -**Note:** Currently auctions use `long` for prices but the actual currency deduction/deposit is not connected to any economy system. - -### Phase 7: Admin Management (NOT STARTED) -Requires implementation: -- [ ] `GuiAuctionAdmin.java` - Admin auction management GUI -- [ ] `GuiAuctionBlacklist.java` - Blacklist management GUI -- [ ] Admin methods in `AuctionController.java` (adminCancelListing, adminEditListing, etc.) -- [ ] Audit logging for admin actions -- [ ] `/cnpc auction admin` command - -### Additional Missing Features -- [ ] Auctioneer NPC setup GUI (`GuiNpcAuctioneerSetup.java`) -- [ ] Trader stock reset timer banner display -- [ ] Comprehensive testing -- [ ] Player balance display (requires Currency System) - ---- - -## Configuration Reference - -All settings are in `ConfigMarket.java`: - -```java -// Currency -public static String CurrencyName = "Gold"; - -// Auction Durations (in hours) -public static int AuctionDurationShort = 12; -public static int AuctionDurationMedium = 24; -public static int AuctionDurationLong = 48; -public static int AuctionDurationVeryLong = 72; - -// Auction Fees (per duration) -public static long AuctionFeeShort = 10; -public static long AuctionFeeMedium = 25; -public static long AuctionFeeLong = 50; -public static long AuctionFeeVeryLong = 100; - -// Listing Configuration -public static int DefaultMaxListings = 5; -public static double ListingFeePercent = 0.0; -public static double SalesTaxPercent = 0.05; -public static double MinBidIncrementPercent = 0.05; -public static int ClaimExpirationDays = 30; -public static boolean RequireAuctioneerNPC = true; - -// Snipe Protection -public static int SnipeProtectionMinutes = 5; -public static int SnipeProtectionThreshold = 5; -``` - ---- - -## File Inventory - -### New Files Created - -| File | Purpose | -|------|---------| -| `constants/EnumAuctionDuration.java` | Auction duration options | -| `constants/EnumAuctionSort.java` | Sort options with lang keys | -| `constants/EnumAuctionStatus.java` | Auction state enum | -| `constants/EnumStockReset.java` | Trader stock reset types | -| `controllers/AuctionController.java` | Main auction controller | -| `controllers/AuctionBlacklist.java` | Item blacklist system | -| `controllers/data/AuctionListing.java` | Auction listing data | -| `controllers/data/AuctionFilter.java` | Search/filter criteria | -| `controllers/data/AuctionClaim.java` | Claim data structure | -| `controllers/data/TraderStock.java` | Trader stock system | -| `controllers/data/attribute/ItemTradeAttribute.java` | Trade restriction attributes | -| `roles/RoleAuctioneer.java` | Auctioneer NPC role | -| `containers/ContainerAuction.java` | Auction GUI container | -| `client/gui/player/GuiAuction.java` | Main auction GUI | -| `client/gui/player/SubGuiAuctionCreate.java` | Create listing GUI | -| `client/gui/player/SubGuiAuctionBid.java` | Place bid GUI | -| `client/gui/player/SubGuiAuctionDetails.java` | Listing details GUI | -| `config/ConfigMarket.java` | Market configuration | -| `network/packets/player/AuctionActionPacket.java` | Auction action packet | -| `network/packets/data/AuctionDataPacket.java` | Auction data sync packet | - -### Files Deleted -| File | Reason | -|------|--------| -| ~~`constants/EnumAuctionCategory.java`~~ | Categories removed | -| ~~`controllers/AuctionLogger.java`~~ | Consolidated into AuctionController | -| ~~`controllers/AuctionNotificationController.java`~~ | Consolidated into AuctionController | - -### Files Modified - -| File | Changes | -|------|---------| -| `CustomNpcs.java` | Initialize AuctionController, AuctionBlacklist | -| `ServerTickHandler.java` | Call AuctionController.onPlayerLogin() | -| `constants/EnumRoleType.java` | Added Auctioneer | -| `constants/EnumGuiType.java` | Added auction GUI types | -| `DataAdvanced.java` | Handle Auctioneer role creation | -| `roles/RoleTrader.java` | Added stock system | -| `containers/ContainerNPCTrader.java` | Stock handling | -| `CustomNpcsPermissions.java` | Auction permission nodes | -| `api/ICustomNpc.java` | Auction API methods | -| `PacketHandler.java` | Register auction packets | -| `en_US.lang` | All auction lang keys | - ---- - -## Localization Keys Added - -```properties -# Main GUI -auction.title=Auction House -auction.browse=Browse -auction.myListings=My Listings -auction.myBids=My Bids -auction.claims=Claims -auction.sell=Sell -auction.search=Search -auction.refresh=Refresh -auction.viewDetails=View Details -auction.placeBid=Place Bid -auction.buyout=Buyout -auction.cancel=Cancel -auction.prevPage=< Prev -auction.nextPage=Next > -auction.noResults=No results -auction.page=Page %d of %d -auction.balance=Balance: %s -auction.listings=%d listings - -# Tab Titles -auction.browseTitle=Browse Auctions -auction.myListingsTitle=My Listings -auction.myBidsTitle=My Active Bids -auction.claimsTitle=Claim Items -auction.sellTitle=Create Listing - -# Listing Details -auction.item=Item: -auction.seller=Seller: -auction.startingPrice=Starting Price: -auction.buyoutPrice=Buyout Price: -auction.currentPrice=Current Price: -auction.currentBid=Current Bid: -auction.minimumBid=Minimum Bid: -auction.highBidder=High Bidder: -auction.totalBids=Total Bids: -auction.quantity=Quantity: -auction.timeLeft=Time Left: -auction.yourBid=Your Bid: -auction.yourBalance=Your Balance: -auction.ended=Ended -auction.noBidsYet=No bids yet - -# Create Listing -auction.createListing=Create Listing -auction.placeItemBelow=Place item in slot below: -auction.noItemSelected=(No item selected) -auction.noBuyoutHint=(0 = no buyout) -auction.duration=Duration: -auction.listingFee=Listing Fee: -auction.create=Create - -# Bid GUI -auction.minBid=Min Bid -auction.bidPlus10=+10% -auction.bidPlus25=+25% -auction.insufficientFunds=Insufficient funds! -auction.auctionEnded=Auction has ended! -auction.auctionDetails=Auction Details - -# Sort Options -auction.sort.newest=Newest First -auction.sort.oldest=Oldest First -auction.sort.priceHigh=Most Expensive -auction.sort.priceLow=Least Expensive -auction.sort.endingSoon=Ending Soon -auction.sort.mostTime=Most Time Left -auction.sort.nameAZ=Name: A to Z -auction.sort.nameZA=Name: Z to A -auction.sort.mostBids=Most Bids -auction.sort.leastBids=Least Bids - -# Duration Options -auction.duration.short=Short (12h) -auction.duration.medium=Medium (24h) -auction.duration.long=Long (48h) -auction.duration.veryLong=Very Long (72h) -``` - ---- - -## Design Decisions (Confirmed) - -1. **Outbid Refunds** → Go to claims (not direct to balance) -2. **Currency** → Shared across all profile slots per player (NOT slot-bound) -3. **Permissions** → Uses existing CustomNPC+ permission pattern (default: 5 slots) -4. **Categories** → Removed for initial implementation (can add later) -5. **Controller Architecture** → Single consolidated AuctionController -6. **Localization** → All client strings use StatCollector with lang keys -7. **StatCollector Usage** → Safe - only called from client-side GUI code -8. **Claim Expiration** → 30 days default (configurable) -9. **Snipe Protection** → Extend TO 5 minutes when bid placed near end -10. **Logging** → Inline in AuctionController using LogWriter - ---- - -## Next Steps - -1. **Currency System** - Implement VaultHelper and CurrencyController to connect actual economy -2. **Admin Management** - Implement admin GUI and commands -3. **Testing** - Comprehensive testing of all auction flows -4. **Auctioneer Setup GUI** - Create NPC configuration interface -5. **Trader Banner** - Add stock reset timer display above trader GUI diff --git a/src/api b/src/api index 866be1fef..339e0f164 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 866be1fef5bf37a002379140771096cebe29f286 +Subproject commit 339e0f164e8c7b08c3469f61027fa25aa9e5c118 diff --git a/src/main/java/foxz/utils/Market.java b/src/main/java/foxz/utils/Market.java index ca260b90b..54905423c 100644 --- a/src/main/java/foxz/utils/Market.java +++ b/src/main/java/foxz/utils/Market.java @@ -31,9 +31,6 @@ public class Market { NBTTagCompound roleCompound = r.writeNBT(new NBTTagCompound()); roleCompound.setString("SaveName", name); putMarketCache(name, roleCompound); - if (r.recordHistory) - return; - saveFile(roleCompound); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java b/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java deleted file mode 100644 index 180f41d02..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java +++ /dev/null @@ -1,308 +0,0 @@ -package kamkeel.npcs.controllers.data.attribute; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.controllers.data.PlayerData; - -import java.util.UUID; - -/** - * Utility class for managing item trade restrictions. - * Checks and sets attributes that control whether items can be traded/auctioned. - */ -public class ItemTradeAttribute { - - // New untradeable attribute - prevents all trading - public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; - - // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) - public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; - public static final String TAG_SOULBIND = "cnpc_soulbind"; - - // ==================== Trade Check Methods ==================== - - /** - * Check if item can be traded/auctioned by the given player. - * An item cannot be traded if: - * - It has the Untradeable flag - * - It is bound to a specific profile slot (ProfileSlot Bound) - * - It is soulbound to a different player - * - * @param item The item to check - * @param player The player trying to trade (null to check without player context) - * @return true if the item can be traded - */ - public static boolean canTrade(ItemStack item, EntityPlayer player) { - if (item == null) { - return true; - } - if (!item.hasTagCompound()) { - return true; - } - - NBTTagCompound tag = item.getTagCompound(); - - // Check untradeable flag - always blocks trading - if (tag.getBoolean(TAG_UNTRADEABLE)) { - return false; - } - - // Check profile slot bound - blocks trading as it's tied to a slot - if (tag.hasKey(TAG_PROFILE_SLOT)) { - return false; - } - - // Check soulbound - blocks trading as it's tied to a player - if (tag.hasKey(TAG_SOULBIND)) { - // If we have a player context, check if they're the owner - if (player != null) { - String soulbindUUID = tag.getString(TAG_SOULBIND); - if (soulbindUUID != null && !soulbindUUID.isEmpty()) { - // Even if bound to this player, still can't trade - return false; - } - } - return false; - } - - return true; - } - - /** - * Check if item can be traded (without player context) - */ - public static boolean canTrade(ItemStack item) { - return canTrade(item, null); - } - - /** - * Get the reason why an item cannot be traded. - * Returns a translation key for localization. - * - * @param item The item to check - * @return Translation key for the block reason, or null if tradeable - */ - public static String getTradeBlockReason(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return null; - } - - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) { - return "gui.auction.untradeable"; - } - if (tag.hasKey(TAG_PROFILE_SLOT)) { - return "gui.auction.profileslotbound"; - } - if (tag.hasKey(TAG_SOULBIND)) { - return "gui.auction.soulbound"; - } - - return null; - } - - // ==================== Untradeable Methods ==================== - - /** - * Check if item is marked as untradeable - */ - public static boolean isUntradeable(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); - } - - /** - * Set the untradeable flag on an item - */ - public static void setUntradeable(ItemStack item, boolean untradeable) { - if (item == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (untradeable) { - item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); - } else { - item.getTagCompound().removeTag(TAG_UNTRADEABLE); - } - } - - // ==================== Profile Slot Bound Methods ==================== - - /** - * Check if item is bound to a profile slot - */ - public static boolean isProfileSlotBound(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); - } - - /** - * Get the profile slot an item is bound to - * @return The profile slot ID, or -1 if not bound - */ - public static int getProfileSlot(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return -1; - } - if (item.getTagCompound().hasKey(TAG_PROFILE_SLOT)) { - return item.getTagCompound().getInteger(TAG_PROFILE_SLOT); - } - return -1; - } - - /** - * Bind an item to a specific profile slot - */ - public static void setProfileSlotBound(ItemStack item, int slot) { - if (item == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (slot >= 0) { - item.getTagCompound().setInteger(TAG_PROFILE_SLOT, slot); - } else { - item.getTagCompound().removeTag(TAG_PROFILE_SLOT); - } - } - - /** - * Bind an item to the player's current profile slot - */ - public static void bindToCurrentSlot(ItemStack item, EntityPlayer player) { - if (item == null || player == null) { - return; - } - PlayerData data = PlayerData.get(player); - if (data != null) { - setProfileSlotBound(item, data.profileSlot); - } - } - - // ==================== Soulbind Methods ==================== - - /** - * Check if item is soulbound to any player - */ - public static boolean isSoulbound(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().hasKey(TAG_SOULBIND); - } - - /** - * Check if item is soulbound to a specific player - */ - public static boolean isSoulboundTo(ItemStack item, EntityPlayer player) { - if (item == null || player == null || !item.hasTagCompound()) { - return false; - } - if (!item.getTagCompound().hasKey(TAG_SOULBIND)) { - return false; - } - String soulbindUUID = item.getTagCompound().getString(TAG_SOULBIND); - return player.getUniqueID().toString().equals(soulbindUUID); - } - - /** - * Get the UUID of the player the item is soulbound to - * @return The UUID string, or null if not soulbound - */ - public static String getSoulbindOwner(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return null; - } - if (item.getTagCompound().hasKey(TAG_SOULBIND)) { - return item.getTagCompound().getString(TAG_SOULBIND); - } - return null; - } - - /** - * Soulbind an item to a player - */ - public static void setSoulbound(ItemStack item, EntityPlayer player) { - if (item == null || player == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - item.getTagCompound().setString(TAG_SOULBIND, player.getUniqueID().toString()); - } - - /** - * Soulbind an item to a player by UUID - */ - public static void setSoulbound(ItemStack item, UUID playerUUID) { - if (item == null || playerUUID == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - item.getTagCompound().setString(TAG_SOULBIND, playerUUID.toString()); - } - - /** - * Remove soulbind from an item - */ - public static void removeSoulbind(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return; - } - item.getTagCompound().removeTag(TAG_SOULBIND); - } - - // ==================== Utility Methods ==================== - - /** - * Clear all trade restriction attributes from an item - */ - public static void clearAllRestrictions(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return; - } - NBTTagCompound tag = item.getTagCompound(); - tag.removeTag(TAG_UNTRADEABLE); - tag.removeTag(TAG_PROFILE_SLOT); - tag.removeTag(TAG_SOULBIND); - } - - /** - * Get a display string describing all trade restrictions on an item - * @return Human-readable string of restrictions, or empty if none - */ - public static String getRestrictionSummary(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return ""; - } - - StringBuilder summary = new StringBuilder(); - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) { - summary.append("Untradeable"); - } - if (tag.hasKey(TAG_PROFILE_SLOT)) { - if (summary.length() > 0) summary.append(", "); - summary.append("Profile Slot Bound (Slot ").append(tag.getInteger(TAG_PROFILE_SLOT)).append(")"); - } - if (tag.hasKey(TAG_SOULBIND)) { - if (summary.length() > 0) summary.append(", "); - summary.append("Soulbound"); - } - - return summary.toString(); - } -} diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index b6d30d62d..3a4c35d05 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -36,7 +36,6 @@ import kamkeel.npcs.network.packets.data.gui.GuiWaypointPacket; import kamkeel.npcs.network.packets.data.gui.IsGuiOpenPacket; import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.network.packets.data.large.PartyDataPacket; -import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollListPacket; import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; @@ -52,13 +51,13 @@ import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; import kamkeel.npcs.network.packets.player.DialogSelectPacket; import kamkeel.npcs.network.packets.player.FollowerPacket; import kamkeel.npcs.network.packets.player.GetNPCRole; +import kamkeel.npcs.network.packets.player.GetTraderData; import kamkeel.npcs.network.packets.player.InputDevicePacket; import kamkeel.npcs.network.packets.player.MailActionPacket; import kamkeel.npcs.network.packets.player.SaveBookPacket; @@ -566,9 +565,6 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); - - // Auction Data - DATA_PACKET.registerPacket(new AuctionDataPacket()); } public void registerPlayerPackets() { @@ -591,6 +587,7 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new CompanionActionPacket()); PLAYER_PACKET.registerPacket(new GetNPCRole()); + PLAYER_PACKET.registerPacket(new GetTraderData()); PLAYER_PACKET.registerPacket(new InputDevicePacket()); @@ -630,9 +627,6 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new PartyAcceptInvitePacket()); PLAYER_PACKET.registerPacket(new PartyIgnoreInvitePacket()); PLAYER_PACKET.registerPacket(new PartyLogToServerPacket()); - - // Auction Packets - PLAYER_PACKET.registerPacket(new AuctionActionPacket()); } public void registerChannels() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index ec8eabf41..ebcbdfc59 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,7 +62,4 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, - - // Auction - AUCTION_DATA, } diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 15c90fce5..48f400fb2 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -13,6 +13,7 @@ public enum EnumPlayerPacket { SaveBook, CompanionAction, GetRole, + GetTraderData, InputDevice, SpecialKeyState, ScreenSize, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java deleted file mode 100644 index f7ca16dd7..000000000 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java +++ /dev/null @@ -1,218 +0,0 @@ -package kamkeel.npcs.network.packets.data.large; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import kamkeel.npcs.network.LargeAbstractPacket; -import kamkeel.npcs.network.PacketChannel; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumDataPacket; -import kamkeel.npcs.util.ByteBufUtils; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; - -import java.io.IOException; -import java.util.List; - -/** - * Large packet for sending auction listing data from server to client. - * Used for initial load and search/filter queries. - */ -public final class AuctionDataPacket extends LargeAbstractPacket { - public static final String packetName = "Large|AuctionData"; - - private DataType dataType; - private List listings; - private int totalResults; - private int currentPage; - private int totalPages; - private long playerBalance; - - public AuctionDataPacket() { - } - - private AuctionDataPacket(DataType type, List listings, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - this.dataType = type; - this.listings = listings; - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - this.playerBalance = playerBalance; - } - - /** - * Send filtered listing results to client - */ - public static void sendFilteredListings(EntityPlayerMP player, AuctionFilter filter) { - if (AuctionController.Instance == null) { - return; - } - - AuctionController.FilterResult result = AuctionController.Instance.getFilteredListings(filter); - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.FILTERED_LISTINGS, - result.listings, - result.totalResults, - result.currentPage, - result.totalPages, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's own listings - */ - public static void sendMyListings(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List myListings = AuctionController.Instance - .getListingsBySeller(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.MY_LISTINGS, - myListings, - myListings.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's active bids - */ - public static void sendMyBids(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List myBids = AuctionController.Instance - .getListingsAsBidder(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.MY_BIDS, - myBids, - myBids.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's claimable items - */ - public static void sendClaimable(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List claimable = AuctionController.Instance - .getClaimableListings(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.CLAIMABLE, - claimable, - claimable.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - @Override - public Enum getType() { - return EnumDataPacket.AUCTION_DATA; - } - - @Override - public PacketChannel getChannel() { - return PacketHandler.DATA_PACKET; - } - - @Override - protected byte[] getData() throws IOException { - ByteBuf buffer = Unpooled.buffer(); - - buffer.writeInt(dataType.ordinal()); - buffer.writeInt(totalResults); - buffer.writeInt(currentPage); - buffer.writeInt(totalPages); - buffer.writeLong(playerBalance); - - // Write listings as NBT - NBTTagList listingList = new NBTTagList(); - if (listings != null) { - for (AuctionListing listing : listings) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - } - - NBTTagCompound compound = new NBTTagCompound(); - compound.setTag("Listings", listingList); - ByteBufUtils.writeNBT(buffer, compound); - - byte[] bytes = new byte[buffer.readableBytes()]; - buffer.readBytes(bytes); - return bytes; - } - - @Override - protected void handleCompleteData(ByteBuf data, EntityPlayer player) throws IOException { - // Client-side handling will be done in a client util class - // Similar to NoppesUtil.setScrollData() - int typeOrdinal = data.readInt(); - int totalResults = data.readInt(); - int currentPage = data.readInt(); - int totalPages = data.readInt(); - long playerBalance = data.readLong(); - - NBTTagCompound compound = ByteBufUtils.readNBT(data); - - DataType type = DataType.values()[typeOrdinal]; - - // Store data for client GUI to access - // This will be implemented in the client-side GUI handler - noppes.npcs.client.NoppesUtil.setAuctionData(type.ordinal(), compound, - totalResults, currentPage, totalPages, playerBalance); - } - - public enum DataType { - FILTERED_LISTINGS, - MY_LISTINGS, - MY_BIDS, - CLAIMABLE - } -} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java deleted file mode 100644 index a63312d80..000000000 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ /dev/null @@ -1,579 +0,0 @@ -package kamkeel.npcs.network.packets.player; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.network.AbstractPacket; -import kamkeel.npcs.network.PacketChannel; -import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumPlayerPacket; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.ChatComponentText; -import noppes.npcs.CustomNpcsPermissions; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumRoleType; -import noppes.npcs.containers.ContainerAuction; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.CurrencyController; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.util.NBTJsonUtil; -import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; - -import java.io.IOException; - -/** - * Packet for all auction-related player actions. - * Client -> Server for actions, Server -> Client for responses. - */ -public class AuctionActionPacket extends AbstractPacket { - public static final String packetName = "Player|Auction"; - - private Action action; - private int listingId; - private long amount; - private long buyoutPrice; - private int durationOrdinal; - private NBTTagCompound itemData; - private NBTTagCompound filterData; // For query operations - private String responseMessage; - private boolean success; - - public AuctionActionPacket() { - } - - private AuctionActionPacket(Action action) { - this.action = action; - } - - @Override - public boolean needsNPC() { - return true; - } - - // ==================== Client -> Server Actions ==================== - - /** - * Create a new auction listing - */ - @SideOnly(Side.CLIENT) - public static void CreateListing(ItemStack item, long startingPrice, long buyoutPrice, EnumAuctionDuration duration) { - AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); - packet.amount = startingPrice; - packet.buyoutPrice = buyoutPrice; - packet.durationOrdinal = duration.ordinal(); - packet.itemData = item.writeToNBT(new NBTTagCompound()); - PacketClient.sendClient(packet); - } - - /** - * Place a bid on an auction - */ - @SideOnly(Side.CLIENT) - public static void PlaceBid(int listingId, long bidAmount) { - AuctionActionPacket packet = new AuctionActionPacket(Action.PlaceBid); - packet.listingId = listingId; - packet.amount = bidAmount; - PacketClient.sendClient(packet); - } - - /** - * Execute buyout on an auction - */ - @SideOnly(Side.CLIENT) - public static void Buyout(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Cancel own listing - */ - @SideOnly(Side.CLIENT) - public static void CancelListing(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.CancelListing); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Claim item from won/expired/cancelled auction - */ - @SideOnly(Side.CLIENT) - public static void ClaimItem(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Claim proceeds from sold auction - */ - @SideOnly(Side.CLIENT) - public static void ClaimProceeds(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimProceeds); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Query filtered listings - */ - @SideOnly(Side.CLIENT) - public static void QueryListings(AuctionFilter filter) { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryListings); - packet.filterData = filter.writeToNBT(new NBTTagCompound()); - PacketClient.sendClient(packet); - } - - /** - * Request player's own listings - */ - @SideOnly(Side.CLIENT) - public static void QueryMyListings() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyListings); - PacketClient.sendClient(packet); - } - - /** - * Request player's active bids - */ - @SideOnly(Side.CLIENT) - public static void QueryMyBids() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyBids); - PacketClient.sendClient(packet); - } - - /** - * Request player's claimable items - */ - @SideOnly(Side.CLIENT) - public static void QueryClaimable() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryClaimable); - PacketClient.sendClient(packet); - } - - @Override - public Enum getType() { - return EnumPlayerPacket.AuctionAction; - } - - @Override - public PacketChannel getChannel() { - return PacketHandler.PLAYER_PACKET; - } - - @Override - @SideOnly(Side.CLIENT) - public void sendData(ByteBuf out) throws IOException { - out.writeInt(action.ordinal()); - - switch (action) { - case CreateListing: - out.writeLong(amount); - out.writeLong(buyoutPrice); - out.writeInt(durationOrdinal); - String itemJson = NBTJsonUtil.Convert(itemData); - byte[] itemBytes = itemJson.getBytes("UTF-8"); - out.writeInt(itemBytes.length); - out.writeBytes(itemBytes); - break; - - case PlaceBid: - out.writeInt(listingId); - out.writeLong(amount); - break; - - case Buyout: - case CancelListing: - case ClaimItem: - case ClaimProceeds: - out.writeInt(listingId); - break; - - case QueryListings: - String filterJson = NBTJsonUtil.Convert(filterData); - byte[] filterBytes = filterJson.getBytes("UTF-8"); - out.writeInt(filterBytes.length); - out.writeBytes(filterBytes); - break; - - case QueryMyListings: - case QueryMyBids: - case QueryClaimable: - // No additional data needed - break; - } - } - - @Override - public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { - Action requestedAction = Action.values()[in.readInt()]; - - if (!(player instanceof EntityPlayerMP)) { - return; - } - - EntityPlayerMP playerMP = (EntityPlayerMP) player; - - // Verify NPC is an Auctioneer - if (npc == null || npc.advanced.role != EnumRoleType.Auctioneer) { - return; - } - - // Check if auction system is enabled - if (!ConfigMarket.AuctionEnabled) { - sendResponse(playerMP, false, "Auction system is disabled"); - return; - } - - // Check controller exists - if (AuctionController.Instance == null) { - sendResponse(playerMP, false, "Auction controller not initialized"); - return; - } - - switch (requestedAction) { - case CreateListing: - handleCreateListing(in, playerMP); - break; - case PlaceBid: - handlePlaceBid(in, playerMP); - break; - case Buyout: - handleBuyout(in, playerMP); - break; - case CancelListing: - handleCancelListing(in, playerMP); - break; - case ClaimItem: - handleClaimItem(in, playerMP); - break; - case ClaimProceeds: - handleClaimProceeds(in, playerMP); - break; - case QueryListings: - handleQueryListings(in, playerMP); - break; - case QueryMyListings: - handleQueryMyListings(playerMP); - break; - case QueryMyBids: - handleQueryMyBids(playerMP); - break; - case QueryClaimable: - handleQueryClaimable(playerMP); - break; - } - } - - private void handleCreateListing(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_LIST)) { - sendResponse(player, false, "You don't have permission to create listings"); - return; - } - - long startingPrice = in.readLong(); - long buyoutPrice = in.readLong(); - int durationOrdinal = in.readInt(); - - int itemBytesLength = in.readInt(); - byte[] itemBytes = new byte[itemBytesLength]; - in.readBytes(itemBytes); - String itemJson = new String(itemBytes, "UTF-8"); - - try { - NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); - ItemStack requestedItem = ItemStack.loadItemStackFromNBT(itemNBT); - - if (!(player.openContainer instanceof ContainerAuction)) { - sendResponse(player, false, "Open the auction listing screen to create a listing"); - return; - } - - ContainerAuction container = (ContainerAuction) player.openContainer; - ItemStack slotItem = container.getListingItem(); - - if (requestedItem == null || slotItem == null) { - sendResponse(player, false, "Place an item in the listing slot"); - return; - } - - if (!ItemStack.areItemStacksEqual(slotItem, requestedItem) || slotItem.stackSize != requestedItem.stackSize) { - sendResponse(player, false, "Listing item mismatch"); - return; - } - - // Validate duration - EnumAuctionDuration duration; - if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { - duration = EnumAuctionDuration.values()[durationOrdinal]; - } else { - duration = EnumAuctionDuration.MEDIUM; - } - - // Validate prices - if (startingPrice < 1) { - sendResponse(player, false, "Starting price must be at least 1"); - return; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - sendResponse(player, false, "Buyout price must be higher than starting price"); - return; - } - - // Create the listing - AuctionListing listing = AuctionController.Instance.createListing( - player, slotItem, startingPrice, buyoutPrice, duration); - - if (listing != null) { - container.clearListingSlot(); - sendResponse(player, true, "Listing created successfully"); - } else { - sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); - } - } catch (Exception e) { - sendResponse(player, false, "Error creating listing: " + e.getMessage()); - } - } - - private void handlePlaceBid(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BID)) { - sendResponse(player, false, "You don't have permission to bid"); - return; - } - - int listingId = in.readInt(); - long bidAmount = in.readLong(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (!listing.isActive()) { - sendResponse(player, false, "This auction has ended"); - return; - } - - if (bidAmount < listing.getMinimumBid()) { - sendResponse(player, false, "Bid must be at least " + listing.getMinimumBid()); - return; - } - - if (!CurrencyController.Instance.canAfford(player, bidAmount)) { - sendResponse(player, false, "You cannot afford this bid"); - return; - } - - if (AuctionController.Instance.placeBid(listingId, player, bidAmount)) { - sendResponse(player, true, "Bid placed successfully"); - } else { - sendResponse(player, false, "Failed to place bid"); - } - } - - private void handleBuyout(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BUYOUT)) { - sendResponse(player, false, "You don't have permission to use buyout"); - return; - } - - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (!listing.isActive()) { - sendResponse(player, false, "This auction has ended"); - return; - } - - if (listing.buyoutPrice <= 0) { - sendResponse(player, false, "This listing has no buyout option"); - return; - } - - if (!CurrencyController.Instance.canAfford(player, listing.buyoutPrice)) { - sendResponse(player, false, "You cannot afford the buyout price"); - return; - } - - if (AuctionController.Instance.buyout(listingId, player)) { - sendResponse(player, true, "Buyout successful! Claim your item."); - } else { - sendResponse(player, false, "Failed to execute buyout"); - } - } - - private void handleCancelListing(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - // Admin can cancel any listing - boolean isAdmin = CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_ADMIN); - - if (!listing.sellerUUID.equals(player.getUniqueID()) && !isAdmin) { - sendResponse(player, false, "You can only cancel your own listings"); - return; - } - - if (listing.hasBids() && !isAdmin) { - sendResponse(player, false, "Cannot cancel listing with active bids"); - return; - } - - if (AuctionController.Instance.cancelListing(listingId, player, isAdmin)) { - sendResponse(player, true, "Listing cancelled. Claim your item."); - } else { - sendResponse(player, false, "Failed to cancel listing"); - } - } - - private void handleClaimItem(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - ItemStack claimed = null; - - // Try buyer claim first - if (listing.highBidderUUID != null && listing.highBidderUUID.equals(player.getUniqueID())) { - claimed = AuctionController.Instance.claimBuyerItem(listingId, player); - } - // Then try seller reclaim (for expired/cancelled) - else if (listing.sellerUUID.equals(player.getUniqueID())) { - claimed = AuctionController.Instance.claimExpiredItem(listingId, player); - } - - if (claimed != null) { - // Give item to player - if (!player.inventory.addItemStackToInventory(claimed)) { - // Drop if inventory full - player.entityDropItem(claimed, 0.5f); - } - sendResponse(player, true, "Item claimed"); - } else { - sendResponse(player, false, "Cannot claim item"); - } - } - - private void handleClaimProceeds(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (AuctionController.Instance.claimSellerProceeds(listingId, player)) { - long proceeds = listing.getSellerProceeds(); - sendResponse(player, true, "Claimed " + proceeds + " " + ConfigMarket.CurrencyName); - } else { - sendResponse(player, false, "Cannot claim proceeds"); - } - } - - // ==================== Query Handlers ==================== - - private void handleQueryListings(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - int filterBytesLength = in.readInt(); - byte[] filterBytes = new byte[filterBytesLength]; - in.readBytes(filterBytes); - String filterJson = new String(filterBytes, "UTF-8"); - - try { - NBTTagCompound filterNBT = NBTJsonUtil.Convert(filterJson); - AuctionFilter filter = new AuctionFilter(); - filter.readFromNBT(filterNBT); - - // Send filtered listings to client - AuctionDataPacket.sendFilteredListings(player, filter); - } catch (Exception e) { - sendResponse(player, false, "Error parsing filter"); - } - } - - private void handleQueryMyListings(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendMyListings(player); - } - - private void handleQueryMyBids(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendMyBids(player); - } - - private void handleQueryClaimable(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendClaimable(player); - } - - /** - * Send a response message to the player - */ - private void sendResponse(EntityPlayerMP player, boolean success, String message) { - // Use chat message for now; can be extended to use GUI updates - if (message != null && !message.isEmpty()) { - player.addChatMessage(new ChatComponentText("[Auction] " + message)); - } - } - - private enum Action { - CreateListing, - PlaceBid, - Buyout, - CancelListing, - ClaimItem, - ClaimProceeds, - QueryListings, - QueryMyListings, - QueryMyBids, - QueryClaimable - } -} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java new file mode 100644 index 000000000..5b7cb7dfe --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java @@ -0,0 +1,77 @@ +package kamkeel.npcs.network.packets.player; + +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.roles.RoleTrader; + +import java.io.IOException; + +/** + * Client requests trader-specific GUI data from server. + * Server responds with player balance, stock info, currency costs via GuiDataPacket. + */ +public class GetTraderData extends AbstractPacket { + public static final String packetName = "Player|GetTraderData"; + + @Override + public Enum getType() { + return EnumPlayerPacket.GetTraderData; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + public boolean needsNPC() { + return true; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + // No data needed for request + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!(npc.roleInterface instanceof RoleTrader)) + return; + + RoleTrader role = (RoleTrader) npc.roleInterface; + NBTTagCompound compound = new NBTTagCompound(); + + // Player balance + PlayerData data = PlayerData.get(player); + compound.setLong("Balance", data.currencyData.getBalance()); + + // Stock info + compound.setBoolean("StockEnabled", role.stock.enableStock); + compound.setLong("ResetTimeMillis", role.getResetTimeRemainingMillis()); + + // Available stock per slot (player-specific if perPlayer mode) + String playerName = player.getCommandSenderName(); + int[] stock = new int[18]; + for (int i = 0; i < 18; i++) { + stock[i] = role.getAvailableStock(i, playerName); + } + compound.setIntArray("Stock", stock); + + // Currency cost per slot + for (int i = 0; i < 18; i++) { + compound.setLong("Cost" + i, role.getCurrencyCost(i)); + } + + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } +} diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index a73c179e4..58f931ab8 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -15,7 +15,6 @@ import noppes.npcs.api.IWorld; import noppes.npcs.blocks.tiles.TileNpcContainer; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; -import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -132,9 +131,6 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.CompanionInv) return new ContainerNPCCompanion(npc, player); - if (gui == EnumGuiType.PlayerAuction) - return new ContainerAuction(npc, player); - return null; } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index b29a6cd0b..41cd8770e 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -59,10 +59,6 @@ import noppes.npcs.controllers.GlobalDataController; import noppes.npcs.controllers.LinkedItemController; import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; -import noppes.npcs.controllers.VaultHelper; -import noppes.npcs.controllers.CurrencyController; -import noppes.npcs.controllers.AuctionBlacklist; -import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -314,10 +310,6 @@ public class CustomNpcs { //Loading items in the about to start event was corrupting items with a damage value @EventHandler public void started(FMLServerStartedEvent event) { - new VaultHelper(); - new CurrencyController(); - new AuctionBlacklist(); - new AuctionController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 34a5fbf50..c83e92c22 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -92,13 +92,6 @@ public class CustomNpcsPermissions { public static final Permission SOULSTONE_ALL = new Permission("customnpcs.soulstone.all"); - // Auction System - public static final Permission AUCTION_USE = new Permission("customnpcs.auction.use"); - public static final Permission AUCTION_LIST = new Permission("customnpcs.auction.list"); - public static final Permission AUCTION_BID = new Permission("customnpcs.auction.bid"); - public static final Permission AUCTION_BUYOUT = new Permission("customnpcs.auction.buyout"); - public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); - // Modify / Save Scripts public static final Permission SCRIPT = new Permission("customnpcs.script"); public static final Permission SCRIPT_NPC = new Permission("customnpcs.script.npc"); diff --git a/src/main/java/noppes/npcs/DataAdvanced.java b/src/main/java/noppes/npcs/DataAdvanced.java index f03714506..45ecfe4a8 100644 --- a/src/main/java/noppes/npcs/DataAdvanced.java +++ b/src/main/java/noppes/npcs/DataAdvanced.java @@ -18,7 +18,6 @@ import noppes.npcs.roles.JobGuard; import noppes.npcs.roles.JobHealer; import noppes.npcs.roles.JobItemGiver; import noppes.npcs.roles.JobSpawner; -import noppes.npcs.roles.RoleAuctioneer; import noppes.npcs.roles.RoleBank; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; @@ -219,8 +218,6 @@ public class DataAdvanced { npc.roleInterface = new RoleCompanion(npc); else if (role == EnumRoleType.Mount && !(npc.roleInterface instanceof RoleMount)) npc.roleInterface = new RoleMount(npc); - else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) - npc.roleInterface = new RoleAuctioneer(npc); } public void setJob(int i) { diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 89bdf58c8..08c04f6f4 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -384,6 +384,7 @@ public class NoppesUtilServer { if (gui == EnumGuiType.PlayerFollower || gui == EnumGuiType.PlayerFollowerHire || gui == EnumGuiType.PlayerTrader || gui == EnumGuiType.PlayerTransporter) { sendRoleData(player, npc); } + // Note: Trader-specific data (balance, stock, costs) is requested by client via GetTraderData packet } private static ArrayList getScrollData(EntityPlayer player, EnumGuiType gui, EntityNPCInterface npc) { diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index fe846df19..3648f00d7 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,7 +10,6 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; -import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; @@ -66,11 +65,6 @@ public class ServerTickHandler { ProfileController.Instance.login(player); SyncController.beginLogin(player); SyncController.syncEffects(player); - - // Send pending auction notifications - if (AuctionController.Instance != null) { - AuctionController.Instance.onPlayerLogin(player); - } } @SubscribeEvent diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index a31b57a7f..4e64f8351 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -104,7 +104,6 @@ import noppes.npcs.client.gui.player.GuiCrate; import noppes.npcs.client.gui.player.GuiMailbox; import noppes.npcs.client.gui.player.GuiMailmanWrite; import noppes.npcs.client.gui.player.GuiNPCBankChest; -import noppes.npcs.client.gui.player.GuiAuction; import noppes.npcs.client.gui.player.GuiNPCTrader; import noppes.npcs.client.gui.player.GuiNpcAnvil; import noppes.npcs.client.gui.player.GuiNpcCarpentryBench; @@ -180,7 +179,6 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.config.StringCache; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; -import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -576,9 +574,6 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.MagicBook) return new GuiNpcMagicBook(); - else if (gui == EnumGuiType.PlayerAuction) - return new GuiAuction(npc, (ContainerAuction) container); - else if (gui == EnumGuiType.GlobalRemote) return new GuiNPCGlobalMainMenu(null); diff --git a/src/main/java/noppes/npcs/client/NoppesUtil.java b/src/main/java/noppes/npcs/client/NoppesUtil.java index 64b26fa5d..620142564 100644 --- a/src/main/java/noppes/npcs/client/NoppesUtil.java +++ b/src/main/java/noppes/npcs/client/NoppesUtil.java @@ -182,67 +182,6 @@ public class NoppesUtil { data = new HashMap(); } - // ==================== Auction Data ==================== - - // Static storage for auction data that the GUI can access - private static NBTTagCompound auctionListingData; - private static int auctionDataType; - private static int auctionTotalResults; - private static int auctionCurrentPage; - private static int auctionTotalPages; - private static long auctionPlayerBalance; - - public static void setAuctionData(int dataType, NBTTagCompound compound, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - auctionDataType = dataType; - auctionListingData = compound; - auctionTotalResults = totalResults; - auctionCurrentPage = currentPage; - auctionTotalPages = totalPages; - auctionPlayerBalance = playerBalance; - - // Notify the current GUI if it's an auction GUI - GuiScreen gui = Minecraft.getMinecraft().currentScreen; - if (gui instanceof IAuctionData) { - ((IAuctionData) gui).onAuctionDataReceived(dataType, compound, - totalResults, currentPage, totalPages, playerBalance); - } - } - - public static NBTTagCompound getAuctionListingData() { - return auctionListingData; - } - - public static int getAuctionDataType() { - return auctionDataType; - } - - public static int getAuctionTotalResults() { - return auctionTotalResults; - } - - public static int getAuctionCurrentPage() { - return auctionCurrentPage; - } - - public static int getAuctionTotalPages() { - return auctionTotalPages; - } - - public static long getAuctionPlayerBalance() { - return auctionPlayerBalance; - } - - /** - * Interface for GUIs that receive auction data - */ - public interface IAuctionData { - void onAuctionDataReceived(int dataType, NBTTagCompound data, - int totalResults, int currentPage, int totalPages, - long playerBalance); - } - public static void guiQuestCompletion(EntityPlayer player, NBTTagCompound read) { Quest quest = new Quest(); quest.readNBT(read); diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 7ee5e8881..885c61d94 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -26,7 +26,6 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; import noppes.npcs.client.gui.roles.GuiNpcSpawner; -import noppes.npcs.client.gui.roles.GuiNpcAuctioneer; import noppes.npcs.client.gui.roles.GuiNpcMount; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; @@ -158,8 +157,6 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { displayGuiScreen(new GuiNpcCompanion(npc)); else if (npc.advanced.role == EnumRoleType.Mount) displayGuiScreen(new GuiNpcMount(npc)); - else if (npc.advanced.role == EnumRoleType.Auctioneer) - displayGuiScreen(new GuiNpcAuctioneer(npc)); } else if (compound.hasKey("JobData")) { if (npc.jobInterface != null) npc.jobInterface.readFromNBT(compound); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java deleted file mode 100644 index 30e857e7a..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ /dev/null @@ -1,841 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.inventory.Slot; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.CustomNpcResourceListener; -import noppes.npcs.client.NoppesUtil; -import noppes.npcs.client.gui.util.*; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.containers.ContainerAuction; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.entity.EntityNPCInterface; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Main auction house browse GUI. - * Displays listings with search, sort, and pagination. - */ -public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, - ICustomScrollListener, NoppesUtil.IAuctionData { - - private static final ResourceLocation BG_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/standardbg.png"); - private static final int LEFT_PANEL_X = 8; - private static final int LEFT_PANEL_WIDTH = 190; - private static final int RIGHT_PANEL_X = 210; - private static final int ACTION_BUTTON_WIDTH = 98; - private static final int LISTING_SCROLL_HEIGHT = 100; - private static final int LISTING_SCROLL_WIDTH = 190; - private static final int PAGE_BUTTON_WIDTH = 60; - private static final int SEARCH_FIELD_WIDTH = 140; - private static final int SEARCH_BUTTON_WIDTH = 46; - private static final int HIDDEN_SLOT_POSITION = -1000; - private static final int INVENTORY_START_X = 8; - private static final int INVENTORY_START_Y = 166; - private static final int HOTBAR_Y_OFFSET = 58; - - // GUI Components - private GuiNpcTextField searchField; - private GuiNpcTextField startingPriceField; - private GuiNpcTextField buyoutPriceField; - private GuiCustomScroll listingScroll; - - // Current state - private AuctionFilter currentFilter = new AuctionFilter(); - private List listings = new ArrayList<>(); - private int totalResults = 0; - private int currentPage = 0; - private int totalPages = 0; - private long playerBalance = 0; - - // Tab state (0=Browse, 1=My Listings, 2=My Bids, 3=Claims, 4=Create) - private int currentTab = 0; - private int selectedDuration = 1; - - // Selected listing for details - private int selectedListingIndex = -1; - - // Countdown update timer - private long lastCountdownUpdate = 0; - - private enum ActionMode { - BID, - CANCEL, - CLAIM - } - - public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { - super(npc, container); - this.title = ""; - this.xSize = 319; - this.ySize = 238; - this.closeOnEsc = true; - this.drawDefaultBackground = true; - - // Request initial data - AuctionActionPacket.QueryListings(currentFilter); - } - - @Override - public void initGui() { - super.initGui(); - - int leftX = guiLeft + LEFT_PANEL_X; - int rightX = guiLeft + RIGHT_PANEL_X; - int actionY = guiTop + 44; - boolean isCreateTab = currentTab == 4; - boolean isBrowseTab = currentTab == 0; - - updateListingSlotPosition(isCreateTab); - - // Tab buttons at top - GuiMenuTopButton browseButton = new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse")); - addTopButton(browseButton); - GuiMenuTopButton myListingsButton = new GuiMenuTopButton(1, browseButton, StatCollector.translateToLocal("auction.myListings")); - addTopButton(myListingsButton); - GuiMenuTopButton myBidsButton = new GuiMenuTopButton(2, myListingsButton, StatCollector.translateToLocal("auction.myBids")); - addTopButton(myBidsButton); - GuiMenuTopButton claimsButton = new GuiMenuTopButton(3, myBidsButton, StatCollector.translateToLocal("auction.claims")); - addTopButton(claimsButton); - GuiMenuTopButton sellButton = new GuiMenuTopButton(4, claimsButton, StatCollector.translateToLocal("auction.sell")); - addTopButton(sellButton); - getTopButton(currentTab).setEnabled(false); - - if (isCreateTab) { - addCreateControls(leftX, rightX); - } else { - if (isBrowseTab) { - // Search field - searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); - searchField.setMaxStringLength(50); - addTextField(searchField); - - // Search button - addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.search"))); - - // Sort button - addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); - } - - // Listing scroll area - listingScroll = new GuiCustomScroll(this, 0); - listingScroll.guiLeft = leftX; - listingScroll.guiTop = guiTop + 44; - listingScroll.setSize(LISTING_SCROLL_WIDTH, LISTING_SCROLL_HEIGHT); - addScroll(listingScroll); - updateListingDisplay(); - - if (isBrowseTab) { - // Pagination buttons - addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.prevPage"))); - addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.nextPage"))); - } - - // Action buttons (right side) - addActionButtons(rightX, actionY); - - // Balance display - addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), - rightX, guiTop + 170, 0x00AA00)); - } - - updateButtonStates(); - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update countdowns every second - long now = System.currentTimeMillis(); - if (now - lastCountdownUpdate >= 1000) { - lastCountdownUpdate = now; - updateListingDisplay(); - } - - if (currentTab == 4 && inventorySlots instanceof ContainerAuction) { - ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); - GuiNpcLabel itemLabel = getLabel(102); - GuiNpcButton createButton = getButton(71); - if (item != null) { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); - itemLabel.color = 0x008800; - } - if (createButton != null) { - createButton.setEnabled(true); - } - } else { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); - itemLabel.color = 0x888888; - } - if (createButton != null) { - createButton.setEnabled(false); - } - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - super.actionPerformed(button); - int id = button.id; - - // Tab buttons - if (id >= 0 && id <= 4) { - switchTab(id); - } - // Search button - else if (id == 11) { - if (currentTab == 0) { - performSearch(); - } - } - // Sort button - else if (id == 30) { - if (currentTab == 0) { - cycleSortOrder(); - } - } - // Pagination - else if (id == 40) { - if (currentTab == 0) { - previousPage(); - } - } else if (id == 42) { - if (currentTab == 0) { - nextPage(); - } - } - // Actions - else if (id == 50) { - viewDetails(); - } else if (id == 51) { - ActionMode mode = getActionMode(); - if (mode == ActionMode.BID) { - placeBid(); - } else if (mode == ActionMode.CANCEL) { - cancelListing(); - } else if (mode == ActionMode.CLAIM) { - claimListing(); - } - } else if (id == 52) { - if (getActionMode() == ActionMode.BID) { - buyout(); - } - } else if (id == 53) { - refresh(); - } else if (id == 70) { - selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; - initGui(); - } else if (id == 71) { - createListing(); - } else if (id == 72) { - switchTab(0); - } - } - - private void switchTab(int tab) { - currentTab = tab; - currentPage = 0; - selectedListingIndex = -1; - - switch (tab) { - case 0: // Browse - AuctionActionPacket.QueryListings(currentFilter); - break; - case 1: // My Listings - AuctionActionPacket.QueryMyListings(); - break; - case 2: // My Bids - AuctionActionPacket.QueryMyBids(); - break; - case 3: // Claims - AuctionActionPacket.QueryClaimable(); - break; - case 4: - break; - } - - initGui(); - } - - private void performSearch() { - currentFilter.searchText = searchField.getText(); - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - } - - private void cycleSortOrder() { - currentFilter.sortOrder = currentFilter.sortOrder.next(); - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - initGui(); - } - - private void previousPage() { - if (currentPage > 0) { - currentPage--; - currentFilter.page = currentPage; - AuctionActionPacket.QueryListings(currentFilter); - } - } - - private void nextPage() { - if (currentPage < totalPages - 1) { - currentPage++; - currentFilter.page = currentPage; - AuctionActionPacket.QueryListings(currentFilter); - } - } - - private void viewDetails() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.item == null) { - return; - } - setSubGui(new SubGuiAuctionDetails(this, listing)); - } - } - - private void placeBid() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.item == null) { - return; - } - setSubGui(new SubGuiAuctionBid(this, listing, playerBalance)); - } - } - - private void buyout() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice) { - AuctionActionPacket.Buyout(listing.id); - refresh(); - } - } - } - - private void refresh() { - switch (currentTab) { - case 0: - AuctionActionPacket.QueryListings(currentFilter); - break; - case 1: - AuctionActionPacket.QueryMyListings(); - break; - case 2: - AuctionActionPacket.QueryMyBids(); - break; - case 3: - AuctionActionPacket.QueryClaimable(); - break; - } - } - - private void updateListingDisplay() { - if (listingScroll == null) return; - - List displayList = new ArrayList<>(); - for (AuctionListing listing : listings) { - String line = formatListingLine(listing); - displayList.add(line); - } - listingScroll.setList(displayList, true, false); - - if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { - listingScroll.selected = selectedListingIndex; - } else { - selectedListingIndex = -1; - listingScroll.selected = -1; - } - } - - private String formatListingLine(AuctionListing listing) { - String itemName = listing.item == null - ? "Unknown" - : listing.item.getDisplayName(); - if (itemName.length() > 15) { - itemName = itemName.substring(0, 12) + "..."; - } - - long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - String priceStr = formatCurrency(price); - - String timeStr = formatTimeRemaining(listing.getTimeRemaining()); - - return String.format("%s - %s (%s)", itemName, priceStr, timeStr); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return days + "d " + (hours % 24) + "h"; - } else if (hours > 0) { - return hours + "h " + (minutes % 60) + "m"; - } else if (minutes > 0) { - return minutes + "m " + (seconds % 60) + "s"; - } else { - return seconds + "s"; - } - } - - private String formatCurrency(long amount) { - if (amount >= 1000000) { - return String.format("%.1fM", amount / 1000000.0); - } else if (amount >= 1000) { - return String.format("%.1fK", amount / 1000.0); - } - return String.valueOf(amount); - } - - private String getPageLabel() { - if (totalPages == 0) { - return StatCollector.translateToLocal("auction.noResults"); - } - return String.format(StatCollector.translateToLocal("auction.page"), currentPage + 1, totalPages); - } - - private void updateButtonStates() { - ActionMode mode = getActionMode(); - AuctionListing listing = getSelectedListing(); - boolean hasSelection = listing != null; - - // Pagination buttons - if (getButton(40) != null) getButton(40).setEnabled(currentPage > 0); - if (getButton(42) != null) getButton(42).setEnabled(currentPage < totalPages - 1); - - // Action buttons based on selection - if (getButton(50) != null) getButton(50).setEnabled(hasSelection); - if (getButton(51) != null) { - if (!hasSelection) { - getButton(51).setEnabled(false); - } else if (mode == ActionMode.BID) { - getButton(51).setEnabled(canPlaceBid(listing)); - } else if (mode == ActionMode.CANCEL) { - getButton(51).setEnabled(canCancelListing(listing)); - } else { - getButton(51).setEnabled(getClaimAction(listing) != ClaimAction.NONE); - } - } - - if (getButton(52) != null) { - if (!hasSelection || mode != ActionMode.BID) { - getButton(52).setEnabled(false); - } else { - getButton(52).setEnabled(canBuyout(listing)); - } - } - } - - private void updateListingSlotPosition(boolean showInventory) { - if (!(inventorySlots instanceof ContainerAuction)) { - return; - } - - ContainerAuction container = (ContainerAuction) inventorySlots; - if (showInventory) { - container.listingSlot.xDisplayPosition = RIGHT_PANEL_X + 41; - container.listingSlot.yDisplayPosition = 64; - - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 9; col++) { - int slotIndex = 1 + col + row * 9; - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = INVENTORY_START_X + col * 18; - slot.yDisplayPosition = INVENTORY_START_Y + row * 18; - } - } - - for (int col = 0; col < 9; col++) { - int slotIndex = 28 + col; - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = INVENTORY_START_X + col * 18; - slot.yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; - } - } else { - container.listingSlot.xDisplayPosition = HIDDEN_SLOT_POSITION; - container.listingSlot.yDisplayPosition = HIDDEN_SLOT_POSITION; - - for (int slotIndex = 1; slotIndex < container.inventorySlots.size(); slotIndex++) { - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = HIDDEN_SLOT_POSITION; - slot.yDisplayPosition = HIDDEN_SLOT_POSITION; - } - } - } - - private void addCreateControls(int leftX, int rightX) { - addLabel(new GuiNpcLabel(100, StatCollector.translateToLocal("auction.createListing"), leftX, guiTop + 8, 0x404040)); - addLabel(new GuiNpcLabel(101, StatCollector.translateToLocal("auction.placeItemBelow"), rightX, guiTop + 32, 0x404040)); - - ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); - if (item != null) { - addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), - rightX, guiTop + 86, 0x008800)); - } else { - addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.noItemSelected"), rightX, guiTop + 86, 0x888888)); - } - - addLabel(new GuiNpcLabel(110, StatCollector.translateToLocal("auction.startingPrice"), leftX, guiTop + 36, 0x404040)); - startingPriceField = new GuiNpcTextField(111, this, leftX + 120, guiTop + 34, 90, 14, "100"); - startingPriceField.setIntegersOnly(); - startingPriceField.setMinMaxDefault(1, 999999999, 100); - addTextField(startingPriceField); - - addLabel(new GuiNpcLabel(120, StatCollector.translateToLocal("auction.buyoutPrice"), leftX, guiTop + 58, 0x404040)); - buyoutPriceField = new GuiNpcTextField(121, this, leftX + 120, guiTop + 56, 90, 14, "0"); - buyoutPriceField.setIntegersOnly(); - buyoutPriceField.setMinMaxDefault(0, 999999999, 0); - addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(122, StatCollector.translateToLocal("auction.noBuyoutHint"), leftX + 214, guiTop + 58, 0x888888)); - - addLabel(new GuiNpcLabel(130, StatCollector.translateToLocal("auction.duration"), leftX, guiTop + 80, 0x404040)); - addButton(new GuiNpcButton(70, leftX + 120, guiTop + 76, 110, 20, getDurationDisplay())); - - long fee = getListingFee(); - addLabel(new GuiNpcLabel(140, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - leftX, guiTop + 104, fee > 0 ? 0xAA0000 : 0x008800)); - - addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.create"))); - addButton(new GuiNpcButton(72, leftX + 100, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.cancel"))); - - if (getButton(71) != null) { - getButton(71).setEnabled(item != null); - } - } - - private String getDurationDisplay() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getDisplayName(); - } - - private long getListingFee() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getListingFee(); - } - - private void createListing() { - ContainerAuction container = (ContainerAuction) inventorySlots; - ItemStack item = container.getListingItem(); - if (item == null) { - return; - } - - long startingPrice = parsePrice(startingPriceField, 1); - long buyoutPrice = parsePrice(buyoutPriceField, 0); - - if (startingPrice < 1) { - startingPrice = 1; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - return; - } - - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); - container.clearListingSlot(); - switchTab(0); - } - - private void cancelListing() { - AuctionListing listing = getSelectedListing(); - if (!canCancelListing(listing)) { - return; - } - - AuctionActionPacket.CancelListing(listing.id); - refresh(); - } - - private void claimListing() { - AuctionListing listing = getSelectedListing(); - if (listing == null) { - return; - } - - ClaimAction action = getClaimAction(listing); - if (action == ClaimAction.ITEM) { - AuctionActionPacket.ClaimItem(listing.id); - } else if (action == ClaimAction.PROCEEDS) { - AuctionActionPacket.ClaimProceeds(listing.id); - } else { - return; - } - - refresh(); - } - - private ActionMode getActionMode() { - if (currentTab == 1) { - return ActionMode.CANCEL; - } - if (currentTab == 3) { - return ActionMode.CLAIM; - } - return ActionMode.BID; - } - - private String getPrimaryActionLabel(ActionMode mode) { - if (mode == ActionMode.CANCEL) { - return StatCollector.translateToLocal("auction.cancel"); - } - if (mode == ActionMode.CLAIM) { - return StatCollector.translateToLocal("auction.claim"); - } - return StatCollector.translateToLocal("auction.placeBid"); - } - - private void addActionButtons(int rightX, int actionY) { - ActionMode mode = getActionMode(); - addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.viewDetails"))); - addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, - getPrimaryActionLabel(mode))); - if (mode == ActionMode.BID) { - addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.buyout"))); - addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.refresh"))); - } else { - addButton(new GuiNpcButton(53, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.refresh"))); - } - } - - private AuctionListing getSelectedListing() { - if (selectedListingIndex < 0 || selectedListingIndex >= listings.size()) { - return null; - } - AuctionListing listing = listings.get(selectedListingIndex); - if (listing == null || listing.item == null) { - return null; - } - return listing; - } - - private boolean canPlaceBid(AuctionListing listing) { - if (listing == null || !listing.isActive()) { - return false; - } - if (isPlayerSeller(listing)) { - return false; - } - return playerBalance >= listing.getMinimumBid(); - } - - private boolean canBuyout(AuctionListing listing) { - if (listing == null || !listing.isActive()) { - return false; - } - if (listing.buyoutPrice <= 0) { - return false; - } - if (isPlayerSeller(listing)) { - return false; - } - return playerBalance >= listing.buyoutPrice; - } - - private boolean canCancelListing(AuctionListing listing) { - if (listing == null || listing.item == null) { - return false; - } - return listing.status == EnumAuctionStatus.ACTIVE && !listing.hasBids(); - } - - private boolean isPlayerSeller(AuctionListing listing) { - if (mc == null || mc.thePlayer == null || listing == null || listing.sellerUUID == null) { - return false; - } - UUID playerId = mc.thePlayer.getUniqueID(); - return listing.sellerUUID.equals(playerId); - } - - private enum ClaimAction { - ITEM, - PROCEEDS, - NONE - } - - private ClaimAction getClaimAction(AuctionListing listing) { - if (listing == null || mc == null || mc.thePlayer == null) { - return ClaimAction.NONE; - } - - UUID playerId = mc.thePlayer.getUniqueID(); - if (listing.sellerUUID != null && listing.sellerUUID.equals(playerId) && !listing.sellerClaimed) { - if (listing.status == EnumAuctionStatus.SOLD) { - return ClaimAction.PROCEEDS; - } - if (listing.status == EnumAuctionStatus.CANCELLED || listing.status == EnumAuctionStatus.EXPIRED) { - return ClaimAction.ITEM; - } - } - - if (listing.highBidderUUID != null && listing.highBidderUUID.equals(playerId) - && listing.status == EnumAuctionStatus.SOLD && !listing.buyerClaimed) { - return ClaimAction.ITEM; - } - - return ClaimAction.NONE; - } - - private long parsePrice(GuiNpcTextField field, long defaultValue) { - if (field == null) { - return defaultValue; - } - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - @Override - protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { - drawDefaultBackground(); - GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - - // Draw background - mc.renderEngine.bindTexture(BG_TEXTURE); - drawTexturedModalRect(guiLeft, guiTop, 0, 0, 252, ySize); - drawTexturedModalRect(guiLeft + 252, guiTop, 188, 0, xSize - 252, ySize); - - // Draw selected item preview - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - drawItemPreview(listing, guiLeft + RIGHT_PANEL_X + 41, guiTop + 192); - } - - super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); - } - - private void drawItemPreview(AuctionListing listing, int x, int y) { - if (listing == null || listing.item == null) return; - - RenderHelper.enableGUIStandardItemLighting(); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_COLOR_MATERIAL); - GL11.glEnable(GL11.GL_LIGHTING); - - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); - - RenderHelper.disableStandardItemLighting(); - } - - @Override - protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { - // Title - String titleText = getTabTitle(); - fontRendererObj.drawString(titleText, (xSize - fontRendererObj.getStringWidth(titleText)) / 2, -8, - CustomNpcResourceListener.DefaultTextColor); - - if (currentTab != 4) { - String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); - fontRendererObj.drawString(listingsLabel, LEFT_PANEL_X, 36, 0x666666); - } - } - - private String getTabTitle() { - switch (currentTab) { - case 0: return StatCollector.translateToLocal("auction.browseTitle"); - case 1: return StatCollector.translateToLocal("auction.myListingsTitle"); - case 2: return StatCollector.translateToLocal("auction.myBidsTitle"); - case 3: return StatCollector.translateToLocal("auction.claimsTitle"); - case 4: return StatCollector.translateToLocal("auction.sellTitle"); - default: return StatCollector.translateToLocal("auction.title"); - } - } - - // ICustomScrollListener - @Override - public void customScrollClicked(int mouseX, int mouseY, int mouseButton, GuiCustomScroll scroll) { - int index = scroll.selected; - selectedListingIndex = index; - updateButtonStates(); - } - - @Override - public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - viewDetails(); - } - } - - // IGuiData - @Override - public void setGuiData(NBTTagCompound compound) { - // This can be used for initial setup data - } - - // IAuctionData - @Override - public void onAuctionDataReceived(int dataType, NBTTagCompound data, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - this.playerBalance = playerBalance; - - // Parse listings from NBT - listings.clear(); - NBTTagList listingList = data.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - NBTTagCompound listingNBT = listingList.getCompoundTagAt(i); - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingNBT); - listings.add(listing); - } - - selectedListingIndex = -1; - updateListingDisplay(); - updateButtonStates(); - - // Update labels - if (getLabel(41) != null) { - getLabel(41).label = getPageLabel(); - } - if (getLabel(60) != null) { - getLabel(60).label = String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)); - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index d23b15398..f11bbb2e6 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -1,136 +1,335 @@ package noppes.npcs.client.gui.player; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.player.GetTraderData; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.util.GuiContainerNPCInterface; +import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.containers.ContainerNPCTrader; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; -public class GuiNPCTrader extends GuiContainerNPCInterface { - private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/trader.png"); +public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { + private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/tradersetup.png"); private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; private ContainerNPCTrader container; + // Layout constants matching ContainerNPCTrader + private static final int COLUMN_WIDTH = ContainerNPCTrader.COLUMN_WIDTH; + private static final int COLUMN_START_X = ContainerNPCTrader.COLUMN_START_X; + private static final int ROW_HEIGHT = ContainerNPCTrader.ROW_HEIGHT; + private static final int ROW_START_Y = ContainerNPCTrader.ROW_START_Y; + private static final int CURRENCY1_OFFSET = ContainerNPCTrader.CURRENCY1_OFFSET; + private static final int CURRENCY2_OFFSET = ContainerNPCTrader.CURRENCY2_OFFSET; + private static final int OUTPUT_OFFSET = ContainerNPCTrader.OUTPUT_OFFSET; + + // Data received from server via IGuiData + private long playerBalance = 0; + private int[] availableStock = new int[18]; + private long[] currencyCost = new long[18]; + private boolean stockEnabled = false; + + // Client-side countdown: target time when stock resets (System.currentTimeMillis()) + private long resetTargetTime = -1; + + // Current hover status for bottom display + private String statusMessage = ""; + private int statusColor = 0xFFFFFF; + public GuiNPCTrader(EntityNPCInterface npc, ContainerNPCTrader container) { super(npc, container); this.container = container; role = (RoleTrader) npc.roleInterface; closeOnEsc = true; - ySize = 224; - xSize = 223; - this.title = "role.trader"; + ySize = 216; + xSize = 256; + this.title = ""; + + // Initialize stock to unlimited + for (int i = 0; i < 18; i++) { + availableStock[i] = Integer.MAX_VALUE; + currencyCost[i] = 0; + } + + // Request trader data from server (balance, stock, currency costs) + PacketClient.sendClient(new GetTraderData()); } @Override - protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { + public void setGuiData(NBTTagCompound compound) { + playerBalance = compound.getLong("Balance"); + stockEnabled = compound.getBoolean("StockEnabled"); + + // Calculate target time for client-side countdown + long remainingMillis = compound.getLong("ResetTimeMillis"); + if (remainingMillis > 0) { + resetTargetTime = System.currentTimeMillis() + remainingMillis; + } else { + resetTargetTime = -1; + } + + int[] stock = compound.getIntArray("Stock"); + if (stock != null && stock.length == 18) { + availableStock = stock; + } + + for (int i = 0; i < 18; i++) { + currencyCost[i] = compound.getLong("Cost" + i); + } + } + + @Override + protected void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) { + // Reset status each frame + statusMessage = ""; + statusColor = 0xFFFFFF; + this.drawWorldBackground(0); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(resource); drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + + // Draw "Trader" label in TOP LEFT (above the GUI) + String traderLabel = StatCollector.translateToLocal("role.trader"); + fontRendererObj.drawString(traderLabel, guiLeft + 4, guiTop - 8, 0xFFFFFF); + + // Draw reset timer in TOP RIGHT (above the GUI) + if (stockEnabled && resetTargetTime > 0) { + String timerText = getResetCountdownText(); + int timerWidth = fontRendererObj.getStringWidth(timerText); + int timerColor = timerText.equals(StatCollector.translateToLocal("trader.reopentrader")) ? 0xFFAA00 : 0xFFFFFF; + fontRendererObj.drawString(timerText, guiLeft + xSize - timerWidth - 4, guiTop - 8, timerColor); + } + RenderHelper.enableGUIStandardItemLighting(); GL11.glEnable(GL12.GL_RESCALE_NORMAL); GL11.glEnable(GL11.GL_COLOR_MATERIAL); GL11.glEnable(GL11.GL_LIGHTING); - mc.renderEngine.bindTexture(slot); - for (int slot = 0; slot < 18; slot++) { - int x = guiLeft + slot % 3 * 72 + 10; - int y = guiTop + slot / 3 * 21 + 6; + // Draw trade slots (3 columns x 6 rows, 80px column width) + for (int slotIdx = 0; slotIdx < 18; slotIdx++) { + int col = slotIdx % 3; + int row = slotIdx / 3; + int x = guiLeft + COLUMN_START_X + col * COLUMN_WIDTH; + int y = guiTop + ROW_START_Y + row * ROW_HEIGHT; - ItemStack item = role.inventoryCurrency.items.get(slot); - ItemStack item2 = role.inventoryCurrency.items.get(slot + 18); + // Get item currencies + ItemStack item = role.inventoryCurrency.items.get(slotIdx); + ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18); if (item == null) { item = item2; item2 = null; } - if (NoppesUtilPlayer.compareItems(item, item2, false, false)) { + if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { item = item.copy(); item.stackSize += item2.stackSize; item2 = null; } - ItemStack sold = role.inventorySold.items.get(slot); + ItemStack sold = role.inventorySold.items.get(slotIdx); + + // Draw slot backgrounds (18x18 each) GL11.glColor4f(1, 1, 1, 1); mc.renderEngine.bindTexture(this.slot); - drawTexturedModalRect(x + 42, y, 0, 0, 18, 18); - if (item != null && sold != null) { + + // Currency slot 1 + drawTexturedModalRect(x + CURRENCY1_OFFSET, y, 0, 0, 18, 18); + // Currency slot 2 + drawTexturedModalRect(x + CURRENCY2_OFFSET, y, 0, 0, 18, 18); + // Output slot + drawTexturedModalRect(x + OUTPUT_OFFSET, y, 0, 0, 18, 18); + + // "=" sign between currency and output + fontRendererObj.drawString("=", x + 40, y + 5, CustomNpcResourceListener.DefaultTextColor); + + // Draw currency items (centered: +1 offset for 16x16 item in 18x18 slot) + if (sold != null) { RenderHelper.enableGUIStandardItemLighting(); + // Draw secondary currency (slot 1) if (item2 != null) { - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1); + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1); } - GL11.glColor4f(1, 1, 1, 1); - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1); + + // Draw primary currency (slot 2) + if (item != null) { + GL11.glColor4f(1, 1, 1, 1); + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1); + } + RenderHelper.disableStandardItemLighting(); - fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor); + // Draw stock count to the RIGHT of the trade (after output slot) + if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) { + String stockText = String.valueOf(availableStock[slotIdx]); + int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000; + fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor); + } + // Draw dark overlay for out of stock items + if (stockEnabled && availableStock[slotIdx] <= 0) { + drawGradientRect(x + OUTPUT_OFFSET + 1, y + 1, x + OUTPUT_OFFSET + 17, y + 17, 0xC0101010, 0xC0101010); + } } } + GL11.glEnable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_DEPTH_TEST); RenderHelper.enableStandardItemLighting(); - super.drawGuiContainerBackgroundLayer(f, i, j); + + super.drawGuiContainerBackgroundLayer(f, mouseX, mouseY); + + // Draw player balance (BOTTOM RIGHT, below GUI) + int infoY = guiTop + ySize + 2; + String balanceText = StatCollector.translateToLocal("trader.balance") + ": $" + formatCurrency(playerBalance); + int balanceWidth = fontRendererObj.getStringWidth(balanceText); + fontRendererObj.drawString(balanceText, guiLeft + xSize - balanceWidth - 4, infoY, 0x36e82a); } @Override - protected void drawGuiContainerForegroundLayer(int par1, int par2) { - for (int slot = 0; slot < 18; slot++) { - int x = slot % 3 * 72 + 10; - int y = slot / 3 * 21 + 6; + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + for (int slotIdx = 0; slotIdx < 18; slotIdx++) { + int col = slotIdx % 3; + int row = slotIdx / 3; + int x = COLUMN_START_X + col * COLUMN_WIDTH; + int y = ROW_START_Y + row * ROW_HEIGHT; - ItemStack item = role.inventoryCurrency.items.get(slot); - ItemStack item2 = role.inventoryCurrency.items.get(slot + 18); + ItemStack item = role.inventoryCurrency.items.get(slotIdx); + ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18); if (item == null) { item = item2; item2 = null; } - if (NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { + if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { item = item.copy(); item.stackSize += item2.stackSize; item2 = null; } - ItemStack sold = role.inventorySold.items.get(slot); - if (item == null || sold == null) + + ItemStack sold = role.inventorySold.items.get(slotIdx); + if (sold == null) { continue; - - if (this.func_146978_c(x + 43, y + 1, 16, 16, par1, par2)) { - if (!container.canBuy(slot, player)) { - GL11.glTranslatef(0, 0, 300); - if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT)) - this.drawGradientRect(x + 17, y, x + 35, y + 18, 0x70771010, 0x70771010); - if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT)) - this.drawGradientRect(x - 1, y, x + 17, y + 18, 0x70771010, 0x70771010); - - String title = StatCollector.translateToLocal("trader.insufficient"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xDD0000); - GL11.glTranslatef(0, 0, -300); - } else if (!container.isSlotEnabled(slot, player)) { - String title = StatCollector.translateToLocal("trader.slotdisabled"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xFF4000); - } else { - String title = StatCollector.translateToLocal("trader.sufficient"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0x00DD00); - } } - if (this.func_146978_c(x, y, 16, 16, par1, par2) && item2 != null) { - this.renderToolTip(item2, par1 - guiLeft, par2 - guiTop); + // Check if hovering over output slot (18x18 area) + if (this.func_146978_c(x + OUTPUT_OFFSET, y, 18, 18, mouseX, mouseY)) { + updateSlotStatus(slotIdx, item, item2, x, y); } - if (this.func_146978_c(x + 18, y, 16, 16, par1, par2)) { - this.renderToolTip(item, par1 - guiLeft, par2 - guiTop); + + // Tooltips for currency items + if (this.func_146978_c(x + CURRENCY1_OFFSET, y, 18, 18, mouseX, mouseY) && item2 != null) { + this.renderToolTip(item2, mouseX - guiLeft, mouseY - guiTop); + } + if (this.func_146978_c(x + CURRENCY2_OFFSET, y, 18, 18, mouseX, mouseY) && item != null) { + this.renderToolTip(item, mouseX - guiLeft, mouseY - guiTop); } } + + // Draw status message in BOTTOM LEFT (in foreground coordinates) + if (!statusMessage.isEmpty()) { + fontRendererObj.drawString(statusMessage, 4, ySize + 4, statusColor); + } + } + + /** + * Update status message when hovering over a trade slot + */ + private void updateSlotStatus(int slotIdx, ItemStack item, ItemStack item2, int x, int y) { + long cost = currencyCost[slotIdx]; + String costSuffix = cost > 0 ? " ($" + formatCurrency(cost) + ")" : ""; + + // Check conditions in order of priority + if (stockEnabled && availableStock[slotIdx] <= 0) { + // Out of stock + statusMessage = StatCollector.translateToLocal("trader.outofstock") + costSuffix; + statusColor = 0xDD0000; + } else if (cost > 0 && playerBalance < cost) { + // Can't afford currency cost + statusMessage = StatCollector.translateToLocal("trader.insufficient.currency") + costSuffix; + statusColor = 0xDD0000; + } else if (!container.canBuy(slotIdx, player)) { + // Missing item currency - highlight which ones + GL11.glTranslatef(0, 0, 300); + if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT)) { + this.drawGradientRect(x + CURRENCY2_OFFSET, y, x + CURRENCY2_OFFSET + 18, y + 18, 0x70771010, 0x70771010); + } + if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT)) { + this.drawGradientRect(x + CURRENCY1_OFFSET, y, x + CURRENCY1_OFFSET + 18, y + 18, 0x70771010, 0x70771010); + } + GL11.glTranslatef(0, 0, -300); + statusMessage = StatCollector.translateToLocal("trader.unavailable") + costSuffix; + statusColor = 0xDD0000; + } else if (!container.isSlotEnabled(slotIdx, player)) { + // Slot disabled + statusMessage = StatCollector.translateToLocal("trader.slotdisabled") + costSuffix; + statusColor = 0xFF4000; + } else { + // Can buy! + statusMessage = StatCollector.translateToLocal("trader.available") + costSuffix; + statusColor = 0x00DD00; + } + } + + /** + * Get the countdown text for stock reset timer. + * Calculated client-side each frame for smooth countdown. + */ + private String getResetCountdownText() { + if (resetTargetTime <= 0) { + return ""; + } + + long remaining = resetTargetTime - System.currentTimeMillis(); + if (remaining <= 0) { + return StatCollector.translateToLocal("trader.reopentrader"); + } + + // Format as HH:MM:SS or D days HH:MM:SS + long seconds = (remaining / 1000) % 60; + long minutes = (remaining / 60000) % 60; + long hours = (remaining / 3600000) % 24; + long days = remaining / 86400000; + + StringBuilder sb = new StringBuilder(); + sb.append(StatCollector.translateToLocal("trader.restock")).append(": "); + + if (days > 0) { + sb.append(days).append("d "); + } + sb.append(String.format("%02d:%02d:%02d", hours, minutes, seconds)); + + return sb.toString(); + } + + /** + * Format currency for display (e.g., 1000 -> "1,000") + */ + private String formatCurrency(long amount) { + if (amount < 1000) { + return String.valueOf(amount); + } + StringBuilder sb = new StringBuilder(); + String str = String.valueOf(amount); + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) { + sb.insert(0, ','); + } + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java deleted file mode 100644 index 99f1e00f1..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ /dev/null @@ -1,221 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; - -/** - * Sub GUI for placing a bid on an auction. - */ -public class SubGuiAuctionBid extends SubGuiInterface { - private AuctionListing listing; - private long playerBalance; - private GuiNpcTextField bidField; - private long lastUpdate = 0; - - public SubGuiAuctionBid(GuiAuction parent, AuctionListing listing, long playerBalance) { - this.listing = listing; - this.playerBalance = playerBalance; - setBackground("menubg.png"); - xSize = 240; - ySize = 165; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - int y = guiTop + 10; - int labelColor = 0x404040; - - if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); - addButton(new GuiNpcButton(31, guiLeft + 80, guiTop + 125, 80, 20, StatCollector.translateToLocal("gui.cancel"))); - return; - } - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 90, y, labelColor)); - y += 18; - - // Item name - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item") + " " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); - y += 14; - - // Current price - long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.currentPrice"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 120, y, 0x008800)); - y += 14; - - // Minimum bid - long minBid = listing.getMinimumBid(); - addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 120, y, 0x0066AA)); - y += 14; - - // Your balance - addLabel(new GuiNpcLabel(6, StatCollector.translateToLocal("auction.yourBalance"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 120, y, - playerBalance >= minBid ? 0x008800 : 0xAA0000)); - y += 14; - - // Time remaining - addLabel(new GuiNpcLabel(8, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, - getTimeColor(listing.getTimeRemaining()))); - y += 18; - - // Bid input - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); - bidField = new GuiNpcTextField(11, this, guiLeft + 100, y, 110, 16, String.valueOf(minBid)); - bidField.setIntegersOnly(); - int minBidInt = (int) Math.min(Integer.MAX_VALUE, Math.max(1L, minBid)); - bidField.setMinMaxDefault(minBidInt, Integer.MAX_VALUE, minBidInt); - addTextField(bidField); - y += 24; - - // Quick bid buttons - addButton(new GuiNpcButton(20, guiLeft + 15, y, 60, 16, StatCollector.translateToLocal("auction.minBid"))); - addButton(new GuiNpcButton(21, guiLeft + 79, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus10"))); - addButton(new GuiNpcButton(22, guiLeft + 143, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus25"))); - y += 22; - - // Place bid button - GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 35, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); - bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); - addButton(bidButton); - - // Cancel button - addButton(new GuiNpcButton(31, guiLeft + 125, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); - - // Error message area - if (playerBalance < minBid) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); - } else if (!listing.isActive()) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); - } - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - - if (hours > 0) { - return String.format("%dh %dm", hours, minutes % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - private int getTimeColor(long millis) { - if (millis <= 0) return 0xAA0000; - if (millis < 5 * 60 * 1000) return 0xFF0000; - if (millis < 30 * 60 * 1000) return 0xFF6600; - return 0x008800; - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update time remaining every second - long now = System.currentTimeMillis(); - if (now - lastUpdate >= 1000) { - lastUpdate = now; - - GuiNpcLabel timeLabel = getLabel(9); - if (timeLabel != null) { - long remaining = listing.getTimeRemaining(); - timeLabel.label = formatTimeRemaining(remaining); - timeLabel.color = getTimeColor(remaining); - } - - // Disable bid button if auction ended - GuiNpcButton bidButton = getButton(30); - if (bidButton != null && !listing.isActive()) { - bidButton.setEnabled(false); - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - int id = button.id; - - long minBid = listing.getMinimumBid(); - - if (id == 20) { - // Min bid - bidField.setText(String.valueOf(minBid)); - } else if (id == 21) { - // +10% - long newBid = (long) (minBid * 1.10); - if (newBid <= playerBalance) { - bidField.setText(String.valueOf(newBid)); - } - } else if (id == 22) { - // +25% - long newBid = (long) (minBid * 1.25); - if (newBid <= playerBalance) { - bidField.setText(String.valueOf(newBid)); - } - } else if (id == 30) { - // Place bid - placeBid(); - } else if (id == 31) { - // Cancel - close(); - } - } - - private void placeBid() { - long bidAmount = parseBid(bidField); - long minBid = listing.getMinimumBid(); - - if (bidAmount < minBid) { - return; - } - - if (bidAmount > playerBalance) { - return; - } - - if (!listing.isActive()) { - return; - } - - // Send bid to server - AuctionActionPacket.PlaceBid(listing.id, bidAmount); - close(); - } - - private long parseBid(GuiNpcTextField field) { - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return 0L; - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java deleted file mode 100644 index 01c650bbd..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ /dev/null @@ -1,178 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.item.ItemStack; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.containers.ContainerAuction; - -/** - * Sub GUI for creating a new auction listing. - */ -public class SubGuiAuctionCreate extends SubGuiInterface { - private ContainerAuction container; - - private GuiNpcTextField startingPriceField; - private GuiNpcTextField buyoutPriceField; - private int selectedDuration = 1; // Default to MEDIUM - - public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { - this.container = container; - setBackground("menubg.png"); - xSize = 260; - ySize = 190; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 80, guiTop + 8, 0x404040)); - - // Item slot instructions - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.placeItemBelow"), guiLeft + 20, guiTop + 28, 0x404040)); - - // Item info (shows when item is in slot) - ItemStack item = container.getListingItem(); - if (item != null) { - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); - } else { - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 20, guiTop + 50, 0x888888)); - } - - // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 74, 0x404040)); - startingPriceField = new GuiNpcTextField(11, this, guiLeft + 120, guiTop + 72, 90, 14, "100"); - startingPriceField.setIntegersOnly(); - startingPriceField.setMinMaxDefault(1, 999999999, 100); - addTextField(startingPriceField); - - // Buyout price (optional) - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 96, 0x404040)); - buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 120, guiTop + 94, 90, 14, "0"); - buyoutPriceField.setIntegersOnly(); - buyoutPriceField.setMinMaxDefault(0, 999999999, 0); - addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 215, guiTop + 96, 0x888888)); - - // Duration selection - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 118, 0x404040)); - addButton(new GuiNpcButton(31, guiLeft + 120, guiTop + 114, 110, 20, getDurationDisplay())); - - // Fee display - long fee = getListingFee(); - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - guiLeft + 20, guiTop + 142, fee > 0 ? 0xAA0000 : 0x008800)); - - // Create button - addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.create"))); - getButton(50).setEnabled(item != null); - - // Cancel button - addButton(new GuiNpcButton(51, guiLeft + 140, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.cancel"))); - } - - private String getDurationDisplay() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getDisplayName(); - } - - private long getListingFee() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getListingFee(); - } - - @Override - public void actionPerformed(GuiButton button) { - int id = button.id; - - if (id == 31) { - // Cycle duration - selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; - initGui(); - } else if (id == 50) { - // Create listing - createListing(); - } else if (id == 51) { - // Cancel - close(); - } - } - - private void createListing() { - ItemStack item = container.getListingItem(); - if (item == null) { - return; - } - - long startingPrice = parsePrice(startingPriceField, 1); - long buyoutPrice = parsePrice(buyoutPriceField, 0); - - if (startingPrice < 1) { - startingPrice = 1; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - // Show error - buyout must be higher than starting - return; - } - - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - - // Send to server - AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); - - // Clear slot and close - container.clearListingSlot(); - close(); - } - - private long parsePrice(GuiNpcTextField field, long defaultValue) { - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update item display if it changed - ItemStack item = container.getListingItem(); - GuiNpcLabel itemLabel = getLabel(2); - GuiNpcButton createButton = getButton(50); - - if (item != null) { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); - itemLabel.color = 0x008800; - } - if (createButton != null) { - createButton.setEnabled(true); - } - } else { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); - itemLabel.color = 0x888888; - } - if (createButton != null) { - createButton.setEnabled(false); - } - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java deleted file mode 100644 index e4131fe94..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ /dev/null @@ -1,200 +0,0 @@ -package noppes.npcs.client.gui.player; - -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; - -/** - * Sub GUI for viewing auction listing details. - */ -public class SubGuiAuctionDetails extends SubGuiInterface { - private AuctionListing listing; - private long lastUpdate = 0; - - public SubGuiAuctionDetails(GuiAuction parent, AuctionListing listing) { - this.listing = listing; - setBackground("menubg.png"); - xSize = 280; - ySize = 210; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - int y = guiTop + 8; - int labelColor = 0x404040; - int valueColor = 0x000000; - - if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 80, y, 0xAA0000)); - addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); - return; - } - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 100, y, labelColor)); - y += 16; - - // Item name - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 100, y, valueColor)); - y += 14; - - // Quantity - addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("auction.quantity"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 100, y, valueColor)); - y += 14; - - // Seller - addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("auction.seller"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 100, y, valueColor)); - y += 14; - - // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 120, y, valueColor)); - y += 14; - - // Current bid - if (listing.currentBid > 0) { - addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 120, y, 0x008800)); - y += 14; - - // High bidder - addLabel(new GuiNpcLabel(14, StatCollector.translateToLocal("auction.highBidder"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 120, y, valueColor)); - y += 14; - - // Bid count - addLabel(new GuiNpcLabel(16, StatCollector.translateToLocal("auction.totalBids"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 120, y, valueColor)); - y += 14; - } else { - addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 120, y, 0x888888)); - y += 14; - } - - // Buyout price - if (listing.buyoutPrice > 0) { - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 120, y, 0xAA6600)); - y += 14; - } - - // Minimum bid - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 120, y, 0x0066AA)); - y += 14; - - // Time remaining (will be updated) - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, - getTimeColor(listing.getTimeRemaining()))); - y += 20; - - // Close button - addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return String.format("%dd %dh %dm", days, hours % 24, minutes % 60); - } else if (hours > 0) { - return String.format("%dh %dm %ds", hours, minutes % 60, seconds % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - private int getTimeColor(long millis) { - if (millis <= 0) return 0xAA0000; - if (millis < 5 * 60 * 1000) return 0xFF0000; // < 5 minutes - red - if (millis < 30 * 60 * 1000) return 0xFF6600; // < 30 minutes - orange - if (millis < 60 * 60 * 1000) return 0xAAAA00; // < 1 hour - yellow - return 0x008800; // green - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update time remaining every second - long now = System.currentTimeMillis(); - if (now - lastUpdate >= 1000) { - lastUpdate = now; - - GuiNpcLabel timeLabel = getLabel(31); - if (timeLabel != null) { - long remaining = listing.getTimeRemaining(); - timeLabel.label = formatTimeRemaining(remaining); - timeLabel.color = getTimeColor(remaining); - } - } - } - - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - super.drawScreen(mouseX, mouseY, partialTicks); - - // Draw item icon - if (listing.item != null) { - int itemX = guiLeft + 230; - int itemY = guiTop + 32; - - RenderHelper.enableGUIStandardItemLighting(); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_COLOR_MATERIAL); - GL11.glEnable(GL11.GL_LIGHTING); - GL11.glScalef(2.0f, 2.0f, 2.0f); - - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, - listing.item, itemX / 2, itemY / 2); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, - listing.item, itemX / 2, itemY / 2); - - GL11.glScalef(0.5f, 0.5f, 0.5f); - RenderHelper.disableStandardItemLighting(); - - // Tooltip on hover - if (mouseX >= itemX && mouseX < itemX + 32 && mouseY >= itemY && mouseY < itemY + 32) { - renderToolTip(listing.item, mouseX, mouseY); - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - if (button.id == 50) { - close(); - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java deleted file mode 100644 index d5fb8e962..000000000 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java +++ /dev/null @@ -1,144 +0,0 @@ -package noppes.npcs.client.gui.roles; - -import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.role.RoleSavePacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.client.gui.util.GuiNPCInterface2; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.roles.RoleAuctioneer; - -public class GuiNpcAuctioneer extends GuiNPCInterface2 implements ITextfieldListener { - - private final RoleAuctioneer role; - - public GuiNpcAuctioneer(EntityNPCInterface npc) { - super(npc); - this.role = npc.roleInterface instanceof RoleAuctioneer ? (RoleAuctioneer) npc.roleInterface : null; - } - - @Override - public void initGui() { - super.initGui(); - if (role == null) { - close(); - return; - } - - int y = guiTop + 10; - - // Left column - Tab visibility - addLabel(new GuiNpcLabel(0, "role.auctioneer.tabs", guiLeft + 10, y, 0x404040)); - y += 18; - - addLabel(new GuiNpcLabel(1, "role.auctioneer.showBrowse", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(1, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showActiveListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(2, "role.auctioneer.showMyListings", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(2, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(3, "role.auctioneer.showMyBids", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(3, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyBids ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(4, "role.auctioneer.showClaims", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(4, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showClaims ? 1 : 0)); - y += 30; - - // Actions - addLabel(new GuiNpcLabel(10, "role.auctioneer.actions", guiLeft + 10, y, 0x404040)); - y += 18; - - addLabel(new GuiNpcLabel(11, "role.auctioneer.allowCreate", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(11, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowCreatingListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(12, "role.auctioneer.allowBid", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(12, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBidding ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(13, "role.auctioneer.allowBuyout", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(13, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBuyout ? 1 : 0)); - - // Right column - Messages - int rightX = guiLeft + 210; - int rightY = guiTop + 10; - - addLabel(new GuiNpcLabel(20, "role.auctioneer.messages", rightX, rightY, 0x404040)); - rightY += 18; - - addLabel(new GuiNpcLabel(21, "role.auctioneer.welcome", rightX, rightY + 5, 0x404040)); - rightY += 18; - GuiNpcTextField welcomeField = new GuiNpcTextField(21, this, rightX, rightY, 180, 20, role.welcomeMessage); - welcomeField.setMaxStringLength(200); - addTextField(welcomeField); - rightY += 28; - - addLabel(new GuiNpcLabel(22, "role.auctioneer.noAuction", rightX, rightY + 5, 0x404040)); - rightY += 18; - GuiNpcTextField noAuctionField = new GuiNpcTextField(22, this, rightX, rightY, 180, 20, role.noAuctionMessage); - noAuctionField.setMaxStringLength(200); - addTextField(noAuctionField); - } - - @Override - public void buttonEvent(GuiButton guibutton) { - if (role == null) return; - - if (guibutton instanceof GuiNpcButton) { - GuiNpcButton button = (GuiNpcButton) guibutton; - boolean value = button.getValue() == 1; - - switch (button.id) { - case 1: - role.showActiveListings = value; - break; - case 2: - role.showMyListings = value; - break; - case 3: - role.showMyBids = value; - break; - case 4: - role.showClaims = value; - break; - case 11: - role.allowCreatingListings = value; - break; - case 12: - role.allowBidding = value; - break; - case 13: - role.allowBuyout = value; - break; - } - } - } - - @Override - public void unFocused(GuiNpcTextField textfield) { - if (role == null) return; - - switch (textfield.id) { - case 21: - role.welcomeMessage = textfield.getText(); - break; - case 22: - role.noAuctionMessage = textfield.getText(); - break; - } - } - - @Override - public void save() { - if (role != null) { - PacketClient.sendClient(new RoleSavePacket(role.writeToNBT(new NBTTagCompound()))); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 43c259da7..6c4745d42 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -8,7 +8,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; -import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.ITextfieldListener; @@ -22,6 +22,9 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; + // Currency cost textfield IDs start at 100 (100-117 for slots 0-17) + private static final int CURRENCY_FIELD_ID_START = 100; + public GuiNpcTraderSetup(EntityNPCInterface npc, ContainerNPCTraderSetup container) { super(npc, container); ySize = 220; @@ -34,17 +37,30 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex super.initGui(); buttonList.clear(); setBackground("tradersetup.png"); + + // Linked Market name addLabel(new GuiNpcLabel(0, "role.marketname", guiLeft + 214, guiTop + 150)); addTextField(new GuiNpcTextField(0, this, guiLeft + 214, guiTop + 160, 180, 20, role.marketName)); - addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 260, guiTop + 29)); - addButton(new GuiNpcButtonYesNo(1, guiLeft + 340, guiTop + 24, role.ignoreDamage)); + // Settings button (opens SubGuiNpcTraderSettings) + addButton(new GuiNpcButton(1, guiLeft + 214, guiTop + 184, 88, 20, "gui.settings")); + getButton(1).setHoverText("gui.settings.hover"); - addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 260, guiTop + 51)); - addButton(new GuiNpcButtonYesNo(2, guiLeft + 340, guiTop + 46, role.ignoreNBT)); + // Stock Options button (opens SubGuiNpcTraderStock) + addButton(new GuiNpcButton(2, guiLeft + 306, guiTop + 184, 88, 20, "stock.options")); + getButton(2).setHoverText("stock.options.hover"); - addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 260, guiTop + 73)); - addButton(new GuiNpcButtonYesNo(3, guiLeft + 340, guiTop + 68, role.recordHistory)); + // Currency cost textfields for each slot (FIRST - before item slots) + // Column width: 130px for better spacing + for (int i = 0; i < 18; i++) { + int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label + int y = guiTop + i / 3 * 22 + 14; + long cost = role.getCurrencyCost(i); + GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : ""); + field.setMaxStringLength(8); + field.setIntegersOnly(); + addTextField(field); + } } @Override @@ -57,13 +73,12 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void actionPerformed(GuiButton guibutton) { if (guibutton.id == 1) { - role.ignoreDamage = ((GuiNpcButtonYesNo) guibutton).getBoolean(); + // Open Settings SubGui + setSubGui(new SubGuiNpcTraderSettings(role)); } if (guibutton.id == 2) { - role.ignoreNBT = ((GuiNpcButtonYesNo) guibutton).getBoolean(); - } - if (guibutton.id == 3) { - role.recordHistory = ((GuiNpcButtonYesNo) guibutton).getBoolean(); + // Open Stock Options SubGui + setSubGui(new SubGuiNpcTraderStock(role)); } } @@ -71,19 +86,31 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { super.drawGuiContainerBackgroundLayer(f, i, j); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + // Column width: 130px for better spacing for (int slot = 0; slot < 18; slot++) { - int x = guiLeft + slot % 3 * 94 + 7; + int x = guiLeft + slot % 3 * 130 + 7; int y = guiTop + slot / 3 * 22 + 4; - mc.renderEngine.bindTexture(this.slot); - GL11.glColor4f(1, 1, 1, 1); - drawTexturedModalRect(x - 1, y, 0, 0, 18, 18); - drawTexturedModalRect(x + 17, y, 0, 0, 18, 18); + // "$" label for currency cost + fontRendererObj.drawString("$", x, y + 5, CustomNpcResourceListener.DefaultTextColor); - fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor); + // Currency textfield is at x + 8 (drawn by textfield system) + + // Item Cost 1 slot mc.renderEngine.bindTexture(this.slot); GL11.glColor4f(1, 1, 1, 1); - drawTexturedModalRect(x + 42, y, 0, 0, 18, 18); + drawTexturedModalRect(x + 50, y, 0, 0, 18, 18); + + // Item Cost 2 slot + drawTexturedModalRect(x + 68, y, 0, 0, 18, 18); + + // "=" sign + fontRendererObj.drawString("=", x + 88, y + 5, CustomNpcResourceListener.DefaultTextColor); + + // Output slot + mc.renderEngine.bindTexture(this.slot); + GL11.glColor4f(1, 1, 1, 1); + drawTexturedModalRect(x + 104, y, 0, 0, 18, 18); } } @@ -95,10 +122,23 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void unFocused(GuiNpcTextField guiNpcTextField) { - String name = guiNpcTextField.getText(); - if (!name.equalsIgnoreCase(role.marketName)) { - role.marketName = name; - PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); + int id = guiNpcTextField.id; + + // Handle market name field + if (id == 0) { + String name = guiNpcTextField.getText(); + if (!name.equalsIgnoreCase(role.marketName)) { + role.marketName = name; + PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); + } + return; + } + + // Handle currency cost fields (IDs 100-117) + if (id >= CURRENCY_FIELD_ID_START && id < CURRENCY_FIELD_ID_START + 18) { + int slot = id - CURRENCY_FIELD_ID_START; + long cost = guiNpcTextField.getInteger(); + role.setCurrencyCost(slot, cost); } } } diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java new file mode 100644 index 000000000..4c72e2059 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java @@ -0,0 +1,52 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Confirmation dialog for resetting trader stock cooldown. + */ +public class SubGuiNpcTraderCooldownReset extends SubGuiInterface { + + private final RoleTrader role; + public boolean confirmed = false; + + public SubGuiNpcTraderCooldownReset(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 200; + ySize = 100; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 15; + + addLabel(new GuiNpcLabel(0, "stock.cooldown.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "stock.cooldown.warning", guiLeft + 10, y)); + + y += 35; + + addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes")); + addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + // Reset the cooldown + role.resetCooldown(); + confirmed = true; + close(); + } else if (guibutton.id == 1) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java new file mode 100644 index 000000000..d9e2a0b40 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java @@ -0,0 +1,66 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +public class SubGuiNpcTraderSettings extends SubGuiInterface { + + private final RoleTrader role; + + public SubGuiNpcTraderSettings(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 176; + ySize = 136; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 14; + + addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(1, guiLeft + 120, y, role.ignoreDamage)); + getButton(1).setHoverText("gui.ignoreDamage.hover"); + + y += 28; + + addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(2, guiLeft + 120, y, role.ignoreNBT)); + getButton(2).setHoverText("gui.ignoreNBT.hover"); + + y += 28; + + addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(3, guiLeft + 120, y, role.recordHistory)); + getButton(3).setHoverText("gui.recordHistory.hover"); + + y += 34; + + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, y, 60, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case 0: + close(); + break; + case 1: + role.ignoreDamage = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 2: + role.ignoreNBT = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 3: + role.recordHistory = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java new file mode 100644 index 000000000..2e369b7e6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java @@ -0,0 +1,159 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.client.gui.util.SubGuiNpcCooldownPicker; +import noppes.npcs.constants.EnumStockReset; +import noppes.npcs.roles.RoleTrader; + +public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiListener, ITextfieldListener { + + private final RoleTrader role; + + public SubGuiNpcTraderStock(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 220; + ySize = 220; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + + // Enable Stock + addLabel(new GuiNpcLabel(1, "stock.enable", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(1, guiLeft + 140, y, role.stock.enableStock)); + getButton(1).setHoverText("stock.enable.hover"); + + y += 24; + + // Per-Player Stock + addLabel(new GuiNpcLabel(2, "stock.perplayer", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(2, guiLeft + 140, y, role.stock.perPlayer)); + getButton(2).setHoverText("stock.perplayer.hover"); + + y += 24; + + // Reset Type + addLabel(new GuiNpcLabel(3, "stock.resettype", guiLeft + 10, y + 5)); + addButton(new GuiNpcButton(3, guiLeft + 90, y, 120, 20, EnumStockReset.getDisplayNames(), role.stock.resetType.ordinal())); + getButton(3).setHoverText("stock.resettype.hover"); + + y += 24; + + // Custom Time button (only visible for custom types) + EnumStockReset resetType = role.stock.resetType; + if (resetType == EnumStockReset.MCCUSTOM || resetType == EnumStockReset.RLCUSTOM) { + addLabel(new GuiNpcLabel(4, "stock.customtime", guiLeft + 10, y + 5)); + addButton(new GuiNpcButton(4, guiLeft + 90, y, 120, 20, "gui.edit")); + getButton(4).setHoverText("stock.customtime.hover"); + y += 24; + } + + // Default Max Stock (applies to all slots as default) + addLabel(new GuiNpcLabel(5, "stock.defaultmax", guiLeft + 10, y + 5)); + GuiNpcTextField defaultStock = new GuiNpcTextField(5, this, fontRendererObj, guiLeft + 140, y, 60, 20, getDefaultStockString()); + defaultStock.setIntegersOnly(); + defaultStock.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); + addTextField(defaultStock); + + y += 26; + + // Per-Slot Stock button + addButton(new GuiNpcButton(5, guiLeft + 10, y, 95, 20, "stock.perslot")); + getButton(5).setHoverText("stock.perslot.hover"); + + // Reset Stock button + addButton(new GuiNpcButton(6, guiLeft + 115, y, 95, 20, "stock.reset")); + getButton(6).setHoverText("stock.reset.hover"); + + y += 24; + + // Reset Cooldown button + addButton(new GuiNpcButton(7, guiLeft + 10, y, 200, 20, "stock.resetcooldown")); + getButton(7).setHoverText("stock.resetcooldown.hover"); + + // Done button + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 40, 60, 20, "gui.done")); + } + + private String getDefaultStockString() { + // Check if all stocks are the same, if so return that value + int first = role.stock.maxStock[0]; + for (int i = 1; i < 18; i++) { + if (role.stock.maxStock[i] != first) { + return "-1"; // Mixed values, return unlimited + } + } + return String.valueOf(first); + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case 0: // Done + close(); + break; + case 1: // Enable Stock + role.stock.enableStock = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 2: // Per-Player + role.stock.perPlayer = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 3: // Reset Type + int value = ((GuiNpcButton) button).getValue(); + role.stock.resetType = EnumStockReset.values()[value]; + initGui(); // Refresh to show/hide custom time button + break; + case 4: // Custom Time + boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM; + setSubGui(new SubGuiNpcCooldownPicker(isMC, role.stock.customResetTime)); + break; + case 5: // Per-Slot Stock + setSubGui(new SubGuiNpcTraderStockSlots(role)); + break; + case 6: // Reset Stock + setSubGui(new SubGuiNpcTraderStockReset(role)); + break; + case 7: // Reset Cooldown + setSubGui(new SubGuiNpcTraderCooldownReset(role)); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subGui) { + if (subGui instanceof SubGuiNpcCooldownPicker) { + long value = ((SubGuiNpcCooldownPicker) subGui).cooldownValue; + // Validate minimum: 1 tick for MC time, 1000ms (1 second) for real time + boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM; + long minimum = isMC ? 1 : 1000; + role.stock.customResetTime = Math.max(minimum, value); + } + // Refresh after per-slot editing + if (subGui instanceof SubGuiNpcTraderStockSlots) { + initGui(); + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + if (textField.id == 5) { + int value = textField.getInteger(); + // Apply to all slots + for (int i = 0; i < 18; i++) { + role.stock.setMaxStock(i, value); + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java new file mode 100644 index 000000000..027108f76 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java @@ -0,0 +1,55 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Confirmation dialog for resetting trader stock. + */ +public class SubGuiNpcTraderStockReset extends SubGuiInterface { + + private final RoleTrader role; + public boolean confirmed = false; + + public SubGuiNpcTraderStockReset(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 200; + ySize = 100; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 15; + + addLabel(new GuiNpcLabel(0, "stock.reset.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "stock.reset.warning", guiLeft + 10, y)); + + y += 35; + + addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes")); + addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + // Reset the stock + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (role.npc != null && role.npc.worldObj != null ? role.npc.worldObj.getTotalWorldTime() : 0); + role.stock.resetStock(currentTime); + confirmed = true; + close(); + } else if (guibutton.id == 1) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java new file mode 100644 index 000000000..b4a76ba48 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java @@ -0,0 +1,76 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Per-slot stock management dialog. + * Shows all 18 slots with their max stock values. + */ +public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextfieldListener { + + private final RoleTrader role; + + public SubGuiNpcTraderStockSlots(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 256; + ySize = 216; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + // Title + addLabel(new GuiNpcLabel(0, "stock.perslot.title", guiLeft + 10, guiTop + 6)); + + // 18 slots in 3 columns x 6 rows + int startY = guiTop + 22; + int colWidth = 80; + + for (int i = 0; i < 18; i++) { + int col = i % 3; + int row = i / 3; + + int x = guiLeft + 10 + col * colWidth; + int y = startY + row * 28; + + // Slot label + addLabel(new GuiNpcLabel(i + 1, "Slot " + (i + 1) + ":", x, y + 5)); + + // Max stock textfield + int maxStock = role.stock.maxStock[i]; + String value = maxStock < 0 ? "-1" : String.valueOf(maxStock); + GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value); + field.setIntegersOnly(); + field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); + addTextField(field); + } + + // Done button + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 26, 60, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton button) { + if (button.id == 0) { + close(); + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + int slot = textField.id; + if (slot >= 0 && slot < 18) { + int value = textField.getInteger(); + role.stock.setMaxStock(slot, value); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java index b4049bc8a..54ca9da5d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java @@ -224,6 +224,15 @@ public abstract class GuiContainerNPCInterface extends GuiContainer { RenderHelper.disableStandardItemLighting(); subgui.drawScreen(i, j, f); } + + // Draw hover text for buttons + boolean subGui = hasSubGui(); + for (GuiCustomScroll scroll : new ArrayList(scrolls.values())) { + scroll.drawHover(i, j); + } + for (GuiNpcButton button : buttons.values()) { + button.drawHover(i, j, subGui); + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java index 3455e4986..5e97ec19a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java @@ -61,9 +61,9 @@ public abstract class GuiContainerNPCInterface2 extends GuiContainerNPCInterface GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(defaultBackground2); if (this instanceof GuiNpcTraderSetup) - drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 256, 220); + drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 210, 220); else - drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 256, 220); + drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 230, 220); menu.drawElements(fontRendererObj, i, j, mc, f); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 1d453a395..7e7d2acd8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -401,10 +401,23 @@ public abstract class GuiNPCInterface extends GuiScreen { GL11.glScalef(bgScale * bgScaleX, bgScale * bgScaleY, bgScale * bgScaleZ); mc.renderEngine.bindTexture(background); if (xSize > 256) { + // GUI wider than texture: draw left portion, then right portion from texture edge drawTexturedModalRect(0, 0, 0, 0, 250, ySize); drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, ySize); - } else + } else if (xSize < 256) { + // GUI narrower than texture: stitch left and right edges together + // Left portion (first half of GUI) + int leftWidth = xSize / 2; + // Right portion width (remaining) + int rightWidth = xSize - leftWidth; + // Draw left side from texture start + drawTexturedModalRect(0, 0, 0, 0, leftWidth, ySize); + // Draw right side from texture end (256 - rightWidth) + drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, ySize); + } else { + // GUI exactly 256px: draw full texture drawTexturedModalRect(0, 0, 0, 0, xSize, ySize); + } GL11.glPopMatrix(); } diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index f0f019290..c84ec3604 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -2,7 +2,6 @@ package noppes.npcs.config; import cpw.mods.fml.common.FMLLog; import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.common.config.Property; import org.apache.logging.log4j.Level; import java.io.File; @@ -14,7 +13,6 @@ public class ConfigMarket { public final static String MARKET = "Market"; public final static String CURRENCY = "Market.Currency"; - public final static String AUCTION = "Market.Auction"; public final static String TRADER = "Market.Trader"; // ========================================= @@ -25,37 +23,10 @@ public class ConfigMarket { // ========================================= // Currency Settings // ========================================= - public static boolean UseVaultIfAvailable = true; - public static String CurrencyName = "Gold"; - public static String FallbackCurrencyItem = "customnpcs:npcCoinGold"; - public static long FallbackItemRatio = 1; - public static long StartingBalance = 1000; - public static long MaxBalance = 999999999999L; - - // ========================================= - // Auction Settings - // ========================================= - public static boolean AuctionEnabled = true; - public static int DefaultMaxListings = 5; - public static double ListingFeePercent = 0.0; - public static double SalesTaxPercent = 0.05; - public static boolean RequireAuctioneerNPC = true; - public static int ClaimExpirationDays = 30; - public static double MinBidIncrementPercent = 0.05; - public static int SnipeProtectionMinutes = 5; - public static int SnipeProtectionThreshold = 5; - - // Auction Duration Settings (in hours) - public static int AuctionDurationShort = 2; - public static int AuctionDurationMedium = 12; - public static int AuctionDurationLong = 24; - public static int AuctionDurationVeryLong = 48; - - // Auction Duration Fees (flat currency amount) - public static long AuctionFeeShort = 0; - public static long AuctionFeeMedium = 0; - public static long AuctionFeeLong = 0; - public static long AuctionFeeVeryLong = 0; + public static boolean UseVault = false; + public static String CurrencyName = "Coins"; + public static long StartingBalance = 0; + public static long MaxBalance = Long.MAX_VALUE; // ========================================= // Trader Settings @@ -78,91 +49,19 @@ public class ConfigMarket { // ========================================= // Currency Settings // ========================================= - config.setCategoryComment(CURRENCY, "Currency system settings for the virtual economy"); + config.setCategoryComment(CURRENCY, "CNPC+ Currency system settings. When UseVault is enabled, all currency operations use Vault instead of the built-in system."); - UseVaultIfAvailable = config.get(CURRENCY, "Use Vault If Available", true, - "Use VaultAPI if available on Bukkit hybrid servers").getBoolean(true); + UseVault = config.get(CURRENCY, "Use Vault", false, + "If true, use Vault API for all currency operations instead of CNPC+ built-in currency. The built-in currency data will be preserved but unused.").getBoolean(false); - CurrencyName = config.get(CURRENCY, "Currency Name", "Gold", - "Display name for the currency").getString(); + CurrencyName = config.get(CURRENCY, "Currency Name", "Coins", + "Display name for the currency (e.g. 'Coins', 'Gold', 'Credits')").getString(); - FallbackCurrencyItem = config.get(CURRENCY, "Fallback Currency Item", "customnpcs:npcCoinGold", - "Item registry name to use as currency when Vault is unavailable. Leave empty to disable.").getString(); + StartingBalance = config.get(CURRENCY, "Starting Balance", 0, + "Starting currency balance for new players").getInt(0); - FallbackItemRatio = config.get(CURRENCY, "Fallback Item Ratio", 1, - "Conversion ratio: 1 fallback item = X virtual currency").getInt(1); - - StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, - "Starting balance for new players (only used when Vault is not available)").getInt(1000); - - Property maxBalanceProp = config.get(CURRENCY, "Maximum Balance", String.valueOf(MaxBalance), - "Maximum balance a player can have (only used when Vault is not available)"); - try { - MaxBalance = Long.parseLong(maxBalanceProp.getString()); - } catch (NumberFormatException e) { - MaxBalance = 999999999999L; - } - if (MaxBalance < 0) { - MaxBalance = 0; - } - - // ========================================= - // Auction Settings - // ========================================= - config.setCategoryComment(AUCTION, "Auction house settings"); - - AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, - "Enable/disable the auction house system").getBoolean(true); - - DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, - "Default maximum auction listings per player (without permission override)").getInt(5); - - ListingFeePercent = config.get(AUCTION, "Listing Fee Percent", 0.0, - "Listing fee as percentage of starting price (0.0 = no fee)").getDouble(0.0); - - SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, - "Sales tax as percentage taken from final sale price (0.05 = 5%)").getDouble(0.05); - - RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, - "Require an Auctioneer NPC to access auction house").getBoolean(true); - - ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, - "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); - - MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, - "Minimum bid increment as percentage (0.05 = 5% minimum increase)").getDouble(0.05); - - SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 5, - "Extend auction TO this many minutes when bid placed near end. Set to 0 to disable.").getInt(5); - - SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, - "How many minutes before end triggers snipe protection").getInt(5); - - // Duration settings - AuctionDurationShort = config.get(AUCTION, "Duration Short Hours", 2, - "Short auction duration in hours").getInt(2); - - AuctionDurationMedium = config.get(AUCTION, "Duration Medium Hours", 12, - "Medium auction duration in hours").getInt(12); - - AuctionDurationLong = config.get(AUCTION, "Duration Long Hours", 24, - "Long auction duration in hours").getInt(24); - - AuctionDurationVeryLong = config.get(AUCTION, "Duration Very Long Hours", 48, - "Very long auction duration in hours").getInt(48); - - // Duration fees - AuctionFeeShort = config.get(AUCTION, "Fee Short Duration", 0, - "Flat fee for short duration auctions").getInt(0); - - AuctionFeeMedium = config.get(AUCTION, "Fee Medium Duration", 0, - "Flat fee for medium duration auctions").getInt(0); - - AuctionFeeLong = config.get(AUCTION, "Fee Long Duration", 0, - "Flat fee for long duration auctions").getInt(0); - - AuctionFeeVeryLong = config.get(AUCTION, "Fee Very Long Duration", 0, - "Flat fee for very long duration auctions").getInt(0); + MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE, + "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE); // ========================================= // Trader Settings @@ -181,32 +80,10 @@ public class ConfigMarket { ))); config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( - "Use Vault If Available", + "Use Vault", "Currency Name", - "Fallback Currency Item", - "Fallback Item Ratio", "Starting Balance", - "Maximum Balance" - ))); - - config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( - "Enable Auction House", - "Default Max Listings", - "Listing Fee Percent", - "Sales Tax Percent", - "Require Auctioneer NPC", - "Claim Expiration Days", - "Min Bid Increment Percent", - "Snipe Protection Minutes", - "Snipe Protection Threshold", - "Duration Short Hours", - "Duration Medium Hours", - "Duration Long Hours", - "Duration Very Long Hours", - "Fee Short Duration", - "Fee Medium Duration", - "Fee Long Duration", - "Fee Very Long Duration" + "Max Balance" ))); config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( @@ -214,26 +91,6 @@ public class ConfigMarket { "Default Reset Type" ))); - // Validation - if (DefaultMaxListings < 1) { - DefaultMaxListings = 1; - } - if (ClaimExpirationDays < 0) { - ClaimExpirationDays = 0; - } - if (SnipeProtectionMinutes < 0) { - SnipeProtectionMinutes = 0; - } - if (SnipeProtectionThreshold < 0) { - SnipeProtectionThreshold = 0; - } - if (StartingBalance < 0) { - StartingBalance = 0; - } - if (MaxBalance < 1) { - MaxBalance = 1; - } - } catch (Exception e) { FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration"); } finally { diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java deleted file mode 100644 index c901d21ce..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java +++ /dev/null @@ -1,82 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.util.StatCollector; -import noppes.npcs.config.ConfigMarket; - -/** - * Duration options for auction listings. - * Values are configurable via ConfigMarket. - */ -public enum EnumAuctionDuration { - SHORT("auction.duration.short"), - MEDIUM("auction.duration.medium"), - LONG("auction.duration.long"), - VERY_LONG("auction.duration.veryLong"); - - private final String langKey; - - EnumAuctionDuration(String langKey) { - this.langKey = langKey; - } - - /** - * Get duration in milliseconds - */ - public long getMillis() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort * 60L * 60L * 1000L; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium * 60L * 60L * 1000L; - case LONG: - return ConfigMarket.AuctionDurationLong * 60L * 60L * 1000L; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong * 60L * 60L * 1000L; - default: - return 24 * 60L * 60L * 1000L; - } - } - - /** - * Get duration in hours - */ - public int getHours() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium; - case LONG: - return ConfigMarket.AuctionDurationLong; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong; - default: - return 24; - } - } - - /** - * Get display name for GUI - */ - public String getDisplayName() { - return StatCollector.translateToLocal(langKey); - } - - /** - * Get listing fee for this duration - */ - public long getListingFee() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionFeeShort; - case MEDIUM: - return ConfigMarket.AuctionFeeMedium; - case LONG: - return ConfigMarket.AuctionFeeLong; - case VERY_LONG: - return ConfigMarket.AuctionFeeVeryLong; - default: - return 0; - } - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java deleted file mode 100644 index 75220861c..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java +++ /dev/null @@ -1,39 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.util.StatCollector; - -/** - * Sorting options for auction listings. - */ -public enum EnumAuctionSort { - NEWEST_FIRST("auction.sort.newest"), - OLDEST_FIRST("auction.sort.oldest"), - PRICE_HIGH_TO_LOW("auction.sort.priceHigh"), - PRICE_LOW_TO_HIGH("auction.sort.priceLow"), - ENDING_SOON("auction.sort.endingSoon"), - MOST_TIME_LEFT("auction.sort.mostTime"), - NAME_A_TO_Z("auction.sort.nameAZ"), - NAME_Z_TO_A("auction.sort.nameZA"), - MOST_BIDS("auction.sort.mostBids"), - LEAST_BIDS("auction.sort.leastBids"); - - private final String langKey; - - EnumAuctionSort(String langKey) { - this.langKey = langKey; - } - - public String getDisplayName() { - return StatCollector.translateToLocal(langKey); - } - - public EnumAuctionSort next() { - EnumAuctionSort[] values = values(); - return values[(this.ordinal() + 1) % values.length]; - } - - public EnumAuctionSort previous() { - EnumAuctionSort[] values = values(); - return values[(this.ordinal() - 1 + values.length) % values.length]; - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java deleted file mode 100644 index 9e614e496..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java +++ /dev/null @@ -1,54 +0,0 @@ -package noppes.npcs.constants; - -/** - * Status of an auction listing. - */ -public enum EnumAuctionStatus { - ACTIVE, // Currently listed and accepting bids - SOLD, // Successfully sold (buyout or auction end with bids) - EXPIRED, // Ended without any bids - CANCELLED, // Cancelled by seller (only allowed before first bid) - CLAIMED; // Item/money has been claimed by respective parties - - /** - * Get display name for GUI - */ - public String getDisplayName() { - switch (this) { - case ACTIVE: return "Active"; - case SOLD: return "Sold"; - case EXPIRED: return "Expired"; - case CANCELLED: return "Cancelled"; - case CLAIMED: return "Claimed"; - default: return "Unknown"; - } - } - - /** - * Get display color for GUI (MC color codes) - */ - public char getColorCode() { - switch (this) { - case ACTIVE: return 'a'; // Green - case SOLD: return '6'; // Gold - case EXPIRED: return '7'; // Gray - case CANCELLED: return 'c'; // Red - case CLAIMED: return '8'; // Dark gray - default: return 'f'; // White - } - } - - /** - * Check if listing can still receive bids - */ - public boolean canBid() { - return this == ACTIVE; - } - - /** - * Check if listing needs claiming (item or money) - */ - public boolean needsClaiming() { - return this == SOLD || this == EXPIRED || this == CANCELLED; - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index 6c85cb4f5..f2d911561 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -64,7 +64,5 @@ public enum EnumGuiType { ScriptBlock, Paintbrush, MagicBook, - ManageAbilities, - PlayerAuction, - SetupAuctioneer; + ManageAbilities; } diff --git a/src/main/java/noppes/npcs/constants/EnumRoleType.java b/src/main/java/noppes/npcs/constants/EnumRoleType.java index 33f36941a..624658fb3 100644 --- a/src/main/java/noppes/npcs/constants/EnumRoleType.java +++ b/src/main/java/noppes/npcs/constants/EnumRoleType.java @@ -2,5 +2,5 @@ package noppes.npcs.constants; public enum EnumRoleType { - None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount, Auctioneer + None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount } diff --git a/src/main/java/noppes/npcs/constants/EnumStockReset.java b/src/main/java/noppes/npcs/constants/EnumStockReset.java index cdc56cb95..485318dd5 100644 --- a/src/main/java/noppes/npcs/constants/EnumStockReset.java +++ b/src/main/java/noppes/npcs/constants/EnumStockReset.java @@ -1,17 +1,25 @@ package noppes.npcs.constants; +import net.minecraft.util.StatCollector; + /** * Stock reset types for Trader NPCs. * Mirrors EnumQuestRepeat pattern for consistency. */ public enum EnumStockReset { - NONE, // Never resets - MCDAILY, // Every 24000 MC ticks (1 MC day) - MCWEEKLY, // Every 168000 MC ticks (7 MC days) - MCCUSTOM, // Custom MC tick interval - RLDAILY, // Every 24 real hours - RLWEEKLY, // Every 7 real days - RLCUSTOM; // Custom real-time interval + NONE("stock.reset.never"), + MCDAILY("stock.reset.mcdaily"), + MCWEEKLY("stock.reset.mcweekly"), + MCCUSTOM("stock.reset.mccustom"), + RLDAILY("stock.reset.rldaily"), + RLWEEKLY("stock.reset.rlweekly"), + RLCUSTOM("stock.reset.rlcustom"); + + private final String langKey; + + EnumStockReset(String langKey) { + this.langKey = langKey; + } /** * Get reset interval in milliseconds for real-time types @@ -47,18 +55,21 @@ public enum EnumStockReset { } /** - * Get display name for GUI + * Get display name for GUI (translated) */ public String getDisplayName() { - switch (this) { - case NONE: return "Never"; - case MCDAILY: return "MC Daily"; - case MCWEEKLY: return "MC Weekly"; - case MCCUSTOM: return "MC Custom"; - case RLDAILY: return "Real Daily"; - case RLWEEKLY: return "Real Weekly"; - case RLCUSTOM: return "Real Custom"; - default: return "Unknown"; + return StatCollector.translateToLocal(langKey); + } + + /** + * Get all display names for dropdown menus + */ + public static String[] getDisplayNames() { + EnumStockReset[] values = values(); + String[] names = new String[values.length]; + for (int i = 0; i < values.length; i++) { + names[i] = values[i].getDisplayName(); } + return names; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java deleted file mode 100644 index 2efd2b1b5..000000000 --- a/src/main/java/noppes/npcs/containers/ContainerAuction.java +++ /dev/null @@ -1,193 +0,0 @@ -package noppes.npcs.containers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import noppes.npcs.entity.EntityNPCInterface; - -/** - * Container for auction house GUI. - * Handles player inventory and item slot for creating listings. - */ -public class ContainerAuction extends Container { - public EntityNPCInterface npc; - public EntityPlayer player; - - // Slot for item being listed - public SlotAuctionItem listingSlot; - private IInventory listingInventory; - - public ContainerAuction(EntityNPCInterface npc, EntityPlayer player) { - this.npc = npc; - this.player = player; - - // Create a single-slot inventory for the item being listed - listingInventory = new InventoryAuctionListing(); - listingSlot = new SlotAuctionItem(listingInventory, 0, 251, 146); - addSlotToContainer(listingSlot); - - // Player inventory slots (standard positioning) - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col + row * 9 + 9, - 8 + col * 18, 166 + row * 18)); - } - } - - // Player hotbar - for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 224)); - } - } - - @Override - public boolean canInteractWith(EntityPlayer player) { - return npc != null && npc.isEntityAlive() && - player.getDistanceSqToEntity(npc) < 64.0D; - } - - @Override - public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { - ItemStack itemstack = null; - Slot slot = (Slot) inventorySlots.get(slotIndex); - - if (slot != null && slot.getHasStack()) { - ItemStack stackInSlot = slot.getStack(); - itemstack = stackInSlot.copy(); - - // If from listing slot, move to player inventory - if (slotIndex == 0) { - if (!mergeItemStack(stackInSlot, 1, 37, true)) { - return null; - } - } - // If from player inventory, move to listing slot - else { - if (!mergeItemStack(stackInSlot, 0, 1, false)) { - return null; - } - } - - if (stackInSlot.stackSize == 0) { - slot.putStack(null); - } else { - slot.onSlotChanged(); - } - } - - return itemstack; - } - - @Override - public void onContainerClosed(EntityPlayer player) { - super.onContainerClosed(player); - - // Return any item in the listing slot to player - ItemStack stack = listingInventory.getStackInSlot(0); - if (stack != null) { - if (!player.inventory.addItemStackToInventory(stack)) { - player.entityDropItem(stack, 0.5f); - } - } - } - - /** - * Get the item currently in the listing slot - */ - public ItemStack getListingItem() { - return listingInventory.getStackInSlot(0); - } - - /** - * Clear the listing slot (after successful listing creation) - */ - public void clearListingSlot() { - listingInventory.setInventorySlotContents(0, null); - } - - /** - * Simple inventory for the listing slot - */ - private class InventoryAuctionListing implements IInventory { - private ItemStack item; - - @Override - public int getSizeInventory() { return 1; } - - @Override - public ItemStack getStackInSlot(int slot) { return item; } - - @Override - public ItemStack decrStackSize(int slot, int amount) { - if (item != null) { - ItemStack result; - if (item.stackSize <= amount) { - result = item; - item = null; - return result; - } else { - result = item.splitStack(amount); - if (item.stackSize == 0) { - item = null; - } - return result; - } - } - return null; - } - - @Override - public ItemStack getStackInSlotOnClosing(int slot) { - ItemStack result = item; - item = null; - return result; - } - - @Override - public void setInventorySlotContents(int slot, ItemStack stack) { - item = stack; - } - - @Override - public String getInventoryName() { return "AuctionListing"; } - - @Override - public boolean hasCustomInventoryName() { return false; } - - @Override - public int getInventoryStackLimit() { return 64; } - - @Override - public void markDirty() {} - - @Override - public boolean isUseableByPlayer(EntityPlayer player) { return true; } - - @Override - public void openInventory() {} - - @Override - public void closeInventory() {} - - @Override - public boolean isItemValidForSlot(int slot, ItemStack stack) { return true; } - } - - /** - * Custom slot for auction listings - */ - public class SlotAuctionItem extends Slot { - public SlotAuctionItem(IInventory inv, int index, int x, int y) { - super(inv, index, x, y); - } - - @Override - public boolean isItemValid(ItemStack stack) { - // Check if item can be traded (not untradeable, soulbound, etc.) - return kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute.canTrade(stack); - } - } -} diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index 936868152..e6b4c86c8 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -1,45 +1,71 @@ package noppes.npcs.containers; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilPlayer; -import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; public class ContainerNPCTrader extends ContainerNpcInterface { public RoleTrader role; + private final EntityNPCInterface npc; + + // Layout constants for 256px wide GUI + public static final int COLUMN_WIDTH = 80; + public static final int COLUMN_START_X = 8; + public static final int ROW_HEIGHT = 22; + public static final int ROW_START_Y = 6; + + // Slot offsets within each column (for 18x18 slots, items render centered) + public static final int CURRENCY1_OFFSET = 2; // Primary currency slot + public static final int CURRENCY2_OFFSET = 20; // Secondary currency slot + public static final int OUTPUT_OFFSET = 50; // Output slot public ContainerNPCTrader(EntityNPCInterface npc, EntityPlayer player) { super(player); + this.npc = npc; role = (RoleTrader) npc.roleInterface; - for (int i = 0; i < 18; i++) { - int x = 53; - x += i % 3 * 72; - int y = 7; - y += i / 3 * 21; + // Register this player as viewing the trader (server-side only) + // Needed for shared stock (not per-player) to sync updates to other viewers + // Works for both linked markets (MarketRegistry) and normal traders (local viewers) + if (player instanceof EntityPlayerMP && !role.stock.perPlayer) { + role.registerViewer((EntityPlayerMP) player); + } - ItemStack item = role.inventoryCurrency.items.get(i); - ItemStack item2 = role.inventoryCurrency.items.get(i + 18); - if (item == null) { - item = item2; - item2 = null; - } - addSlotToContainer(new Slot(role.inventorySold, i, x, y)); + // Trade slots: 3 columns x 6 rows, 80px column width + // Layout per column: [Slot1][Slot2] = [Output] + for (int i = 0; i < 18; i++) { + int col = i % 3; + int row = i / 3; + int x = COLUMN_START_X + col * COLUMN_WIDTH; + int y = ROW_START_Y + row * ROW_HEIGHT; + // Output slot position (slot texture is 18x18, item renders centered at +1) + addSlotToContainer(new Slot(role.inventorySold, i, x + OUTPUT_OFFSET + 1, y + 1)); } for (int i1 = 0; i1 < 3; i1++) { for (int l1 = 0; l1 < 9; l1++) { - addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 32 + l1 * 18, 140 + i1 * 18)); + addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 48 + l1 * 18, 137 + i1 * 18)); } - } + // Player hotbar for (int j1 = 0; j1 < 9; j1++) { - addSlotToContainer(new Slot(player.inventory, j1, 32 + j1 * 18, 198)); + addSlotToContainer(new Slot(player.inventory, j1, 48 + j1 * 18, 195)); + } + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + // Unregister this player from viewer tracking (only if registered for shared stock) + if (player instanceof EntityPlayerMP && !role.stock.perPlayer) { + role.unregisterViewer((EntityPlayerMP) player); } } @@ -71,25 +97,27 @@ public class ContainerNPCTrader extends ContainerNpcInterface { return null; // Out of stock } - // Handle currency-based purchase - if (role.hasSlotPrice(i)) { - long price = role.getSlotPrice(i); - if (CurrencyController.Instance == null) { - return null; // Currency system not available + // Item-based currency + if (!canBuy(i, entityplayer)) + return null; + + // Currency cost check (additive to item costs) + long currencyCost = role.getCurrencyCost(i); + if (currencyCost > 0) { + PlayerData data = PlayerData.get(entityplayer); + if (data.currencyData.getBalance() < currencyCost) { + return null; // Can't afford currency cost } - if (!CurrencyController.Instance.canAfford(entityplayer, price)) { - return null; // Can't afford - } - // Withdraw currency - if (!CurrencyController.Instance.withdraw(entityplayer, price)) { - return null; // Withdrawal failed - } - } else { - // Item-based currency - existing logic - if (!canBuy(i, entityplayer)) - return null; - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + } + + // Consume item currency + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + + // Withdraw currency cost (after item consumption to maintain order) + if (currencyCost > 0) { + PlayerData data = PlayerData.get(entityplayer); + data.currencyData.withdraw(currencyCost); } // Consume stock @@ -98,6 +126,18 @@ public class ContainerNPCTrader extends ContainerNpcInterface { ItemStack soldItem = item.copy(); givePlayer(soldItem, entityplayer); role.addPurchase(i, entityplayer.getDisplayName()); + + // Sync updated data after purchase + // For per-player stocks: sync only to the purchasing player + // For shared stocks: consumeStock() handles sync (market-wide or local) + if (entityplayer instanceof EntityPlayerMP) { + if (role.stock.perPlayer) { + // Per-player: only sync to purchasing player (their own stock/balance) + role.syncToPlayer((EntityPlayerMP) entityplayer); + } + // Shared stock sync is already handled in consumeStock() + } + return soldItem; } diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java b/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java index 8209f22a8..f5d347e16 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java @@ -13,15 +13,19 @@ public class ContainerNPCTraderSetup extends Container { public ContainerNPCTraderSetup(EntityNPCInterface npc, EntityPlayer player) { role = (RoleTrader) npc.roleInterface; + // New layout: $[Currency TextField], Item Cost 1, Item Cost 2, =, Output + // Column width: 130px for better spacing for (int i = 0; i < 18; i++) { int x = 7; - x += i % 3 * 94; + x += i % 3 * 130; int y = 15; y += i / 3 * 22; - addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x, y)); - addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 18, y)); - addSlotToContainer(new Slot(role.inventorySold, i, x + 43, y)); - + // Item Cost 1 (primary currency) + addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 51, y)); + // Item Cost 2 (secondary currency) + addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x + 69, y)); + // Output (sold item) + addSlotToContainer(new Slot(role.inventorySold, i, x + 105, y)); } for (int i1 = 0; i1 < 3; i1++) { diff --git a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java deleted file mode 100644 index 020bb0bcb..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java +++ /dev/null @@ -1,419 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.nbt.NBTTagString; -import noppes.npcs.CustomNpcs; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.HashSet; -import java.util.Set; - -/** - * Controller for managing auction house item blacklisting. - * Allows administrators to block specific items, entire mods, - * or items with certain NBT tags from being listed. - */ -public class AuctionBlacklist { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static AuctionBlacklist Instance; - - // Blacklist by item registry name (e.g., "minecraft:diamond_sword") - private Set blacklistedItems = new HashSet<>(); - - // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) - private Set blacklistedMods = new HashSet<>(); - - // Blacklist by NBT tag presence (e.g., items with specific custom data) - private Set blacklistedNBTTags = new HashSet<>(); - - public AuctionBlacklist() { - Instance = this; - load(); - } - - // ==================== Blacklist Checking ==================== - - /** - * Check if an item is blacklisted from the auction house. - * - * @param item The item to check - * @return true if the item is blacklisted - */ - public boolean isBlacklisted(ItemStack item) { - if (item == null || item.getItem() == null) { - return false; - } - - // Check item registry name - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - // Direct item blacklist - if (blacklistedItems.contains(itemName)) { - return true; - } - - // Also check with metadata for specific variants - String itemNameWithMeta = itemName + ":" + item.getItemDamage(); - if (blacklistedItems.contains(itemNameWithMeta)) { - return true; - } - - // Check mod ID - String modId = getModIdFromName(itemName); - if (modId != null && blacklistedMods.contains(modId)) { - return true; - } - } - - // Check NBT tags - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (hasNestedTag(tag, nbtTag)) { - return true; - } - } - } - - return false; - } - - /** - * Get the reason why an item is blacklisted - * @return Description of why blacklisted, or null if not blacklisted - */ - public String getBlacklistReason(ItemStack item) { - if (item == null || item.getItem() == null) { - return null; - } - - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - if (blacklistedItems.contains(itemName)) { - return "Item is blacklisted: " + itemName; - } - - String itemNameWithMeta = itemName + ":" + item.getItemDamage(); - if (blacklistedItems.contains(itemNameWithMeta)) { - return "Item variant is blacklisted: " + itemNameWithMeta; - } - - String modId = getModIdFromName(itemName); - if (modId != null && blacklistedMods.contains(modId)) { - return "Mod is blacklisted: " + modId; - } - } - - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (hasNestedTag(tag, nbtTag)) { - return "Item has blacklisted NBT tag: " + nbtTag; - } - } - } - - return null; - } - - /** - * Extract mod ID from registry name (e.g., "minecraft" from "minecraft:stone") - */ - private String getModIdFromName(String registryName) { - if (registryName == null) { - return null; - } - int colonIndex = registryName.indexOf(':'); - if (colonIndex > 0) { - return registryName.substring(0, colonIndex); - } - return null; - } - - /** - * Check for nested NBT tag (supports dot notation like "display.Name") - */ - private boolean hasNestedTag(NBTTagCompound tag, String path) { - if (tag == null || path == null || path.isEmpty()) { - return false; - } - - String[] parts = path.split("\\."); - NBTTagCompound current = tag; - - for (int i = 0; i < parts.length - 1; i++) { - if (!current.hasKey(parts[i])) { - return false; - } - if (current.hasKey(parts[i], 10)) { // 10 = Compound tag - current = current.getCompoundTag(parts[i]); - } else { - return false; - } - } - - return current.hasKey(parts[parts.length - 1]); - } - - // ==================== Item Blacklist Management ==================== - - /** - * Add an item to the blacklist by registry name - * @param itemName Registry name (e.g., "minecraft:diamond_sword" or "minecraft:wool:14") - */ - public void addItem(String itemName) { - if (itemName != null && !itemName.isEmpty()) { - blacklistedItems.add(itemName); - save(); - logger.info("Added item to auction blacklist: " + itemName); - } - } - - /** - * Add an item to the blacklist from an ItemStack - */ - public void addItem(ItemStack item, boolean includeMeta) { - if (item == null || item.getItem() == null) { - return; - } - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - if (includeMeta && item.getHasSubtypes()) { - addItem(itemName + ":" + item.getItemDamage()); - } else { - addItem(itemName); - } - } - } - - /** - * Remove an item from the blacklist - */ - public void removeItem(String itemName) { - if (blacklistedItems.remove(itemName)) { - save(); - logger.info("Removed item from auction blacklist: " + itemName); - } - } - - /** - * Check if a specific item is blacklisted - */ - public boolean isItemBlacklisted(String itemName) { - return blacklistedItems.contains(itemName); - } - - /** - * Get all blacklisted items - */ - public Set getBlacklistedItems() { - return new HashSet<>(blacklistedItems); - } - - // ==================== Mod Blacklist Management ==================== - - /** - * Add an entire mod to the blacklist - * @param modId The mod ID (e.g., "minecraft", "customnpcs") - */ - public void addMod(String modId) { - if (modId != null && !modId.isEmpty()) { - blacklistedMods.add(modId); - save(); - logger.info("Added mod to auction blacklist: " + modId); - } - } - - /** - * Remove a mod from the blacklist - */ - public void removeMod(String modId) { - if (blacklistedMods.remove(modId)) { - save(); - logger.info("Removed mod from auction blacklist: " + modId); - } - } - - /** - * Check if a mod is blacklisted - */ - public boolean isModBlacklisted(String modId) { - return blacklistedMods.contains(modId); - } - - /** - * Get all blacklisted mods - */ - public Set getBlacklistedMods() { - return new HashSet<>(blacklistedMods); - } - - // ==================== NBT Tag Blacklist Management ==================== - - /** - * Add an NBT tag to the blacklist - * Items containing this tag will be blocked - * @param tagPath The tag path (e.g., "display.Name" or "CustomData") - */ - public void addNBTTag(String tagPath) { - if (tagPath != null && !tagPath.isEmpty()) { - blacklistedNBTTags.add(tagPath); - save(); - logger.info("Added NBT tag to auction blacklist: " + tagPath); - } - } - - /** - * Remove an NBT tag from the blacklist - */ - public void removeNBTTag(String tagPath) { - if (blacklistedNBTTags.remove(tagPath)) { - save(); - logger.info("Removed NBT tag from auction blacklist: " + tagPath); - } - } - - /** - * Check if an NBT tag is blacklisted - */ - public boolean isNBTTagBlacklisted(String tagPath) { - return blacklistedNBTTags.contains(tagPath); - } - - /** - * Get all blacklisted NBT tags - */ - public Set getBlacklistedNBTTags() { - return new HashSet<>(blacklistedNBTTags); - } - - // ==================== Persistence ==================== - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - try { - NBTTagCompound compound = new NBTTagCompound(); - - // Save blacklisted items - NBTTagList itemList = new NBTTagList(); - for (String item : blacklistedItems) { - itemList.appendTag(new NBTTagString(item)); - } - compound.setTag("Items", itemList); - - // Save blacklisted mods - NBTTagList modList = new NBTTagList(); - for (String mod : blacklistedMods) { - modList.appendTag(new NBTTagString(mod)); - } - compound.setTag("Mods", modList); - - // Save blacklisted NBT tags - NBTTagList nbtList = new NBTTagList(); - for (String nbt : blacklistedNBTTags) { - nbtList.appendTag(new NBTTagString(nbt)); - } - compound.setTag("NBTTags", nbtList); - - // Write to file - File file = new File(saveDir, "auction_blacklist.dat"); - File backup = new File(saveDir, "auction_blacklist.dat_old"); - - if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } - file.renameTo(backup); - } - - file = new File(saveDir, "auction_blacklist.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - - } catch (Exception e) { - logger.error("Error saving auction blacklist", e); - } - } - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - blacklistedItems.clear(); - blacklistedMods.clear(); - blacklistedNBTTags.clear(); - - try { - File file = new File(saveDir, "auction_blacklist.dat"); - if (!file.exists()) { - // Try backup - file = new File(saveDir, "auction_blacklist.dat_old"); - if (!file.exists()) { - return; - } - } - - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - // Load blacklisted items - NBTTagList itemList = compound.getTagList("Items", 8); // 8 = String tag - for (int i = 0; i < itemList.tagCount(); i++) { - blacklistedItems.add(itemList.getStringTagAt(i)); - } - - // Load blacklisted mods - NBTTagList modList = compound.getTagList("Mods", 8); - for (int i = 0; i < modList.tagCount(); i++) { - blacklistedMods.add(modList.getStringTagAt(i)); - } - - // Load blacklisted NBT tags - NBTTagList nbtList = compound.getTagList("NBTTags", 8); - for (int i = 0; i < nbtList.tagCount(); i++) { - blacklistedNBTTags.add(nbtList.getStringTagAt(i)); - } - - logger.info("Loaded auction blacklist: {} items, {} mods, {} NBT tags", - blacklistedItems.size(), blacklistedMods.size(), blacklistedNBTTags.size()); - - } catch (Exception e) { - logger.error("Error loading auction blacklist", e); - } - } - - /** - * Clear all blacklist entries - */ - public void clearAll() { - blacklistedItems.clear(); - blacklistedMods.clear(); - blacklistedNBTTags.clear(); - save(); - logger.info("Cleared all auction blacklist entries"); - } - - /** - * Get total number of blacklist entries - */ - public int getTotalEntries() { - return blacklistedItems.size() + blacklistedMods.size() + blacklistedNBTTags.size(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java deleted file mode 100644 index 8333fd01a..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ /dev/null @@ -1,834 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.ChatComponentText; -import net.minecraft.util.EnumChatFormatting; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionSort; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -/** - * Controller for the global auction house. - * Manages all auction listings, bidding, claims, notifications, and logging. - */ -public class AuctionController { - private static final String LOG_PREFIX = "[Auction] "; - - public static AuctionController Instance; - - // All auction listings by ID - private Map listings = new ConcurrentHashMap<>(); - - // Next available listing ID - private int nextId = 1; - - // Indices for fast lookup - private Map> listingsBySeller = new ConcurrentHashMap<>(); - private Map> listingsByBidder = new ConcurrentHashMap<>(); - - // Pending notifications for offline players - private Map> pendingNotifications = new ConcurrentHashMap<>(); - - public AuctionController() { - Instance = this; - load(); - } - - // ==================== Listing Management ==================== - - /** - * Create a new auction listing - * @return The new listing, or null if creation failed - */ - public AuctionListing createListing(EntityPlayer seller, ItemStack item, - long startingPrice, long buyoutPrice, - EnumAuctionDuration duration) { - // Validate item - String validation = AuctionListing.validateItem(item); - if (validation != null) { - LogWriter.info(LOG_PREFIX + "Listing validation failed: " + validation); - return null; - } - - // Check seller's listing limit - int currentListings = getActiveListingsCount(seller.getUniqueID()); - int maxListings = getMaxListings(seller); - if (currentListings >= maxListings) { - return null; - } - - // Calculate and charge listing fee - AuctionListing listing = new AuctionListing( - seller.getUniqueID(), - seller.getCommandSenderName(), - item, - startingPrice, - buyoutPrice, - duration - ); - - long listingFee = listing.getListingFee(); - if (listingFee > 0) { - if (!CurrencyController.Instance.canAfford(seller, listingFee)) { - return null; - } - CurrencyController.Instance.withdraw(seller, listingFee); - } - - // Assign ID and store - listing.id = nextId++; - listings.put(listing.id, listing); - - // Update indices - addToSellerIndex(seller.getUniqueID(), listing.id); - - save(); - - // Log to CNPC+ - logEvent("LISTING_CREATED", String.format("ID=%d, Seller=%s, Item=%s x%d, StartPrice=%d, Buyout=%s", - listing.id, listing.sellerName, item.getDisplayName(), item.stackSize, - listing.startingPrice, listing.buyoutPrice > 0 ? listing.buyoutPrice : "None")); - - return listing; - } - - /** - * Get a listing by ID - */ - public AuctionListing getListing(int id) { - return listings.get(id); - } - - /** - * Get all active listings - */ - public List getActiveListings() { - processExpiredAuctions(); - return listings.values().stream() - .filter(AuctionListing::isActive) - .collect(Collectors.toList()); - } - - /** - * Search listings by item name - */ - public List searchListings(String query) { - String lowerQuery = query.toLowerCase(); - return getActiveListings().stream() - .filter(l -> l.item != null) - .filter(l -> l.item.getDisplayName().toLowerCase().contains(lowerQuery)) - .collect(Collectors.toList()); - } - - // ==================== Filtering & Sorting ==================== - - /** - * Get filtered and sorted auction listings. - * @param filter The filter criteria - * @return FilterResult containing matching listings and metadata - */ - public FilterResult getFilteredListings(AuctionFilter filter) { - processExpiredAuctions(); - - // Get all active listings - List results = listings.values().stream() - .filter(AuctionListing::isActive) - .filter(filter::matches) - .collect(Collectors.toList()); - - // Sort the results - sortListings(results, filter.sortOrder); - - // Calculate pagination - int totalResults = results.size(); - int totalPages = totalResults == 0 - ? 0 - : Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); - int currentPage = Math.max(0, filter.page); - if (totalPages > 0 && currentPage >= totalPages) { - currentPage = totalPages - 1; - } - int startIndex = currentPage * filter.pageSize; - int endIndex = Math.min(startIndex + filter.pageSize, totalResults); - - // Get the page slice - List pageResults; - if (startIndex >= totalResults) { - pageResults = new ArrayList<>(); - } else { - pageResults = new ArrayList<>(results.subList(startIndex, endIndex)); - } - - return new FilterResult(pageResults, totalResults, currentPage, totalPages); - } - - /** - * Sort listings according to the specified sort order - */ - public void sortListings(List listings, EnumAuctionSort sortOrder) { - if (listings == null || listings.isEmpty()) { - return; - } - - Comparator comparator; - - switch (sortOrder) { - case NEWEST_FIRST: - comparator = Comparator.comparingLong(l -> -l.listingTime); - break; - case OLDEST_FIRST: - comparator = Comparator.comparingLong(l -> l.listingTime); - break; - case PRICE_HIGH_TO_LOW: - comparator = Comparator.comparingLong((AuctionListing l) -> - -(l.currentBid > 0 ? l.currentBid : l.startingPrice)); - break; - case PRICE_LOW_TO_HIGH: - comparator = Comparator.comparingLong(l -> - l.currentBid > 0 ? l.currentBid : l.startingPrice); - break; - case ENDING_SOON: - comparator = Comparator.comparingLong(l -> l.expirationTime); - break; - case MOST_TIME_LEFT: - comparator = Comparator.comparingLong(l -> -l.expirationTime); - break; - case NAME_A_TO_Z: - comparator = Comparator.comparing(l -> getListingName(l).toLowerCase()); - break; - case NAME_Z_TO_A: - comparator = Comparator.comparing((AuctionListing l) -> - getListingName(l).toLowerCase()).reversed(); - break; - case MOST_BIDS: - comparator = Comparator.comparingInt(l -> -l.bidCount); - break; - case LEAST_BIDS: - comparator = Comparator.comparingInt(l -> l.bidCount); - break; - default: - comparator = Comparator.comparingLong(l -> l.expirationTime); - } - - listings.sort(comparator); - } - - /** - * Result container for filtered queries - */ - public static class FilterResult { - public final List listings; - public final int totalResults; - public final int currentPage; - public final int totalPages; - - public FilterResult(List listings, int totalResults, - int currentPage, int totalPages) { - this.listings = listings; - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - } - - public boolean hasNextPage() { - return currentPage < totalPages - 1; - } - - public boolean hasPreviousPage() { - return currentPage > 0; - } - - public boolean isEmpty() { - return listings == null || listings.isEmpty(); - } - } - - /** - * Get all listings by a seller - */ - public List getListingsBySeller(UUID sellerUUID) { - List ids = listingsBySeller.get(sellerUUID); - if (ids == null) { - return new ArrayList<>(); - } - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Get active listing count for a seller - */ - public int getActiveListingsCount(UUID sellerUUID) { - return (int) getListingsBySeller(sellerUUID).stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .count(); - } - - /** - * Get listings where player is the high bidder - */ - public List getListingsAsBidder(UUID bidderUUID) { - List ids = listingsByBidder.get(bidderUUID); - if (ids == null) { - return new ArrayList<>(); - } - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .filter(AuctionListing::isActive) - .filter(l -> bidderUUID.equals(l.highBidderUUID)) - .collect(Collectors.toList()); - } - - /** - * Get listings that need claiming by a player - */ - public List getClaimableListings(UUID playerUUID) { - List claimable = new ArrayList<>(); - - for (AuctionListing listing : listings.values()) { - // Seller can claim proceeds from sold items - if (listing.sellerUUID.equals(playerUUID) && - listing.status == EnumAuctionStatus.SOLD && - !listing.sellerClaimed) { - claimable.add(listing); - } - - // Seller can claim back cancelled/expired items - if (listing.sellerUUID.equals(playerUUID) && - (listing.status == EnumAuctionStatus.CANCELLED || - listing.status == EnumAuctionStatus.EXPIRED) && - !listing.sellerClaimed) { - claimable.add(listing); - } - - // Buyer can claim won items - if (listing.highBidderUUID != null && - listing.highBidderUUID.equals(playerUUID) && - listing.status == EnumAuctionStatus.SOLD && - !listing.buyerClaimed) { - claimable.add(listing); - } - } - - return claimable; - } - - // ==================== Bidding ==================== - - /** - * Place a bid on an auction - */ - public boolean placeBid(int listingId, EntityPlayer bidder, long amount) { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return false; - } - - if (!CurrencyController.Instance.canAfford(bidder, amount)) { - return false; - } - - // Store previous bidder for refund - UUID previousBidder = listing.highBidderUUID; - String previousBidderName = listing.highBidderName; - long previousBidAmount = listing.currentBid; - - if (!listing.placeBid(bidder.getUniqueID(), bidder.getCommandSenderName(), amount)) { - return false; - } - - CurrencyController.Instance.withdraw(bidder, amount); - - // Refund previous bidder - if (previousBidder != null) { - CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, - "Auction Outbid", String.valueOf(listingId)); - - // Notify outbid player - notifyPlayer(previousBidder, EnumChatFormatting.RED + - "You have been outbid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + "! New bid: " + formatCurrency(amount)); - - logEvent("OUTBID", String.format("ID=%d, OutbidPlayer=%s, PrevBid=%d, NewBid=%d", - listingId, previousBidderName, previousBidAmount, amount)); - } - - // Notify seller - notifyPlayer(listing.sellerUUID, EnumChatFormatting.AQUA + - bidder.getCommandSenderName() + " placed a bid of " + formatCurrency(amount) + - " on your " + listing.item.getDisplayName() + "!"); - - addToBidderIndex(bidder.getUniqueID(), listingId); - save(); - - logEvent("BID_PLACED", String.format("ID=%d, Bidder=%s, Amount=%d", - listingId, bidder.getCommandSenderName(), amount)); - - return true; - } - - /** - * Execute buyout on an auction - */ - public boolean buyout(int listingId, EntityPlayer buyer) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.buyoutPrice <= 0) { - return false; - } - - if (!CurrencyController.Instance.canAfford(buyer, listing.buyoutPrice)) { - return false; - } - - UUID previousBidder = listing.highBidderUUID; - String previousBidderName = listing.highBidderName; - long previousBidAmount = listing.currentBid; - - if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { - return false; - } - - CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); - - // Refund previous bidder - if (previousBidder != null) { - CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, - "Auction Outbid (Buyout)", String.valueOf(listingId)); - - notifyPlayer(previousBidder, EnumChatFormatting.RED + - "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was outbid by a buyout. Your " + - formatCurrency(previousBidAmount) + " has been refunded."); - } - - addToBidderIndex(buyer.getUniqueID(), listingId); - save(); - - // Notify buyer and seller - notifyPlayer(buyer.getUniqueID(), EnumChatFormatting.GREEN + - "Buyout successful! Claim your " + listing.item.getDisplayName() + " from an Auctioneer."); - - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + - "Your " + listing.item.getDisplayName() + " was bought out for " + - formatCurrency(listing.buyoutPrice) + "! Claim your proceeds."); - - logEvent("BUYOUT", String.format("ID=%d, Buyer=%s, Price=%d, Seller=%s", - listingId, buyer.getCommandSenderName(), listing.buyoutPrice, listing.sellerName)); - - return true; - } - - /** - * Cancel an auction listing - */ - public boolean cancelListing(int listingId, EntityPlayer requester, boolean forceCancel) { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return false; - } - - boolean byAdmin = !listing.sellerUUID.equals(requester.getUniqueID()); - if (byAdmin && !forceCancel) { - return false; - } - if (!forceCancel && listing.hasBids()) { - return false; - } - if (listing.status != EnumAuctionStatus.ACTIVE) { - return false; - } - - if (forceCancel && listing.hasBids() && listing.highBidderUUID != null) { - CurrencyController.Instance.addClaim(listing.highBidderUUID, listing.currentBid, - "Auction Cancelled", String.valueOf(listingId)); - notifyPlayer(listing.highBidderUUID, EnumChatFormatting.RED + - "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was cancelled. Your " + - formatCurrency(listing.currentBid) + " has been refunded."); - } - - listing.status = EnumAuctionStatus.CANCELLED; - - save(); - - if (byAdmin) { - notifyPlayer(listing.sellerUUID, EnumChatFormatting.RED + - "Your auction for " + listing.item.getDisplayName() + - " was cancelled by an administrator."); - } - - logEvent("CANCELLED", String.format("ID=%d, CancelledBy=%s, ByAdmin=%b", - listingId, requester.getCommandSenderName(), byAdmin)); - - return true; - } - - // ==================== Claiming ==================== - - /** - * Claim seller proceeds from a sold auction - */ - public boolean claimSellerProceeds(int listingId, EntityPlayer seller) { - AuctionListing listing = listings.get(listingId); - if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || - listing.status != EnumAuctionStatus.SOLD || listing.sellerClaimed) { - return false; - } - - long proceeds = listing.getSellerProceeds(); - CurrencyController.Instance.deposit(seller, proceeds); - - listing.sellerClaimed = true; - checkAndMarkClaimed(listing); - save(); - - logEvent("PROCEEDS_CLAIMED", String.format("ID=%d, Seller=%s, Amount=%d", - listingId, seller.getCommandSenderName(), proceeds)); - - return true; - } - - /** - * Claim item from won auction - */ - public ItemStack claimBuyerItem(int listingId, EntityPlayer buyer) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.highBidderUUID == null || - !listing.highBidderUUID.equals(buyer.getUniqueID()) || - listing.status != EnumAuctionStatus.SOLD || listing.buyerClaimed) { - return null; - } - - listing.buyerClaimed = true; - checkAndMarkClaimed(listing); - save(); - - logEvent("ITEM_CLAIMED", String.format("ID=%d, Buyer=%s, Item=%s", - listingId, buyer.getCommandSenderName(), listing.item.getDisplayName())); - - return listing.item.copy(); - } - - /** - * Claim item from cancelled or expired auction - */ - public ItemStack claimExpiredItem(int listingId, EntityPlayer seller) { - AuctionListing listing = listings.get(listingId); - if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || - (listing.status != EnumAuctionStatus.CANCELLED && listing.status != EnumAuctionStatus.EXPIRED) || - listing.sellerClaimed) { - return null; - } - - listing.sellerClaimed = true; - listing.status = EnumAuctionStatus.CLAIMED; - save(); - - logEvent("ITEM_RECLAIMED", String.format("ID=%d, Seller=%s, Item=%s", - listingId, seller.getCommandSenderName(), listing.item.getDisplayName())); - - return listing.item.copy(); - } - - private void checkAndMarkClaimed(AuctionListing listing) { - if (listing.status == EnumAuctionStatus.SOLD && - listing.sellerClaimed && listing.buyerClaimed) { - listing.status = EnumAuctionStatus.CLAIMED; - } - } - - // ==================== Expiration Processing ==================== - - /** - * Process all expired auctions - */ - public void processExpiredAuctions() { - boolean changed = false; - - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { - listing.finalize(); - changed = true; - - if (listing.status == EnumAuctionStatus.SOLD) { - // Notify winner and seller - notifyPlayer(listing.highBidderUUID, EnumChatFormatting.GREEN + - "You won the auction for " + listing.item.getDisplayName() + - " for " + formatCurrency(listing.currentBid) + "! Claim your item."); - - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + - "Your " + listing.item.getDisplayName() + " sold for " + - formatCurrency(listing.currentBid) + "! Claim your proceeds."); - - logEvent("AUCTION_SOLD", String.format("ID=%d, Winner=%s, Price=%d, Seller=%s", - listing.id, listing.highBidderName, listing.currentBid, listing.sellerName)); - } else { - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GRAY + - "Your auction for " + listing.item.getDisplayName() + - " has expired with no bids. Claim your item."); - - logEvent("AUCTION_EXPIRED", String.format("ID=%d, Item=%s, Seller=%s", - listing.id, listing.item.getDisplayName(), listing.sellerName)); - } - } - } - - if (changed) { - save(); - } - } - - /** - * Clean up old claimed listings - */ - public void cleanupOldListings() { - long claimExpirationMs = ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L; - if (claimExpirationMs <= 0) return; - - long cutoffTime = System.currentTimeMillis() - claimExpirationMs; - List toRemove = new ArrayList<>(); - - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.CLAIMED && - listing.expirationTime < cutoffTime) { - toRemove.add(listing.id); - } - } - - for (int id : toRemove) { - AuctionListing removed = listings.remove(id); - if (removed != null) { - removeFromSellerIndex(removed.sellerUUID, id); - if (removed.highBidderUUID != null) { - removeFromBidderIndex(removed.highBidderUUID, id); - } - } - } - - if (!toRemove.isEmpty()) { - save(); - logEvent("CLEANUP", "Removed " + toRemove.size() + " old listings"); - } - } - - // ==================== Notifications ==================== - - /** - * Send notification to player (or queue if offline) - */ - private void notifyPlayer(UUID playerUUID, String message) { - if (playerUUID == null) return; - - EntityPlayerMP player = getOnlinePlayer(playerUUID); - String fullMessage = EnumChatFormatting.YELLOW + "[Auction] " + message; - - if (player != null) { - player.addChatMessage(new ChatComponentText(fullMessage)); - } else { - pendingNotifications.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(fullMessage); - // Limit queue size - List queue = pendingNotifications.get(playerUUID); - while (queue.size() > 50) { - queue.remove(0); - } - } - } - - /** - * Send pending notifications on player login - */ - public void onPlayerLogin(EntityPlayerMP player) { - UUID uuid = player.getUniqueID(); - - List notifications = pendingNotifications.remove(uuid); - if (notifications != null && !notifications.isEmpty()) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + - "You have " + notifications.size() + " auction notification(s):")); - - for (String msg : notifications) { - player.addChatMessage(new ChatComponentText(msg)); - } - } - - // Check claimable items - int claimableCount = getClaimableListings(uuid).size(); - if (claimableCount > 0) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "You have " + claimableCount + " item(s) waiting to be claimed!")); - } - } - - private EntityPlayerMP getOnlinePlayer(UUID uuid) { - if (CustomNpcs.getServer() == null) return null; - - for (Object obj : CustomNpcs.getServer().getConfigurationManager().playerEntityList) { - EntityPlayerMP player = (EntityPlayerMP) obj; - if (player.getUniqueID().equals(uuid)) { - return player; - } - } - return null; - } - - // ==================== Index Management ==================== - - private void addToSellerIndex(UUID seller, int listingId) { - listingsBySeller.computeIfAbsent(seller, k -> new ArrayList<>()).add(listingId); - } - - private void removeFromSellerIndex(UUID seller, int listingId) { - List ids = listingsBySeller.get(seller); - if (ids != null) { - ids.remove(Integer.valueOf(listingId)); - } - } - - private void addToBidderIndex(UUID bidder, int listingId) { - listingsByBidder.computeIfAbsent(bidder, k -> new ArrayList<>()).add(listingId); - } - - private void removeFromBidderIndex(UUID bidder, int listingId) { - List ids = listingsByBidder.get(bidder); - if (ids != null) { - ids.remove(Integer.valueOf(listingId)); - } - } - - private void rebuildIndices() { - listingsBySeller.clear(); - listingsByBidder.clear(); - - for (AuctionListing listing : listings.values()) { - addToSellerIndex(listing.sellerUUID, listing.id); - if (listing.highBidderUUID != null) { - addToBidderIndex(listing.highBidderUUID, listing.id); - } - } - } - - // ==================== Helpers ==================== - - public int getMaxListings(EntityPlayer player) { - return ConfigMarket.DefaultMaxListings; - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private void logEvent(String event, String details) { - LogWriter.info(LOG_PREFIX + event + ": " + details); - } - - private String getListingName(AuctionListing listing) { - if (listing == null || listing.item == null) { - return ""; - } - return listing.item.getDisplayName(); - } - - // ==================== Persistence ==================== - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) return; - - try { - NBTTagCompound compound = new NBTTagCompound(); - compound.setInteger("NextId", nextId); - - NBTTagList listingList = new NBTTagList(); - for (AuctionListing listing : listings.values()) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - compound.setTag("Listings", listingList); - - File file = new File(saveDir, "auctions.dat"); - File backup = new File(saveDir, "auctions.dat_old"); - - if (file.exists()) { - if (backup.exists()) backup.delete(); - file.renameTo(backup); - } - - file = new File(saveDir, "auctions.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - - } catch (Exception e) { - LogWriter.error(LOG_PREFIX + "Error saving auctions", e); - } - } - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) return; - - listings.clear(); - - try { - File file = new File(saveDir, "auctions.dat"); - if (!file.exists()) { - file = new File(saveDir, "auctions.dat_old"); - if (!file.exists()) return; - } - - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - nextId = compound.getInteger("NextId"); - if (nextId < 1) nextId = 1; - - NBTTagList listingList = compound.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingList.getCompoundTagAt(i)); - listings.put(listing.id, listing); - } - - rebuildIndices(); - - logEvent("SYSTEM_STARTUP", String.format("Loaded %d listings, %d active", - listings.size(), getActiveListingCount())); - - } catch (Exception e) { - LogWriter.error(LOG_PREFIX + "Error loading auctions", e); - } - } - - // ==================== Statistics ==================== - - public int getActiveListingCount() { - return (int) listings.values().stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .count(); - } - - public int getTotalListingCount() { - return listings.size(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/CurrencyController.java b/src/main/java/noppes/npcs/controllers/CurrencyController.java deleted file mode 100644 index 16d6c99f4..000000000 --- a/src/main/java/noppes/npcs/controllers/CurrencyController.java +++ /dev/null @@ -1,400 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.CustomNpcs; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.PlayerData; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.*; - -/** - * Controller for managing currency operations including: - * - Claims system (for auction refunds, NPC payments, etc.) - * - Transaction history - * - Currency administration - * - * Delegates actual balance operations to VaultHelper. - */ -public class CurrencyController { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static CurrencyController Instance; - - // Pending claims per player UUID - claims that can be collected - private Map> pendingClaims = new HashMap<>(); - - // File path for persistence - private String filePath = ""; - - public CurrencyController() { - Instance = this; - load(); - } - - /** - * Get player's current balance - */ - public long getBalance(EntityPlayer player) { - return VaultHelper.Instance.getBalance(player); - } - - /** - * Deposit currency directly to player's balance - */ - public boolean deposit(EntityPlayer player, long amount) { - return VaultHelper.Instance.deposit(player, amount); - } - - /** - * Withdraw currency from player's balance - */ - public boolean withdraw(EntityPlayer player, long amount) { - return VaultHelper.Instance.withdraw(player, amount); - } - - /** - * Check if player can afford an amount - */ - public boolean canAfford(EntityPlayer player, long amount) { - return VaultHelper.Instance.canAfford(player, amount); - } - - /** - * Transfer currency between players - */ - public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { - return VaultHelper.Instance.transfer(from, to, amount); - } - - /** - * Format amount for display - */ - public String formatAmount(long amount) { - return VaultHelper.Instance.formatAmount(amount); - } - - // ==================== CLAIMS SYSTEM ==================== - - /** - * Add a claim for a player (e.g., auction refund, NPC payment) - * Claims can be collected later by the player. - * - * @param playerUUID The player's UUID - * @param amount The amount to claim - * @param source Description of where this claim came from - * @param sourceId Optional ID of the source (e.g., auction ID) - */ - public void addClaim(UUID playerUUID, long amount, String source, String sourceId) { - if (amount <= 0) { - return; - } - - CurrencyClaim claim = new CurrencyClaim(); - claim.amount = amount; - claim.source = source; - claim.sourceId = sourceId; - claim.timestamp = System.currentTimeMillis(); - claim.expiresAt = claim.timestamp + (ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L); - - List claims = pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()); - claims.add(claim); - - save(); - logger.info("Added claim of {} for player {} from {}", amount, playerUUID, source); - } - - /** - * Get all pending claims for a player - */ - public List getPendingClaims(EntityPlayer player) { - return getPendingClaims(player.getUniqueID()); - } - - /** - * Get all pending claims for a player by UUID - */ - public List getPendingClaims(UUID playerUUID) { - List claims = pendingClaims.get(playerUUID); - if (claims == null) { - return new ArrayList<>(); - } - - // Filter out expired claims - long now = System.currentTimeMillis(); - claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); - - return new ArrayList<>(claims); - } - - /** - * Get total pending claim amount for a player - */ - public long getTotalPendingClaims(EntityPlayer player) { - return getTotalPendingClaims(player.getUniqueID()); - } - - /** - * Get total pending claim amount for a player by UUID - */ - public long getTotalPendingClaims(UUID playerUUID) { - List claims = getPendingClaims(playerUUID); - long total = 0; - for (CurrencyClaim claim : claims) { - total += claim.amount; - } - return total; - } - - /** - * Claim all pending claims for a player - * @return Amount successfully claimed - */ - public long claimAll(EntityPlayer player) { - List claims = getPendingClaims(player); - if (claims.isEmpty()) { - return 0; - } - - long totalClaimed = 0; - - for (CurrencyClaim claim : claims) { - if (deposit(player, claim.amount)) { - totalClaimed += claim.amount; - } - } - - // Clear claims after successful deposit - pendingClaims.remove(player.getUniqueID()); - save(); - - return totalClaimed; - } - - /** - * Claim a specific claim by index - * @return true if successful - */ - public boolean claimSingle(EntityPlayer player, int claimIndex) { - List claims = pendingClaims.get(player.getUniqueID()); - if (claims == null || claimIndex < 0 || claimIndex >= claims.size()) { - return false; - } - - CurrencyClaim claim = claims.get(claimIndex); - if (deposit(player, claim.amount)) { - claims.remove(claimIndex); - if (claims.isEmpty()) { - pendingClaims.remove(player.getUniqueID()); - } - save(); - return true; - } - - return false; - } - - /** - * Check if player has any pending claims - */ - public boolean hasPendingClaims(EntityPlayer player) { - return !getPendingClaims(player).isEmpty(); - } - - // ==================== PERSISTENCE ==================== - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - filePath = saveDir.getAbsolutePath(); - pendingClaims.clear(); - - try { - File file = new File(saveDir, "currency_claims.dat"); - if (file.exists()) { - loadFromFile(file); - } - } catch (Exception e) { - logger.error("Error loading currency claims", e); - try { - File file = new File(saveDir, "currency_claims.dat_old"); - if (file.exists()) { - loadFromFile(file); - } - } catch (Exception ee) { - logger.error("Error loading backup currency claims", ee); - } - } - - // Clean expired claims - cleanExpiredClaims(); - } - - private void loadFromFile(File file) throws Exception { - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - NBTTagList claimsList = compound.getTagList("Claims", 10); - for (int i = 0; i < claimsList.tagCount(); i++) { - NBTTagCompound playerCompound = claimsList.getCompoundTagAt(i); - UUID playerUUID = UUID.fromString(playerCompound.getString("UUID")); - - List playerClaims = new ArrayList<>(); - NBTTagList playerClaimsList = playerCompound.getTagList("PlayerClaims", 10); - for (int j = 0; j < playerClaimsList.tagCount(); j++) { - NBTTagCompound claimCompound = playerClaimsList.getCompoundTagAt(j); - CurrencyClaim claim = new CurrencyClaim(); - claim.readFromNBT(claimCompound); - playerClaims.add(claim); - } - - if (!playerClaims.isEmpty()) { - pendingClaims.put(playerUUID, playerClaims); - } - } - - logger.info("Loaded {} player claim records", pendingClaims.size()); - } - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - try { - File file = new File(saveDir, "currency_claims.dat"); - File backup = new File(saveDir, "currency_claims.dat_old"); - - if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } - file.renameTo(backup); - } - - NBTTagCompound compound = new NBTTagCompound(); - NBTTagList claimsList = new NBTTagList(); - - for (Map.Entry> entry : pendingClaims.entrySet()) { - if (entry.getValue().isEmpty()) { - continue; - } - - NBTTagCompound playerCompound = new NBTTagCompound(); - playerCompound.setString("UUID", entry.getKey().toString()); - - NBTTagList playerClaimsList = new NBTTagList(); - for (CurrencyClaim claim : entry.getValue()) { - NBTTagCompound claimCompound = new NBTTagCompound(); - claim.writeToNBT(claimCompound); - playerClaimsList.appendTag(claimCompound); - } - playerCompound.setTag("PlayerClaims", playerClaimsList); - - claimsList.appendTag(playerCompound); - } - - compound.setTag("Claims", claimsList); - - file = new File(saveDir, "currency_claims.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - } catch (Exception e) { - logger.error("Error saving currency claims", e); - } - } - - /** - * Clean up expired claims - */ - public void cleanExpiredClaims() { - long now = System.currentTimeMillis(); - boolean changed = false; - - Iterator>> it = pendingClaims.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry> entry = it.next(); - List claims = entry.getValue(); - - int before = claims.size(); - claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); - - if (claims.size() != before) { - changed = true; - } - - if (claims.isEmpty()) { - it.remove(); - } - } - - if (changed) { - save(); - } - } - - // ==================== CLAIM DATA CLASS ==================== - - public static class CurrencyClaim { - public long amount; - public String source; // e.g., "Auction Refund", "NPC Payment", "Quest Reward" - public String sourceId; // e.g., auction ID - public long timestamp; // When the claim was created - public long expiresAt; // When the claim expires (0 = never) - - public void writeToNBT(NBTTagCompound compound) { - compound.setLong("Amount", amount); - compound.setString("Source", source != null ? source : ""); - compound.setString("SourceId", sourceId != null ? sourceId : ""); - compound.setLong("Timestamp", timestamp); - compound.setLong("ExpiresAt", expiresAt); - } - - public void readFromNBT(NBTTagCompound compound) { - amount = compound.getLong("Amount"); - source = compound.getString("Source"); - sourceId = compound.getString("SourceId"); - timestamp = compound.getLong("Timestamp"); - expiresAt = compound.getLong("ExpiresAt"); - } - - /** - * Get formatted time remaining until expiration - */ - public String getTimeRemaining() { - if (expiresAt <= 0) { - return "Never"; - } - - long remaining = expiresAt - System.currentTimeMillis(); - if (remaining <= 0) { - return "Expired"; - } - - long days = remaining / (24L * 60L * 60L * 1000L); - long hours = (remaining % (24L * 60L * 60L * 1000L)) / (60L * 60L * 1000L); - - if (days > 0) { - return days + " days, " + hours + " hours"; - } else if (hours > 0) { - return hours + " hours"; - } else { - long minutes = remaining / (60L * 1000L); - return minutes + " minutes"; - } - } - } -} diff --git a/src/main/java/noppes/npcs/controllers/MarketRegistry.java b/src/main/java/noppes/npcs/controllers/MarketRegistry.java new file mode 100644 index 000000000..1110f9e5f --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/MarketRegistry.java @@ -0,0 +1,214 @@ +package noppes.npcs.controllers; + +import foxz.utils.Market; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.roles.RoleTrader; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Central registry for linked market synchronization. + * Tracks which RoleTrader instances use which markets, + * and which players are viewing each market. + * + * This enables immediate sync across all traders sharing + * the same market when a purchase occurs. + */ +public class MarketRegistry { + + // marketName -> Set of traders using this market (WeakHashMap for auto-cleanup) + private static final Map> marketTraders = new ConcurrentHashMap<>(); + + // marketName -> Set of players viewing this market (across all NPCs) + private static final Map> marketViewers = new ConcurrentHashMap<>(); + + // ==================== Trader Registration ==================== + + /** + * Register a trader as using a linked market. + * Should be called when NPC loads or marketName is set. + */ + public static void registerTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (traders) { + traders.add(trader); + } + } + + /** + * Unregister a trader from a linked market. + * Should be called when NPC unloads or marketName changes. + */ + public static void unregisterTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.get(marketName); + if (traders != null) { + synchronized (traders) { + traders.remove(trader); + if (traders.isEmpty()) { + marketTraders.remove(marketName); + } + } + } + } + + /** + * Change a trader's market registration. + * Handles unregistering from old market and registering to new. + */ + public static void updateTraderMarket(String oldMarket, String newMarket, RoleTrader trader) { + if (trader == null) return; + + if (oldMarket != null && !oldMarket.isEmpty()) { + unregisterTrader(oldMarket, trader); + } + if (newMarket != null && !newMarket.isEmpty()) { + registerTrader(newMarket, trader); + } + } + + // ==================== Viewer Registration ==================== + + /** + * Register a player as viewing a market (via any NPC). + * Should be called when player opens trader GUI with linked market. + */ + public static void registerViewer(String marketName, EntityPlayerMP player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (viewers) { + viewers.add(player); + } + } + + /** + * Unregister a player from viewing a market. + * Should be called when player closes trader GUI. + */ + public static void unregisterViewer(String marketName, EntityPlayerMP player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.get(marketName); + if (viewers != null) { + synchronized (viewers) { + viewers.remove(player); + if (viewers.isEmpty()) { + marketViewers.remove(marketName); + } + } + } + } + + // ==================== Synchronization ==================== + + /** + * Sync stock data to all traders and viewers of a market. + * Called after a purchase to update all linked traders and their viewers. + * + * @param marketName The market to sync + */ + public static void syncMarket(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + // Get latest data from cache + NBTTagCompound marketData = Market.getMarketCache(marketName); + if (marketData == null) return; + + // Update all traders' internal stock data + Set traders = marketTraders.get(marketName); + if (traders != null) { + NBTTagCompound stockData = marketData.getCompoundTag("Stock"); + + synchronized (traders) { + // Clean up any null references (WeakHashMap cleanup) + traders.removeIf(t -> t == null || t.npc == null); + + for (RoleTrader trader : traders) { + // Update stock data from cache + if (stockData != null && stockData.hasKey("EnableStock")) { + trader.stock.readFromNBT(stockData); + } + } + } + } + + // Sync all viewers (across all NPCs for this market) + syncMarketViewers(marketName); + } + + /** + * Sync stock data to all players viewing a specific market. + * Each viewer gets data relative to their own player (for per-player stock display). + */ + public static void syncMarketViewers(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + Set viewers = marketViewers.get(marketName); + if (viewers == null || viewers.isEmpty()) return; + + // Find any trader with this market to use for sync (they all have same data now) + Set traders = marketTraders.get(marketName); + if (traders == null || traders.isEmpty()) return; + + RoleTrader syncSource = null; + synchronized (traders) { + for (RoleTrader trader : traders) { + if (trader != null && trader.npc != null) { + syncSource = trader; + break; + } + } + } + + if (syncSource == null) return; + + // Sync to all viewers + synchronized (viewers) { + // Remove disconnected players + viewers.removeIf(p -> p == null || p.playerNetServerHandler == null); + + for (EntityPlayerMP viewer : viewers) { + syncSource.syncToPlayer(viewer); + } + } + } + + // ==================== Utility ==================== + + /** + * Get count of traders registered to a market (for debugging/info). + */ + public static int getTraderCount(String marketName) { + Set traders = marketTraders.get(marketName); + return traders != null ? traders.size() : 0; + } + + /** + * Get count of viewers for a market (for debugging/info). + */ + public static int getViewerCount(String marketName) { + Set viewers = marketViewers.get(marketName); + return viewers != null ? viewers.size() : 0; + } + + /** + * Clear all registrations (for server shutdown/reload). + */ + public static void clear() { + marketTraders.clear(); + marketViewers.clear(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java deleted file mode 100644 index 54b9235ef..000000000 --- a/src/main/java/noppes/npcs/controllers/VaultHelper.java +++ /dev/null @@ -1,272 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import noppes.npcs.CustomNpcs; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.controllers.PlayerDataController; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.lang.reflect.Method; - -/** - * Helper class for VaultAPI integration on Bukkit hybrid servers. - * Uses reflection to integrate with Vault when available. - * Falls back to internal currency system when Vault is not present. - */ -public class VaultHelper { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static VaultHelper Instance; - - // Vault reflection references - private boolean vaultAvailable = false; - private Object economy = null; - private Method getBalance; - private Method depositPlayer; - private Method withdrawPlayer; - private Method hasAccount; - private Method createPlayerAccount; - private Method responseSuccess; - - // Bukkit references for getting offline player - private Class bukkitClass; - private Method getOfflinePlayer; - - public VaultHelper() { - Instance = this; - - if (!ConfigMarket.UseVaultIfAvailable) { - logger.info("Vault integration disabled by config"); - return; - } - - try { - // Check for Bukkit first - bukkitClass = Class.forName("org.bukkit.Bukkit"); - getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); - - // Check for Vault - Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); - - // Get the Economy service from Bukkit's ServicesManager - Method getServicesManager = bukkitClass.getMethod("getServicesManager"); - Object servicesManager = getServicesManager.invoke(null); - - Method getRegistration = servicesManager.getClass().getMethod("getRegistration", Class.class); - Object registration = getRegistration.invoke(servicesManager, vaultClass); - - if (registration != null) { - Method getProvider = registration.getClass().getMethod("getProvider"); - economy = getProvider.invoke(registration); - - if (economy != null) { - // Get Economy methods - using OfflinePlayer versions for broader compatibility - Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); - getBalance = economy.getClass().getMethod("getBalance", offlinePlayerClass); - depositPlayer = economy.getClass().getMethod("depositPlayer", offlinePlayerClass, double.class); - withdrawPlayer = economy.getClass().getMethod("withdrawPlayer", offlinePlayerClass, double.class); - hasAccount = economy.getClass().getMethod("hasAccount", offlinePlayerClass); - createPlayerAccount = economy.getClass().getMethod("createPlayerAccount", offlinePlayerClass); - - // EconomyResponse.transactionSuccess() method - Class economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); - responseSuccess = economyResponseClass.getField("transactionSuccess").getClass().getMethod("booleanValue"); - - vaultAvailable = true; - logger.info("Vault Economy integration enabled - using external economy"); - } - } - - if (!vaultAvailable) { - logger.info("Vault found but no Economy provider registered - using internal currency"); - } - } catch (ClassNotFoundException e) { - // Vault not present - use internal currency - logger.info("Vault not found - using internal currency system"); - } catch (Exception e) { - logger.warn("Error initializing Vault integration - using internal currency: " + e.getMessage()); - } - } - - /** - * Check if Vault economy is available and enabled - */ - public boolean isVaultEnabled() { - return vaultAvailable && ConfigMarket.UseVaultIfAvailable; - } - - /** - * Get player's balance - * Uses Vault if available, otherwise internal currency - */ - public long getBalance(EntityPlayer player) { - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - Double balance = (Double) getBalance.invoke(economy, offlinePlayer); - return balance.longValue(); - } catch (Exception e) { - logger.warn("Vault getBalance failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - return data.currencyData.getBalance(); - } - - /** - * Deposit currency to player's balance - * Uses Vault if available, otherwise internal currency - * @return true if successful - */ - public boolean deposit(EntityPlayer player, long amount) { - if (amount <= 0) { - return false; - } - - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - - // Ensure account exists - Boolean hasAcc = (Boolean) hasAccount.invoke(economy, offlinePlayer); - if (!hasAcc) { - createPlayerAccount.invoke(economy, offlinePlayer); - } - - Object response = depositPlayer.invoke(economy, offlinePlayer, (double) amount); - // Check if transaction succeeded by checking the transactionSuccess field - Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); - - if (success) { - // Also update internal tracking for statistics - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(getBalance(player)); - return true; - } - return false; - } catch (Exception e) { - logger.warn("Vault deposit failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - boolean success = data.currencyData.deposit(amount); - if (success) { - data.save(); - } - return success; - } - - /** - * Withdraw currency from player's balance - * Uses Vault if available, otherwise internal currency - * @return true if successful - */ - public boolean withdraw(EntityPlayer player, long amount) { - if (amount <= 0) { - return false; - } - - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - - // Check if player has enough - Double currentBalance = (Double) getBalance.invoke(economy, offlinePlayer); - if (currentBalance < amount) { - return false; - } - - Object response = withdrawPlayer.invoke(economy, offlinePlayer, (double) amount); - Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); - - if (success) { - // Also update internal tracking for statistics - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(getBalance(player)); - return true; - } - return false; - } catch (Exception e) { - logger.warn("Vault withdraw failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - boolean success = data.currencyData.withdraw(amount); - if (success) { - data.save(); - } - return success; - } - - /** - * Check if player can afford an amount - */ - public boolean canAfford(EntityPlayer player, long amount) { - return getBalance(player) >= amount; - } - - /** - * Transfer currency from one player to another - * @return true if successful - */ - public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { - if (amount <= 0) { - return false; - } - - if (!canAfford(from, amount)) { - return false; - } - - // Withdraw from sender first - if (!withdraw(from, amount)) { - return false; - } - - // Deposit to receiver - if (!deposit(to, amount)) { - // Rollback - deposit back to sender - deposit(from, amount); - return false; - } - - return true; - } - - /** - * Set player's balance directly (admin operation) - * Only works with internal currency - Vault manages its own balances - */ - public void setBalance(EntityPlayer player, long amount) { - if (isVaultEnabled()) { - logger.warn("Cannot set balance directly when using Vault - use Vault's economy commands"); - return; - } - - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(amount); - data.save(); - } - - /** - * Format an amount for display with currency name - */ - public String formatAmount(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - /** - * Get the currency name - */ - public String getCurrencyName() { - return ConfigMarket.CurrencyName; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java deleted file mode 100644 index 176896fae..000000000 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ /dev/null @@ -1,226 +0,0 @@ -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.constants.EnumAuctionSort; - -/** - * Filter/search criteria for auction listings. - * Used to query the auction controller with specific criteria. - */ -public class AuctionFilter { - - /** Search text to match against item names */ - public String searchText = ""; - - /** How to sort results */ - public EnumAuctionSort sortOrder = EnumAuctionSort.ENDING_SOON; - - /** Minimum current bid/price filter */ - public long minPrice = 0; - - /** Maximum current bid/price filter (0 = no max) */ - public long maxPrice = 0; - - /** Only show listings with buyout option */ - public boolean buyoutOnly = false; - - /** Only show listings without any bids yet */ - public boolean noBidsOnly = false; - - /** Page number for pagination (0-indexed) */ - public int page = 0; - - /** Items per page */ - public int pageSize = 20; - - /** Filter by specific seller UUID string (empty = all sellers) */ - public String sellerUUID = ""; - - /** Filter by specific seller name (partial match, empty = all) */ - public String sellerName = ""; - - public AuctionFilter() { - } - - /** - * Create a filter with search text - */ - public static AuctionFilter search(String text) { - AuctionFilter filter = new AuctionFilter(); - filter.searchText = text != null ? text : ""; - return filter; - } - - /** - * Create a filter for a specific seller - */ - public static AuctionFilter seller(String sellerUUID) { - AuctionFilter filter = new AuctionFilter(); - filter.sellerUUID = sellerUUID != null ? sellerUUID : ""; - return filter; - } - - // Builder-style methods for chaining - - public AuctionFilter withSearch(String text) { - this.searchText = text != null ? text : ""; - return this; - } - - public AuctionFilter withSort(EnumAuctionSort sort) { - this.sortOrder = sort; - return this; - } - - public AuctionFilter withPriceRange(long min, long max) { - this.minPrice = min; - this.maxPrice = max; - return this; - } - - public AuctionFilter withBuyoutOnly(boolean buyoutOnly) { - this.buyoutOnly = buyoutOnly; - return this; - } - - public AuctionFilter withNoBidsOnly(boolean noBidsOnly) { - this.noBidsOnly = noBidsOnly; - return this; - } - - public AuctionFilter withPage(int page, int pageSize) { - this.page = Math.max(0, page); - this.pageSize = Math.max(1, Math.min(100, pageSize)); - return this; - } - - public AuctionFilter withSeller(String sellerUUID, String sellerName) { - this.sellerUUID = sellerUUID != null ? sellerUUID : ""; - this.sellerName = sellerName != null ? sellerName : ""; - return this; - } - - /** - * Check if a listing matches this filter's criteria - */ - public boolean matches(AuctionListing listing) { - if (listing == null) { - return false; - } - if (listing.item == null) { - return false; - } - - // Search text filter (case-insensitive) - if (!searchText.isEmpty()) { - String lowerSearch = searchText.toLowerCase(); - String itemName = listing.item.getDisplayName().toLowerCase(); - if (!itemName.contains(lowerSearch)) { - return false; - } - } - - // Price range filter (uses current bid or starting price) - long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - if (minPrice > 0 && price < minPrice) { - return false; - } - if (maxPrice > 0 && price > maxPrice) { - return false; - } - - // Buyout only filter - if (buyoutOnly && listing.buyoutPrice <= 0) { - return false; - } - - // No bids only filter - if (noBidsOnly && listing.hasBids()) { - return false; - } - - // Seller UUID filter - if (!sellerUUID.isEmpty() && !listing.sellerUUID.toString().equals(sellerUUID)) { - return false; - } - - // Seller name filter (partial match) - if (!sellerName.isEmpty()) { - String lowerName = sellerName.toLowerCase(); - if (!listing.sellerName.toLowerCase().contains(lowerName)) { - return false; - } - } - - return true; - } - - /** - * Reset all filters to defaults - */ - public void reset() { - searchText = ""; - sortOrder = EnumAuctionSort.ENDING_SOON; - minPrice = 0; - maxPrice = 0; - buyoutOnly = false; - noBidsOnly = false; - page = 0; - sellerUUID = ""; - sellerName = ""; - } - - /** - * Write filter to NBT for network transmission - */ - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("SearchText", searchText); - compound.setInteger("SortOrder", sortOrder.ordinal()); - compound.setLong("MinPrice", minPrice); - compound.setLong("MaxPrice", maxPrice); - compound.setBoolean("BuyoutOnly", buyoutOnly); - compound.setBoolean("NoBidsOnly", noBidsOnly); - compound.setInteger("Page", page); - compound.setInteger("PageSize", pageSize); - compound.setString("SellerUUID", sellerUUID); - compound.setString("SellerName", sellerName); - return compound; - } - - /** - * Read filter from NBT - */ - public void readFromNBT(NBTTagCompound compound) { - searchText = compound.getString("SearchText"); - - int sortOrdinal = compound.getInteger("SortOrder"); - if (sortOrdinal >= 0 && sortOrdinal < EnumAuctionSort.values().length) { - sortOrder = EnumAuctionSort.values()[sortOrdinal]; - } - - minPrice = compound.getLong("MinPrice"); - maxPrice = compound.getLong("MaxPrice"); - buyoutOnly = compound.getBoolean("BuyoutOnly"); - noBidsOnly = compound.getBoolean("NoBidsOnly"); - page = Math.max(0, compound.getInteger("Page")); - pageSize = compound.getInteger("PageSize"); - if (pageSize <= 0) { - pageSize = 20; - } else { - pageSize = Math.min(100, pageSize); - } - sellerUUID = compound.getString("SellerUUID"); - sellerName = compound.getString("SellerName"); - } - - @Override - public String toString() { - return "AuctionFilter{" + - "search='" + searchText + '\'' + - ", sort=" + sortOrder + - ", price=" + minPrice + "-" + maxPrice + - ", buyoutOnly=" + buyoutOnly + - ", page=" + page + - '}'; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java deleted file mode 100644 index 5366e367b..000000000 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ /dev/null @@ -1,397 +0,0 @@ -package noppes.npcs.controllers.data; - -import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.AuctionBlacklist; - -import java.util.UUID; - -/** - * Represents a single auction listing. - * Contains all data about the item, seller, bidding, and timing. - */ -public class AuctionListing { - // Unique identifier - public int id; - - // Seller info - public UUID sellerUUID; - public String sellerName; - - // Item being sold - public ItemStack item; - - // Pricing - public long startingPrice; - public long buyoutPrice; // 0 = no buyout option - public long currentBid; - - // Current high bidder - public UUID highBidderUUID; - public String highBidderName; - - // Previous bidder (for refund notifications) - public UUID previousBidderUUID; - public String previousBidderName; - public long previousBidAmount; - - // Timing - public long listingTime; // When the auction was created - public long expirationTime; // When the auction ends - public EnumAuctionDuration duration; - - // Status - public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; - - // Claim tracking - public boolean sellerClaimed = false; // Has seller claimed their proceeds? - public boolean buyerClaimed = false; // Has buyer claimed their item? - - // Bid tracking - public int bidCount = 0; // Number of bids placed - - public AuctionListing() { - } - - /** - * Alias for listingTime (when the auction was created) - */ - public long getCreationTime() { - return listingTime; - } - - // For sorting compatibility - public long creationTime() { - return listingTime; - } - - /** - * Create a new auction listing - */ - public AuctionListing(UUID seller, String sellerName, ItemStack item, - long startPrice, long buyout, EnumAuctionDuration duration) { - this.sellerUUID = seller; - this.sellerName = sellerName; - this.item = item.copy(); - this.startingPrice = startPrice; - this.buyoutPrice = buyout; - this.currentBid = 0; // No bids yet - this.duration = duration; - this.listingTime = System.currentTimeMillis(); - this.expirationTime = listingTime + duration.getMillis(); - this.status = EnumAuctionStatus.ACTIVE; - } - - // ==================== Status Checks ==================== - - /** - * Check if auction has expired - */ - public boolean isExpired() { - return System.currentTimeMillis() >= expirationTime; - } - - /** - * Check if auction is still active - */ - public boolean isActive() { - return status == EnumAuctionStatus.ACTIVE && !isExpired(); - } - - /** - * Get time remaining in milliseconds - */ - public long getTimeRemaining() { - return Math.max(0, expirationTime - System.currentTimeMillis()); - } - - /** - * Get formatted time remaining - */ - public String getTimeRemainingFormatted() { - long remaining = getTimeRemaining(); - - if (remaining <= 0) { - return "Ended"; - } - - long hours = remaining / (60L * 60L * 1000L); - long minutes = (remaining % (60L * 60L * 1000L)) / (60L * 1000L); - - if (hours > 24) { - long days = hours / 24; - hours = hours % 24; - return days + "d " + hours + "h"; - } else if (hours > 0) { - return hours + "h " + minutes + "m"; - } else { - long seconds = (remaining % (60L * 1000L)) / 1000L; - return minutes + "m " + seconds + "s"; - } - } - - /** - * Check if auction has any bids - */ - public boolean hasBids() { - return highBidderUUID != null; - } - - // ==================== Bidding Methods ==================== - - /** - * Calculate minimum valid bid amount - */ - public long getMinimumBid() { - if (!hasBids()) { - return startingPrice; - } - // Minimum increment is configured percent or 1, whichever is greater - long minIncrement = Math.max(1, (long) (currentBid * ConfigMarket.MinBidIncrementPercent)); - return currentBid + minIncrement; - } - - /** - * Place a bid - * @return true if bid was successful - */ - public boolean placeBid(UUID bidder, String bidderName, long amount) { - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - if (isExpired()) { - return false; - } - if (amount < getMinimumBid()) { - return false; - } - if (bidder.equals(sellerUUID)) { - return false; // Can't bid on own item - } - - // Store previous bidder for refund notification - if (highBidderUUID != null) { - previousBidderUUID = highBidderUUID; - previousBidderName = highBidderName; - previousBidAmount = currentBid; - } - - // Update high bidder - highBidderUUID = bidder; - highBidderName = bidderName; - currentBid = amount; - bidCount++; - - // Snipe protection - extend if bid placed in final minutes - if (ConfigMarket.SnipeProtectionMinutes > 0) { - long timeRemaining = getTimeRemaining(); - long thresholdMs = ConfigMarket.SnipeProtectionThreshold * 60L * 1000L; - - if (timeRemaining < thresholdMs) { - // Extend TO snipe protection time, not by it - long newExpiration = System.currentTimeMillis() + - (ConfigMarket.SnipeProtectionMinutes * 60L * 1000L); - // Only extend if it would actually extend the auction - if (newExpiration > expirationTime) { - expirationTime = newExpiration; - } - } - } - - return true; - } - - /** - * Execute buyout (instant purchase at buyout price) - * @return true if buyout was successful - */ - public boolean buyout(UUID buyer, String buyerName) { - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - if (isExpired()) { - return false; - } - if (buyoutPrice <= 0) { - return false; // No buyout available - } - if (buyer.equals(sellerUUID)) { - return false; // Can't buy own item - } - - // Store previous bidder for refund - if (highBidderUUID != null) { - previousBidderUUID = highBidderUUID; - previousBidderName = highBidderName; - previousBidAmount = currentBid; - } - - highBidderUUID = buyer; - highBidderName = buyerName; - currentBid = buyoutPrice; - status = EnumAuctionStatus.SOLD; - - return true; - } - - /** - * Cancel listing (seller only, before any bids) - * @return true if cancellation was successful - */ - public boolean cancel(UUID requester) { - if (!requester.equals(sellerUUID)) { - return false; // Only seller can cancel - } - if (hasBids()) { - return false; // Can't cancel with active bids - } - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - - status = EnumAuctionStatus.CANCELLED; - return true; - } - - /** - * Finalize auction when it expires - * Call this when auction timer runs out - */ - public void finalize() { - if (status != EnumAuctionStatus.ACTIVE) { - return; - } - - if (hasBids()) { - status = EnumAuctionStatus.SOLD; - } else { - status = EnumAuctionStatus.EXPIRED; - } - } - - // ==================== Validation ==================== - - /** - * Validate that the item can be listed - * @return null if valid, error message if invalid - */ - public static String validateItem(ItemStack item) { - if (item == null || item.getItem() == null) { - return "Invalid item"; - } - - // Check trade restrictions (Untradeable, ProfileSlot Bound, Soulbound) - if (!ItemTradeAttribute.canTrade(item)) { - String reason = ItemTradeAttribute.getTradeBlockReason(item); - return reason != null ? reason : "Item cannot be traded"; - } - - // Check blacklist - if (AuctionBlacklist.Instance != null && AuctionBlacklist.Instance.isBlacklisted(item)) { - return "Item is blacklisted from auction"; - } - - return null; // Valid - } - - // ==================== Financial Calculations ==================== - - /** - * Get listing fee for this auction - */ - public long getListingFee() { - long flatFee = duration.getListingFee(); - long percentFee = (long) (startingPrice * ConfigMarket.ListingFeePercent); - return flatFee + percentFee; - } - - /** - * Get seller's proceeds after sales tax - */ - public long getSellerProceeds() { - if (status != EnumAuctionStatus.SOLD) { - return 0; - } - long tax = (long) (currentBid * ConfigMarket.SalesTaxPercent); - return currentBid - tax; - } - - // ==================== NBT Serialization ==================== - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("Id", id); - compound.setString("SellerUUID", sellerUUID.toString()); - compound.setString("SellerName", sellerName); - compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); - compound.setLong("StartPrice", startingPrice); - compound.setLong("BuyoutPrice", buyoutPrice); - compound.setLong("CurrentBid", currentBid); - - if (highBidderUUID != null) { - compound.setString("BidderUUID", highBidderUUID.toString()); - compound.setString("BidderName", highBidderName); - } - - if (previousBidderUUID != null) { - compound.setString("PrevBidderUUID", previousBidderUUID.toString()); - compound.setString("PrevBidderName", previousBidderName); - compound.setLong("PrevBidAmount", previousBidAmount); - } - - compound.setLong("ListingTime", listingTime); - compound.setLong("ExpirationTime", expirationTime); - compound.setInteger("Duration", duration.ordinal()); - compound.setInteger("Status", status.ordinal()); - compound.setBoolean("SellerClaimed", sellerClaimed); - compound.setBoolean("BuyerClaimed", buyerClaimed); - compound.setInteger("BidCount", bidCount); - - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - id = compound.getInteger("Id"); - sellerUUID = UUID.fromString(compound.getString("SellerUUID")); - sellerName = compound.getString("SellerName"); - item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); - startingPrice = compound.getLong("StartPrice"); - buyoutPrice = compound.getLong("BuyoutPrice"); - currentBid = compound.getLong("CurrentBid"); - - if (compound.hasKey("BidderUUID")) { - highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); - highBidderName = compound.getString("BidderName"); - } - - if (compound.hasKey("PrevBidderUUID")) { - previousBidderUUID = UUID.fromString(compound.getString("PrevBidderUUID")); - previousBidderName = compound.getString("PrevBidderName"); - previousBidAmount = compound.getLong("PrevBidAmount"); - } - - listingTime = compound.getLong("ListingTime"); - expirationTime = compound.getLong("ExpirationTime"); - - int durationOrdinal = compound.getInteger("Duration"); - if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { - duration = EnumAuctionDuration.values()[durationOrdinal]; - } else { - duration = EnumAuctionDuration.LONG; - } - - int statusOrdinal = compound.getInteger("Status"); - if (statusOrdinal >= 0 && statusOrdinal < EnumAuctionStatus.values().length) { - status = EnumAuctionStatus.values()[statusOrdinal]; - } else { - status = EnumAuctionStatus.ACTIVE; - } - - sellerClaimed = compound.getBoolean("SellerClaimed"); - buyerClaimed = compound.getBoolean("BuyerClaimed"); - bidCount = compound.getInteger("BidCount"); - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java index 511321dbb..1271594ba 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -1,24 +1,49 @@ package noppes.npcs.controllers.data; +import kamkeel.npcs.util.VaultUtil; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IPlayerCurrencyData; import noppes.npcs.config.ConfigMarket; /** * Stores player currency balance. * This data is NOT profile slot bound - all profile slots share the same currency pool. - * When Vault API is available, this serves as a cache/fallback. + * When Vault API is available and UseVault config is enabled, operations delegate to Vault. + * The built-in CNPC+ currency data is always preserved in NBT, but unused when Vault is active. */ -public class PlayerCurrencyData { +public class PlayerCurrencyData implements IPlayerCurrencyData { + private final PlayerData playerData; + + // Built-in CNPC+ currency (always saved, used as fallback) private long balance; private long lifetimeEarned; private long lifetimeSpent; - public PlayerCurrencyData() { + public PlayerCurrencyData(PlayerData playerData) { + this.playerData = playerData; this.balance = ConfigMarket.StartingBalance; this.lifetimeEarned = 0; this.lifetimeSpent = 0; } + /** + * Check if Vault should be used for currency operations. + * Requires both config enabled AND Vault actually available. + */ + private boolean shouldUseVault() { + return ConfigMarket.UseVault && VaultUtil.isEnabled(); + } + + /** + * Get player name for Vault operations + */ + private String getPlayerName() { + if (playerData != null && playerData.player != null) { + return playerData.player.getCommandSenderName(); + } + return playerData != null ? playerData.playername : null; + } + public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("CurrencyBalance")) { this.balance = compound.getLong("CurrencyBalance"); @@ -30,6 +55,7 @@ public class PlayerCurrencyData { } public void writeToNBT(NBTTagCompound compound) { + // Always save built-in currency data (preserved even when using Vault) compound.setLong("CurrencyBalance", this.balance); compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); @@ -38,13 +64,21 @@ public class PlayerCurrencyData { /** * Get the current balance */ + @Override public long getBalance() { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return (long) VaultUtil.getBalance(playerName); + } + } return balance; } /** * Set the balance directly (use with caution) */ + @Override public void setBalance(long balance) { if (balance < 0) { balance = 0; @@ -52,6 +86,14 @@ public class PlayerCurrencyData { if (balance > ConfigMarket.MaxBalance) { balance = ConfigMarket.MaxBalance; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + VaultUtil.setBalance(playerName, balance); + return; + } + } this.balance = balance; } @@ -60,10 +102,24 @@ public class PlayerCurrencyData { * @param amount Amount to add * @return true if successful, false if would exceed max balance */ + @Override public boolean deposit(long amount) { if (amount <= 0) { return false; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.addMoney(playerName, amount); + if (success) { + this.lifetimeEarned += amount; + } + return success; + } + } + + // Built-in currency long newBalance = this.balance + amount; if (newBalance > ConfigMarket.MaxBalance) { return false; @@ -82,10 +138,24 @@ public class PlayerCurrencyData { * @param amount Amount to remove * @return true if successful, false if insufficient funds */ + @Override public boolean withdraw(long amount) { if (amount <= 0) { return false; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.withdrawMoney(playerName, amount); + if (success) { + this.lifetimeSpent += amount; + } + return success; + } + } + + // Built-in currency if (this.balance < amount) { return false; } @@ -97,13 +167,21 @@ public class PlayerCurrencyData { /** * Check if player can afford an amount */ + @Override public boolean canAfford(long amount) { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return VaultUtil.has(playerName, amount); + } + } return this.balance >= amount; } /** * Get lifetime earned currency */ + @Override public long getLifetimeEarned() { return lifetimeEarned; } @@ -111,6 +189,7 @@ public class PlayerCurrencyData { /** * Get lifetime spent currency */ + @Override public long getLifetimeSpent() { return lifetimeSpent; } @@ -118,7 +197,11 @@ public class PlayerCurrencyData { /** * Format the balance for display */ + @Override public String formatBalance() { + if (shouldUseVault()) { + return VaultUtil.format(getBalance()); + } return formatAmount(this.balance); } @@ -128,4 +211,20 @@ public class PlayerCurrencyData { public static String formatAmount(long amount) { return String.format("%,d %s", amount, ConfigMarket.CurrencyName); } + + /** + * Check if Vault is being used for currency operations. + * Returns true only if config is enabled AND Vault is actually available. + */ + @Override + public boolean isUsingVault() { + return shouldUseVault(); + } + + /** + * Check if Vault is configured (regardless of availability) + */ + public boolean isVaultConfigured() { + return ConfigMarket.UseVault; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 08fdebdd0..d2a949074 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -17,6 +17,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; +import noppes.npcs.api.handler.IPlayerCurrencyData; import noppes.npcs.api.handler.IPlayerData; import noppes.npcs.api.handler.IPlayerDialogData; import noppes.npcs.api.handler.IPlayerFactionData; @@ -60,7 +61,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public MagicData magicData = new MagicData(); // Currency data - NOT slot-bound, shared across all profile slots - public PlayerCurrencyData currencyData = new PlayerCurrencyData(); + public PlayerCurrencyData currencyData = new PlayerCurrencyData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -400,6 +401,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return mailData; } + public IPlayerCurrencyData getCurrencyData() { + return currencyData; + } + public synchronized void save() { // Don't Save this is a Modification of a Profile's PlayerData Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid)); @@ -409,7 +414,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); - // Add currency data to main save (NOT slot-bound, preserved across profile changes) + // Currency data is saved at player level, NOT per profile slot currencyData.writeToNBT(compound); final String filename; @@ -449,11 +454,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { data = getNBT(); } - // Load currency BEFORE setNBT so we can preserve it - // Currency is stored in main file but NOT in slot data - if (data.hasKey("CurrencyBalance")) { - currencyData.readFromNBT(data); - } + // Load currency BEFORE setNBT - currency is player-level, NOT per profile slot + currencyData.readFromNBT(data); setNBT(data); } diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index f3c5f4db9..68bc96c53 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -45,7 +45,7 @@ public class TraderStock { * @param playerName The player name (for per-player mode) * @return Available stock count, or Integer.MAX_VALUE if unlimited */ - public int getAvailableStock(int slot, String playerName) { + public synchronized int getAvailableStock(int slot, String playerName) { if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { return Integer.MAX_VALUE; // Unlimited } @@ -75,7 +75,7 @@ public class TraderStock { * Consume stock when purchase is made * @return true if successful, false if insufficient stock */ - public boolean consumeStock(int slot, String playerName, int amount) { + public synchronized boolean consumeStock(int slot, String playerName, int amount) { if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { return true; // Unlimited, always succeeds } @@ -115,7 +115,7 @@ public class TraderStock { * Reset all stock to max values * @param currentTime Current time to record as reset time */ - public void resetStock(long currentTime) { + public synchronized void resetStock(long currentTime) { lastResetTime = currentTime; // Reset per-server stock @@ -123,7 +123,7 @@ public class TraderStock { currentStock[i] = maxStock[i]; } - // Clear per-player stock + // Clear per-player stock (cleanup per-player data on reset) playerStock.clear(); } @@ -217,6 +217,44 @@ public class TraderStock { } } + /** + * Validate and clamp all current stock values to not exceed max stock. + * Should be called when trader GUI opens to ensure consistency. + * @return true if any stock was clamped + */ + public boolean validateStock() { + boolean changed = false; + for (int i = 0; i < 18; i++) { + if (maxStock[i] >= 0) { + // Has a max limit + if (currentStock[i] < 0) { + // Not initialized, set to max + currentStock[i] = maxStock[i]; + changed = true; + } else if (currentStock[i] > maxStock[i]) { + // Exceeds max, clamp down + currentStock[i] = maxStock[i]; + changed = true; + } + } + } + + // Also validate per-player stock + for (PlayerTraderStock pStock : playerStock.values()) { + for (int i = 0; i < 18; i++) { + if (maxStock[i] >= 0) { + int available = pStock.getStock(i, maxStock[i]); + if (available < 0) { + // Over-purchased somehow, reset + pStock.purchasedAmounts[i] = maxStock[i]; + changed = true; + } + } + } + } + return changed; + } + // ==================== NBT Serialization ==================== public NBTTagCompound writeToNBT(NBTTagCompound compound) { @@ -280,7 +318,6 @@ public class TraderStock { public static class PlayerTraderStock { public int[] purchasedAmounts = new int[18]; - public long lastPurchaseTime = 0; public PlayerTraderStock() { for (int i = 0; i < 18; i++) { @@ -297,7 +334,6 @@ public class TraderStock { if (slot < 0 || slot >= 18) return false; if (getStock(slot, maxStock) >= amount) { purchasedAmounts[slot] += amount; - lastPurchaseTime = System.currentTimeMillis(); return true; } return false; @@ -305,7 +341,6 @@ public class TraderStock { public void writeToNBT(NBTTagCompound compound) { compound.setIntArray("Purchased", purchasedAmounts); - compound.setLong("LastPurchase", lastPurchaseTime); } public void readFromNBT(NBTTagCompound compound) { @@ -313,7 +348,6 @@ public class TraderStock { if (loaded != null && loaded.length == 18) { purchasedAmounts = loaded; } - lastPurchaseTime = compound.getLong("LastPurchase"); } } } diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java deleted file mode 100644 index 4d8febaf1..000000000 --- a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java +++ /dev/null @@ -1,135 +0,0 @@ -package noppes.npcs.roles; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NoppesUtilServer; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumGuiType; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.data.Line; -import noppes.npcs.entity.EntityNPCInterface; - -/** - * NPC role for Auctioneer - provides access to the global auction house. - */ -public class RoleAuctioneer extends RoleInterface { - - // Configuration - public boolean showActiveListings = true; // Show active listings tab - public boolean showMyListings = true; // Show player's listings tab - public boolean showMyBids = true; // Show player's bids tab - public boolean showClaims = true; // Show claimable items tab - public boolean allowCreatingListings = true; // Allow creating new listings - public boolean allowBidding = true; // Allow bidding on items - public boolean allowBuyout = true; // Allow buyout purchases - - // Custom greeting messages - public String welcomeMessage = ""; - public String noAuctionMessage = ""; - - public RoleAuctioneer(EntityNPCInterface npc) { - super(npc); - } - - @Override - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setBoolean("ShowActiveListings", showActiveListings); - compound.setBoolean("ShowMyListings", showMyListings); - compound.setBoolean("ShowMyBids", showMyBids); - compound.setBoolean("ShowClaims", showClaims); - compound.setBoolean("AllowCreatingListings", allowCreatingListings); - compound.setBoolean("AllowBidding", allowBidding); - compound.setBoolean("AllowBuyout", allowBuyout); - compound.setString("WelcomeMessage", welcomeMessage); - compound.setString("NoAuctionMessage", noAuctionMessage); - return compound; - } - - @Override - public void readFromNBT(NBTTagCompound compound) { - showActiveListings = compound.hasKey("ShowActiveListings") ? - compound.getBoolean("ShowActiveListings") : true; - showMyListings = compound.hasKey("ShowMyListings") ? - compound.getBoolean("ShowMyListings") : true; - showMyBids = compound.hasKey("ShowMyBids") ? - compound.getBoolean("ShowMyBids") : true; - showClaims = compound.hasKey("ShowClaims") ? - compound.getBoolean("ShowClaims") : true; - allowCreatingListings = compound.hasKey("AllowCreatingListings") ? - compound.getBoolean("AllowCreatingListings") : true; - allowBidding = compound.hasKey("AllowBidding") ? - compound.getBoolean("AllowBidding") : true; - allowBuyout = compound.hasKey("AllowBuyout") ? - compound.getBoolean("AllowBuyout") : true; - welcomeMessage = compound.getString("WelcomeMessage"); - noAuctionMessage = compound.getString("NoAuctionMessage"); - } - - @Override - public void interact(EntityPlayer player) { - // Check if auction system is enabled - if (!ConfigMarket.AuctionEnabled) { - if (!noAuctionMessage.isEmpty()) { - npc.say(player, new Line(noAuctionMessage)); - } else { - npc.say(player, new Line("The auction house is currently closed.")); - } - return; - } - - // Process expired auctions before opening GUI - if (AuctionController.Instance != null) { - AuctionController.Instance.processExpiredAuctions(); - } - - // Say welcome message - if (!welcomeMessage.isEmpty()) { - npc.say(player, new Line(welcomeMessage)); - } else { - npc.say(player, npc.advanced.getInteractLine()); - } - - // Open auction GUI - NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); - } - - /** - * Check if player has claims waiting - */ - public boolean hasClaimsWaiting(EntityPlayer player) { - if (AuctionController.Instance == null) { - return false; - } - return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); - } - - /** - * Get number of active listings in the auction house - */ - public int getActiveListingCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingCount(); - } - - /** - * Get player's current listing count - */ - public int getPlayerListingCount(EntityPlayer player) { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); - } - - /** - * Get player's maximum allowed listings - */ - public int getPlayerMaxListings(EntityPlayer player) { - if (AuctionController.Instance == null) { - return ConfigMarket.DefaultMaxListings; - } - return AuctionController.Instance.getMaxListings(player); - } -} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 6215eef2f..107076a44 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -1,7 +1,9 @@ package noppes.npcs.roles; import foxz.utils.Market; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; 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; @@ -10,10 +12,14 @@ import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.NoppesUtilServer; import noppes.npcs.NpcMiscInventory; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.MarketRegistry; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.TraderStock; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -35,9 +41,11 @@ public class RoleTrader extends RoleInterface { // Stock system public TraderStock stock = new TraderStock(); - // Currency system - public boolean useCurrency = false; // Use virtual currency instead of items - public long[] slotPrices = new long[18]; // Currency prices per slot + // Currency cost per trade slot (ADDITIVE to item costs, 0 = no currency required) + public long[] currencyCost = new long[18]; + + // Local viewer tracking for non-linked traders (per-NPC sync) + private final Set localViewers = new HashSet<>(); public RoleTrader(EntityNPCInterface npc) { super(npc); @@ -72,24 +80,27 @@ public class RoleTrader extends RoleInterface { // Stock system nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); - // Currency system - nbttagcompound.setBoolean("UseCurrency", useCurrency); - NBTTagList priceList = new NBTTagList(); - for (long price : slotPrices) { - NBTTagCompound priceTag = new NBTTagCompound(); - priceTag.setLong("Price", price); - priceList.appendTag(priceTag); + // Currency cost per slot (additive to item costs) + NBTTagList currencyList = new NBTTagList(); + for (long cost : currencyCost) { + NBTTagCompound costTag = new NBTTagCompound(); + costTag.setLong("Cost", cost); + currencyList.appendTag(costTag); } - nbttagcompound.setTag("SlotPrices", priceList); + nbttagcompound.setTag("CurrencyCost", currencyList); return nbttagcompound; } @Override public void readFromNBT(NBTTagCompound nbttagcompound) { + String oldMarket = marketName; marketName = nbttagcompound.getString("TraderMarket"); readNBT(nbttagcompound); + // Register with MarketRegistry for linked market sync + MarketRegistry.updateTraderMarket(oldMarket, marketName, this); + try { Market.getMarket(this, marketName); } catch (Exception ex) { @@ -123,39 +134,50 @@ public class RoleTrader extends RoleInterface { stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); } - // Currency system - useCurrency = nbttagcompound.getBoolean("UseCurrency"); - if (nbttagcompound.hasKey("SlotPrices")) { - NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); - for (int i = 0; i < priceList.tagCount() && i < 18; i++) { - slotPrices[i] = priceList.getCompoundTagAt(i).getLong("Price"); + // Currency cost per slot (additive to item costs) + if (nbttagcompound.hasKey("CurrencyCost")) { + NBTTagList currencyList = nbttagcompound.getTagList("CurrencyCost", 10); + for (int i = 0; i < currencyList.tagCount() && i < 18; i++) { + currencyCost[i] = currencyList.getCompoundTagAt(i).getLong("Cost"); } } } @Override public void interact(EntityPlayer player) { - // Check for stock reset before opening GUI - if (stock.enableStock) { - long currentTime = stock.resetType.isRealTime() - ? System.currentTimeMillis() - : player.worldObj.getTotalWorldTime(); - - if (stock.shouldReset(currentTime)) { - stock.resetStock(currentTime); - // Save to market if using shared market - if (!marketName.isEmpty()) { - Market.save(this, marketName); - } - } - } - npc.say(player, npc.advanced.getInteractLine()); + + // Load from market FIRST to get latest shared data try { Market.getMarket(this, marketName); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); } + + // THEN check for stock reset (after loading latest market data) + if (stock.enableStock) { + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + boolean needsSave = false; + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + needsSave = true; + } + + // Validate stock levels (clamp current to max if exceeded) + if (stock.validateStock()) { + needsSave = true; + } + + // Save to market if using shared market and changes were made + if (needsSave && !marketName.isEmpty()) { + Market.save(this, marketName); + } + } + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); } @@ -213,15 +235,35 @@ public class RoleTrader extends RoleInterface { } /** - * Consume stock after purchase + * Consume stock after purchase. + * For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise). + * + * @param slot The slot purchased from + * @param playerName The player making the purchase + * @param amount Amount to consume + * @return true if stock was consumed successfully */ public boolean consumeStock(int slot, String playerName, int amount) { if (!stock.enableStock) { return true; // Stock system disabled } boolean consumed = stock.consumeStock(slot, playerName, amount); - if (consumed && !marketName.isEmpty()) { - Market.save(this, marketName); + if (consumed) { + // Save to market if linked + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + + // Sync viewers for shared stock (not per-player) + if (!stock.perPlayer) { + if (!marketName.isEmpty()) { + // Linked market: sync all traders and viewers market-wide + MarketRegistry.syncMarket(marketName); + } else { + // No linked market: sync only this NPC's viewers + syncLocalViewers(); + } + } } return consumed; } @@ -239,29 +281,175 @@ public class RoleTrader extends RoleInterface { return stock.getTimeUntilResetFormatted(currentTime); } - // ==================== Currency System Methods ==================== - /** - * Get the currency price for a slot + * Get time until stock reset in real-time milliseconds. + * Converts MC ticks to approximate real-time for client countdown display. + * @return Remaining time in milliseconds, or -1 if no reset scheduled */ - public long getSlotPrice(int slot) { - if (slot < 0 || slot >= 18) return 0; - return slotPrices[slot]; + public long getResetTimeRemainingMillis() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return -1; + } + + long currentTime; + long remaining; + + if (stock.resetType.isRealTime()) { + // Real time - already in milliseconds + currentTime = System.currentTimeMillis(); + remaining = stock.getTimeUntilReset(currentTime); + } else { + // MC time - convert ticks to real milliseconds (1 tick = 50ms) + currentTime = (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + long remainingTicks = stock.getTimeUntilReset(currentTime); + remaining = remainingTicks * 50L; + } + + return remaining; } /** - * Set the currency price for a slot + * Reset the stock cooldown timer (for admin use) */ - public void setSlotPrice(int slot, long price) { + public void resetCooldown() { + stock.lastResetTime = 0; + } + + // ==================== Currency Cost Methods ==================== + + /** + * Get the currency cost for a slot (additive to item costs) + */ + public long getCurrencyCost(int slot) { + if (slot < 0 || slot >= 18) return 0; + return currencyCost[slot]; + } + + /** + * Set the currency cost for a slot (additive to item costs) + */ + public void setCurrencyCost(int slot, long cost) { if (slot >= 0 && slot < 18) { - slotPrices[slot] = Math.max(0, price); + currencyCost[slot] = Math.max(0, cost); } } /** - * Check if a slot has a currency price set + * Check if a slot has a currency cost (greater than 0) */ - public boolean hasSlotPrice(int slot) { - return useCurrency && slot >= 0 && slot < 18 && slotPrices[slot] > 0; + public boolean hasCurrencyCost(int slot) { + return slot >= 0 && slot < 18 && currencyCost[slot] > 0; + } + + // ==================== Viewer Tracking Methods ==================== + + /** + * Register a player as viewing this trader's GUI. + * - For linked markets: uses MarketRegistry (market-wide sync) + * - For normal traders: uses local viewer set (per-NPC sync) + */ + public void registerViewer(EntityPlayerMP player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + // Linked market: use MarketRegistry for market-wide tracking + MarketRegistry.registerViewer(marketName, player); + } else { + // No linked market: use local viewer set + localViewers.add(player); + } + } + + /** + * Unregister a player from viewing this trader's GUI. + * - For linked markets: uses MarketRegistry + * - For normal traders: uses local viewer set + */ + public void unregisterViewer(EntityPlayerMP player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + MarketRegistry.unregisterViewer(marketName, player); + } else { + localViewers.remove(player); + } + } + + /** + * Sync trader data to all players viewing this trader. + * - For linked markets: syncs ALL viewers across ALL NPCs sharing the market + * - For normal traders: syncs only viewers of THIS specific NPC + */ + public void syncAllViewers() { + if (!marketName.isEmpty()) { + // Linked market: sync all viewers across all NPCs with this market + MarketRegistry.syncMarketViewers(marketName); + } else { + // No linked market: sync only local viewers + syncLocalViewers(); + } + } + + /** + * Sync to all players viewing this specific NPC (for non-linked traders). + */ + private void syncLocalViewers() { + // Remove disconnected players + localViewers.removeIf(player -> player.playerNetServerHandler == null); + + for (EntityPlayerMP viewer : localViewers) { + syncToPlayer(viewer); + } + } + + /** + * Called when this trader is unloaded (NPC death, chunk unload, etc.) + * Cleans up MarketRegistry registration. + */ + public void onUnload() { + if (!marketName.isEmpty()) { + MarketRegistry.unregisterTrader(marketName, this); + } + localViewers.clear(); + } + + /** + * Send trader data to a specific player + */ + public void syncToPlayer(EntityPlayerMP player) { + if (player == null || player.playerNetServerHandler == null) { + return; + } + + NBTTagCompound compound = new NBTTagCompound(); + + // Player balance + PlayerData data = PlayerData.get(player); + compound.setLong("Balance", data.currencyData.getBalance()); + + // Stock info + compound.setBoolean("StockEnabled", stock.enableStock); + compound.setString("ResetTime", getTimeUntilResetFormatted()); + + // Available stock per slot (player-specific if perPlayer mode) + String playerName = player.getCommandSenderName(); + int[] stockArr = new int[18]; + for (int i = 0; i < 18; i++) { + stockArr[i] = getAvailableStock(i, playerName); + } + compound.setIntArray("Stock", stockArr); + + // Currency cost per slot + for (int i = 0; i < 18; i++) { + compound.setLong("Cost" + i, getCurrencyCost(i)); + } + + GuiDataPacket.sendGuiData(player, compound); + } + + @Override + public void delete() { + onUnload(); + super.delete(); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 9fc547260..85a15041e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,14 +49,11 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.PlayerQuestController; import noppes.npcs.controllers.QuestController; -import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.data.Dialog; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -899,185 +896,42 @@ public class ScriptPlayer extends ScriptLivingBase return (IPlayer[]) list.toArray(new IPlayer[list.size()]); } - // ==================== CURRENCY METHODS ==================== + // ========================================= + // Currency Methods + // ========================================= - /** - * Get the player's current currency balance. - * Uses Vault if available, otherwise uses internal currency system. - * @return The player's currency balance - * @since 1.11 - */ + @Override public long getCurrencyBalance() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.getBalance(player); + return getData().currencyData.getBalance(); } - /** - * Add currency to the player's balance. - * @param amount The amount to add (must be positive) - * @return true if successful, false if failed (e.g., would exceed max balance) - * @since 1.11 - */ - public boolean addCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.deposit(player, amount); + @Override + public void setCurrencyBalance(long amount) { + getData().currencyData.setBalance(amount); } - /** - * Remove currency from the player's balance. - * @param amount The amount to remove (must be positive) - * @return true if successful, false if insufficient funds - * @since 1.11 - */ - public boolean removeCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.withdraw(player, amount); + @Override + public boolean depositCurrency(long amount) { + return getData().currencyData.deposit(amount); } - /** - * Check if the player can afford a specific amount. - * @param amount The amount to check - * @return true if player has sufficient balance - * @since 1.11 - */ + @Override + public boolean withdrawCurrency(long amount) { + return getData().currencyData.withdraw(amount); + } + + @Override public boolean canAffordCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.canAfford(player, amount); + return getData().currencyData.canAfford(amount); } - /** - * Get the configured currency name (e.g., "Gold"). - * @return The currency name - * @since 1.11 - */ - public String getCurrencyName() { - if (VaultHelper.Instance == null) { - return "Currency"; - } - return VaultHelper.Instance.getCurrencyName(); + @Override + public boolean isUsingVaultCurrency() { + return getData().currencyData.isUsingVault(); } - /** - * Format a currency amount for display (e.g., "1,000 Gold"). - * @param amount The amount to format - * @return Formatted string - * @since 1.11 - */ - public String formatCurrency(long amount) { - if (CurrencyController.Instance == null) { - return String.valueOf(amount); - } - return CurrencyController.Instance.formatAmount(amount); - } - - /** - * Check if the player has pending currency claims. - * Claims are created by auction refunds, NPC payments, etc. - * @return true if there are pending claims - * @since 1.11 - */ - public boolean hasPendingClaims() { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.hasPendingClaims(player); - } - - /** - * Get the total amount of pending currency claims. - * @return Total pending claim amount - * @since 1.11 - */ - public long getPendingClaimsTotal() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.getTotalPendingClaims(player); - } - - /** - * Claim all pending currency claims and add them to balance. - * @return The total amount claimed - * @since 1.11 - */ - public long claimAllCurrency() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.claimAll(player); - } - - // ==================== AUCTION METHODS ==================== - - /** - * Get the number of active auction listings the player has. - * @return Number of active listings - * @since 1.11 - */ - public int getAuctionListingCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); - } - - /** - * Get the maximum number of auction listings the player is allowed. - * @return Maximum listing count - * @since 1.11 - */ - public int getMaxAuctionListings() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getMaxListings(player); - } - - /** - * Check if the player has auction items waiting to be claimed. - * This includes won items, expired listings, and sale proceeds. - * @return true if there are claimable items - * @since 1.11 - */ - public boolean hasAuctionClaims() { - if (AuctionController.Instance == null) { - return false; - } - return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); - } - - /** - * Get the number of auction items waiting to be claimed. - * @return Number of claimable items/listings - * @since 1.11 - */ - public int getAuctionClaimCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getClaimableListings(player.getUniqueID()).size(); - } - - /** - * Get the number of auctions where the player is the current high bidder. - * @return Number of active bids - * @since 1.11 - */ - public int getActiveAuctionBidCount() { - if (AuctionController.Instance == null) { - return 0; - } - return (int) AuctionController.Instance.getListingsAsBidder(player.getUniqueID()) - .stream() - .filter(l -> l.isActive()) - .count(); + @Override + public String getFormattedCurrencyBalance() { + return getData().currencyData.formatBalance(); } } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java index a2588059f..6736369b9 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java @@ -1,7 +1,6 @@ package noppes.npcs.scripted.item; import com.google.common.collect.Multimap; -import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; import kamkeel.npcs.util.AttributeItemUtil; @@ -593,117 +592,4 @@ public class ScriptItemStack implements IItemStack { Set keys = reqTag.func_150296_c(); return keys.toArray(new String[keys.size()]); } - - // ==================== Trade Restriction Methods ==================== - - /** - * Check if this item can be traded or auctioned. - * An item cannot be traded if it is: - * - Marked as untradeable - * - Bound to a profile slot - * - Soulbound to a player - * @return true if the item can be traded - * @since 1.11 - */ - public boolean canTrade() { - return ItemTradeAttribute.canTrade(item); - } - - /** - * Get the reason why this item cannot be traded. - * @return Localization key for the block reason, or null if tradeable - * @since 1.11 - */ - public String getTradeBlockReason() { - return ItemTradeAttribute.getTradeBlockReason(item); - } - - /** - * Check if this item is marked as untradeable - * @return true if untradeable - * @since 1.11 - */ - public boolean isUntradeable() { - return ItemTradeAttribute.isUntradeable(item); - } - - /** - * Set whether this item is untradeable - * @param untradeable true to mark as untradeable - * @since 1.11 - */ - public void setUntradeable(boolean untradeable) { - ItemTradeAttribute.setUntradeable(item, untradeable); - } - - /** - * Check if this item is bound to a profile slot - * @return true if profile slot bound - * @since 1.11 - */ - public boolean isProfileSlotBound() { - return ItemTradeAttribute.isProfileSlotBound(item); - } - - /** - * Get the profile slot this item is bound to - * @return Profile slot ID, or -1 if not bound - * @since 1.11 - */ - public int getProfileSlot() { - return ItemTradeAttribute.getProfileSlot(item); - } - - /** - * Bind this item to a specific profile slot - * @param slot The profile slot ID, or -1 to unbind - * @since 1.11 - */ - public void setProfileSlotBound(int slot) { - ItemTradeAttribute.setProfileSlotBound(item, slot); - } - - /** - * Check if this item is soulbound to any player - * @return true if soulbound - * @since 1.11 - */ - public boolean isSoulbound() { - return ItemTradeAttribute.isSoulbound(item); - } - - /** - * Get the UUID of the player this item is soulbound to - * @return UUID string, or null if not soulbound - * @since 1.11 - */ - public String getSoulbindOwner() { - return ItemTradeAttribute.getSoulbindOwner(item); - } - - /** - * Remove the soulbind from this item - * @since 1.11 - */ - public void removeSoulbind() { - ItemTradeAttribute.removeSoulbind(item); - } - - /** - * Clear all trade restrictions from this item - * (untradeable, profile slot bound, soulbound) - * @since 1.11 - */ - public void clearTradeRestrictions() { - ItemTradeAttribute.clearAllRestrictions(item); - } - - /** - * Get a summary of all trade restrictions on this item - * @return Human-readable string of restrictions, or empty if none - * @since 1.11 - */ - public String getRestrictionSummary() { - return ItemTradeAttribute.getRestrictionSummary(item); - } } diff --git a/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java b/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java index 825c2e46b..3574e529e 100644 --- a/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java +++ b/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java @@ -42,7 +42,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader public IItemStack getSellOption(int slot) { if (slot >= 18 || slot < 0) return null; if (role.inventorySold.items.get(slot) == null) return null; - return NpcAPI.Instance().getIItemStack(role.inventoryCurrency.items.get(slot)); + return NpcAPI.Instance().getIItemStack(role.inventorySold.items.get(slot)); } public IItemStack[] getCurrency(int slot) { @@ -100,7 +100,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader public boolean isSlotEnabled(int slot) { if (slot >= 18 || slot < 0) return false; - return role.disableSlot[slot] > 0; + return role.disableSlot[slot] <= 0; // enabled when NOT disabled } public boolean isSlotEnabled(int slot, IPlayer player) { @@ -128,6 +128,132 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader role.getArrayByName(player.getDisplayName(), role.playerDisableSlot)[slot] = 0; } + // ==================== Stock System ==================== + + public boolean isStockEnabled() { + return role.stock.enableStock; + } + + public void setStockEnabled(boolean enabled) { + role.stock.enableStock = enabled; + } + + public boolean isPerPlayerStock() { + return role.stock.perPlayer; + } + + public void setPerPlayerStock(boolean perPlayer) { + role.stock.perPlayer = perPlayer; + } + + public int getStockResetType() { + return role.stock.resetType.ordinal(); + } + + public void setStockResetType(int type) { + noppes.npcs.constants.EnumStockReset[] values = noppes.npcs.constants.EnumStockReset.values(); + if (type >= 0 && type < values.length) { + role.stock.resetType = values[type]; + } + } + + public long getCustomResetTime() { + return role.stock.customResetTime; + } + + public void setCustomResetTime(long time) { + role.stock.customResetTime = Math.max(0, time); + } + + public int getMaxStock(int slot) { + if (slot < 0 || slot >= 18) return -1; + return role.stock.maxStock[slot]; + } + + public void setMaxStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + role.stock.setMaxStock(slot, amount); + } + } + + public int getAvailableStock(int slot) { + if (slot < 0 || slot >= 18) return 0; + return role.stock.getAvailableStock(slot, ""); + } + + public int getAvailableStock(int slot, IPlayer player) { + if (slot < 0 || slot >= 18) return 0; + return role.stock.getAvailableStock(slot, player.getDisplayName()); + } + + public void resetStock() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + role.stock.resetStock(currentTime); + } + + public void resetCooldown() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + role.stock.lastResetTime = currentTime; + } + + public int getCurrentStock(int slot) { + if (slot < 0 || slot >= 18) return -1; + return role.stock.currentStock[slot]; + } + + public void setCurrentStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + role.stock.currentStock[slot] = amount; + } + } + + public int getPlayerPurchased(int slot, IPlayer player) { + if (slot < 0 || slot >= 18) return 0; + noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock = + role.stock.playerStock.get(player.getDisplayName()); + if (pStock == null) return 0; + return pStock.purchasedAmounts[slot]; + } + + public void setPlayerPurchased(int slot, IPlayer player, int amount) { + if (slot < 0 || slot >= 18) return; + noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock = + role.stock.playerStock.computeIfAbsent( + player.getDisplayName(), + k -> new noppes.npcs.controllers.data.TraderStock.PlayerTraderStock() + ); + pStock.purchasedAmounts[slot] = Math.max(0, amount); + } + + public long getLastResetTime() { + return role.stock.lastResetTime; + } + + public long getTimeUntilReset() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + return role.stock.getTimeUntilReset(currentTime); + } + + // ==================== Currency Cost System ==================== + + public long getCurrencyCost(int slot) { + return role.getCurrencyCost(slot); + } + + public void setCurrencyCost(int slot, long cost) { + role.setCurrencyCost(slot, cost); + } + + public boolean hasCurrencyCost(int slot) { + return role.hasCurrencyCost(slot); + } + @Override public int getType() { return RoleType.TRADER; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 7f7314d1c..715ca4884 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -443,9 +443,54 @@ role.auctioneer.messages=Messages role.auctioneer.welcome=Welcome Message role.auctioneer.noAuction=Auction Disabled Message -trader.insufficient=Insufficient Currency -trader.sufficient=Sufficient Currency +trader.unavailable=Unavailable +trader.insufficient.currency=Insufficient Balance +trader.available=Available trader.slotdisabled=Slot Disabled +trader.currencycost=Cost +trader.outofstock=Out of Stock +trader.instock=in stock +trader.restock=Restock +trader.balance=Balance +trader.reopentrader=Reopen Trader + +stock.options=Stock Options +stock.enable=Enable Stock +stock.perplayer=Per-Player +stock.resettype=Reset Type +stock.customtime=Custom Time +stock.defaultmax=Default Max +stock.perslot=Per-Slot Stock +stock.reset=Reset Stock +stock.resetcooldown=Reset Cooldown +stock.perslot.title=Per-Slot Stock Limits +stock.reset.confirm=Reset all stock? +stock.reset.warning=This cannot be undone. +stock.cooldown.confirm=Reset cooldown timer? +stock.cooldown.warning=Stock will reset on next open. + +stock.reset.never=Never +stock.reset.mcdaily=MC Daily +stock.reset.mcweekly=MC Weekly +stock.reset.mccustom=MC Custom +stock.reset.rldaily=Real Daily +stock.reset.rlweekly=Real Weekly +stock.reset.rlcustom=Real Custom + +stock.options.hover=Configure stock limits and reset timers +stock.enable.hover=Enable limited stock for trade slots +stock.perplayer.hover=Track stock separately for each player +stock.resettype.hover=When stock automatically replenishes +stock.customtime.hover=Set a custom reset interval +stock.defaultmax.hover=Default max stock for all slots (-1 = unlimited) +stock.perslot.hover=Set max stock for individual slots +stock.reset.hover=Reset all stock to max values +stock.resetcooldown.hover=Force stock to reset on next trader open + +gui.settings.hover=Configure trading options +gui.ignoreDamage.hover=Ignore item damage when comparing currency +gui.ignoreNBT.hover=Ignore NBT data when comparing currency +gui.recordHistory.hover=Record purchase history for analytics job.name=Job job.none=No Job @@ -1660,72 +1705,3 @@ condition.hit_count=Hit Count condition.percent=Threshold condition.hits=Required Hits condition.within=Within (ticks) - -auction.title=Auction House -auction.browse=Browse -auction.myListings=My Listings -auction.myBids=My Bids -auction.claims=Claims -auction.sell=Sell -auction.search=Search -auction.refresh=Refresh -auction.viewDetails=View Details -auction.placeBid=Place Bid -auction.buyout=Buyout -auction.cancel=Cancel -auction.prevPage=< Prev -auction.nextPage=Next > -auction.noResults=No results -auction.page=Page %d of %d -auction.balance=Balance: %s -auction.listings=%d listings -auction.browseTitle=Browse Auctions -auction.myListingsTitle=My Listings -auction.myBidsTitle=My Active Bids -auction.claimsTitle=Claim Items -auction.sellTitle=Create Listing -auction.currentPrice=Current Price: -auction.minimumBid=Minimum Bid: -auction.yourBalance=Your Balance: -auction.timeLeft=Time Left: -auction.yourBid=Your Bid: -auction.minBid=Min Bid -auction.bidPlus10=+10% -auction.bidPlus25=+25% -auction.insufficientFunds=Insufficient funds! -auction.auctionEnded=Auction has ended! -auction.ended=Ended -auction.item=Item: -auction.seller=Seller: -auction.startingPrice=Starting Price: -auction.buyoutPrice=Buyout Price: -auction.noBuyout=None -auction.bidCount=Bid Count: -auction.highBidder=High Bidder: -auction.noBids=No bids -auction.claim=Claim -auction.createListing=Create Listing -auction.duration=Duration: -auction.listingFee=Listing Fee: -auction.sort.newest=Newest First -auction.sort.oldest=Oldest First -auction.sort.priceHigh=Most Expensive -auction.sort.priceLow=Least Expensive -auction.sort.endingSoon=Ending Soon -auction.sort.mostTime=Most Time Left -auction.sort.nameAZ=Name: A to Z -auction.sort.nameZA=Name: Z to A -auction.sort.mostBids=Most Bids -auction.sort.leastBids=Least Bids -auction.duration.short=Short (12h) -auction.duration.medium=Medium (24h) -auction.duration.long=Long (48h) -auction.duration.veryLong=Very Long (72h) -auction.placeItemBelow=Place item in slot below: -auction.noItemSelected=(No item selected) -auction.noBuyoutHint=(0 = no buyout) -auction.quantity=Quantity: -auction.totalBids=Total Bids: -auction.noBidsYet=No bids yet -auction.auctionDetails=Auction Details -auction.currentBid=Current Bid: