diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md index e512e7aa0..4236e0e63 100644 --- a/1.11_Market_Update.md +++ b/1.11_Market_Update.md @@ -44,8 +44,8 @@ The currency system provides a unified economy for CNPC+ features. ### Key Files | File | Purpose | |------|---------| -| `PlayerCurrencyData.java` | Per-player currency storage, Vault delegation | -| `IPlayerCurrencyData.java` | API interface for currency operations | +| `PlayerTradeData.java` | Per-player trade data (currency + auction claims), Vault delegation | +| `IPlayerTradeData.java` | API interface for trade operations (currency + claims) | | `ConfigMarket.java` | Currency configuration (UseVault, CurrencyName, StartingBalance, MaxBalance) | | `VaultUtil.java` | Vault API integration via reflection | @@ -67,6 +67,9 @@ 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 + +// Via IPlayerData +player.getData().getTradeData() // Get IPlayerTradeData interface ``` --- @@ -125,7 +128,7 @@ Each trade slot can have: #### Implementation Details - Currency cost stored as `long[] currencyCost[18]` in RoleTrader -- Uses `data.currencyData.getBalance()` and `data.currencyData.withdraw()` +- Uses `data.tradeData.getBalance()` and `data.tradeData.withdraw()` - Automatically uses Vault when `ConfigMarket.UseVault` is enabled - NBT serialization for persistence ✅ @@ -165,20 +168,22 @@ Full API available via `IRoleTrader` interface. ``` ┌─────────────────────────────────────────────────────────────────┐ │ AuctionController │ -│ - Manages all auction data with player indices │ +│ - Manages auction listings with player indices │ │ - Handles listing/bidding/buyout logic │ -│ - Race-condition protected with ReentrantReadWriteLock │ -│ - Async saves on CNPC+ thread │ -│ - 20-second update cycle for performance │ +│ - Uses ConcurrentHashMap for thread-safety (no locks) │ +│ - Async saves on CNPC+ thread every 30 seconds │ +│ - 30-second update cycle for processing ended auctions │ └─────────────────────────────────────────────────────────────────┘ │ Uses PlayerCurrencyData (Vault-aware) │ ┌─────────────────────────────────────────────────────────────────┐ -│ AuctionNotification │ -│ - Tracks pending notifications per player │ -│ - Sends notifications on login │ -│ - Types: Won, Outbid, Sold, Expired, Claimed │ +│ PlayerTradeData │ +│ - Combined currency + auction claims (shared across profiles) │ +│ - Currency: Vault integration with local fallback │ +│ - Claims: Thread-safe CopyOnWriteArrayList for async additions │ +│ - Expired claims processed on login │ +│ - Notifications sent immediately to online players │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -308,6 +313,8 @@ All actions are validated server-side: | Auction expiry check | `placeBid()/buyout()` | Double-check inside write lock | | Currency validation | All actions | Verify balance before operations | | Claim ownership | `claimItem()/claimCurrency()` | `claim.isForPlayer(playerUUID)` check | +| Blacklist check | `createListing()` | `AuctionBlacklist.isBlacklistedForPlayer()` check | +| Bound item check | `createListing()` | Auto-blocks Soulbound/Profile-Slotbound items | ### 3.7 Snipe Protection @@ -320,6 +327,53 @@ Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 --- +### 3.8 Item Blacklist System + +Prevents specific items from being listed on the auction house. + +#### Blacklist Types + +| Type | Description | Example | +|------|-------------|---------| +| **Items** | Block specific items by registry name | `minecraft:diamond_sword` | +| **Wildcards** | Block items matching pattern | `minecraft:diamond*` | +| **Mods** | Block all items from a mod | `customnpcs` | +| **NBT Tags** | Block items with specific NBT | `ench` (enchanted items) | + +#### Auto-Blocked Items (Always Blocked) + +These items are **always** blocked regardless of blacklist settings: +- **Soulbound Items** - Items with `cnpc_soulbind` requirement +- **Profile-Slotbound Items** - Items with `cnpc_profile_slot` requirement + +The system checks for these under `RPGCore/Requirements` in the item's NBT. + +#### Bypass Permission + +Players with OP status or the `customnpcs.auction.blacklist.bypass` permission can list blacklisted items. + +#### Configuration + +``` +Market.Auction.Blacklist: + Enabled: true + BlacklistedItems: ["minecraft:barrier", "minecraft:command_block*"] + BlacklistedMods: [] + BlacklistedNBTTags: [] +``` + +#### Commands + +| Command | Description | +|---------|-------------| +| `/cnpc auction blacklist add ` | Add to blacklist (runtime only) | +| `/cnpc auction blacklist remove ` | Remove from blacklist (runtime only) | +| `/cnpc auction blacklist list [item\|mod\|nbt]` | List blacklisted entries | +| `/cnpc auction blacklist reload` | Reload blacklist from config | +| `/cnpc auction blacklist check` | Check if held item is blacklisted | + +--- + ## 4. AuctionController Performance & Thread Safety ### 4.1 Threading Model @@ -328,14 +382,26 @@ Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 ┌─────────────────────────────────────────────────────────────────┐ │ AuctionController │ │ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ ReentrantRWLock │ │ AtomicBoolean │ │ CNPC+ Thread │ │ -│ │ (dataLock) │ │ dirty, saving │ │ (async saves) │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────────┐ ┌─────────────────┐ ┌───────────────┐ │ +│ │ ConcurrentHashMap │ │ AtomicBoolean │ │ CNPC+ Thread │ │ +│ │ (listings, indices) │ │ dirty, saving │ │ (async saves) │ │ +│ └─────────────────────┘ └─────────────────┘ └───────────────┘ │ │ │ -│ Read operations → Read Lock (concurrent) │ -│ Write operations → Write Lock (exclusive) │ -│ Save operations → Async on CNPC+ thread │ +│ All operations use ConcurrentHashMap - no explicit locking │ +│ Minecraft is single-threaded for gameplay - inherent safety │ +│ Async saves snapshot data before writing to disk │ +└─────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────────┐ +│ PlayerTradeData │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Currency: long balance (or Vault delegation) │ │ +│ │ Claims: CopyOnWriteArrayList │ │ +│ │ - Thread-safe for concurrent read/write │ │ +│ │ - Claims can be added from CNPC+ thread for offline players │ │ +│ │ - Stored in PlayerData (persisted with player, not auctions) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -345,29 +411,33 @@ Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 |-------|------|---------| | `playerListingIds` | `Map>` | Active listings by seller | | `playerBidIds` | `Map>` | Listings where player has active bid | -| `playerClaimIds` | `Map>` | Unclaimed claims by player | + +**Claims are now stored in PlayerData:** +- `PlayerTradeData.claims` - CopyOnWriteArrayList per player +- Accessed via `PlayerData.get(player).tradeData` +- Shared across all profile slots (combined with currency) **Performance Impact:** - `getPlayerListingCount()`: O(n) → O(1) - `getPlayerActiveListings()`: O(n) → O(k) where k = player's listings - `getPlayerActiveBids()`: O(n) → O(k) -- `getPlayerClaims()`: O(n) → O(k) +- `getPlayerClaims()`: Direct access to player's claims list ### 4.3 Update Intervals | Operation | Interval | Rationale | |-----------|----------|-----------| -| Process ended auctions | 20 seconds (400 ticks) | Reduces server load | -| Process expired claims | 20 seconds (400 ticks) | Same cycle as auctions | -| Save dirty data | 60 seconds (1200 ticks) | Batches writes, reduces I/O | +| Process ended auctions | 30 seconds (600 ticks) | Reduces server load | +| Save auction data | 30 seconds (600 ticks) | Batches writes, reduces I/O | +| Process expired claims | On player login | Per-player, not global tick | -### 4.4 Race Condition Prevention +### 4.4 Data Safety -1. **Bid vs Auction End** - Double-check `isExpired()` inside write lock -2. **Currency Deduction** - Withdraw before updating listing -3. **Previous Bidder Refund** - Store info before modification -4. **Concurrent Saves** - `AtomicBoolean` prevents duplicates -5. **Index Consistency** - All updates within same write lock +1. **Currency Deduction** - Withdraw before updating listing +2. **Previous Bidder Refund** - Store info before modification +3. **Concurrent Saves** - `AtomicBoolean` prevents duplicates +4. **Offline Player Claims** - Loaded async on CNPC+ thread, saved to PlayerData +5. **Claim Migration** - Old claims in auction.dat migrated to PlayerData on load ### 4.5 Server Shutdown Handling @@ -391,12 +461,14 @@ constants/ controllers/ AuctionController.java ✅ (with threading, indices, Vault support) + AuctionConfigSync.java ✅ (server-side config sync utility) controllers/data/ AuctionListing.java ✅ AuctionClaim.java ✅ AuctionFilter.java ✅ - AuctionNotification.java ✅ + AuctionBlacklist.java ✅ (item blacklist checking + bypass) + PlayerTradeData.java ✅ (currency + claims storage per player) roles/ RoleAuctioneer.java ✅ @@ -428,10 +500,18 @@ client/gui/util/ GuiAuctionNavButton.java ✅ network/packets/player/ - AuctionDataPacket.java ✅ AuctionActionPacket.java ✅ ``` +### New Commands + +``` +command/ + AuctionCommand.java ✅ /cnpc auction with list, view, open, blacklist + command/auction/ + BlacklistSubCommand.java ✅ /cnpc auction blacklist subcommands +``` + ### Files Modified ``` @@ -444,6 +524,7 @@ ClientProxy.java ✅ Handle auction GUI types DataAdvanced.java ✅ Handle Auctioneer role ConfigMarket.java ✅ Added Auction section LoginPacket.java ✅ Sync auction config to client +PlayerData.java ✅ Added tradeData field (currency + claims, shared across profiles) en_US.lang ✅ Added auction lang keys ``` @@ -465,19 +546,23 @@ public static long MaxBalance = Long.MAX_VALUE; // Balance cap // ========================================= // Auction (Market.Auction) // ========================================= +public static final int MAX_TRADE_SLOTS = 45; // 9x5 grid maximum +public static final int MIN_TRADE_SLOTS = 1; // Minimum allowed + public static boolean AuctionEnabled = true; -public static int AuctionDurationHours = 48; // Auction length +public static int AuctionDurationHours = 24; // Auction length (hours) public static long ListingFee = 10; // Fee to create listing +public static long MinimumListingPrice = 1; // Minimum starting price public static double SalesTaxPercent = 0.05; // 5% tax on sales -public static int DefaultMaxListings = 10; // Per player +public static int DefaultMaxTrades = 8; // Per player (listings + bids + claims) public static int SnipeProtectionMinutes = 2; // Anti-snipe timer -public static int ClaimExpirationDays = 30; // Days before unclaimed deleted +public static int ClaimExpirationDays = 20; // Days before unclaimed deleted public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase // ========================================= // Auction Logging // ========================================= -public static boolean AuctionLoggingEnabled = true; +public static boolean AuctionLoggingEnabled = false; public static boolean LogAuctionCreated = true; public static boolean LogAuctionBid = true; public static boolean LogAuctionBuyout = true; @@ -485,11 +570,82 @@ public static boolean LogAuctionSold = true; public static boolean LogAuctionExpired = true; public static boolean LogAuctionCancelled = true; public static boolean LogAuctionClaimed = true; + +// ========================================= +// Auction Blacklist (Market.Auction.Blacklist) +// ========================================= +public static boolean BlacklistEnabled = true; +public static String[] BlacklistedItems = {}; // Registry names (supports wildcards) +public static String[] BlacklistedMods = {}; // Mod IDs to block entirely +public static String[] BlacklistedNBTTags = {}; // NBT tag keys to block +``` + +### Auction Permissions + +| Permission | Description | +|------------|-------------| +| `customnpcs.auction.trades.*` | Unlimited trade slots (up to 45) | +| `customnpcs.auction.trades.X` | Exactly X trade slots (where X is 1-45) | +| `customnpcs.auction.blacklist.bypass` | Bypass item blacklist restrictions | + +**Trade Slot Permissions:** +- Default is 8 trade slots +- Permissions only **increase** the limit (permissions below 8 are ignored) +- Maximum is capped at 45 (9x5 grid) +- Trade slots = Active Listings + Active Bids + Pending Claims + +**Blacklist Bypass:** +- OP players or those with `customnpcs.auction.blacklist.bypass` can list blacklisted items +- Soulbound and Profile-Slotbound items are **always** blocked (no bypass) + +--- + +## 7. Commands + +### `/cnpc auction` Command + +Allows interaction with the Auction House without needing an Auctioneer NPC. + +| Subcommand | Usage | Permission | Description | +|------------|-------|------------|-------------| +| `list` | `/cnpc auction list [page]` | OP | View active auctions in chat | +| `view` | `/cnpc auction view ` | OP | View a player's auction activity | +| `open` | `/cnpc auction open` | Any | Opens the Auction House GUI | + +**Examples:** +``` +/cnpc auction list - Show first page of active auctions +/cnpc auction list 2 - Show page 2 of auctions +/cnpc auction view Steve - Show Steve's listings, bids, and claims +/cnpc auction open - Open the full Auction House GUI +``` + +### `/cnpc auction blacklist` Command + +Manage the auction item blacklist (runtime changes, not persisted to config). + +| Subcommand | Usage | Permission | Description | +|------------|-------|------------|-------------| +| `add` | `/cnpc auction blacklist add ` | OP | Add to blacklist | +| `remove` | `/cnpc auction blacklist remove ` | OP | Remove from blacklist | +| `list` | `/cnpc auction blacklist list [item\|mod\|nbt]` | OP | List blacklisted entries | +| `reload` | `/cnpc auction blacklist reload` | OP | Reload from config | +| `check` | `/cnpc auction blacklist check` | Any | Check if held item is blacklisted | + +**Examples:** +``` +/cnpc auction blacklist add item minecraft:diamond_sword +/cnpc auction blacklist add item minecraft:diamond* (wildcard) +/cnpc auction blacklist add mod industrialcraft2 +/cnpc auction blacklist add nbt ench +/cnpc auction blacklist remove item minecraft:diamond_sword +/cnpc auction blacklist list item +/cnpc auction blacklist check (checks held item) ``` --- -## 7. Implementation Status +## 8. Implementation Status ### Phase 1: Currency System - COMPLETE ✅ ### Phase 2: Trader Enhancements - COMPLETE ✅ @@ -509,31 +665,65 @@ public static boolean LogAuctionClaimed = true; ### Remaining Work -- [ ] GuiManageAuction (Admin GUI) -- [ ] Permission system integration +- [ ] **GuiManageAuction (Admin GUI)** - Admin interface to view all auctions, cancel listings, manage claims - [ ] Additional testing +### Implemented Since Initial Plan + +- [x] **`/auction` Command** - Console/player command with subcommands: + - `/auction list [page]` - View active auctions from command line + - `/auction view ` - View a player's auction activity (listings, bids, claims) + - `/auction open` - Open Auction House GUI without needing an Auctioneer NPC +- [x] **Permission system integration** - Per-player trade slot limits via `customnpcs.auction.trades.X` permissions + - Default: 8 trade slots + - Maximum: 45 (9x5 grid) + - Synced to client when viewing My Trades + - Cached on login for performance +- [x] **Minimum Listing Price** - Configurable minimum starting price (default: 1) +- [x] **Buyout enabled by default** - Buyout toggle is ON by default when creating listings +- [x] **Search by seller** - Filter can now search by item name OR seller name +- [x] **Rebid prevention** - Players cannot rebid on auctions where they're already the highest bidder +- [x] **Buyout price difference** - When buying out an auction you're bidding on, only pay the difference +- [x] **Immediate claim cleanup** - Claims are removed from storage immediately when claimed +- [x] **Listing cleanup** - Ended listings are removed when all related claims are processed +- [x] **Live notifications** - Notifications sent immediately to online players instead of queued +- [x] **Login claim summary** - Shows detailed claim summary on login (X items and Y currency to claim) +- [x] **PlayerTradeData** - Merged currency + claims into single class (shared across profiles) +- [x] **No locking system** - Pure ConcurrentHashMap for better performance +- [x] **30-second intervals** - Combined process/save cycle for simplicity +- [x] **Async offline player claims** - Claims added to offline players via CNPC+ thread +- [x] **Claim migration** - Old claims in auction.dat automatically migrated to PlayerData +- [x] **Item Blacklist System** - Block items by registry name, mod, NBT tag, or wildcard pattern + - Auto-blocks Soulbound and Profile-Slotbound items (checks `RPGCore/Requirements`) + - Per-player bypass via `customnpcs.auction.blacklist.bypass` permission + - Runtime add/remove commands (not persisted to config) + - Check command to test held item + --- -## 8. Design Decisions +## 9. Design Decisions | Decision | Choice | Rationale | |----------|--------|-----------| -| Claims System | Everything to claims | Prevents offline handling issues | +| Claims Storage | In PlayerData, not auction.dat | Claims persist with player, reduces auction.dat size, allows offline claim addition | +| Thread Safety | ConcurrentHashMap (no locks) | Minecraft is single-threaded for gameplay; locks add overhead without benefit | | Vault Integration | Via PlayerCurrencyData | Single point of delegation, reused by Trader & Auction | -| Update Interval | 20 seconds | Performance vs responsiveness tradeoff | +| Update Interval | 30 seconds | Performance vs responsiveness tradeoff | | Player Indices | ConcurrentHashMap + Sets | O(1) lookups, thread-safe | | Async Saves | CNPC+ Thread | Doesn't block server tick | +| Offline Claims | Load PlayerData async, add claim, save | Claims can be added to offline players without blocking | | Config Sync | LoginPacket → AuctionClientConfig | Client never accesses server config directly | +| Item Blacklist | Static utility class | Simple, fast checks without instantiation; auto-blocks bound items | +| Bound Item Detection | Check `RPGCore/Requirements` NBT path | Uses AttributeItemUtil constants for consistency | --- -## 9. Future Considerations (Not in 1.11) +## 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 +- Category-based filtering (by item type, mod, etc.) +- Auction history/statistics tracking +- Auction house tax sink (currency removed from economy) +- Reserve prices (hidden minimum price) +- Private auctions (invite-only) +- Watchlist/favorites system +- Price history graphs diff --git a/Auction_Update.md b/Auction_Update.md new file mode 100644 index 000000000..ae839832f --- /dev/null +++ b/Auction_Update.md @@ -0,0 +1,127 @@ +# 🏪 CustomNPC+ 1.11 - Market Update 🏪 + +--- + +The **Market Update** brings a complete economy system to CNPC+! Create bustling marketplaces with **Traders** that have limited stock, or let players trade with each other through the brand new **Auction House**! + +--- + +## Currency System 💰 + +CNPC+ now has a **built-in currency** used across all Market features! + +- **Fully Configurable** - Name it Coins, Gold, Credits, whatever fits your world +- **Profile Persistent** - Currency carries across all your character slots +- **Vault Support** - CNPC+ integrates seamlessly with Bukkit-compatible economy plugins if Vault is installed + +> Full Scripting API support for currency operations on players. + +--- + +## Auction House 🏛️ + +A **global player marketplace** where players can list items, bid against each other, and purchase instantly! + +### Selling + +List any item with a **Starting Price** for competitive bidding. Set an optional **Buyout Price** for players who want to skip the wait and buy immediately. + +> A configurable **Listing Fee** is charged when creating auctions, and a **Sales Tax** is taken from successful sales. + +### Bidding + +Compete for items by placing bids! **Snipe Protection** extends auctions when last-second bids come in, giving everyone a fair shot. If you get outbid, your currency is **automatically refunded**. + +### Buyout + +See something you need right now? Hit **Buy Now** to purchase instantly at the seller's Buyout Price. If you're currently the highest bidder on an item, you only pay the **difference** between your bid and the buyout. + +### My Trades + +One screen to manage all your auction activity: + +- **Active Listings** - Items you're currently selling +- **Active Bids** - Auctions you're competing in +- **Ready to Claim** - Won items and currency waiting for pickup + +> Players can **cancel** their own listings. Items are returned as a claim. + +### Notifications + +Get notified immediately when your item sells, when you're outbid, or when you win an auction. Offline? You'll get a full **claim summary** when you log back in showing exactly what's waiting for you. + +### Trade Slots + +Each player has a limited number of **Trade Slots** (default 8). Trade Slots are shared between Active Listings, Active Bids, and Pending Claims. + +> Server owners can grant players more slots via permissions up to a maximum of 45. + +--- + +## Item Blacklist 🚫 + +Server owners have full control over what items can be listed on the Auction House. + +**Block by:** +- **Item** - Specific registry names like `minecraft:diamond_sword` +- **Wildcard** - Patterns like `minecraft:diamond*` to block all diamond items +- **Mod** - Block all items from a specific mod +- **NBT Tag** - Block items containing certain NBT data + +> **Soulbound** and **Profile-Slotbound** items are always blocked automatically. Bound items cannot be traded! + +**Blacklist Commands:** +- `/kam auction blacklist add ` - Add entry +- `/kam auction blacklist remove ` - Remove entry +- `/kam auction blacklist list` - View all blacklisted entries +- `/kam auction blacklist check` - Check if your held item is blacklisted +- `/kam auction blacklist reload` - Reload from config + +--- + +## Trader Enhancements 🛒 + +### Stock System + +Traders now support **limited stock** with automatic restocking! + +**Stock Modes:** +- **Global** - All players share the same stock pool +- **Per-Player** - Each player has their own purchase limits + +**Restock Timers:** +- **MC Daily / Weekly** - Restock based on Minecraft time +- **RL Daily / Weekly** - Restock based on real-life time +- **Custom** - Set your own interval in MC ticks or real-time + +**Visual Feedback:** +- Stock count displayed next to each trade +- **Out of Stock** overlay when unavailable +- Live **countdown timer** until next restock + +> Admins can manually reset stock or cooldowns via the Trader GUI. + + +### Currency Trades + +Trades can now require **currency on top of items**. Combine material costs with coin costs for balanced economy progression! + +--- + +## Auctioneer Role 🧑‍💼 + +New NPC role for the Auction House! Set the role on any NPC and players can access the full Auction House by interacting with them. +> No configuration needed + +### Auction Command + +Players can also access the Auction House via command: + +- `/kam auction open` - Opens the Auction House GUI + +**Admin Commands:** +- `/kam auction list [page]` - View active auctions in chat +- `/kam auction view ` - View a player's auction activity + +--- + diff --git a/src/api b/src/api index a6b4a131c..2e5d86993 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit a6b4a131c843f27bbd3c561168f18498cb9d59ec +Subproject commit 2e5d86993cd47e6df4572323514d4fe2166c570c diff --git a/src/main/java/foxz/command/CommandNoppes.java b/src/main/java/foxz/command/CommandNoppes.java index 8c051aa85..f5b5ccd73 100644 --- a/src/main/java/foxz/command/CommandNoppes.java +++ b/src/main/java/foxz/command/CommandNoppes.java @@ -2,9 +2,16 @@ package foxz.command; import net.minecraft.command.CommandBase; import net.minecraft.command.ICommandSender; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; import java.util.List; +/** + * @deprecated The /noppes command is deprecated and no longer supported. + * Please use the /kam command instead for all CNPC+ operations. + */ +@Deprecated public class CommandNoppes extends CommandBase { public CmdNoppes noppes = new CmdNoppes(this); @@ -21,6 +28,15 @@ public class CommandNoppes extends CommandBase { @Override public void processCommand(ICommandSender var1, String[] var2) { + // Send deprecation warning + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[DEPRECATED] " + + EnumChatFormatting.YELLOW + "The /noppes command is deprecated and no longer supported.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "Please use " + EnumChatFormatting.GREEN + "/kam" + EnumChatFormatting.YELLOW + " commands instead.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + + "Type " + EnumChatFormatting.WHITE + "/kam help" + EnumChatFormatting.GRAY + " for available commands.")); + + // Still execute the command for backwards compatibility noppes.processCommand(var1, var2); } diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 37e1232c4..0cac8805e 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -1,10 +1,10 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.player.EntityPlayer; diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java new file mode 100644 index 000000000..87d7e4385 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java @@ -0,0 +1,179 @@ +package kamkeel.npcs.command; + +import kamkeel.npcs.command.auction.BlacklistSubCommand; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.controllers.data.PlayerData; + +import java.util.List; +import java.util.UUID; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendMessage; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class AuctionCommand extends CommandKamkeelBase { + + public AuctionCommand() { + super(); + registerNestedCommand(new BlacklistSubCommand()); + } + + @Override + public String getCommandName() { + return "auction"; + } + + @Override + public String getDescription() { + return "Auction operations"; + } + + @SubCommand( + desc = "List active auctions", + usage = "[page]" + ) + public void list(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(sender, "Auction system is not available"); + return; + } + + int page = 1; + if (args.length > 0) { + try { + page = Integer.parseInt(args[0]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid page number: " + args[0]); + return; + } + } + + List listings = controller.getActiveListings(null, page - 1, 10); + int total = controller.getTotalActiveListings(null); + int totalPages = Math.max(1, (int) Math.ceil((double) total / 10)); + + if (listings.isEmpty()) { + sendMessage(sender, "\u00A77No active auctions found"); + return; + } + + sendMessage(sender, String.format("\u00A76=== Active Auctions (Page %d/%d) ===", page, totalPages)); + for (AuctionListing listing : listings) { + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown"; + String price = listing.hasBids() ? + String.format("%,d (bid)", listing.currentBid) : + String.format("%,d (start)", listing.startingPrice); + sendMessage(sender, String.format("\u00A7e%s \u00A77by \u00A7b%s\u00A77 - \u00A76%s %s", + itemName, listing.sellerName, price, ConfigMarket.CurrencyName)); + } + } + + @SubCommand( + desc = "View player's auction activity", + usage = "" + ) + public void view(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(sender, "Auction system is not available"); + return; + } + + String playername = args[0]; + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + UUID playerUUID = UUID.fromString(playerdata.uuid); + List activeListings = controller.getPlayerActiveListings(playerUUID); + List activeBids = controller.getPlayerActiveBids(playerUUID); + int claimCount = controller.getPlayerClaims(playerUUID).size(); + + sendMessage(sender, String.format("\u00A76=== Auction Activity for %s ===", playerdata.playername)); + sendMessage(sender, String.format("\u00A77Active Listings: \u00A7e%d", activeListings.size())); + sendMessage(sender, String.format("\u00A77Active Bids: \u00A7e%d", activeBids.size())); + sendMessage(sender, String.format("\u00A77Pending Claims: \u00A7e%d", claimCount)); + + if (!activeListings.isEmpty()) { + sendMessage(sender, "\u00A7a--- Selling ---"); + for (AuctionListing listing : activeListings) { + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown"; + String price = listing.hasBids() ? + String.format("%,d (bid)", listing.currentBid) : + String.format("%,d (start)", listing.startingPrice); + sendMessage(sender, String.format(" \u00A7e%s \u00A77- \u00A76%s", itemName, price)); + } + } + + if (!activeBids.isEmpty()) { + sendMessage(sender, "\u00A7e--- Bidding ---"); + for (AuctionListing bid : activeBids) { + String itemName = bid.item != null ? bid.item.getDisplayName() : "Unknown"; + sendMessage(sender, String.format(" \u00A7e%s \u00A77- \u00A76%,d %s", + itemName, bid.currentBid, ConfigMarket.CurrencyName)); + } + } + } + } + + @SubCommand( + desc = "Open the Auction GUI", + usage = "", + permission = 0 + ) + public void open(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + if (!(sender instanceof EntityPlayer)) { + sendError(sender, "This command can only be used by players"); + return; + } + + EntityPlayer player = (EntityPlayer) sender; + + // Open the auction GUI without requiring an NPC + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, null); + sendResult(sender, "Opening Auction..."); + } + + @Override + public List addTabCompletionOptions(ICommandSender sender, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); + } + + // Delegate to nested commands for deeper tab completion + List nestedCompletions = getNestedTabCompletions(sender, args); + if (nestedCompletions != null) { + return nestedCompletions; + } + + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index 356ca8a31..d0341f211 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -10,6 +10,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.server.MinecraftServer; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.config.ConfigMain; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.scripted.CustomNPCsException; import java.lang.reflect.Method; @@ -44,6 +45,9 @@ public class CommandKamkeel extends CommandBase { registerCommand(new CommandCommand()); registerCommand(new EffectCommand()); registerCommand(new AbilityCommand()); + registerCommand(new MoneyCommand()); + if (ConfigMarket.AuctionEnabled) + registerCommand(new AuctionCommand()); if (ConfigMain.AttributesEnabled) registerCommand(new AttributeCommand()); } diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java index af2366f89..df4d34998 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java @@ -11,13 +11,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public abstract class CommandKamkeelBase extends CommandBase { public Map subcommands = new HashMap(); + public Map nestedCommands = new HashMap(); public CommandKamkeelBase() { for (Method m : this.getClass().getDeclaredMethods()) { @@ -31,6 +29,14 @@ public abstract class CommandKamkeelBase extends CommandBase { } } + /** + * Register a nested command class as a subcommand. + * The nested command's getCommandName() becomes the subcommand name. + */ + public void registerNestedCommand(CommandKamkeelBase command) { + nestedCommands.put(command.getCommandName().toLowerCase(), command); + } + @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { @@ -51,7 +57,7 @@ public abstract class CommandKamkeelBase extends CommandBase { } public boolean runSubCommands() { - return !subcommands.isEmpty(); + return !subcommands.isEmpty() || !nestedCommands.isEmpty(); } @Retention(value = RetentionPolicy.RUNTIME) @@ -71,6 +77,22 @@ public abstract class CommandKamkeelBase extends CommandBase { } public void processSubCommand(ICommandSender sender, String command, String[] args) throws CommandException { + // Check nested commands first + CommandKamkeelBase nested = nestedCommands.get(command.toLowerCase()); + if (nested != null) { + if (!canSendNestedCommand(sender, nested)) { + throw new CommandException("You are not allowed to use this command: " + command); + } + + if (args.length == 0 || !nested.runSubCommands()) { + nested.processCommand(sender, args); + } else { + nested.processSubCommand(sender, args[0], Arrays.copyOfRange(args, 1, args.length)); + } + return; + } + + // Fall back to method subcommands Method m = subcommands.get(command.toLowerCase()); if (m == null) throw new CommandException("Unknown subcommand " + command); @@ -132,4 +154,55 @@ public abstract class CommandKamkeelBase extends CommandBase { return false; } + + public boolean canSendNestedCommand(ICommandSender sender, CommandKamkeelBase nested) { + int permission = nested.getRequiredPermissionLevel(); + String subCommand = nested.getCommandName(); + + if (sender.canCommandSenderUseCommand(permission, getSubUniversalPermission())) { + return true; + } + + if (sender.canCommandSenderUseCommand(permission, getSubCommandPermission(subCommand))) { + return true; + } + + if (sender instanceof EntityPlayer) { + return CustomNpcsPermissions.hasCustomPermission((EntityPlayer) sender, getSubCommandPermission(subCommand)); + } + + return false; + } + + // ========================================= + // Tab Completion Helpers + // ========================================= + + /** + * Get all subcommand names (both method-based and nested) + */ + public String[] getAllSubCommandNames() { + Set names = new HashSet<>(); + names.addAll(subcommands.keySet()); + names.addAll(nestedCommands.keySet()); + return names.toArray(new String[0]); + } + + /** + * Get tab completions for nested command at given depth + */ + public List getNestedTabCompletions(ICommandSender sender, String[] args) { + if (args.length == 0) return null; + + String subCmd = args[0].toLowerCase(); + CommandKamkeelBase nested = nestedCommands.get(subCmd); + + if (nested != null && args.length > 1) { + // Delegate to nested command + String[] nestedArgs = Arrays.copyOfRange(args, 1, args.length); + return nested.addTabCompletionOptions(sender, nestedArgs); + } + + return null; + } } diff --git a/src/main/java/kamkeel/npcs/command/MoneyCommand.java b/src/main/java/kamkeel/npcs/command/MoneyCommand.java new file mode 100644 index 000000000..ee034243d --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/MoneyCommand.java @@ -0,0 +1,168 @@ +package kamkeel.npcs.command; + +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; + +import java.util.List; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class MoneyCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "money"; + } + + @Override + public String getDescription() { + return "Currency operations"; + } + + @SubCommand( + desc = "Show player's balance", + usage = "" + ) + public void balance(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + long balance = currency.getBalance(); + sendResult(sender, String.format("Player \u00A7b%s\u00A77 has \u00A76%,d %s", + playerdata.playername, balance, ConfigMarket.CurrencyName)); + } + } + + @SubCommand( + desc = "Give currency to player", + usage = " " + ) + public void give(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount <= 0) { + sendError(sender, "Amount must be positive"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + if (currency.deposit(amount)) { + playerdata.save(); + sendResult(sender, String.format("Gave \u00A76%,d %s\u00A77 to player \u00A7b%s\u00A77. New balance: \u00A76%,d", + amount, ConfigMarket.CurrencyName, playerdata.playername, currency.getBalance())); + } else { + sendError(sender, String.format("Failed to give currency to %s (would exceed max balance)", + playerdata.playername)); + } + } + } + + @SubCommand( + desc = "Withdraw currency from player", + usage = " " + ) + public void withdraw(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount <= 0) { + sendError(sender, "Amount must be positive"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + if (currency.withdraw(amount)) { + playerdata.save(); + sendResult(sender, String.format("Withdrew \u00A76%,d %s\u00A77 from player \u00A7b%s\u00A77. New balance: \u00A76%,d", + amount, ConfigMarket.CurrencyName, playerdata.playername, currency.getBalance())); + } else { + sendError(sender, String.format("Failed to withdraw from %s (insufficient funds)", + playerdata.playername)); + } + } + } + + @SubCommand( + desc = "Set player's balance", + usage = " " + ) + public void set(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount < 0) { + sendError(sender, "Amount cannot be negative"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + currency.setBalance(amount); + playerdata.save(); + sendResult(sender, String.format("Set balance of player \u00A7b%s\u00A77 to \u00A76%,d %s", + playerdata.playername, currency.getBalance(), ConfigMarket.CurrencyName)); + } + } + + @Override + public List addTabCompletionOptions(ICommandSender par1, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, new String[]{"balance", "give", "withdraw", "set"}); + } + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java new file mode 100644 index 000000000..a9902a890 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java @@ -0,0 +1,226 @@ +package kamkeel.npcs.command.auction; + +import kamkeel.npcs.command.CommandKamkeelBase; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.data.AuctionBlacklist; + +import java.util.List; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendMessage; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class BlacklistSubCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "blacklist"; + } + + @Override + public String getDescription() { + return "Manage auction item blacklist"; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) throws CommandException { + // Show help when called with no subcommand + sendHelp(sender); + } + + private void sendHelp(ICommandSender sender) { + sendMessage(sender, "\u00A76=== Auction Blacklist Commands ==="); + sendMessage(sender, "\u00A7e/cnpc auction blacklist add "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist remove "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist list [item|mod|nbt]"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist reload"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist check \u00A77(checks held item)"); + } + + @SubCommand( + desc = "Add item, mod, or NBT tag to blacklist", + usage = " " + ) + public void add(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist add "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.addItem(value)) { + sendResult(sender, "Added item to blacklist: " + value); + } else { + sendError(sender, "Failed to add item to blacklist"); + } + break; + case "mod": + if (AuctionBlacklist.addMod(value)) { + sendResult(sender, "Added mod to blacklist: " + value); + } else { + sendError(sender, "Failed to add mod to blacklist"); + } + break; + case "nbt": + if (AuctionBlacklist.addNBTTag(value)) { + sendResult(sender, "Added NBT tag to blacklist: " + value); + } else { + sendError(sender, "Failed to add NBT tag to blacklist"); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "Remove item, mod, or NBT tag from blacklist", + usage = " " + ) + public void remove(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist remove "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.removeItem(value)) { + sendResult(sender, "Removed item from blacklist: " + value); + } else { + sendError(sender, "Item not found in blacklist: " + value); + } + break; + case "mod": + if (AuctionBlacklist.removeMod(value)) { + sendResult(sender, "Removed mod from blacklist: " + value); + } else { + sendError(sender, "Mod not found in blacklist: " + value); + } + break; + case "nbt": + if (AuctionBlacklist.removeNBTTag(value)) { + sendResult(sender, "Removed NBT tag from blacklist: " + value); + } else { + sendError(sender, "NBT tag not found in blacklist: " + value); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "List blacklisted items, mods, or NBT tags", + usage = "[item|mod|nbt]" + ) + public void list(ICommandSender sender, String[] args) throws CommandException { + String filter = args.length > 0 ? args[0].toLowerCase() : "all"; + + if (filter.equals("all") || filter.equals("item")) { + List items = AuctionBlacklist.getBlacklistedItems(); + sendMessage(sender, "\u00A76=== Blacklisted Items (" + items.size() + ") ==="); + if (items.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String item : items) { + sendMessage(sender, "\u00A7e " + item); + } + } + } + + if (filter.equals("all") || filter.equals("mod")) { + List mods = AuctionBlacklist.getBlacklistedMods(); + sendMessage(sender, "\u00A76=== Blacklisted Mods (" + mods.size() + ") ==="); + if (mods.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String mod : mods) { + sendMessage(sender, "\u00A7e " + mod); + } + } + } + + if (filter.equals("all") || filter.equals("nbt")) { + List tags = AuctionBlacklist.getBlacklistedNBTTags(); + sendMessage(sender, "\u00A76=== Blacklisted NBT Tags (" + tags.size() + ") ==="); + if (tags.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String tag : tags) { + sendMessage(sender, "\u00A7e " + tag); + } + } + } + + sendMessage(sender, "\u00A77Note: Soulbound and Profile-Slotbound items are always blocked."); + } + + @SubCommand( + desc = "Reload blacklist from config" + ) + public void reload(ICommandSender sender, String[] args) throws CommandException { + AuctionBlacklist.reload(); + sendResult(sender, "Blacklist reloaded from config"); + } + + @SubCommand( + desc = "Check if held item is blacklisted", + permission = 0 + ) + public void check(ICommandSender sender, String[] args) throws CommandException { + if (!(sender instanceof EntityPlayer)) { + sendError(sender, "This command can only be used by players"); + return; + } + + EntityPlayer player = (EntityPlayer) sender; + ItemStack heldItem = player.getHeldItem(); + + if (heldItem == null) { + sendError(sender, "You are not holding an item"); + return; + } + + String registryName = AuctionBlacklist.getRegistryName(heldItem); + sendMessage(sender, "\u00A76=== Blacklist Check ==="); + sendMessage(sender, "\u00A77Item: \u00A7e" + heldItem.getDisplayName()); + sendMessage(sender, "\u00A77Registry: \u00A7e" + (registryName != null ? registryName : "unknown")); + + boolean isBlacklisted = AuctionBlacklist.isBlacklisted(heldItem); + boolean canBypass = AuctionBlacklist.canBypass(player); + + if (isBlacklisted) { + sendMessage(sender, "\u00A7cStatus: BLACKLISTED"); + if (canBypass) { + sendMessage(sender, "\u00A7aYou have bypass permission"); + } + } else { + sendMessage(sender, "\u00A7aStatus: ALLOWED"); + } + } + + @Override + public List addTabCompletionOptions(ICommandSender sender, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); + } + if (args.length == 2) { + String subCmd = args[0].toLowerCase(); + if (subCmd.equals("add") || subCmd.equals("remove") || subCmd.equals("list")) { + return getListOfStringsMatchingLastWord(args, new String[]{"item", "mod", "nbt"}); + } + } + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/ProfileController.java b/src/main/java/kamkeel/npcs/controllers/ProfileController.java index f7ac06bb1..103da1309 100644 --- a/src/main/java/kamkeel/npcs/controllers/ProfileController.java +++ b/src/main/java/kamkeel/npcs/controllers/ProfileController.java @@ -832,6 +832,15 @@ public class ProfileController implements IProfileHandler { else compound = (NBTTagCompound) compound.copy(); playerData.setNBT(compound); + + // Trade data is shared across all profile slots - copy from player's live data + PlayerData liveData = PlayerData.get(player); + if (liveData != null) { + NBTTagCompound tradeCompound = new NBTTagCompound(); + liveData.tradeData.writeToNBT(tradeCompound); + playerData.tradeData.readFromNBT(tradeCompound); + } + return playerData; } diff --git a/src/main/java/kamkeel/npcs/controllers/TelegraphController.java b/src/main/java/kamkeel/npcs/controllers/TelegraphController.java new file mode 100644 index 000000000..5449245a6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/TelegraphController.java @@ -0,0 +1,392 @@ +package kamkeel.npcs.controllers; + +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.world.World; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ITelegraph; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.handler.ITelegraphHandler; +import noppes.npcs.scripted.ScriptTelegraph; +import noppes.npcs.scripted.ScriptTelegraphInstance; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Server-side controller for managing telegraphs. + * Provides factory methods for creating telegraphs and spawn methods for sending to clients. + * Also manages saved telegraph presets. + */ +public class TelegraphController implements ITelegraphHandler { + + public static TelegraphController Instance; + + // Saved telegraph presets (name -> Telegraph) + private HashMap savedTelegraphs = new HashMap<>(); + + // Active server-side telegraph instances (for removal tracking) + private ConcurrentHashMap activeInstances = new ConcurrentHashMap<>(); + + public TelegraphController() { + } + + public static void init() { + Instance = new TelegraphController(); + Instance.load(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INTERNAL SPAWN METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Spawn a telegraph at a fixed position. + * + * @param telegraph The telegraph configuration + * @param world The world + * @param x X position + * @param y Y position + * @param z Z position + * @param yaw Rotation yaw (for directional telegraphs like LINE/CONE) + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, World world, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(world); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToDimension(instance, world.provider.dimensionId); + return instance; + } + + /** + * Spawn a telegraph at a position with default yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, World world, double x, double y, double z) { + return spawn(telegraph, world, x, y, z, 0); + } + + /** + * Spawn a telegraph that follows an entity. + * + * @param telegraph The telegraph configuration + * @param entity The entity to follow + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, Entity entity) { + return spawn(telegraph, entity, 0); + } + + /** + * Spawn a telegraph that follows an entity with yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, Entity entity, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, entity.posX, entity.posY, entity.posZ, yaw); + instance.setWorld(entity.worldObj); + instance.setEntityIdToFollow(entity.getEntityId()); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, entity); + return instance; + } + + /** + * Spawn a telegraph to a specific player only. + */ + public TelegraphInstance spawnToPlayer(Telegraph telegraph, EntityPlayerMP player, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(player.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.send(instance, player); + return instance; + } + + /** + * Spawn a telegraph to all players on the server. + */ + public TelegraphInstance spawnToAll(Telegraph telegraph, World world, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(world); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToAll(instance); + return instance; + } + + /** + * Spawn a telegraph to all players tracking an entity. + */ + public TelegraphInstance spawnToTracking(Telegraph telegraph, Entity entity, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(entity.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, entity); + return instance; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // REMOVAL METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Remove a telegraph by instance ID. + */ + public void remove(String instanceId) { + TelegraphInstance instance = activeInstances.remove(instanceId); + if (instance != null && instance.getWorld() != null) { + TelegraphRemovePacket.sendToDimension(instanceId, instance.getDimensionId()); + } + } + + /** + * Remove a telegraph instance. + */ + public void remove(TelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } + + /** + * Remove a telegraph for a specific player only. + */ + public void removeFromPlayer(String instanceId, EntityPlayerMP player) { + activeInstances.remove(instanceId); + TelegraphRemovePacket.send(instanceId, player); + } + + /** + * Get an active telegraph instance by ID. + */ + public TelegraphInstance getActiveInstance(String instanceId) { + return activeInstances.get(instanceId); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PRESET MANAGEMENT + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Save a telegraph preset (internal use). + * + * @param name The name for the preset + * @param telegraph The telegraph configuration to save + */ + public void saveInternal(String name, Telegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + saveTelegraphFile(name, telegraph); + } + + /** + * Get a saved telegraph preset by name (internal use). + * + * @param name The preset name + * @return The telegraph configuration, or null if not found + */ + public Telegraph getInternal(String name) { + return savedTelegraphs.get(name); + } + + /** + * Delete a saved telegraph preset. + * + * @param name The preset name + */ + public void delete(String name) { + Telegraph removed = savedTelegraphs.remove(name); + if (removed != null) { + File file = new File(getDir(), name + ".json"); + if (file.exists()) { + file.delete(); + } + } + } + + /** + * Check if a preset exists. + */ + public boolean has(String name) { + return savedTelegraphs.containsKey(name); + } + + /** + * Get all saved preset names. + */ + public String[] getSavedNames() { + return savedTelegraphs.keySet().toArray(new String[0]); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PERSISTENCE + // ═══════════════════════════════════════════════════════════════════════════ + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "telegraphs"); + } + + public void load() { + savedTelegraphs.clear(); + LogWriter.info("Loading telegraph presets..."); + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + return; + } + + File[] files = dir.listFiles(); + if (files == null) return; + + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + Telegraph telegraph = new Telegraph(); + telegraph.readNBT(NBTJsonUtil.LoadFile(file)); + String name = file.getName().substring(0, file.getName().length() - 5); + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + } catch (Exception e) { + LogWriter.error("Error loading telegraph preset: " + file.getName(), e); + } + } + + LogWriter.info("Loaded " + savedTelegraphs.size() + " telegraph presets."); + } + + private void saveTelegraphFile(String name, Telegraph telegraph) { + try { + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + } + + File file = new File(dir, name + ".json_new"); + File file2 = new File(dir, name + ".json"); + + NBTJsonUtil.SaveFile(file, telegraph.writeNBT()); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + } catch (Exception e) { + LogWriter.error("Error saving telegraph preset: " + name, e); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ITelegraphHandler INTERFACE IMPLEMENTATION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public ITelegraph createCircle(float radius) { + return new ScriptTelegraph(Telegraph.circle(radius)); + } + + @Override + public ITelegraph createRing(float outerRadius, float innerRadius) { + return new ScriptTelegraph(Telegraph.ring(outerRadius, innerRadius)); + } + + @Override + public ITelegraph createLine(float length, float width) { + return new ScriptTelegraph(Telegraph.line(length, width)); + } + + @Override + public ITelegraph createCone(float length, float angle) { + return new ScriptTelegraph(Telegraph.cone(length, angle)); + } + + @Override + public ITelegraph createPoint() { + return new ScriptTelegraph(Telegraph.point()); + } + + @Override + public ITelegraph create(String type) { + if (type == null) return null; + try { + TelegraphType telegraphType = TelegraphType.valueOf(type.toUpperCase()); + Telegraph telegraph; + switch (telegraphType) { + case CIRCLE: + telegraph = Telegraph.circle(3.0f); + break; + case RING: + telegraph = Telegraph.ring(5.0f, 2.0f); + break; + case LINE: + telegraph = Telegraph.line(5.0f, 2.0f); + break; + case CONE: + telegraph = Telegraph.cone(5.0f, 45.0f); + break; + case POINT: + telegraph = Telegraph.point(); + break; + default: + telegraph = Telegraph.circle(3.0f); + } + return new ScriptTelegraph(telegraph); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld world, double x, double y, double z) { + return spawn(telegraph, world, x, y, z, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld world, double x, double y, double z, float yaw) { + if (telegraph == null || world == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (World) world.getMCWorld(), x, y, z, yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity entity) { + return spawn(telegraph, entity, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity entity, float yaw) { + if (telegraph == null || entity == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (Entity) entity.getMCEntity(), yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraph get(String name) { + Telegraph telegraph = savedTelegraphs.get(name); + return telegraph != null ? new ScriptTelegraph(new Telegraph(telegraph)) : null; + } + + @Override + public void save(String name, ITelegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + saveInternal(name, internal); + } + + @Override + public void remove(ITelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 48cf46cb3..65a4e8ac5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -2,9 +2,9 @@ package kamkeel.npcs.controllers.data.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index a071fcb30..3c448d0b6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index abe05c643..6c43ea1cb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -4,9 +4,9 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -29,7 +29,7 @@ public class AbilityCharge extends Ability { // Type-specific parameters private float chargeSpeed = 0.8f; - private float damage = 15.0f; + private float damage = 8.0f; private float knockback = 3.0f; private float hitWidth = 1.5f; @@ -296,7 +296,7 @@ public class AbilityCharge extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { this.chargeSpeed = nbt.hasKey("chargeSpeed") ? nbt.getFloat("chargeSpeed") : 0.8f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 3.0f; if (nbt.hasKey("hitWidth")) { this.hitWidth = nbt.getFloat("hitWidth"); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 89ff0e3f0..611d0a4a6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; @@ -35,7 +35,7 @@ public class AbilityCutter extends Ability { private float arcAngle = 90.0f; private float range = 6.0f; - private float damage = 12.0f; + private float damage = 7.0f; private float knockback = 1.5f; private SweepMode sweepMode = SweepMode.SWIPE; @@ -217,7 +217,7 @@ public class AbilityCutter extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.arcAngle = nbt.hasKey("arcAngle") ? nbt.getFloat("arcAngle") : 90.0f; this.range = nbt.hasKey("range") ? nbt.getFloat("range") : 6.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 12.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; try { this.sweepMode = SweepMode.valueOf(nbt.getString("sweepMode")); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 0c0f9cf2c..4edc18f82 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 2427255fe..fa46c2341 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -28,7 +28,7 @@ public class AbilityGuard extends Ability { private float damageReduction = 0.5f; private boolean canCounter = false; private CounterType counterType = CounterType.FLAT; - private float counterValue = 10.0f; + private float counterValue = 6.0f; private float counterChance = 0.3f; private String counterSound = "random.wood_click"; private int counterAnimationId = -1; @@ -207,7 +207,7 @@ public class AbilityGuard extends Ability { } else if (nbt.hasKey("counterDamage")) { this.counterValue = nbt.getFloat("counterDamage"); } else { - this.counterValue = 10.0f; + this.counterValue = 6.0f; } this.counterChance = nbt.hasKey("counterChance") ? nbt.getFloat("counterChance") : 0.3f; this.counterSound = nbt.hasKey("counterSound") ? nbt.getString("counterSound") : "random.wood_click"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index b8ce70929..028259826 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -4,8 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; @@ -48,7 +48,7 @@ public class AbilityHazard extends Ability { private HazardShape shape = HazardShape.CIRCLE; private PlacementMode placement = PlacementMode.AT_TARGET; - private float damagePerTick = 1.0f; + private float damagePerSecond = 1.0f; private int damageInterval = 20; private boolean ignoreInvulnFrames = false; @@ -290,12 +290,12 @@ public class AbilityHazard extends Ability { if (damagedThisTick.contains(entity.getEntityId())) continue; if (!isInZone(entity, npc)) continue; - if (damagePerTick > 0) { + if (damagePerSecond > 0) { if (ignoreInvulnFrames) { entity.hurtResistantTime = 0; } - // Apply damage with scripted event support (no knockback for hazard ticks) - boolean wasHit = applyAbilityDamage(npc, entity, damagePerTick, 0); + // Apply damage with scripted event support (no knockback for hazard) + boolean wasHit = applyAbilityDamage(npc, entity, damagePerSecond, 0); if (!wasHit) continue; // Skip debuffs if hit was cancelled } @@ -369,7 +369,7 @@ public class AbilityHazard extends Ability { nbt.setFloat("coneAngle", coneAngle); nbt.setString("shape", shape.name()); nbt.setString("placement", placement.name()); - nbt.setFloat("damagePerTick", damagePerTick); + nbt.setFloat("damagePerSecond", damagePerSecond); nbt.setInteger("damageInterval", damageInterval); nbt.setBoolean("ignoreInvulnFrames", ignoreInvulnFrames); nbt.setInteger("slownessLevel", slownessLevel); @@ -401,7 +401,14 @@ public class AbilityHazard extends Ability { } catch (Exception e) { this.placement = PlacementMode.AT_TARGET; } - this.damagePerTick = nbt.hasKey("damagePerTick") ? nbt.getFloat("damagePerTick") : 1.0f; + // Support legacy "damagePerTick" key for backwards compatibility + if (nbt.hasKey("damagePerSecond")) { + this.damagePerSecond = nbt.getFloat("damagePerSecond"); + } else if (nbt.hasKey("damagePerTick")) { + this.damagePerSecond = nbt.getFloat("damagePerTick"); + } else { + this.damagePerSecond = 1.0f; + } this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20; this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames"); this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : -1; @@ -459,12 +466,24 @@ public class AbilityHazard extends Ability { this.placement = placement; } - public float getDamagePerTick() { - return damagePerTick; + public float getDamagePerSecond() { + return damagePerSecond; } - public void setDamagePerTick(float damagePerTick) { - this.damagePerTick = damagePerTick; + public void setDamagePerSecond(float damagePerSecond) { + this.damagePerSecond = damagePerSecond; + } + + /** @deprecated Use {@link #getDamagePerSecond()} instead */ + @Deprecated + public float getDamagePerTick() { + return damagePerSecond; + } + + /** @deprecated Use {@link #setDamagePerSecond(float)} instead */ + @Deprecated + public void setDamagePerTick(float damage) { + this.damagePerSecond = damage; } public int getDamageInterval() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index cae5f6815..834b9c055 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index caafc9d5d..408120694 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; @@ -21,7 +21,7 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class AbilityHeavyHit extends Ability { - private float damage = 15.0f; + private float damage = 8.0f; private float knockback = 2.0f; private int slownessLevel = 1; private int weaknessLevel = 0; @@ -100,7 +100,7 @@ public class AbilityHeavyHit extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 2.0f; this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : 1; this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : 0; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index a3677a9db..99583a5d0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; @@ -27,7 +27,7 @@ public class AbilityOrb extends Ability { private float orbSpeed = 0.5f; private float orbSize = 1.0f; - private float damage = 15.0f; + private float damage = 7.0f; private float knockback = 1.0f; private float maxDistance = 30.0f; private int maxLifetime = 200; @@ -301,7 +301,7 @@ public class AbilityOrb extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.orbSpeed = nbt.hasKey("orbSpeed") ? nbt.getFloat("orbSpeed") : 0.5f; this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index b47cd244a..8ff9f5410 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -23,7 +23,7 @@ import java.util.List; public class AbilityProjectile extends Ability { // Type-specific parameters - private float damage = 8.0f; + private float damage = 6.0f; private float speed = 1.5f; private float knockback = 0.5f; private String projectileType = "fireball"; @@ -179,7 +179,7 @@ public class AbilityProjectile extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 1.5f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; this.projectileType = nbt.hasKey("projectileType") ? nbt.getString("projectileType") : "fireball"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index e15cf683a..af3489214 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; @@ -27,7 +27,7 @@ public class AbilityShockwave extends Ability { private float pushRadius = 8.0f; private float pushStrength = 1.5f; - private float damage = 10.0f; + private float damage = 8.0f; private int stunDuration = 0; private int maxTargets = 10; @@ -153,7 +153,7 @@ public class AbilityShockwave extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.pushRadius = nbt.hasKey("pushRadius") ? nbt.getFloat("pushRadius") : 8.0f; this.pushStrength = nbt.hasKey("pushStrength") ? nbt.getFloat("pushStrength") : 1.5f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 10; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 1b0eb246f..00da63e7f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -4,9 +4,9 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -31,7 +31,7 @@ import java.util.List; public class AbilitySlam extends Ability { // Type-specific config - private float damage = 20.0f; + private float damage = 10.0f; private float radius = 5.0f; private float knockbackStrength = 1.5f; private float leapSpeed = 1.0f; @@ -420,7 +420,7 @@ public class AbilitySlam extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 20.0f; + damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 5.0f; knockbackStrength = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; leapSpeed = nbt.hasKey("leapSpeed") ? nbt.getFloat("leapSpeed") : 1.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 50d59e729..ad5989f25 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 021b1f7d6..05eb43798 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -4,8 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; @@ -42,7 +42,7 @@ public class AbilityTrap extends Ability { private int armTime = 20; private int maxTriggers = 1; private int triggerCooldown = 20; - private float damage = 10.0f; + private float damage = 6.0f; private float damageRadius = 0.0f; private float knockback = 0.5f; private int stunDuration = 0; @@ -352,7 +352,7 @@ public class AbilityTrap extends Ability { this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20; this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1; this.triggerCooldown = nbt.hasKey("triggerCooldown") ? nbt.getInteger("triggerCooldown") : 20; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index ab7171461..dee010da0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java index ccbae74ef..e4eb04dfe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java @@ -5,6 +5,7 @@ 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.controllers.data.PlayerTradeData; import java.util.ArrayList; import java.util.List; @@ -19,7 +20,10 @@ public class CNPCData implements IProfileData { @Override public NBTTagCompound getCurrentNBT(EntityPlayer player) { PlayerData customNPCData = PlayerData.get(player); - return customNPCData.getNBT(); + NBTTagCompound compound = customNPCData.getNBT(); + compound.removeTag(PlayerTradeData.NBT_KEY); + + return compound; } @Override @@ -32,6 +36,10 @@ public class CNPCData implements IProfileData { @Override public void setNBT(EntityPlayer player, NBTTagCompound replace) { PlayerData customNPCData = PlayerData.get(player); + + NBTTagCompound sharedData = new NBTTagCompound(); + customNPCData.tradeData.writeToNBT(sharedData); + if (replace.hasNoTags()) { PlayerData newData = new PlayerData(); newData.player = player; @@ -39,6 +47,8 @@ public class CNPCData implements IProfileData { } else { customNPCData.setNBT(replace); } + + customNPCData.tradeData.readFromNBT(sharedData); customNPCData.updateClient = true; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java similarity index 97% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java index b8f92de17..3ba858acc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java @@ -1,10 +1,10 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.nbt.NBTTagCompound; /** * Telegraph configuration - defines a visual warning shape. - * Telegraphs warn players about incoming attacks. + * Telegraphs warn players about incoming attacks or mark areas of effect. */ public class Telegraph { @@ -146,6 +146,10 @@ public class Telegraph { return t; } + public static Telegraph point() { + return new Telegraph("", TelegraphType.POINT); + } + // ═══════════════════════════════════════════════════════════════════ // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java similarity index 94% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index 2d705fb15..9f90bc29b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -1,4 +1,4 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.block.Block; import net.minecraft.entity.Entity; @@ -34,6 +34,10 @@ public class TelegraphInstance { // State private boolean isWarning = false; + // Server-side tracking (for removal) + private transient World world; + private transient int dimensionId; + public TelegraphInstance() { this.instanceId = UUID.randomUUID().toString().substring(0, 8); } @@ -303,4 +307,23 @@ public class TelegraphInstance { public void setWarning(boolean warning) { isWarning = warning; } + + public World getWorld() { + return world; + } + + public void setWorld(World world) { + this.world = world; + if (world != null) { + this.dimensionId = world.provider.dimensionId; + } + } + + public int getDimensionId() { + return dimensionId; + } + + public void setDimensionId(int dimensionId) { + this.dimensionId = dimensionId; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java similarity index 98% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java index 3dc4f9e0f..57b245fd4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java @@ -1,4 +1,4 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.world.World; @@ -95,7 +95,7 @@ public class TelegraphManager { } // ═══════════════════════════════════════════════════════════════════ - // HELPER METHODS FOR ABILITIES + // HELPER METHODS FOR SPAWNING // ═══════════════════════════════════════════════════════════════════ /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java similarity index 89% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java index c7e4978b6..4a2fe4b12 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java @@ -1,4 +1,4 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; /** * Types of telegraph shapes for visual attack warnings. diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 8d008ceea..d09882cfc 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,8 +26,8 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; import kamkeel.npcs.network.packets.data.gui.GuiErrorPacket; import kamkeel.npcs.network.packets.data.gui.GuiOpenBookPacket; diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java similarity index 75% rename from src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java rename to src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java index e0c5023cb..9e84bb224 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java @@ -1,9 +1,9 @@ -package kamkeel.npcs.network.packets.data.ability; +package kamkeel.npcs.network.packets.data.telegraph; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; @@ -16,7 +16,7 @@ import java.io.IOException; /** * Packet to remove a telegraph on the client. - * Sent from server to client when an ability's telegraph should be removed early. + * Sent from server to client when a telegraph should be removed early. */ public final class TelegraphRemovePacket extends AbstractPacket { public static final String packetName = "Data|TelegraphRemove"; @@ -67,4 +67,18 @@ public final class TelegraphRemovePacket extends AbstractPacket { public static void sendToTracking(String instanceId, net.minecraft.entity.Entity entity) { PacketHandler.Instance.sendTracking(new TelegraphRemovePacket(instanceId), entity); } + + /** + * Send remove to all players in a dimension. + */ + public static void sendToDimension(String instanceId, int dimensionId) { + PacketHandler.Instance.sendToDimension(new TelegraphRemovePacket(instanceId), dimensionId); + } + + /** + * Send remove to all players on the server. + */ + public static void sendToAll(String instanceId) { + PacketHandler.Instance.sendToAll(new TelegraphRemovePacket(instanceId)); + } } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java similarity index 84% rename from src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java rename to src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java index e9c846764..fa30229ce 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java @@ -1,10 +1,10 @@ -package kamkeel.npcs.network.packets.data.ability; +package kamkeel.npcs.network.packets.data.telegraph; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; @@ -18,7 +18,7 @@ import java.io.IOException; /** * Packet to spawn a telegraph on the client. - * Sent from server to client when an ability creates a telegraph. + * Sent from server to client when a telegraph is created. */ public final class TelegraphSpawnPacket extends AbstractPacket { public static final String packetName = "Data|TelegraphSpawn"; @@ -78,4 +78,11 @@ public final class TelegraphSpawnPacket extends AbstractPacket { public static void sendToDimension(TelegraphInstance instance, int dimensionId) { PacketHandler.Instance.sendToDimension(new TelegraphSpawnPacket(instance), dimensionId); } + + /** + * Send telegraph to all players on the server. + */ + public static void sendToAll(TelegraphInstance instance) { + PacketHandler.Instance.sendToAll(new TelegraphSpawnPacket(instance)); + } } 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 45d38068b..3ce74eb4c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -20,6 +20,7 @@ import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; @@ -56,7 +57,7 @@ public class AuctionActionPacket extends AbstractPacket { } @Override - public boolean needsNPC() { return true; } + public boolean needsNPC() { return false; } // Allow opening via command without NPC // ========================================= // Client-side static methods @@ -173,11 +174,23 @@ public class AuctionActionPacket extends AbstractPacket { if (!(player instanceof EntityPlayerMP)) return; EntityPlayerMP playerMP = (EntityPlayerMP) player; - if (npc.advanced.role != EnumRoleType.Auctioneer) return; + // Skip role check if NPC is null (opened via command/script) + // Otherwise require Auctioneer role + if (npc != null && npc.advanced.role != EnumRoleType.Auctioneer) return; if (!ConfigMarket.AuctionEnabled) return; - Action requestedAction = Action.values()[in.readInt()]; + int actionOrdinal = in.readInt(); + if (actionOrdinal < 0 || actionOrdinal >= Action.values().length) { + return; // Invalid action + } + Action requestedAction = Action.values()[actionOrdinal]; + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(playerMP, "Auction system is not available."); + return; + } + String result = null; switch (requestedAction) { @@ -292,8 +305,10 @@ public class AuctionActionPacket extends AbstractPacket { String result = controller.createListing(player, item, startingPrice, buyoutPrice); if (result != null) { - // Failed - return item - player.inventory.addItemStackToInventory(item); + // Failed - return item (drop if inventory full to prevent item loss) + if (!player.inventory.addItemStackToInventory(item)) { + player.entityDropItem(item, 0.5f); + } } return result; } @@ -308,6 +323,49 @@ public class AuctionActionPacket extends AbstractPacket { default: guiType = EnumGuiType.PlayerAuction; break; } NoppesUtilServer.sendOpenGui(player, guiType, npc); + + // Send trades data when opening the trades page + if (page == 2) { + AuctionController controller = AuctionController.getInstance(); + if (controller != null) { + sendTradesData(player, controller); + } + } + } + + /** Send full trades data to player (listings, bids, claims) */ + private void sendTradesData(EntityPlayerMP player, AuctionController controller) { + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("TradesData", true); + + // Send player's max trade slots (permission-aware) + response.setInteger("MaxTradeSlots", controller.getMaxTradesForPlayer(player)); + + // Active listings (items player is selling) + List listings = controller.getPlayerActiveListings(player.getUniqueID()); + NBTTagList listingsNBT = new NBTTagList(); + for (AuctionListing listing : listings) { + listingsNBT.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + response.setTag("ActiveListings", listingsNBT); + + // Active bids (items player is bidding on) + List bids = controller.getPlayerActiveBids(player.getUniqueID()); + NBTTagList bidsNBT = new NBTTagList(); + for (AuctionListing bid : bids) { + bidsNBT.appendTag(bid.writeToNBT(new NBTTagCompound())); + } + response.setTag("ActiveBids", bidsNBT); + + // Claims (items/currency to claim) - now from PlayerData + List claims = controller.getPlayerClaims(player); + NBTTagList claimsNBT = new NBTTagList(); + for (AuctionClaim claim : claims) { + claimsNBT.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + response.setTag("Claims", claimsNBT); + + GuiDataPacket.sendGuiData(player, response); } private void handleOpenBidding(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { @@ -368,26 +426,80 @@ public class AuctionActionPacket extends AbstractPacket { // Helpers // ========================================= - private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) { - for (int i = 0; i < player.inventory.getSizeInventory(); i++) { - ItemStack stack = player.inventory.getStackInSlot(i); - if (stack != null && ItemStack.areItemStacksEqual(stack, target)) { - player.inventory.setInventorySlotContents(i, null); - return true; + /** + * Checks if two ItemStacks are the same type (item, damage, NBT) ignoring stack size. + */ + private boolean areItemsSameType(ItemStack item1, ItemStack item2) { + if (item1 == null || item2 == null) + return item1 == item2; + if (item1.getItem() != item2.getItem()) + return false; + if (item1.getItemDamage() != item2.getItemDamage()) + return false; + return ItemStack.areItemStackTagsEqual(item1, item2); + } + + /** + * Counts total matching items across all inventory stacks. + */ + private int countItemsInInventory(EntityPlayerMP player, ItemStack target) { + int count = 0; + for (int i = 0; i < player.inventory.mainInventory.length; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack != null && areItemsSameType(stack, target)) { + count += stack.stackSize; } } - return false; + return count; + } + + /** + * Removes items from inventory, consuming from multiple stacks if needed. + * Returns true if successful, false if not enough items. + */ + private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) { + if (target == null) return false; + + int needed = target.stackSize; + + // First verify player has enough total items + int available = countItemsInInventory(player, target); + if (available < needed) { + return false; + } + + // Consume items from matching stacks + for (int i = 0; i < player.inventory.mainInventory.length && needed > 0; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack == null || !areItemsSameType(stack, target)) + continue; + + if (needed >= stack.stackSize) { + // Need entire stack + needed -= stack.stackSize; + player.inventory.mainInventory[i] = null; + } else { + // Need only part of stack + stack.splitStack(needed); + needed = 0; + } + } + + player.inventory.markDirty(); + return true; } private void sendError(EntityPlayerMP player, String message) { player.addChatMessage(new net.minecraft.util.ChatComponentText( - net.minecraft.util.EnumChatFormatting.RED + "[Auction House] " + message)); + net.minecraft.util.EnumChatFormatting.RED + "[Auction] " + message)); } private void sendTradesUpdate(EntityPlayerMP player) { - NBTTagCompound compound = new NBTTagCompound(); - compound.setBoolean("TradesUpdate", true); - GuiDataPacket.sendGuiData(player, compound); + // Send full trades data to refresh the client + AuctionController controller = AuctionController.getInstance(); + if (controller != null) { + sendTradesData(player, controller); + } } private void sendListingsUpdate(EntityPlayerMP player) { diff --git a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java index 5b7cb7dfe..a04628962 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java @@ -53,7 +53,7 @@ public class GetTraderData extends AbstractPacket { // Player balance PlayerData data = PlayerData.get(player); - compound.setLong("Balance", data.currencyData.getBalance()); + compound.setLong("Balance", data.tradeData.getBalance()); // Stock info compound.setBoolean("StockEnabled", role.stock.enableStock); diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 5977d7263..ed084c578 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -24,6 +24,7 @@ import kamkeel.npcs.command.profile.CommandProfile; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.TelegraphController; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; @@ -331,7 +332,7 @@ public class CustomNpcs { new AnimationController(); new AbilityController(); AbilityController.Instance.load(); - + TelegraphController.init(); LinkedItemController.getInstance().load(); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index c83e92c22..0d758ad41 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -110,6 +110,9 @@ public class CustomNpcsPermissions { public static final Permission PROFILE_REGION_BYPASS = new Permission("customnpcs.profile.region-bypass"); public static final Permission PROFILE_MAX = new Permission("customnpcs.profile.max.*"); + // Auction + public static final Permission AUCTION_TRADES_MAX = new Permission("customnpcs.auction.trades.*"); + public static CustomNpcsPermissions Instance; private Method hasPermission; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 3bed6075f..8d390f5cc 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -3,9 +3,9 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 27995f3cb..2639c6ab5 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -68,6 +68,11 @@ public class ServerTickHandler { playerData.onLogout(); } + // Clear auction permission cache on logout + if (AuctionController.Instance != null) { + AuctionController.Instance.onPlayerLogout(event.player.getUniqueID()); + } + // Save and unload the player's profile data on logout ProfileController.Instance.logout(event.player); } diff --git a/src/main/java/noppes/npcs/client/AuctionClientConfig.java b/src/main/java/noppes/npcs/client/AuctionClientConfig.java index aaae441bf..7d6854e4c 100644 --- a/src/main/java/noppes/npcs/client/AuctionClientConfig.java +++ b/src/main/java/noppes/npcs/client/AuctionClientConfig.java @@ -15,11 +15,11 @@ import net.minecraft.nbt.NBTTagCompound; public class AuctionClientConfig { // Cached values from server private static long listingFee = 0; - private static String currencyName = "Gold"; - private static int auctionDurationHours = 48; + private static String currencyName = "Coins"; + private static int auctionDurationHours = 24; private static double minBidIncrement = 0.05; - private static int maxActiveListings = 10; - private static int claimExpirationDays = 30; + private static int maxActiveListings = 8; + private static int claimExpirationDays = 20; private static boolean auctionEnabled = false; /** @@ -47,11 +47,11 @@ public class AuctionClientConfig { */ public static void reset() { listingFee = 0; - currencyName = "Gold"; - auctionDurationHours = 48; + currencyName = "Coins"; + auctionDurationHours = 24; minBidIncrement = 0.05; - maxActiveListings = 10; - claimExpirationDays = 30; + maxActiveListings = 8; + claimExpirationDays = 20; auctionEnabled = false; } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index f5f7a3a4d..5d95560b8 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -7,7 +7,7 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ObfuscationReflectionHelper; import kamkeel.npcs.addon.client.DBCClient; import kamkeel.npcs.client.renderer.TelegraphRenderer; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.model.ModelBiped; diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 78a473f41..6142de16c 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,7 +5,7 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.InputDevicePacket; 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 1146ea4ae..58e133bc8 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -3,7 +3,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.Condition; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index e44eb6984..1ec615d7a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -47,9 +47,9 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { y += 24; - // Row 3: Damage Per Tick + Damage Interval - addLabel(new GuiNpcLabel(104, "ability.dmgPerTick", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerTick())); + // Row 3: Damage Per Second + Damage Interval + addLabel(new GuiNpcLabel(104, "ability.dmgPerSec", labelX, y + 5)); + addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerSecond())); addLabel(new GuiNpcLabel(105, "ability.dmgInterval", col2LabelX, y + 5)); addTextField(createIntField(105, col2FieldX, y, 50, hazard.getDamageInterval())); @@ -99,7 +99,7 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); break; case 104: - hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); + hazard.setDamagePerSecond(parseFloat(field, hazard.getDamagePerSecond())); break; case 105: hazard.setDamageInterval(field.getInteger()); 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 26942e57f..ff46f9174 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -50,7 +50,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { 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", "role.auctioneer"}, npc.advanced.role.ordinal())); - getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); + getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman && npc.advanced.role != EnumRoleType.Auctioneer); this.addButton(new GuiNpcButton(4, guiLeft + 85 + 160, y += 22, 52, 20, "selectServer.edit")); this.addButton(new GuiNpcButton(5, guiLeft + 85, y, 155, 20, new String[]{"job.none", "job.bard", "job.healer", "job.guard", "job.itemgiver", "role.follower", "job.spawner", "job.conversation", "job.chunkloader"}, npc.advanced.job.ordinal())); @@ -84,7 +84,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { hasChanges = true; npc.advanced.setRole(button.getValue()); - getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); + getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman && npc.advanced.role != EnumRoleType.Auctioneer); } if (button.id == 4) { save(); diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index f8f58e29b..33966d1e2 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -8,7 +8,9 @@ import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.StatCollector; import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.constants.EnumClaimType; import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerAuctionTrades; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -41,6 +43,10 @@ public class AuctionTooltipHandler { else if (screen instanceof GuiAuctionTrades) { addTradesTooltip((GuiAuctionTrades) screen, stack, tooltip); } + // Check if we're in the sell GUI + else if (screen instanceof GuiAuctionSell) { + addSellTooltip((GuiAuctionSell) screen, stack, tooltip); + } } /** @@ -54,10 +60,7 @@ public class AuctionTooltipHandler { AuctionListing listing = container.getListingForItem(stack); if (listing == null) return; - // Add separator tooltip.add(""); - tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + - StatCollector.translateToLocal("auction.title") + EnumChatFormatting.DARK_GRAY + " ---"); // Seller tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller") @@ -94,22 +97,21 @@ public class AuctionTooltipHandler { ContainerAuctionTrades container = (ContainerAuctionTrades) gui.inventorySlots; if (container == null) return; - // Check if it's an active listing - AuctionListing listing = container.getListingForItem(stack); - if (listing != null) { + // Check if it's an item I'm selling + AuctionListing sellingListing = container.getSellingListingForItem(stack); + if (sellingListing != null) { tooltip.add(""); - tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + - StatCollector.translateToLocal("auction.trades.activeListing") + EnumChatFormatting.DARK_GRAY + " ---"); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.selling")); - if (listing.hasBids()) { + if (sellingListing.hasBids()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); - tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(sellingListing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), sellingListing.bidCount)); } else { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); } - long timeRemaining = listing.getTimeRemaining(); + long timeRemaining = sellingListing.getTimeRemaining(); String timeText = StatCollector.translateToLocal("auction.timeLeft") .replace("%s", formatTimeRemaining(timeRemaining)); tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); @@ -119,29 +121,65 @@ public class AuctionTooltipHandler { return; } + // Check if it's an item I'm bidding on + AuctionListing biddingListing = container.getBiddingListingForItem(stack); + if (biddingListing != null) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding")); + + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(biddingListing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), biddingListing.bidCount)); + + long timeRemaining = biddingListing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", formatTimeRemaining(timeRemaining)); + tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid")); + return; + } + // Check if it's a claim AuctionClaim claim = container.getClaimForItem(stack); if (claim != null) { tooltip.add(""); - String claimTitle; + // Title based on claim type switch (claim.type) { case ITEM: - claimTitle = StatCollector.translateToLocal("auction.claim.item"); + if (claim.isReturned) { + // Expired - returned to seller + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expired")); + } else { + // Won item + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.won")); + } break; case CURRENCY: - claimTitle = StatCollector.translateToLocal("auction.claim.currency"); + // Sold - money to claim + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.sold")); + if (claim.itemName != null && !claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem") + .replace("%s", EnumChatFormatting.WHITE + claim.itemName)); + } + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } break; case REFUND: - claimTitle = StatCollector.translateToLocal("auction.claim.refund"); + // Outbid - refund available + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid")); + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } break; - default: - claimTitle = "Claim"; } - tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + - claimTitle + EnumChatFormatting.DARK_GRAY + " ---"); - + // Amount for currency claims (including REFUND) if (claim.type.isCurrency()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") .replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency))); @@ -158,10 +196,40 @@ public class AuctionTooltipHandler { } tooltip.add(""); - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + + // Show rebid option for REFUND claims with item (active auction) + if (claim.type == EnumClaimType.REFUND && claim.item != null) { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + } } } + /** + * Add sell page tooltip info. + */ + private static void addSellTooltip(GuiAuctionSell gui, ItemStack stack, List tooltip) { + ContainerAuctionSell container = (ContainerAuctionSell) gui.inventorySlots; + if (container == null) return; + + // Check if this is the sell slot item + ItemStack sellItem = container.getItemToSell(); + if (sellItem != null && ItemStack.areItemStacksEqual(stack, sellItem)) { + // This is the item in the sell slot + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); + return; + } + + // This is an inventory item - show add controls + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); + } + /** * Format currency with commas. */ diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java index 5159b09de..cc5e5d804 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -29,20 +29,19 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis private static final ResourceLocation ICON_BID = new ResourceLocation("customnpcs", "textures/items/npcCoinDiamond.png"); private static final ResourceLocation ICON_BUYOUT = new ResourceLocation("customnpcs", "textures/items/npcCoinEmerald.png"); - // Layout - non-final for testing/positioning - protected int itemSlotX = 56; - protected int itemSlotY = 52; - protected int btnBidX = 56; - protected int btnBidY = 80; - protected int btnBuyoutX = 80; - protected int btnBuyoutY = 80; - protected int infoX = 110; - protected int infoY = 50; - protected int infoWidth = 130; + // Layout + private int itemSlotX = 70; + private int itemSlotY = 75; + private int btnBidX = 58; + private int btnBidY = 110; + private int btnBuyoutX = 82; + private int btnBuyoutY = 110; + private int infoX = 110; + private int infoY = 50; // Button IDs - private static final int BTN_BID = 200; - private static final int BTN_BUYOUT = 201; + private int btnBidId = 200; + private int btnBuyoutId = 201; private final ContainerAuctionBidding biddingContainer; private AuctionListing listing; @@ -64,12 +63,12 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis super.initGui(); // Bid button (diamond coin) - btnBid = new GuiAuctionNavButton(BTN_BID, guiLeft + btnBidX, guiTop + btnBidY, + btnBid = new GuiAuctionNavButton(btnBidId, guiLeft + btnBidX, guiTop + btnBidY, "auction.bid.place", ICON_BID); addButton(btnBid); // Buy Now button (emerald coin) - only visible if listing has buyout - btnBuyout = new GuiAuctionNavButton(BTN_BUYOUT, guiLeft + btnBuyoutX, guiTop + btnBuyoutY, + btnBuyout = new GuiAuctionNavButton(btnBuyoutId, guiLeft + btnBuyoutX, guiTop + btnBuyoutY, "auction.bid.buyout", ICON_BUYOUT); addButton(btnBuyout); @@ -112,15 +111,12 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis if (listing == null) return; - switch (button.id) { - case BTN_BID: - openBidSubGui(); - break; - case BTN_BUYOUT: - if (listing.hasBuyout()) { - openBuyNowSubGui(); - } - break; + if (button.id == btnBidId) { + openBidSubGui(); + } else if (button.id == btnBuyoutId) { + if (listing.hasBuyout()) { + openBuyNowSubGui(); + } } } @@ -190,38 +186,46 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis /** Draw auction listing information on the right side */ private void drawAuctionInfo() { - int x = guiLeft + infoX; + int x = guiLeft + 105; int y = guiTop + infoY; - int lineHeight = 12; - String currencyName = AuctionClientConfig.getCurrencyName(); + int xRight = guiLeft + 215; + + int lineHeight = 9; // Seller String sellerLabel = StatCollector.translateToLocal("auction.seller").replace("%s", ""); fontRendererObj.drawString(EnumChatFormatting.GRAY + sellerLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.WHITE + listing.sellerName, x + 10, y, 0xFFFFFF); + + String label = listing.sellerName; + int length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + listing.sellerName, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; // Current Bid or Starting Price if (listing.hasBids()) { - String bidLabel = StatCollector.translateToLocal("auction.info.currentBid"); + String bidLabel = StatCollector.translateToLocal("auction.info.currentBid") + ":"; fontRendererObj.drawString(EnumChatFormatting.YELLOW + bidLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.GOLD + formatCurrency(listing.currentBid) + " " + currencyName, x + 10, y, 0xFFFFFF); + + label = formatCurrency(listing.currentBid); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; // Current Bidder String bidderLabel = StatCollector.translateToLocal("auction.info.highBidder"); fontRendererObj.drawString(EnumChatFormatting.GRAY + bidderLabel, x, y, 0xFFFFFF); - y += lineHeight; - String bidderName = listing.highBidderName != null ? listing.highBidderName : "???"; - fontRendererObj.drawString(EnumChatFormatting.WHITE + bidderName, x + 10, y, 0xFFFFFF); + + label = listing.highBidderName != null ? listing.highBidderName : "???"; + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; } else { String startLabel = StatCollector.translateToLocal("auction.info.startingPrice"); fontRendererObj.drawString(EnumChatFormatting.YELLOW + startLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.GOLD + formatCurrency(listing.startingPrice) + " " + currencyName, x + 10, y, 0xFFFFFF); + + label = formatCurrency(listing.startingPrice); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; } @@ -229,25 +233,31 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis if (listing.hasBuyout()) { String buyoutLabel = StatCollector.translateToLocal("auction.info.buyoutPrice"); fontRendererObj.drawString(EnumChatFormatting.GREEN + buyoutLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice) + " " + currencyName, x + 10, y, 0xFFFFFF); + + label = formatCurrency(listing.buyoutPrice); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GREEN + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; } // Time Remaining String timeLabel = StatCollector.translateToLocal("auction.info.timeLeft"); fontRendererObj.drawString(EnumChatFormatting.GRAY + timeLabel, x, y, 0xFFFFFF); - y += lineHeight; long timeRemaining = listing.getTimeRemaining(); EnumChatFormatting timeColor = timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; - fontRendererObj.drawString(timeColor + formatTimeRemaining(timeRemaining), x + 10, y, 0xFFFFFF); + + label = timeColor + formatTimeRemaining(timeRemaining); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; // Bid Count String bidCountLabel = StatCollector.translateToLocal("auction.info.bidCount"); fontRendererObj.drawString(EnumChatFormatting.GRAY + bidCountLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.WHITE + String.valueOf(listing.bidCount), x + 10, y, 0xFFFFFF); + + label = String.valueOf(listing.bidCount); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index 268ddc701..8b288c83f 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -16,7 +16,6 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.entity.EntityNPCInterface; -import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.List; @@ -30,23 +29,23 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis private static final ResourceLocation ICON_COIN = new ResourceLocation("customnpcs", "textures/items/npcCoinBronze.png"); private static final ResourceLocation ICON_BAG = new ResourceLocation("customnpcs", "textures/items/npcBag.png"); - // Layout - non-final for testing/positioning - protected int contentX = 50; - protected int contentY = 50; - protected int confirmBtnX = 180; - protected int confirmBtnY = 120; - protected int navYBuyout = 109; + // Layout + private int contentX = 58; + private int contentY = 50; + private int confirmBtnX = 200; + private int confirmBtnY = 118; + private int navYBuyout = 109; // Component IDs - private static final int TXT_STARTING_PRICE = 1; - private static final int TXT_BUYOUT_PRICE = 2; - private static final int BTN_CONFIRM = 10; - private static final int BTN_ALLOW_BUYOUT = 11; + private int txtStartingPriceId = 1; + private int txtBuyoutPriceId = 2; + private int btnConfirmId = 10; + private int btnAllowBuyoutId = 11; private final ContainerAuctionSell sellContainer; private long startingPrice = 0; private long buyoutPrice = 0; - private boolean allowBuyout = false; + private boolean allowBuyout = true; private String errorMessage = null; // Buttons @@ -67,7 +66,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Starting Price label and field addLabel(new GuiNpcLabel(1, "auction.sell.startingPrice", guiLeft + contentX, y + 5, 0xFFFFFF)); - GuiNpcTextField startingField = new GuiNpcTextField(TXT_STARTING_PRICE, this, fontRendererObj, + GuiNpcTextField startingField = new GuiNpcTextField(txtStartingPriceId, this, fontRendererObj, guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? String.valueOf(startingPrice) : ""); startingField.setIntegersOnly(); addTextField(startingField); @@ -78,20 +77,20 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis lblBuyout = new GuiNpcLabel(2, "auction.sell.buyoutPrice", guiLeft + contentX, y + 5, 0xFFFFFF); addLabel(lblBuyout); - txtBuyout = new GuiNpcTextField(TXT_BUYOUT_PRICE, this, fontRendererObj, + txtBuyout = new GuiNpcTextField(txtBuyoutPriceId, this, fontRendererObj, guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? String.valueOf(buyoutPrice) : ""); txtBuyout.setIntegersOnly(); addTextField(txtBuyout); // Allow Buyout toggle button (18x18 with bag icon) - btnAllowBuyout = new GuiAuctionNavButton(BTN_ALLOW_BUYOUT, guiLeft + navX, guiTop + navYBuyout, + btnAllowBuyout = new GuiAuctionNavButton(btnAllowBuyoutId, guiLeft + navX, guiTop + navYBuyout, "auction.sell.allowBuyout", ICON_BAG); btnAllowBuyout.setToggle(true); // This is a toggle, not a nav button btnAllowBuyout.setSelected(allowBuyout); addButton(btnAllowBuyout); // Confirm button (custom textured button - using GuiAuctionNavButton) - GuiAuctionNavButton btnConfirm = new GuiAuctionNavButton(BTN_CONFIRM, guiLeft + confirmBtnX, guiTop + confirmBtnY, + GuiAuctionNavButton btnConfirm = new GuiAuctionNavButton(btnConfirmId, guiLeft + confirmBtnX - 20, guiTop + confirmBtnY - 10, "auction.sell.confirm", ICON_COIN); addButton(btnConfirm); @@ -141,13 +140,13 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis @Override public void unFocused(GuiNpcTextField textfield) { - if (textfield.id == TXT_STARTING_PRICE) { + if (textfield.id == txtStartingPriceId) { try { startingPrice = Long.parseLong(textfield.getText()); } catch (NumberFormatException e) { startingPrice = 0; } - } else if (textfield.id == TXT_BUYOUT_PRICE) { + } else if (textfield.id == txtBuyoutPriceId) { try { String text = textfield.getText().trim(); buyoutPrice = text.isEmpty() ? 0 : Long.parseLong(text); @@ -162,9 +161,9 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis public void actionPerformed(GuiButton button) { super.actionPerformed(button); - if (button.id == BTN_CONFIRM) { + if (button.id == btnConfirmId) { createListing(); - } else if (button.id == BTN_ALLOW_BUYOUT) { + } else if (button.id == btnAllowBuyoutId) { allowBuyout = !allowBuyout; btnAllowBuyout.setSelected(allowBuyout); updateBuyoutVisibility(); @@ -175,8 +174,8 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis private void createListing() { // Get latest values from text fields - GuiNpcTextField startingField = getTextField(TXT_STARTING_PRICE); - GuiNpcTextField buyoutField = getTextField(TXT_BUYOUT_PRICE); + GuiNpcTextField startingField = getTextField(txtStartingPriceId); + GuiNpcTextField buyoutField = getTextField(txtBuyoutPriceId); if (startingField != null) { try { @@ -234,21 +233,20 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis @Override protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { // Draw sell slot background (centered in content area) - int slotX = guiLeft + contentX + 30; - int slotY = guiTop + contentY + 55; + int slotX = guiLeft + contentX; + int slotY = guiTop + contentY + 50; drawAuctionSlot(slotX, slotY); // Draw listing fee info (uses cached config from server) String feeText = StatCollector.translateToLocal("auction.sell.fee") .replace("%s", formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName()); - fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 80, 0xFFFFFF); + fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 77, 0xFFFFFF); // Draw error message if any if (errorMessage != null) { fontRendererObj.drawString(EnumChatFormatting.RED + errorMessage, guiLeft + contentX, guiTop + contentY + 95, 0xFFFFFF); } - // Draw item count indicator for sell slot drawSellSlotIndicator(slotX, slotY); } @@ -261,14 +259,11 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Color: green if valid, red if over limit int color = sellItem.stackSize <= inventoryCount ? 0x55FF55 : 0xFF5555; - - int textWidth = fontRendererObj.getStringWidth(countText); fontRendererObj.drawStringWithShadow(countText, slotX + 18 + 4, slotY + 5, color); } else { // Draw "Place item here" hint String hint = StatCollector.translateToLocal("auction.sell.placeItem"); - int hintWidth = fontRendererObj.getStringWidth(hint); - fontRendererObj.drawString(EnumChatFormatting.GRAY + hint, slotX + 9 - hintWidth / 2 + 20, slotY + 5, 0xFFFFFF); + fontRendererObj.drawString(EnumChatFormatting.GRAY + hint, slotX + 20, slotY + 5, 0xFFFFFF); } } @@ -309,14 +304,8 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis super.drawGuiContainerForegroundLayer(mouseX, mouseY); if (hasSubGui()) return; - // Draw tooltips for buttons + // Draw tooltips for buttons (icons, not items - item tooltips handled by AuctionTooltipHandler) drawSellPageTooltips(mouseX, mouseY); - - // Sell slot tooltip - drawSellSlotTooltip(mouseX, mouseY); - - // Inventory item tooltip (only add extra info, item tooltip is handled by AuctionTooltipHandler) - drawInventoryTooltip(mouseX, mouseY); } /** Draw tooltips for sell page buttons */ @@ -324,7 +313,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis List tooltip = null; // Check all buttons - GuiNpcButton confirmBtn = getButton(BTN_CONFIRM); + GuiNpcButton confirmBtn = getButton(btnConfirmId); if (confirmBtn instanceof GuiAuctionNavButton) { GuiAuctionNavButton btn = (GuiAuctionNavButton) confirmBtn; if (btn.isHovered()) { @@ -337,64 +326,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis } if (tooltip != null && !tooltip.isEmpty()) { - drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); } } - - /** Draw tooltip for sell slot */ - private void drawSellSlotTooltip(int mouseX, int mouseY) { - int slotX = guiLeft + contentX + 30; - int slotY = guiTop + contentY + 55; - - if (mouseX >= slotX && mouseX < slotX + 18 && mouseY >= slotY && mouseY < slotY + 18) { - ItemStack sellItem = sellContainer.getItemToSell(); - if (sellItem != null) { - List tooltip = new ArrayList<>(); - tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.sell.slotTitle")); - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); - drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop); - } - } - } - - /** Draw tooltip for inventory items */ - private void drawInventoryTooltip(int mouseX, int mouseY) { - Slot hoveredSlot = getSlotUnderMouse(mouseX, mouseY); - if (hoveredSlot != null && sellContainer.isPlayerInventorySlot(hoveredSlot.slotNumber)) { - if (hoveredSlot.getStack() != null) { - List tooltip = new ArrayList<>(); - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); - drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop + 40); - } - } - } - - /** Get slot under mouse position */ - private Slot getSlotUnderMouse(int mouseX, int mouseY) { - for (int i = 0; i < inventorySlots.inventorySlots.size(); i++) { - Slot slot = (Slot) inventorySlots.inventorySlots.get(i); - if (isMouseOverSlot(slot, mouseX, mouseY)) { - return slot; - } - } - return null; - } - - /** Check if mouse is over a slot */ - private boolean isMouseOverSlot(Slot slot, int mouseX, int mouseY) { - int x = guiLeft + slot.xDisplayPosition; - int y = guiTop + slot.yDisplayPosition; - return mouseX >= x && mouseX < x + 16 && mouseY >= y && mouseY < y + 16; - } - - /** Draw tooltip with proper GL state */ - private void drawTooltip(List tooltip, int x, int y) { - GL11.glPushMatrix(); - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - drawHoveringText(tooltip, x, y, fontRendererObj); - GL11.glPopAttrib(); - GL11.glPopMatrix(); - } } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java index df9298426..21b0f6f01 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java @@ -28,6 +28,7 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { // Icon textures private static final ResourceLocation ICON_X = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon.png"); private static final ResourceLocation ICON_CHECK = new ResourceLocation("customnpcs", "textures/gui/auction/check_icon.png"); + private static final ResourceLocation ICON_COIN = new ResourceLocation("customnpcs", "textures/items/npcCoinGold.png"); // Grid layout private static final int GRID_X = 56; @@ -37,9 +38,10 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { private static final int TOTAL_SLOTS = COLS * ROWS; // Slot tint colors (ARGB) - private static final int TINT_BLUE = 0x303060FF; // Active listing/bid - private static final int TINT_GREEN = 0x3030FF60; // Claimable - private static final int TINT_RED = 0x30FF4040; // Refund + private static final int TINT_BLUE = 0x303060FF; // Active listing/bid (selling) + private static final int TINT_GREEN = 0x3030FF30; // Sold (currency claim) or Won (item claim) + private static final int TINT_YELLOW = 0x30FFFF30; // Outbid (refund claim) + private static final int TINT_RED = 0x30FF3030; // Expired/Returned (item returned to seller) private final ContainerAuctionTrades tradesContainer; private int maxTradeSlots; @@ -53,7 +55,8 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { public GuiAuctionTrades(EntityNPCInterface npc, ContainerAuctionTrades container) { super(npc, container); this.tradesContainer = container; - this.maxTradeSlots = AuctionClientConfig.getMaxActiveListings() * 2; + // Initial value from client config, updated when server sends trades data + this.maxTradeSlots = AuctionClientConfig.getMaxActiveListings(); } @Override @@ -89,10 +92,28 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { // Right-click on own listing = show cancel setPending(slot, PendingOp.CANCEL); NoppesUtil.clickSound(); - } else if (claim != null && mouseButton == 0) { - // Left-click on claim = show claim - setPending(slot, PendingOp.CLAIM); + } else if (listing != null && tradesContainer.isBiddingAt(slot) && mouseButton == 0) { + // Left-click on active bid = open bidding GUI to increase bid + AuctionActionPacket.openBidding(listing.id); NoppesUtil.clickSound(); + } else if (claim != null) { + // Handle claim clicks + if (claim.type == EnumClaimType.REFUND && claim.item != null) { + // REFUND with item = can rebid + if (mouseButton == 0) { + // Left-click = open bidding to rebid + AuctionActionPacket.openBidding(claim.listingId); + NoppesUtil.clickSound(); + } else if (mouseButton == 1) { + // Right-click = show refund claim confirmation + setPending(slot, PendingOp.CLAIM); + NoppesUtil.clickSound(); + } + } else if (mouseButton == 0) { + // Normal claim - left-click shows claim confirmation + setPending(slot, PendingOp.CLAIM); + NoppesUtil.clickSound(); + } } } @@ -106,18 +127,31 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { playConfirmSound(); } clearPending(); - } else if (pendingOp == PendingOp.CLAIM && mouseButton == 0) { - // Left-click confirms claim + } else if (pendingOp == PendingOp.CLAIM) { AuctionClaim claim = tradesContainer.getClaimAt(pendingSlot); if (claim != null) { - if (claim.type.isItem()) { - AuctionActionPacket.claimItem(claim.id); + // For REFUND claims with item (rebid option), right-click confirms refund + // For all others, left-click confirms claim + boolean shouldClaim = (claim.type == EnumClaimType.REFUND && claim.item != null) + ? mouseButton == 1 // Right-click for rebiddable refunds + : mouseButton == 0; // Left-click for normal claims + + if (shouldClaim) { + if (claim.type.isItem()) { + AuctionActionPacket.claimItem(claim.id); + } else { + AuctionActionPacket.claimCurrency(claim.id); + } + playConfirmSound(); + clearPending(); } else { - AuctionActionPacket.claimCurrency(claim.id); + // Wrong button - cancel operation + clearPending(); + NoppesUtil.clickSound(); } - playConfirmSound(); + } else { + clearPending(); } - clearPending(); } else { // Wrong button - cancel operation clearPending(); @@ -156,7 +190,6 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { int col = relX / 18; int row = relY / 18; if (col >= COLS || row >= ROWS) return -1; - if (relX % 18 >= 18 || relY % 18 >= 18) return -1; return col + row * COLS; } @@ -184,13 +217,23 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { drawDarkenedOverlay(x, y); } - // Pending operation icon + // Pending operation icon or currency claim coin if (slot == pendingSlot) { if (pendingOp == PendingOp.CANCEL) { drawIconOverlay(x, y, ICON_X); } else if (pendingOp == PendingOp.CLAIM) { drawIconOverlay(x, y, ICON_CHECK); } + } else { + // Draw coin icon for currency claims or refund claims without item + AuctionClaim claim = tradesContainer.getClaimAt(slot); + if (claim != null) { + // Show coin if: CURRENCY claim OR REFUND claim without item + if (claim.type == EnumClaimType.CURRENCY || + (claim.type == EnumClaimType.REFUND && claim.item == null)) { + drawIconOverlay(x, y, ICON_COIN); + } + } } } } @@ -202,9 +245,27 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { AuctionClaim claim = tradesContainer.getClaimAt(slot); if (listing != null) { - return TINT_BLUE; + if (tradesContainer.isSellingAt(slot)) { + // Selling - Blue + return TINT_BLUE; + } else { + // Bidding - Yellow + return TINT_YELLOW; + } } else if (claim != null) { - return (claim.type == EnumClaimType.REFUND) ? TINT_RED : TINT_GREEN; + switch (claim.type) { + case CURRENCY: + // Sold item - Green + return TINT_GREEN; + case REFUND: + // Outbid - Red (whether rebiddable or not) + return TINT_RED; + case ITEM: + // Won item (green) or Returned/Expired (red) + return claim.isReturned ? TINT_RED : TINT_GREEN; + default: + return TINT_GREEN; + } } return 0; } @@ -247,12 +308,41 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { } else if (tradesContainer.isBiddingAt(slot)) { tooltip.add(EnumChatFormatting.AQUA + StatCollector.translateToLocal("auction.trades.activeBid")); tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.winningBid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.leftClickToIncrease")); } } else if (claim != null) { - if (claim.type == EnumClaimType.REFUND) { - tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.refundClaim")); - } else { - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.pendingClaim")); + switch (claim.type) { + case CURRENCY: + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.soldClaim")); + if (!claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.WHITE + claim.itemName); + } + if (!claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.buyer") + ": " + EnumChatFormatting.AQUA + claim.otherPlayerName); + } + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + break; + case REFUND: + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid")); + if (!claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.outbidBy") + ": " + EnumChatFormatting.AQUA + claim.otherPlayerName); + } + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + // Show rebid option if item is present (active listing) + if (claim.item != null) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund")); + return; // Skip the generic claim message + } + break; + case ITEM: + if (claim.isReturned) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expiredClaim")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.wonClaim")); + } + break; } tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.leftClickToClaim")); } else if (slot >= maxTradeSlots) { @@ -280,7 +370,14 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { @Override public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("TradesUpdate")) { + if (compound.hasKey("TradesData")) { + // Full trades data received from server + tradesContainer.setTradesData(compound); + // Update max trade slots from server (permission-aware) + this.maxTradeSlots = tradesContainer.getMaxTradeSlots(); + clearPending(); + } else if (compound.hasKey("TradesUpdate")) { + // Legacy flag - should not happen now but keep for safety tradesContainer.refreshData(); } } 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 da828b7a9..de7ab50e4 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -16,9 +16,9 @@ import noppes.npcs.client.gui.util.SubGuiInterface; * Shows minimum bid, player balance, and input field. */ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListener { - private static final int TXT_BID = 1; - private static final int BTN_BID = 10; - private static final int BTN_CANCEL = 11; + private int txtBidId = 1; + private int btnBidId = 10; + private int btnCancelId = 11; private final String listingId; private final long minimumBid; @@ -47,25 +47,21 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe int y = guiTop + 15; String currencyName = AuctionClientConfig.getCurrencyName(); - // Title - addLabel(new GuiNpcLabel(0, "auction.bid.title", centerX - 50, y, 0xFFFFFF)); - y += 18; - // Minimum bid info String minText = StatCollector.translateToLocal("auction.bid.minimum") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(minimumBid) + " " + currencyName); - addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y, 0xAAAAAA)); + .replace("%s", EnumChatFormatting.DARK_RED + formatCurrency(minimumBid) + " " + currencyName); + addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y)); y += 14; // Your balance String balanceText = StatCollector.translateToLocal("auction.bid.yourBalance") - .replace("%s", EnumChatFormatting.WHITE + formatCurrency(playerBalance) + " " + currencyName); - addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y, 0xAAAAAA)); + .replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y)); y += 18; // Bid amount field - addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4, 0xFFFFFF)); - GuiNpcTextField bidField = new GuiNpcTextField(TXT_BID, this, fontRendererObj, + addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4)); + GuiNpcTextField bidField = new GuiNpcTextField(txtBidId, this, fontRendererObj, guiLeft + 80, y, 100, 18, String.valueOf(minimumBid)); bidField.setIntegersOnly(); addTextField(bidField); @@ -81,13 +77,13 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe int totalBtnWidth = btnWidth * 2 + btnSpacing; int btnX = centerX - totalBtnWidth / 2; - addButton(new GuiNpcButton(BTN_BID, btnX, y, btnWidth, 20, "auction.bid.placeBid")); - addButton(new GuiNpcButton(BTN_CANCEL, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + addButton(new GuiNpcButton(btnBidId, btnX, y, btnWidth, 20, "auction.bid.placeBid")); + addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); } @Override public void unFocused(GuiNpcTextField textfield) { - if (textfield.id == TXT_BID) { + if (textfield.id == txtBidId) { try { bidAmount = Long.parseLong(textfield.getText()); } catch (NumberFormatException e) { @@ -100,19 +96,16 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe @Override public void buttonEvent(GuiButton button) { - switch (button.id) { - case BTN_BID: - placeBid(); - break; - case BTN_CANCEL: - close(); - break; + if (button.id == btnBidId) { + placeBid(); + } else if (button.id == btnCancelId) { + close(); } } private void placeBid() { // Get latest value from text field - GuiNpcTextField bidField = getTextField(TXT_BID); + GuiNpcTextField bidField = getTextField(txtBidId); if (bidField != null) { try { bidAmount = Long.parseLong(bidField.getText()); diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java index 4f20b4ded..f4061299b 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -14,8 +14,8 @@ import noppes.npcs.client.gui.util.SubGuiInterface; * Shows buyout price, player balance, and confirmation buttons. */ public class SubGuiAuctionBuyNow extends SubGuiInterface { - private static final int BTN_CONFIRM = 10; - private static final int BTN_CANCEL = 11; + private int btnConfirmId = 10; + private int btnCancelId = 11; private final String listingId; private final long buyoutPrice; @@ -44,25 +44,25 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { // Title String title = StatCollector.translateToLocal("auction.buyout.title"); int titleWidth = fontRendererObj.getStringWidth(title); - addLabel(new GuiNpcLabel(0, title, centerX - titleWidth / 2, y, 0xFFFFFF)); + addLabel(new GuiNpcLabel(0, title, centerX - titleWidth / 2, y)); y += 20; // Confirmation message String confirmMsg = StatCollector.translateToLocal("auction.buyout.confirm"); int confirmWidth = fontRendererObj.getStringWidth(confirmMsg); - addLabel(new GuiNpcLabel(1, confirmMsg, centerX - confirmWidth / 2, y, 0xAAAAAA)); + addLabel(new GuiNpcLabel(1, confirmMsg, centerX - confirmWidth / 2, y)); y += 16; // Buyout price - String priceText = EnumChatFormatting.GREEN + formatCurrency(buyoutPrice) + " " + currencyName; + String priceText = EnumChatFormatting.DARK_RED + formatCurrency(buyoutPrice) + " " + currencyName; int priceWidth = fontRendererObj.getStringWidth(priceText); - addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y, 0xFFFFFF)); + addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y)); y += 20; // Your balance String balanceLabel = StatCollector.translateToLocal("auction.bid.yourBalance"); - String balanceText = balanceLabel.replace("%s", EnumChatFormatting.WHITE + formatCurrency(playerBalance) + " " + currencyName); - addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y, 0xAAAAAA)); + String balanceText = balanceLabel.replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y)); y += 14; // Insufficient funds warning @@ -78,23 +78,20 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { int totalBtnWidth = btnWidth * 2 + btnSpacing; int btnX = centerX - totalBtnWidth / 2; - GuiNpcButton confirmBtn = new GuiNpcButton(BTN_CONFIRM, btnX, y, btnWidth, 20, "gui.yes"); + GuiNpcButton confirmBtn = new GuiNpcButton(btnConfirmId, btnX, y, btnWidth, 20, "gui.yes"); confirmBtn.enabled = playerBalance >= buyoutPrice; addButton(confirmBtn); - addButton(new GuiNpcButton(BTN_CANCEL, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); } @Override public void buttonEvent(GuiButton button) { - switch (button.id) { - case BTN_CONFIRM: - if (playerBalance >= buyoutPrice) { - confirmPurchase(); - } - break; - case BTN_CANCEL: - close(); - break; + if (button.id == btnConfirmId) { + if (playerBalance >= buyoutPrice) { + confirmPurchase(); + } + } else if (button.id == btnCancelId) { + close(); } } diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 557a08b6f..e14861033 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -11,16 +11,14 @@ 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 TRADER = "Market.Trader"; public final static String AUCTION = "Market.Auction"; + public final static String AUCTION_BLACKLIST = "Market.Auction.Blacklist"; public final static String AUCTION_LOGGING = "Market.Auction.Logging"; - // ========================================= - // Market General - // ========================================= - public static boolean MarketEnabled = true; + // Max trades grid = 9x5 = 45 slots + public static final int MAX_TRADE_SLOTS = 45; + public static final int MIN_TRADE_SLOTS = 1; // ========================================= // Currency Settings @@ -30,25 +28,28 @@ public class ConfigMarket { public static long StartingBalance = 0; public static long MaxBalance = Long.MAX_VALUE; - // ========================================= - // Trader Settings - // ========================================= - public static boolean EnableStockByDefault = false; - public static String DefaultResetType = "NONE"; - // ========================================= // Auction Settings // ========================================= public static boolean AuctionEnabled = true; public static int AuctionDurationHours = 24; public static long ListingFee = 10; + public static long MinimumListingPrice = 1; public static double SalesTaxPercent = 0.05; - public static int DefaultMaxListings = 5; + public static int DefaultMaxTrades = 8; public static int SnipeProtectionMinutes = 2; public static int ClaimExpirationDays = 20; public static double MinBidIncrementPercent = 0.05; public static double CancellationPenaltyPercent = 0.10; + // ========================================= + // Auction Blacklist Settings + // ========================================= + public static boolean BlacklistEnabled = true; + public static String[] BlacklistedItems = new String[]{}; + public static String[] BlacklistedMods = new String[]{}; + public static String[] BlacklistedNBTTags = new String[]{}; + // ========================================= // Auction Logging Settings // ========================================= @@ -67,12 +68,6 @@ public class ConfigMarket { try { config.load(); - // ========================================= - // Market General - // ========================================= - MarketEnabled = config.get(MARKET, "Enable Market System", true, - "Enable the market/economy system").getBoolean(true); - // ========================================= // Currency Settings // ========================================= @@ -90,24 +85,13 @@ public class ConfigMarket { MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE, "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE); - // ========================================= - // 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(); - // ========================================= // Auction Settings // ========================================= - config.setCategoryComment(AUCTION, "Auction House system settings. The Auction House allows players to list items for sale with bidding and buyout options."); + config.setCategoryComment(AUCTION, "Auction system settings. The Auction allows players to list items for sale with bidding and buyout options."); - AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, - "Enable the Auction House system").getBoolean(true); + AuctionEnabled = config.get(AUCTION, "Enable Auction", true, + "Enable the Auction system").getBoolean(true); AuctionDurationHours = config.get(AUCTION, "Auction Duration Hours", 24, "Default duration for auctions in hours").getInt(24); @@ -115,11 +99,16 @@ public class ConfigMarket { ListingFee = config.get(AUCTION, "Listing Fee", 10, "Flat fee charged when creating a listing").getInt(10); + MinimumListingPrice = config.get(AUCTION, "Minimum Listing Price", 1, + "Minimum starting price for auction listings").getInt(1); + if (MinimumListingPrice < 1) MinimumListingPrice = 1; + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, "Percentage of sale price taken as tax (0.05 = 5%). Tax is deleted as a currency sink.").getDouble(0.05); - DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, - "Default maximum number of active listings per player").getInt(5); + int maxTrades = config.get(AUCTION, "Default Max Trades", 8, + "Default maximum number of trade slots per player (listings + bids + claims). Min: 1, Max: 45. Players can have more via customnpcs.auction.trades.X permissions.").getInt(8); + DefaultMaxTrades = Math.max(MIN_TRADE_SLOTS, Math.min(MAX_TRADE_SLOTS, maxTrades)); SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 2, "When a bid is placed with less than this many minutes remaining, the auction is extended to this duration").getInt(2); @@ -133,10 +122,49 @@ public class ConfigMarket { CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10, "Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10); + // ========================================= + // Auction Blacklist Settings + // ========================================= + config.setCategoryComment(AUCTION_BLACKLIST, + "Item Blacklist settings for the Auction House.\n" + + "Prevents specific items, mods, or items with certain NBT tags from being listed.\n\n" + + "ITEM FORMAT: Use 'modid:itemname' format (e.g., 'minecraft:bedrock', 'customnpcs:npcWand')\n" + + "WILDCARDS: Use * for wildcards (e.g., 'customnpcs:npc*' blocks all items starting with 'npc')\n" + + "MOD FORMAT: Use just the mod ID (e.g., 'projecte' blocks all items from that mod)\n" + + "NBT FORMAT: Use the NBT tag key name (e.g., 'AdminOnly' blocks items with that tag)"); + + BlacklistEnabled = config.get(AUCTION_BLACKLIST, "Enable Blacklist", true, + "Enable item blacklist checking when creating listings").getBoolean(true); + + BlacklistedItems = config.get(AUCTION_BLACKLIST, "Blacklisted Items", new String[]{ + "minecraft:bedrock", + "minecraft:command_block", + "customnpcs:npcWand", + "customnpcs:npcMobCloner", + "customnpcs:npcScripter", + "customnpcs:npcMovingPath", + "customnpcs:npcMounter", + "customnpcs:npcTeleporter", + "customnpcs:npcTool", + "customnpcs:npcSoulstoneFilled" + }, + "Items that cannot be listed on the Auction House.\n" + + "Format: modid:itemname (supports * wildcards)\n" + + "Example: 'minecraft:diamond_sword' or 'customnpcs:npc*'").getStringList(); + + BlacklistedMods = config.get(AUCTION_BLACKLIST, "Blacklisted Mods", new String[]{}, + "All items from these mods are blocked from the Auction House.\n" + + "Format: modid (e.g., 'projecte', 'thaumcraft')").getStringList(); + + BlacklistedNBTTags = config.get(AUCTION_BLACKLIST, "Blacklisted NBT Tags", new String[]{}, + "Items containing any of these NBT tag keys are blocked.\n" + + "Checks the root level of the item's NBT compound.\n" + + "Example: 'AdminOnly', 'CreativeMode'").getStringList(); + // ========================================= // Auction Logging Settings // ========================================= - config.setCategoryComment(AUCTION_LOGGING, "Auction House logging settings. Enable specific log types to track auction activity."); + config.setCategoryComment(AUCTION_LOGGING, "Auction logging settings. Enable specific log types to track auction activity."); AuctionLoggingEnabled = config.get(AUCTION_LOGGING, "Enable Auction Logging", false, "Master switch for auction logging. If false, no auction events are logged.").getBoolean(false); @@ -163,10 +191,6 @@ public class ConfigMarket { "Log when claims are collected").getBoolean(true); // Set category order - config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList( - "Enable Market System" - ))); - config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( "Use Vault", "Currency Name", @@ -174,23 +198,26 @@ public class ConfigMarket { "Max Balance" ))); - config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( - "Enable Stock By Default", - "Default Reset Type" - ))); - config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( - "Enable Auction House", + "Enable Auction", "Auction Duration Hours", "Listing Fee", + "Minimum Listing Price", "Sales Tax Percent", - "Default Max Listings", + "Default Max Trades", "Snipe Protection Minutes", "Claim Expiration Days", "Min Bid Increment Percent", "Cancellation Penalty Percent" ))); + config.setCategoryPropertyOrder(AUCTION_BLACKLIST, new ArrayList<>(Arrays.asList( + "Enable Blacklist", + "Blacklisted Items", + "Blacklisted Mods", + "Blacklisted NBT Tags" + ))); + config.setCategoryPropertyOrder(AUCTION_LOGGING, new ArrayList<>(Arrays.asList( "Enable Auction Logging", "Log Created", diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java index 1d1faebe9..2b40000ac 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java @@ -14,9 +14,9 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class ContainerAuctionBidding extends ContainerAuction { // Display slot layout - public static final int ITEM_SLOT_X = 75; - public static final int ITEM_SLOT_Y = 70; - public static final int ITEM_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; + private int itemSlotX = 71; + private int itemSlotY = 76; + private int itemSlotIndex = PLAYER_INV_SLOT_COUNT; private final IInventory displayInventory; private AuctionListing listing; @@ -26,7 +26,7 @@ public class ContainerAuctionBidding extends ContainerAuction { displayInventory = new InventoryBasic("Display", false, 1); // Display slot (read-only) - addSlotToContainer(new Slot(displayInventory, 0, ITEM_SLOT_X, ITEM_SLOT_Y) { + addSlotToContainer(new Slot(displayInventory, 0, itemSlotX, itemSlotY) { @Override public boolean isItemValid(ItemStack stack) { return false; } @Override @@ -58,6 +58,6 @@ public class ContainerAuctionBidding extends ContainerAuction { * Check if this is the display slot. */ public boolean isDisplaySlot(int slotIndex) { - return slotIndex == ITEM_SLOT_INDEX; + return slotIndex == itemSlotIndex; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java index a4bab6efe..14b381f40 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java @@ -13,10 +13,10 @@ import noppes.npcs.entity.EntityNPCInterface; * Items are cloned from inventory, not moved. Validation happens on submit. */ public class ContainerAuctionSell extends ContainerAuction { - public static final int SELL_SLOT_X = 101; - public static final int SELL_SLOT_Y = 106; - public static final int SELL_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; - private static final int MAX_STACK = 64; + private int sellSlotX = 59; + private int sellSlotY = 101; + private int sellSlotIndex = PLAYER_INV_SLOT_COUNT; + private int maxStack = 64; private final IInventory sellInventory; @@ -25,7 +25,7 @@ public class ContainerAuctionSell extends ContainerAuction { sellInventory = new InventoryBasic("Sell", false, 1); // Sell slot blocks direct interaction - addSlotToContainer(new Slot(sellInventory, 0, SELL_SLOT_X, SELL_SLOT_Y) { + addSlotToContainer(new Slot(sellInventory, 0, sellSlotX, sellSlotY) { @Override public boolean isItemValid(ItemStack stack) { return false; } @Override @@ -59,11 +59,11 @@ public class ContainerAuctionSell extends ContainerAuction { if (sellStack == null) { // Empty slot - create new stack ItemStack newStack = source.copy(); - newStack.stackSize = Math.min(toAdd, MAX_STACK); + newStack.stackSize = Math.min(toAdd, maxStack); sellInventory.setInventorySlotContents(0, newStack); } else if (itemsMatch(sellStack, source)) { // Same item - add to existing (capped at 64) - int space = MAX_STACK - sellStack.stackSize; + int space = maxStack - sellStack.stackSize; int add = Math.min(toAdd, space); if (add > 0) { sellStack.stackSize += add; @@ -71,7 +71,7 @@ public class ContainerAuctionSell extends ContainerAuction { } else { // Different item - replace ItemStack newStack = source.copy(); - newStack.stackSize = Math.min(toAdd, MAX_STACK); + newStack.stackSize = Math.min(toAdd, maxStack); sellInventory.setInventorySlotContents(0, newStack); } } @@ -93,7 +93,7 @@ public class ContainerAuctionSell extends ContainerAuction { /** Check if sell slot is this container slot index */ public boolean isSellSlot(int slotIndex) { - return slotIndex == SELL_SLOT_INDEX; + return slotIndex == sellSlotIndex; } /** Check if two stacks match using NoppesUtilPlayer.compareItems (same item, damage, and NBT) */ diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java index 8481612d5..eab491c70 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java @@ -2,6 +2,9 @@ package noppes.npcs.containers; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.constants.EnumClaimType; import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -13,6 +16,7 @@ import java.util.List; /** * Container for viewing player's auction trades. * Shows active listings, bids, and claims in a 9x5 grid. + * Order: SOLD claims, OUTBID claims, WON claims, SELLING, BIDDING, EXPIRED claims */ public class ContainerAuctionTrades extends ContainerAuction { // Grid layout @@ -27,6 +31,11 @@ public class ContainerAuctionTrades extends ContainerAuction { private List activeBids = new ArrayList<>(); private List claims = new ArrayList<>(); private int hiddenSlot = -1; + private int maxTradeSlots = 8; // Player's max trade slots (permission-aware, synced from server) + + // Slot type tracking for proper accessor methods + private int[] slotTypes = new int[SLOT_COUNT]; // 0=empty, 1=selling, 2=bidding, 3=claim + private int[] slotDataIndex = new int[SLOT_COUNT]; // Index into respective list public ContainerAuctionTrades(EntityNPCInterface npc, EntityPlayer player) { super(npc, player); @@ -44,34 +53,163 @@ public class ContainerAuctionTrades extends ContainerAuction { refreshData(); } - /** Refresh data from controller */ + /** Refresh data from controller (server-side only) */ public void refreshData() { + // Clear existing data + clearData(); + + // Only works on server where AuctionController exists + if (AuctionController.Instance == null) return; + + List newListings = AuctionController.Instance.getPlayerActiveListings(player.getUniqueID()); + List newBids = AuctionController.Instance.getPlayerActiveBids(player.getUniqueID()); + List newClaims = AuctionController.Instance.getPlayerClaims(player.getUniqueID()); + + populateDisplay(newListings, newBids, newClaims); + } + + /** Set data from NBT received from server (client-side) */ + public void setTradesData(NBTTagCompound compound) { + // Clear existing data + clearData(); + + // Read player's max trade slots (permission-aware) + if (compound.hasKey("MaxTradeSlots")) { + maxTradeSlots = compound.getInteger("MaxTradeSlots"); + } + + List newListings = new ArrayList<>(); + List newBids = new ArrayList<>(); + List newClaims = new ArrayList<>(); + + // Parse active listings + if (compound.hasKey("ActiveListings")) { + NBTTagList listingsNBT = compound.getTagList("ActiveListings", 10); + for (int i = 0; i < listingsNBT.tagCount(); i++) { + newListings.add(AuctionListing.fromNBT(listingsNBT.getCompoundTagAt(i))); + } + } + + // Parse active bids + if (compound.hasKey("ActiveBids")) { + NBTTagList bidsNBT = compound.getTagList("ActiveBids", 10); + for (int i = 0; i < bidsNBT.tagCount(); i++) { + newBids.add(AuctionListing.fromNBT(bidsNBT.getCompoundTagAt(i))); + } + } + + // Parse claims + if (compound.hasKey("Claims")) { + NBTTagList claimsNBT = compound.getTagList("Claims", 10); + for (int i = 0; i < claimsNBT.tagCount(); i++) { + newClaims.add(AuctionClaim.fromNBT(claimsNBT.getCompoundTagAt(i))); + } + } + + populateDisplay(newListings, newBids, newClaims); + } + + /** Clear all data and reset display */ + private void clearData() { activeListings.clear(); activeBids.clear(); claims.clear(); displayInventory.clear(); - if (AuctionController.Instance == null) return; + // Reset slot tracking + for (int i = 0; i < SLOT_COUNT; i++) { + slotTypes[i] = 0; + slotDataIndex[i] = -1; + } + } - activeListings = AuctionController.Instance.getPlayerActiveListings(player.getUniqueID()); - activeBids = AuctionController.Instance.getPlayerActiveBids(player.getUniqueID()); - claims = AuctionController.Instance.getPlayerClaims(player.getUniqueID()); + /** Populate display from provided data lists */ + private void populateDisplay(List newListings, List newBids, List allClaims) { + activeListings = new ArrayList<>(newListings); + activeBids = new ArrayList<>(newBids); - // Populate display: listings first, then bids, then claims + // Sort claims into categories for proper display order + List soldClaims = new ArrayList<>(); // CURRENCY - sold items + List outbidClaims = new ArrayList<>(); // REFUND - outbid refunds + List wonClaims = new ArrayList<>(); // ITEM won (isReturned=false) + List expiredClaims = new ArrayList<>(); // ITEM returned (isReturned=true) + + for (AuctionClaim claim : allClaims) { + if (claim.type == EnumClaimType.CURRENCY) { + soldClaims.add(claim); + } else if (claim.type == EnumClaimType.REFUND) { + outbidClaims.add(claim); + } else if (claim.type == EnumClaimType.ITEM) { + if (claim.isReturned) { + expiredClaims.add(claim); + } else { + wonClaims.add(claim); + } + } + } + + // Populate display in order: SOLD, OUTBID, WON, SELLING, BIDDING, EXPIRED int slot = 0; + + // 1. Sold claims (green) - you have money to claim! + for (AuctionClaim claim : soldClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 2. Outbid claims (yellow) - refunds available + for (AuctionClaim claim : outbidClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 3. Won claims (green) - items to claim! + for (AuctionClaim claim : wonClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 4. Active listings (blue) - items you're selling + int listingIdx = 0; for (AuctionListing listing : activeListings) { if (slot >= SLOT_COUNT) break; if (listing.item != null) displayInventory.setInventorySlotContents(slot, listing.item.copy()); + slotTypes[slot] = 1; // selling + slotDataIndex[slot] = listingIdx; slot++; + listingIdx++; } + + // 5. Active bids (blue) - items you're bidding on + int bidIdx = 0; for (AuctionListing bid : activeBids) { if (slot >= SLOT_COUNT) break; if (bid.item != null) displayInventory.setInventorySlotContents(slot, bid.item.copy()); + slotTypes[slot] = 2; // bidding + slotDataIndex[slot] = bidIdx; slot++; + bidIdx++; } - for (AuctionClaim claim : claims) { + + // 6. Expired claims (red) - returned items (lowest priority) + for (AuctionClaim claim : expiredClaims) { if (slot >= SLOT_COUNT) break; + claims.add(claim); displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; slot++; } } @@ -80,37 +218,42 @@ public class ContainerAuctionTrades extends ContainerAuction { /** Get listing at slot (or null if claim/empty) */ public AuctionListing getListingAt(int slot) { - if (slot >= 0 && slot < activeListings.size()) { - return activeListings.get(slot); - } - int bidIdx = slot - activeListings.size(); - if (bidIdx >= 0 && bidIdx < activeBids.size()) { - return activeBids.get(bidIdx); + if (slot < 0 || slot >= SLOT_COUNT) return null; + int type = slotTypes[slot]; + int idx = slotDataIndex[slot]; + if (idx < 0) return null; + + if (type == 1 && idx < activeListings.size()) { + return activeListings.get(idx); + } else if (type == 2 && idx < activeBids.size()) { + return activeBids.get(idx); } return null; } /** Get claim at slot (or null if listing/empty) */ public AuctionClaim getClaimAt(int slot) { - int claimIdx = slot - activeListings.size() - activeBids.size(); - if (claimIdx >= 0 && claimIdx < claims.size()) { - return claims.get(claimIdx); + if (slot < 0 || slot >= SLOT_COUNT) return null; + if (slotTypes[slot] == 3) { + int idx = slotDataIndex[slot]; + if (idx >= 0 && idx < claims.size()) { + return claims.get(idx); + } } return null; } /** Check if slot is player's active listing */ public boolean isSellingAt(int slot) { - return slot >= 0 && slot < activeListings.size(); + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 1; } /** Check if slot is player's active bid */ public boolean isBiddingAt(int slot) { - int bidIdx = slot - activeListings.size(); - return bidIdx >= 0 && bidIdx < activeBids.size(); + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 2; } - /** Find listing matching item */ + /** Find listing matching item (searches both selling and bidding) */ public AuctionListing getListingForItem(ItemStack stack) { if (stack == null) return null; for (AuctionListing listing : activeListings) { @@ -122,6 +265,24 @@ public class ContainerAuctionTrades extends ContainerAuction { return null; } + /** Find listing in selling list */ + public AuctionListing getSellingListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing listing : activeListings) { + if (listing.item != null && ItemStack.areItemStacksEqual(listing.item, stack)) return listing; + } + return null; + } + + /** Find listing in bidding list */ + public AuctionListing getBiddingListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing bid : activeBids) { + if (bid.item != null && ItemStack.areItemStacksEqual(bid.item, stack)) return bid; + } + return null; + } + /** Find claim matching item */ public AuctionClaim getClaimForItem(ItemStack stack) { if (stack == null) return null; @@ -159,26 +320,34 @@ public class ContainerAuctionTrades extends ContainerAuction { hiddenSlot = -1; } - /** Restore item at slot from data */ + /** Restore item at slot from data using slot tracking */ private void restoreSlotItem(int slot) { - if (slot < activeListings.size()) { - AuctionListing listing = activeListings.get(slot); + if (slot < 0 || slot >= SLOT_COUNT) return; + + int type = slotTypes[slot]; + int idx = slotDataIndex[slot]; + if (idx < 0) return; + + if (type == 1 && idx < activeListings.size()) { + // Selling + AuctionListing listing = activeListings.get(idx); if (listing != null && listing.item != null) { displayInventory.setInventorySlotContents(slot, listing.item.copy()); } - } else if (slot < activeListings.size() + activeBids.size()) { - int bidIdx = slot - activeListings.size(); - AuctionListing bid = activeBids.get(bidIdx); + } else if (type == 2 && idx < activeBids.size()) { + // Bidding + AuctionListing bid = activeBids.get(idx); if (bid != null && bid.item != null) { displayInventory.setInventorySlotContents(slot, bid.item.copy()); } - } else { - int claimIdx = slot - activeListings.size() - activeBids.size(); - if (claimIdx < claims.size()) { - displayInventory.setClaimItem(slot, claims.get(claimIdx)); - } + } else if (type == 3 && idx < claims.size()) { + // Claim + displayInventory.setClaimItem(slot, claims.get(idx)); } } public int getHiddenSlot() { return hiddenSlot; } + + /** Get player's max trade slots (synced from server based on permissions) */ + public int getMaxTradeSlots() { return maxTradeSlots; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index e6b4c86c8..807190816 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -105,7 +105,7 @@ public class ContainerNPCTrader extends ContainerNpcInterface { long currencyCost = role.getCurrencyCost(i); if (currencyCost > 0) { PlayerData data = PlayerData.get(entityplayer); - if (data.currencyData.getBalance() < currencyCost) { + if (data.tradeData.getBalance() < currencyCost) { return null; // Can't afford currency cost } } @@ -117,7 +117,7 @@ public class ContainerNPCTrader extends ContainerNpcInterface { // Withdraw currency cost (after item consumption to maintain order) if (currencyCost > 0) { PlayerData data = PlayerData.get(entityplayer); - data.currencyData.withdraw(currencyCost); + data.tradeData.withdraw(currencyCost); } // Consume stock diff --git a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java index 20c03f18f..50d2b50e7 100644 --- a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java +++ b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java @@ -3,7 +3,6 @@ package noppes.npcs.containers; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.IInventory; import net.minecraft.item.ItemStack; -import noppes.npcs.CustomItems; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -49,19 +48,26 @@ public class InventoryAuctionDisplay implements IInventory { /** * Set a claim item in the inventory. - * For ITEM claims, shows the item. - * For CURRENCY/REFUND claims, shows a gold coin as visual representation. + * Shows actual item for ITEM claims. + * For CURRENCY claims, shows coin icon (null item). + * For REFUND claims, shows item if present (for rebid option), otherwise coin icon. */ public void setClaimItem(int slot, AuctionClaim claim) { if (slot >= 0 && slot < size) { claims.set(slot, claim); listings.set(slot, null); if (claim != null) { - if (claim.type == EnumClaimType.ITEM && claim.item != null) { - items[slot] = claim.item; + if (claim.type == EnumClaimType.CURRENCY) { + // Sold - gold coin icon drawn by GUI + items[slot] = null; + } else if (claim.type == EnumClaimType.REFUND) { + // Outbid - show item if present (for rebid), otherwise coin + items[slot] = claim.item != null ? claim.item.copy() : null; + } else if (claim.item != null) { + // Show actual item for ITEM claims + items[slot] = claim.item.copy(); } else { - // For currency claims, show a gold coin - items[slot] = new ItemStack(CustomItems.coinGold); + items[slot] = null; } } else { items[slot] = null; diff --git a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java index 58e4b8d86..9b8c1f995 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java +++ b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java @@ -25,7 +25,7 @@ public class AuctionConfigSync { compound.setString("CurrencyName", getEffectiveCurrencyName()); compound.setInteger("AuctionDurationHours", ConfigMarket.AuctionDurationHours); compound.setDouble("MinBidIncrement", ConfigMarket.MinBidIncrementPercent); - compound.setInteger("MaxActiveListings", ConfigMarket.DefaultMaxListings); + compound.setInteger("MaxActiveListings", ConfigMarket.DefaultMaxTrades); compound.setInteger("ClaimExpirationDays", ConfigMarket.ClaimExpirationDays); return compound; } diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 92f790b99..e6ca0c6cf 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -10,18 +10,23 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.util.ChatComponentText; import net.minecraft.util.EnumChatFormatting; import noppes.npcs.CustomNpcs; +import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.LogWriter; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.IAuctionHandler; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.constants.EnumNotificationType; +import noppes.npcs.controllers.data.AuctionBlacklist; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.controllers.data.AuctionNotification; -import noppes.npcs.controllers.data.PlayerCurrencyData; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; import noppes.npcs.util.CustomNPCsThreader; import java.io.File; @@ -30,28 +35,26 @@ import java.io.FileOutputStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * Main controller for the Auction House system. + * Main controller for the Auction system. * * Thread Safety: - * - All data modifications go through synchronized methods or use proper locking - * - Player indices are maintained for O(1) lookups + * - Uses ConcurrentHashMap for all data stores - no locking required + * - Claims are stored in PlayerData, not here - reduces contention * - Saves are performed asynchronously on the CNPC+ thread * * Performance: - * - Auction processing runs every 20 seconds (400 ticks) to reduce server load + * - Auction processing runs every 30 seconds (600 ticks) + * - Save interval is every 30 seconds (600 ticks) * - Player-specific queries use indexed maps for fast lookups - * - Read operations use read locks, write operations use write locks */ -public class AuctionController { +public class AuctionController implements IAuctionHandler { public static AuctionController Instance; - // Main data stores + // Main data stores - listings only, claims are stored in PlayerData private final Map listings = new ConcurrentHashMap<>(); - private final Map claims = new ConcurrentHashMap<>(); - private final List pendingNotifications = Collections.synchronizedList(new ArrayList<>()); + // Claims are now stored in PlayerData.tradeData, not here // ========================================= // Player Indices for O(1) Lookups @@ -63,16 +66,13 @@ public class AuctionController { /** Listings where player is current high bidder */ private final Map> playerBidIds = new ConcurrentHashMap<>(); - /** Claims by player UUID - for quick "my claims" lookups */ - private final Map> playerClaimIds = new ConcurrentHashMap<>(); + /** Cached max trade slots per player - computed on login, cleared on logout */ + private final Map playerMaxTradesCache = new ConcurrentHashMap<>(); // ========================================= // Threading & State // ========================================= - /** Lock for data modifications - allows concurrent reads */ - private final ReentrantReadWriteLock dataLock = new ReentrantReadWriteLock(); - /** Flag indicating data needs to be saved */ private final AtomicBoolean dirty = new AtomicBoolean(false); @@ -82,15 +82,12 @@ public class AuctionController { private String filePath = ""; private int tickCounter = 0; - /** Process auctions every 20 seconds (400 ticks) for performance */ - private static final int PROCESS_INTERVAL_TICKS = 400; - - /** Save dirty data every 60 seconds (1200 ticks) */ - private static final int SAVE_INTERVAL_TICKS = 1200; - private int saveTickCounter = 0; + /** Process auctions and save every 30 seconds (600 ticks) */ + private static final int TICK_INTERVAL = 600; public AuctionController() { Instance = this; + AuctionBlacklist.reload(); load(); } @@ -116,18 +113,11 @@ public class AuctionController { if (!ConfigMarket.AuctionEnabled) return; tickCounter++; - saveTickCounter++; - // Process ended auctions every 20 seconds - if (tickCounter >= PROCESS_INTERVAL_TICKS) { + // Process auctions and save every 30 seconds + if (tickCounter >= TICK_INTERVAL) { tickCounter = 0; processEndedAuctions(); - processExpiredClaims(); - } - - // Save dirty data periodically - if (saveTickCounter >= SAVE_INTERVAL_TICKS) { - saveTickCounter = 0; if (dirty.get()) { saveAsync(); } @@ -135,25 +125,19 @@ public class AuctionController { } private void processEndedAuctions() { - dataLock.writeLock().lock(); - try { - boolean changed = false; - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { - endAuction(listing); - changed = true; - } + boolean changed = false; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { + endAuction(listing); + changed = true; } - if (changed) { - markDirty(); - } - } finally { - dataLock.writeLock().unlock(); + } + if (changed) { + markDirty(); } } private void endAuction(AuctionListing listing) { - // Must be called within write lock listing.status = EnumAuctionStatus.ENDED; // Remove from seller's active listings @@ -164,68 +148,42 @@ public class AuctionController { removeFromPlayerBids(listing.highBidderUUID, listing.id); } + // Get item display name safely + String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; + if (listing.hasBids()) { // Auction sold - create claims - AuctionClaim itemClaim = AuctionClaim.createItemClaim( + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim( listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); - addClaim(itemClaim); + addClaimToPlayer(listing.highBidderUUID, itemClaim); long saleAmount = listing.currentBid; long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); long sellerReceives = saleAmount - tax; AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( - listing.sellerUUID, listing.sellerName, listing.id, sellerReceives); - addClaim(currencyClaim); + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + itemDisplayName, listing.highBidderName); + addClaimToPlayer(listing.sellerUUID, currencyClaim); // Notifications - queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, - "You won the auction for " + listing.item.getDisplayName() + "!"); - queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, - "Your " + listing.item.getDisplayName() + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, + "You won the auction for " + itemDisplayName + "!"); + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); - logAuction("SOLD", listing.sellerName, listing.item.getDisplayName(), saleAmount, + logAuction("SOLD", listing.sellerName, itemDisplayName, saleAmount, "Winner: " + listing.highBidderName + ", Tax: " + tax); } else { // No bids - return item to seller - AuctionClaim returnClaim = AuctionClaim.createItemClaim( + AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim( listing.sellerUUID, listing.sellerName, listing.id, listing.item); - addClaim(returnClaim); + addClaimToPlayer(listing.sellerUUID, returnClaim); - queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, - "Your auction for " + listing.item.getDisplayName() + " expired with no bids."); + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, + "Your auction for " + itemDisplayName + " expired with no bids."); - logAuction("EXPIRED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, "No bids"); - } - } - - private void processExpiredClaims() { - int expirationDays = ConfigMarket.ClaimExpirationDays; - - dataLock.writeLock().lock(); - try { - Iterator> iterator = claims.entrySet().iterator(); - boolean changed = false; - - while (iterator.hasNext()) { - AuctionClaim claim = iterator.next().getValue(); - if (!claim.claimed && claim.isExpired(expirationDays)) { - logAuction("CLAIM_EXPIRED", claim.playerName, - claim.type.isItem() ? (claim.item != null ? claim.item.getDisplayName() : "Unknown") : "Currency", - claim.currency, "Claim expired after " + expirationDays + " days"); - - // Remove from player index - removeFromPlayerClaims(claim.playerUUID, claim.id); - iterator.remove(); - changed = true; - } - } - - if (changed) { - markDirty(); - } - } finally { - dataLock.writeLock().unlock(); + logAuction("EXPIRED", listing.sellerName, itemDisplayName, listing.startingPrice, "No bids"); } } @@ -261,143 +219,144 @@ public class AuctionController { } } - private void addToPlayerClaims(UUID playerUUID, String claimId) { - playerClaimIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(claimId); - } + /** + * Add a claim to a player's trade data. + * If player is online, adds directly. If offline, loads their data file and updates it. + */ + private void addClaimToPlayer(UUID playerUUID, AuctionClaim claim) { + if (playerUUID == null || claim == null) return; - private void removeFromPlayerClaims(UUID playerUUID, String claimId) { - Set ids = playerClaimIds.get(playerUUID); - if (ids != null) { - ids.remove(claimId); - if (ids.isEmpty()) { - playerClaimIds.remove(playerUUID); - } + PlayerData data = PlayerDataController.Instance.getData(playerUUID); + if (data != null) { + data.tradeData.addClaim(claim); + data.updateClient = true; + data.save(); } } - /** Add a claim and update indices */ - private void addClaim(AuctionClaim claim) { - claims.put(claim.id, claim); - addToPlayerClaims(claim.playerUUID, claim.id); - } - // ========================================= // Listing Management // ========================================= public String createListing(EntityPlayer player, ItemStack item, long startingPrice, long buyoutPrice) { if (!ConfigMarket.AuctionEnabled) { - return "Auction House is disabled."; + return "Auction is disabled."; + } + + // Check blacklist (with bypass for admins) + if (AuctionBlacklist.isBlacklistedForPlayer(item, player)) { + return "This item cannot be listed on the Auction House."; + } + + // Validate minimum price + if (startingPrice < ConfigMarket.MinimumListingPrice) { + return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; + } + + // Validate buyout price if set + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return "Buyout price must be higher than starting price."; } UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); - dataLock.writeLock().lock(); - try { - // Check listing limit - int currentListings = getPlayerListingCountFast(playerUUID); - int maxListings = getMaxListingsForPlayer(player); - if (currentListings >= maxListings) { - return "You have reached your maximum listings (" + maxListings + ")."; - } - - // Check listing fee - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerCurrencyData currency = playerData.currencyData; - long fee = ConfigMarket.ListingFee; - if (!currency.canAfford(fee)) { - return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; - } - - // Deduct fee - if (!currency.withdraw(fee)) { - return "Failed to deduct listing fee."; - } - - // Calculate duration - long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L; - - // Create listing - AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs); - listings.put(listing.id, listing); - addToPlayerListings(playerUUID, listing.id); - - markDirty(); - - logAuction("CREATED", playerName, item.getDisplayName(), startingPrice, - "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + // Check trade slot limit (listings + bids + claims) + int currentTrades = getPlayerTradeSlotCount(player); + int maxTrades = getMaxTradesForPlayer(player); + if (currentTrades >= maxTrades) { + return "You have reached your maximum trade slots (" + maxTrades + ")."; } + + // Check listing fee + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + long fee = ConfigMarket.ListingFee; + if (!currency.canAfford(fee)) { + return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; + } + + // Deduct fee + if (!currency.withdraw(fee)) { + return "Failed to deduct listing fee."; + } + + // Calculate duration + long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L; + + // Create listing + AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs); + listings.put(listing.id, listing); + addToPlayerListings(playerUUID, listing.id); + + markDirty(); + + logAuction("CREATED", playerName, item.getDisplayName(), startingPrice, + "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); + + return null; // Success } public String cancelListing(String listingId, EntityPlayer player, boolean isAdmin) { - dataLock.writeLock().lock(); - try { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return "Listing not found."; - } - - if (!listing.status.canCancel()) { - return "This auction cannot be cancelled."; - } - - UUID playerUUID = player.getUniqueID(); - if (!isAdmin && !listing.isSeller(playerUUID)) { - return "You can only cancel your own listings."; - } - - // Handle penalty if there are bids - if (listing.hasBids()) { - long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); - - // Refund bidder - AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid); - addClaim(refundClaim); - - // Remove from bidder's active bids - removeFromPlayerBids(listing.highBidderUUID, listing.id); - - queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, - "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); - - // Return item to seller - AuctionClaim itemClaim = AuctionClaim.createItemClaim( - listing.sellerUUID, listing.sellerName, listing.id, listing.item); - addClaim(itemClaim); - - logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.currentBid, - "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + - (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); - } else { - // No bids - just return item - AuctionClaim itemClaim = AuctionClaim.createItemClaim( - listing.sellerUUID, listing.sellerName, listing.id, listing.item); - addClaim(itemClaim); - - logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, - "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); - } - - // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); - - listing.status = EnumAuctionStatus.CANCELLED; - markDirty(); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; } + + if (!listing.status.canCancel()) { + return "This auction cannot be cancelled."; + } + + UUID playerUUID = player.getUniqueID(); + if (!isAdmin && !listing.isSeller(playerUUID)) { + return "You can only cancel your own listings."; + } + + // Handle penalty if there are bids + if (listing.hasBids()) { + long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); + + // Refund bidder (no item - can't rebid on cancelled auction) + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); + + // Return item to seller + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.currentBid, + "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } else { + // No bids - just return item + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, + "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.CANCELLED; + markDirty(); + + return null; // Success } // ========================================= @@ -406,301 +365,332 @@ public class AuctionController { public String placeBid(String listingId, EntityPlayer player, long bidAmount) { if (!ConfigMarket.AuctionEnabled) { - return "Auction House is disabled."; + return "Auction is disabled."; } UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); - dataLock.writeLock().lock(); - try { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return "Listing not found."; - } - - // Double-check status and expiry inside lock to prevent race conditions - if (!listing.status.canBid()) { - return "This auction has ended."; - } - - if (listing.isExpired()) { - return "This auction has ended."; - } - - // CRITICAL: Prevent bidding on own items - if (listing.isSeller(playerUUID)) { - return "You cannot bid on your own auction."; - } - - // Check minimum bid - long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); - if (bidAmount < minBid) { - return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; - } - - // Check currency - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerCurrencyData currency = playerData.currencyData; - if (!currency.canAfford(bidAmount)) { - return "You cannot afford this bid."; - } - - // Store previous bidder info for refund - UUID previousBidder = listing.highBidderUUID; - long previousBid = listing.currentBid; - - // Deduct bid from new bidder FIRST - if (!currency.withdraw(bidAmount)) { - return "Failed to deduct bid amount."; - } - - // Refund previous bidder if any - if (listing.hasBids() && previousBidder != null) { - AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - previousBidder, listing.highBidderName, listing.id, previousBid); - addClaim(refundClaim); - - // Remove from previous bidder's active bids - removeFromPlayerBids(previousBidder, listing.id); - - queueNotification(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id, - "You were outbid on " + listing.item.getDisplayName() + "!"); - } - - // Update listing - listing.highBidderUUID = playerUUID; - listing.highBidderName = playerName; - listing.currentBid = bidAmount; - listing.bidCount++; - - // Add to new bidder's active bids - addToPlayerBids(playerUUID, listing.id); - - // Snipe protection - listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); - - markDirty(); - - logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount, - "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; } + + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + // CRITICAL: Prevent bidding on own items + if (listing.isSeller(playerUUID)) { + return "You cannot bid on your own auction."; + } + + // Prevent rebidding if already highest bidder + if (listing.isHighBidder(playerUUID)) { + return "You are already the highest bidder. Use buyout if you want to purchase immediately."; + } + + // Check minimum bid + long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); + if (bidAmount < minBid) { + return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // Store previous bidder info + UUID previousBidder = listing.highBidderUUID; + long previousBid = listing.currentBid; + + // Full bid amount charged since we don't allow rebidding + long amountToCharge = bidAmount; + + if (!currency.canAfford(amountToCharge)) { + return "You cannot afford this bid."; + } + + // Deduct bid from new bidder FIRST + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct bid amount."; + } + + // Refund previous bidder (include item for rebid option) + if (listing.hasBids() && previousBidder != null) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + previousBidder, listing.highBidderName, listing.id, previousBid, + listing.item.getDisplayName(), playerName, listing.item); + addClaimToPlayer(previousBidder, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(previousBidder, listing.id); + + sendNotificationToPlayer(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id, + "You were outbid on " + listing.item.getDisplayName() + "!"); + } + + // Update listing + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = bidAmount; + listing.bidCount++; + + // Add to new bidder's active bids + addToPlayerBids(playerUUID, listing.id); + + // Snipe protection + listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); + + markDirty(); + + logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount, + "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); + + return null; // Success } public String buyout(String listingId, EntityPlayer player) { if (!ConfigMarket.AuctionEnabled) { - return "Auction House is disabled."; + return "Auction is disabled."; } UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); - dataLock.writeLock().lock(); - try { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return "Listing not found."; - } - - // Double-check status and expiry inside lock - if (!listing.status.canBid()) { - return "This auction has ended."; - } - - if (listing.isExpired()) { - return "This auction has ended."; - } - - if (!listing.hasBuyout()) { - return "This auction does not have a buyout price."; - } - - // CRITICAL: Prevent buying own items - if (listing.isSeller(playerUUID)) { - return "You cannot buy your own auction."; - } - - // Check currency - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerCurrencyData currency = playerData.currencyData; - if (!currency.canAfford(listing.buyoutPrice)) { - return "You cannot afford the buyout price."; - } - - // Deduct buyout FIRST - if (!currency.withdraw(listing.buyoutPrice)) { - return "Failed to deduct buyout amount."; - } - - // Refund previous bidder if any - if (listing.hasBids() && listing.highBidderUUID != null) { - AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid); - addClaim(refundClaim); - - // Remove from previous bidder's active bids - removeFromPlayerBids(listing.highBidderUUID, listing.id); - - queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, - "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded."); - } - - // Create claims - AuctionClaim itemClaim = AuctionClaim.createItemClaim(playerUUID, playerName, listing.id, listing.item); - addClaim(itemClaim); - - long saleAmount = listing.buyoutPrice; - long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); - long sellerReceives = saleAmount - tax; - - AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( - listing.sellerUUID, listing.sellerName, listing.id, sellerReceives); - addClaim(currencyClaim); - - // Notifications - queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, - "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); - - // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); - - listing.status = EnumAuctionStatus.ENDED; - listing.highBidderUUID = playerUUID; - listing.highBidderName = playerName; - listing.currentBid = listing.buyoutPrice; - - markDirty(); - - logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice, - "Seller: " + listing.sellerName + ", Tax: " + tax); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; } + + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + if (!listing.hasBuyout()) { + return "This auction does not have a buyout price."; + } + + // CRITICAL: Prevent buying own items + if (listing.isSeller(playerUUID)) { + return "You cannot buy your own auction."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // If buyer is already the high bidder, only charge the difference + boolean isCurrentBidder = listing.isHighBidder(playerUUID); + long amountToCharge = isCurrentBidder + ? (listing.buyoutPrice - listing.currentBid) + : listing.buyoutPrice; + + if (!currency.canAfford(amountToCharge)) { + if (isCurrentBidder) { + return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout."; + } + return "You cannot afford the buyout price."; + } + + // Deduct buyout amount + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct buyout amount."; + } + + // Refund previous bidder if any AND they're not the buyer (no item - auction ended via buyout) + if (listing.hasBids() && listing.highBidderUUID != null && !isCurrentBidder) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), playerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded."); + } else if (isCurrentBidder) { + // Current bidder is buying out - just remove from their active bids + removeFromPlayerBids(playerUUID, listing.id); + } + + // Create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); + addClaimToPlayer(playerUUID, itemClaim); + + long saleAmount = listing.buyoutPrice; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + listing.item.getDisplayName(), playerName); + addClaimToPlayer(listing.sellerUUID, currencyClaim); + + // Notifications + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.ENDED; + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = listing.buyoutPrice; + + markDirty(); + + logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice, + "Seller: " + listing.sellerName + ", Tax: " + tax); + + return null; // Success } // ========================================= - // Claims + // Claims - Now stored in PlayerData // ========================================= /** - * Get player claims using indexed lookup - O(n) where n = player's claims, not all claims + * Get player claims from PlayerData. */ - public List getPlayerClaims(UUID playerUUID) { - Set claimIds = playerClaimIds.get(playerUUID); - if (claimIds == null || claimIds.isEmpty()) { + public List getPlayerClaims(EntityPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { return new ArrayList<>(); } + return playerData.tradeData.getClaimsList(); + } - List playerClaims = new ArrayList<>(); - for (String claimId : claimIds) { - AuctionClaim claim = claims.get(claimId); - if (claim != null && !claim.claimed) { - playerClaims.add(claim); - } + /** + * Get player claims by UUID (for login notification). + */ + public List getPlayerClaims(UUID playerUUID) { + EntityPlayerMP onlinePlayer = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID); + if (onlinePlayer != null) { + return getPlayerClaims(onlinePlayer); } - return playerClaims; + return new ArrayList<>(); } public String claimItem(String claimId, EntityPlayer player) { - dataLock.writeLock().lock(); - try { - AuctionClaim claim = claims.get(claimId); - if (claim == null) { - return "Claim not found."; - } - - if (claim.claimed) { - return "Already claimed."; - } - - if (!claim.isForPlayer(player.getUniqueID())) { - return "This claim is not for you."; - } - - if (!claim.type.isItem()) { - return "This is not an item claim."; - } - - if (claim.item == null) { - return "Item data is missing."; - } - - // Check inventory space - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { - return "Your inventory is full."; - } - - claim.claimed = true; - removeFromPlayerClaims(claim.playerUUID, claim.id); - markDirty(); - - logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isItem()) { + return "This is not an item claim."; + } + + if (claim.item == null) { + return "Item data is missing."; + } + + // Check inventory space + if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + return "Your inventory is full."; + } + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); + + return null; // Success } public String claimCurrency(String claimId, EntityPlayer player) { - dataLock.writeLock().lock(); - try { - AuctionClaim claim = claims.get(claimId); - if (claim == null) { - return "Claim not found."; - } - - if (claim.claimed) { - return "Already claimed."; - } - - if (!claim.isForPlayer(player.getUniqueID())) { - return "This claim is not for you."; - } - - if (!claim.type.isCurrency()) { - return "This is not a currency claim."; - } - - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - if (!playerData.currencyData.deposit(claim.currency)) { - return "Failed to deposit currency."; - } - - claim.claimed = true; - removeFromPlayerClaims(claim.playerUUID, claim.id); - markDirty(); - - logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, - claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isCurrency()) { + return "This is not a currency claim."; + } + + if (!playerData.tradeData.deposit(claim.currency)) { + return "Failed to deposit currency."; + } + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); + + return null; // Success + } + + /** + * Clean up a listing if it's ended and no longer needed. + * Now just removes ended listings after some delay since claims are in PlayerData. + */ + private void cleanupListingIfComplete(String listingId) { + if (listingId == null || listingId.isEmpty()) return; + + AuctionListing listing = listings.get(listingId); + if (listing == null) return; + + // Only cleanup ended/cancelled listings + if (listing.status == EnumAuctionStatus.ACTIVE) return; + + // Remove ended listings (claims are now in PlayerData, not here) + listings.remove(listingId); + markDirty(); } public int claimAll(EntityPlayer player) { int claimedCount = 0; - List playerClaims = getPlayerClaims(player.getUniqueID()); + List playerClaims = getPlayerClaims(player); for (AuctionClaim claim : playerClaims) { String result; @@ -724,21 +714,17 @@ public class AuctionController { public List getActiveListings(AuctionFilter filter, int page, int pageSize) { List result = new ArrayList<>(); - dataLock.readLock().lock(); - try { - for (AuctionListing listing : listings.values()) { - if (listing.status != EnumAuctionStatus.ACTIVE) continue; + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; - // Apply search filter - if (filter.hasSearchText()) { - String itemName = listing.item != null ? listing.item.getDisplayName() : ""; - if (!filter.matchesSearch(itemName)) continue; - } - - result.add(listing); + // Apply search filter (searches item name AND seller name) + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; } - } finally { - dataLock.readLock().unlock(); + + result.add(listing); } // Sort @@ -778,22 +764,19 @@ public class AuctionController { public int getTotalActiveListings(AuctionFilter filter) { int count = 0; - dataLock.readLock().lock(); - try { - for (AuctionListing listing : listings.values()) { - if (listing.status != EnumAuctionStatus.ACTIVE) continue; - if (filter.hasSearchText()) { - String itemName = listing.item != null ? listing.item.getDisplayName() : ""; - if (!filter.matchesSearch(itemName)) continue; - } - count++; + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; } - } finally { - dataLock.readLock().unlock(); + count++; } return count; } + @Override public AuctionListing getListing(String listingId) { return listings.get(listingId); } @@ -803,25 +786,18 @@ public class AuctionController { */ public long getPlayerBalance(EntityPlayer player) { PlayerData playerData = PlayerData.get(player); - if (playerData == null || playerData.currencyData == null) { + if (playerData == null || playerData.tradeData == null) { return 0; } - return playerData.currencyData.getBalance(); + return playerData.tradeData.getBalance(); } /** - * Fast O(1) lookup for player listing count using index - */ - public int getPlayerListingCountFast(UUID playerUUID) { - Set ids = playerListingIds.get(playerUUID); - return ids != null ? ids.size() : 0; - } - - /** - * Legacy method - still iterates for accuracy (in case index is stale) + * Get player listing count using index */ public int getPlayerListingCount(UUID playerUUID) { - return getPlayerListingCountFast(playerUUID); + Set ids = playerListingIds.get(playerUUID); + return ids != null ? ids.size() : 0; } /** @@ -866,7 +842,28 @@ public class AuctionController { /** * Get the total number of trade slots used by a player. - * Uses indexed lookups for O(1) performance. + * Includes active listings, active bids, and pending claims. + */ + public int getPlayerTradeSlotCount(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + int count = 0; + // Active listings + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds != null) count += listingIds.size(); + // Active bids + Set bidIds = playerBidIds.get(playerUUID); + if (bidIds != null) count += bidIds.size(); + // Pending claims (from PlayerData) + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + count += playerData.tradeData.getClaimCount(); + } + return count; + } + + /** + * Get the total number of trade slots used by a player (UUID version). + * For offline players, only returns listings + bids (claims are in their PlayerData). */ public int getPlayerTradeSlotCount(UUID playerUUID) { int count = 0; @@ -876,71 +873,168 @@ public class AuctionController { // Active bids Set bidIds = playerBidIds.get(playerUUID); if (bidIds != null) count += bidIds.size(); - // Pending claims - Set claimIds = playerClaimIds.get(playerUUID); - if (claimIds != null) count += claimIds.size(); + // Can't get claims for offline player without loading their data return count; } - private int getMaxListingsForPlayer(EntityPlayer player) { - // TODO: Check permissions for customnpcs.auction.slots.X - return ConfigMarket.DefaultMaxListings; + /** + * Get the maximum trade slots for a player. + * Uses cached value if available, otherwise computes from permissions. + */ + public int getMaxTradesForPlayer(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + + // Check cache first + Integer cached = playerMaxTradesCache.get(playerUUID); + if (cached != null) { + return cached; + } + + // Compute and cache + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + return maxTrades; + } + + /** + * Compute max trade slots from permissions. + * Checks for customnpcs.auction.trades.X permissions where X is the slot count. + * Uses the highest permission found, or DefaultMaxTrades if no permissions or lower. + * Capped at MAX_TRADE_SLOTS (45). + */ + private int computeMaxTradesForPlayer(EntityPlayer player) { + // Check for unlimited permission + if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.auction.trades.*")) { + return ConfigMarket.MAX_TRADE_SLOTS; + } + + // Find highest allowed slot count from permissions + int highestAllowed = 0; + for (int i = 1; i <= ConfigMarket.MAX_TRADE_SLOTS; i++) { + String perm = "customnpcs.auction.trades." + i; + if (CustomNpcsPermissions.hasCustomPermission(player, perm)) { + highestAllowed = i; + } + } + + // Use default if no permissions found or permission is lower than default + if (highestAllowed == 0 || highestAllowed < ConfigMarket.DefaultMaxTrades) { + highestAllowed = ConfigMarket.DefaultMaxTrades; + } + + // Cap at maximum + return Math.min(highestAllowed, ConfigMarket.MAX_TRADE_SLOTS); + } + + /** + * Refresh a player's cached max trades (call when permissions change). + */ + public void refreshPlayerMaxTrades(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + } + + /** + * Clear a player's cached max trades (call on logout). + */ + public void clearPlayerMaxTradesCache(UUID playerUUID) { + playerMaxTradesCache.remove(playerUUID); } // ========================================= // Notifications // ========================================= - private void queueNotification(UUID playerUUID, EnumNotificationType type, String listingId, String message) { - AuctionNotification notification = new AuctionNotification(playerUUID, type, listingId, message); - pendingNotifications.add(notification); - + /** + * Send a notification to a player. + * If player is online, sends immediately. Otherwise, discards (claims will notify on login). + */ + private void sendNotificationToPlayer(UUID playerUUID, EnumNotificationType type, String listingId, String message) { // Try to send immediately if player is online - EntityPlayerMP player = getOnlinePlayer(playerUUID); + EntityPlayerMP player = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID); if (player != null) { - sendNotification(player, notification); - notification.sent = true; + sendNotificationMessage(player, type, message); } + // If offline, no need to store - they'll get notified about claims on login + } + + /** + * Send a notification chat message to a player. + */ + private void sendNotificationMessage(EntityPlayerMP player, EnumNotificationType type, String message) { + EnumChatFormatting color; + switch (type) { + case AUCTION_WON: + case AUCTION_SOLD: + color = EnumChatFormatting.GREEN; + break; + case AUCTION_OUTBID: + color = EnumChatFormatting.YELLOW; + break; + case AUCTION_EXPIRED: + color = EnumChatFormatting.RED; + break; + default: + color = EnumChatFormatting.GRAY; + } + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.GOLD + "[Auction] " + color + message)); } public void onPlayerLogin(EntityPlayer player) { - UUID playerUUID = player.getUniqueID(); - boolean hasClaims = !getPlayerClaims(playerUUID).isEmpty(); + // Cache player's max trades permission + refreshPlayerMaxTrades(player); - synchronized (pendingNotifications) { - Iterator iterator = pendingNotifications.iterator(); - while (iterator.hasNext()) { - AuctionNotification notification = iterator.next(); - if (notification.isForPlayer(playerUUID) && !notification.sent) { - sendNotification((EntityPlayerMP) player, notification); - notification.sent = true; - iterator.remove(); + // Process expired claims for this player + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + int expired = playerData.tradeData.processExpiredClaims(); + if (expired > 0) { + playerData.updateClient = true; + playerData.save(); + logAuction("CLAIM_EXPIRED", player.getCommandSenderName(), "Multiple", expired, + expired + " claims expired on login"); + } + } + + // Check for pending claims and notify + List claims = getPlayerClaims(player); + if (!claims.isEmpty()) { + int itemClaims = 0; + int currencyClaims = 0; + long totalCurrency = 0; + + for (AuctionClaim claim : claims) { + if (claim.type.isItem()) { + itemClaims++; + } else { + currencyClaims++; + totalCurrency += claim.currency; } } - } - if (hasClaims) { - player.addChatMessage(new ChatComponentText(EnumChatFormatting.GOLD + - "[Auction House] " + EnumChatFormatting.YELLOW + "You have items or currency to claim!")); - } - } + StringBuilder msg = new StringBuilder(); + msg.append(EnumChatFormatting.GOLD).append("[Auction] "); + msg.append(EnumChatFormatting.YELLOW).append("You have "); - private void sendNotification(EntityPlayerMP player, AuctionNotification notification) { - player.addChatMessage(new ChatComponentText(EnumChatFormatting.GOLD + - "[Auction House] " + EnumChatFormatting.WHITE + notification.message)); - } - - private EntityPlayerMP getOnlinePlayer(UUID uuid) { - MinecraftServer server = MinecraftServer.getServer(); - if (server == null) return null; - - for (Object obj : server.getConfigurationManager().playerEntityList) { - EntityPlayerMP player = (EntityPlayerMP) obj; - if (player.getUniqueID().equals(uuid)) { - return player; + if (itemClaims > 0 && currencyClaims > 0) { + msg.append(itemClaims).append(" item(s) and "); + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); + } else if (itemClaims > 0) { + msg.append(itemClaims).append(" item(s)"); + } else { + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); } + msg.append(" to claim!"); + + player.addChatMessage(new ChatComponentText(msg.toString())); } - return null; + } + + public void onPlayerLogout(UUID playerUUID) { + // Clear cached permission + clearPlayerMaxTradesCache(playerUUID); } // ========================================= @@ -1013,13 +1107,7 @@ public class AuctionController { } // Take a snapshot of the data for async save - final NBTTagCompound compound; - dataLock.readLock().lock(); - try { - compound = writeToNBT(new NBTTagCompound()); - } finally { - dataLock.readLock().unlock(); - } + final NBTTagCompound compound = writeToNBT(new NBTTagCompound()); CustomNPCsThreader.customNPCThread.execute(() -> { try { @@ -1036,14 +1124,11 @@ public class AuctionController { /** Internal synchronous save */ private void saveInternal() { - dataLock.readLock().lock(); try { NBTTagCompound compound = writeToNBT(new NBTTagCompound()); saveToFile(compound); } catch (Exception e) { LogWriter.error("Error saving auction data", e); - } finally { - dataLock.readLock().unlock(); } } @@ -1072,11 +1157,8 @@ public class AuctionController { public void load() { listings.clear(); - claims.clear(); - pendingNotifications.clear(); playerListingIds.clear(); playerBidIds.clear(); - playerClaimIds.clear(); File saveDir = CustomNpcs.getWorldSaveDirectory(); if (saveDir == null) return; @@ -1111,7 +1193,6 @@ public class AuctionController { private void rebuildIndices() { playerListingIds.clear(); playerBidIds.clear(); - playerClaimIds.clear(); // Index listings for (AuctionListing listing : listings.values()) { @@ -1123,15 +1204,9 @@ public class AuctionController { } } - // Index claims - for (AuctionClaim claim : claims.values()) { - if (!claim.claimed) { - addToPlayerClaims(claim.playerUUID, claim.id); - } - } - + // Claims are now stored in PlayerData, not indexed here LogWriter.info("Rebuilt auction indices: " + playerListingIds.size() + " sellers, " + - playerBidIds.size() + " bidders, " + playerClaimIds.size() + " claimants"); + playerBidIds.size() + " bidders"); } private void loadFromFile(File file) throws Exception { @@ -1140,55 +1215,178 @@ public class AuctionController { } public NBTTagCompound writeToNBT(NBTTagCompound compound) { - // Listings + // Listings only - claims are stored in PlayerData NBTTagList listingsList = new NBTTagList(); for (AuctionListing listing : listings.values()) { listingsList.appendTag(listing.writeToNBT(new NBTTagCompound())); } compound.setTag("Listings", listingsList); - // Claims - NBTTagList claimsList = new NBTTagList(); - for (AuctionClaim claim : claims.values()) { - claimsList.appendTag(claim.writeToNBT(new NBTTagCompound())); - } - compound.setTag("Claims", claimsList); - - // Pending Notifications - NBTTagList notificationsList = new NBTTagList(); - synchronized (pendingNotifications) { - for (AuctionNotification notification : pendingNotifications) { - if (!notification.sent) { - notificationsList.appendTag(notification.writeToNBT(new NBTTagCompound())); - } - } - } - compound.setTag("PendingNotifications", notificationsList); - return compound; } public void readFromNBT(NBTTagCompound compound) { - // Listings NBTTagList listingsList = compound.getTagList("Listings", 10); for (int i = 0; i < listingsList.tagCount(); i++) { AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i)); listings.put(listing.id, listing); } + } - // Claims - NBTTagList claimsList = compound.getTagList("Claims", 10); - for (int i = 0; i < claimsList.tagCount(); i++) { - AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); - claims.put(claim.id, claim); - } + // ========================================= + // IAuctionHandler Implementation + // ========================================= - // Pending Notifications - NBTTagList notificationsList = compound.getTagList("PendingNotifications", 10); - for (int i = 0; i < notificationsList.tagCount(); i++) { - AuctionNotification notification = AuctionNotification.fromNBT(notificationsList.getCompoundTagAt(i)); - pendingNotifications.add(notification); + @Override + public boolean isEnabled() { + return ConfigMarket.AuctionEnabled; + } + + @Override + public IAuctionListing[] getActiveListings() { + List active = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + active.add(listing); + } } + return active.toArray(new IAuctionListing[0]); + } + + @Override + public IAuctionListing[] getListingsBySeller(String sellerUUID) { + if (sellerUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(sellerUUID); + List result = getPlayerActiveListings(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public IAuctionListing[] getListingsByBidder(String bidderUUID) { + if (bidderUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(bidderUUID); + List result = getPlayerActiveBids(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public int getActiveListingCount() { + int count = 0; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + count++; + } + } + return count; + } + + @Override + public IAuctionListing createListing(IPlayer player, IItemStack item, long startingPrice, long buyoutPrice) { + if (player == null || item == null) return null; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + ItemStack mcItem = item.getMCItemStack(); + String result = createListing(entityPlayer, mcItem, startingPrice, buyoutPrice); + if (result != null) { + return null; // Failed + } + // Find the just-created listing + Set playerListings = playerListingIds.get(entityPlayer.getUniqueID()); + if (playerListings != null && !playerListings.isEmpty()) { + // Get the most recent one + long latestTime = 0; + AuctionListing latestListing = null; + for (String id : playerListings) { + AuctionListing listing = listings.get(id); + if (listing != null && listing.createdTime > latestTime) { + latestTime = listing.createdTime; + latestListing = listing; + } + } + return latestListing; + } + return null; + } + + @Override + public String placeBid(String listingId, IPlayer player, long amount) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return placeBid(listingId, entityPlayer, amount); + } + + @Override + public String buyout(String listingId, IPlayer player) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return buyout(listingId, entityPlayer); + } + + @Override + public String cancelListing(String listingId, IPlayer player, boolean isAdmin) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return cancelListing(listingId, entityPlayer, isAdmin); + } + + @Override + public long getListingFee() { + return ConfigMarket.ListingFee; + } + + @Override + public double getSalesTaxPercent() { + return ConfigMarket.SalesTaxPercent; + } + + @Override + public double getMinBidIncrementPercent() { + return ConfigMarket.MinBidIncrementPercent; + } + + @Override + public int getAuctionDurationHours() { + return ConfigMarket.AuctionDurationHours; + } + + @Override + public int getSnipeProtectionMinutes() { + return ConfigMarket.SnipeProtectionMinutes; + } + + @Override + public String getCurrencyName() { + return ConfigMarket.CurrencyName; + } + + @Override + public long getMinimumListingPrice() { + return ConfigMarket.MinimumListingPrice; + } + + @Override + public IAuctionListing[] searchListings(String searchText) { + if (searchText == null || searchText.trim().isEmpty()) { + return getActiveListings(); + } + String search = searchText.toLowerCase().trim(); + List result = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE || listing.isExpired()) continue; + String itemName = listing.item != null ? listing.item.getDisplayName().toLowerCase() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName.toLowerCase() : ""; + if (itemName.contains(search) || sellerName.contains(search)) { + result.add(listing); + } + } + return result.toArray(new IAuctionListing[0]); } // ========================================= @@ -1203,33 +1401,16 @@ public class AuctionController { cancelListing(listingId, admin, true); } - public void clearClaimedData() { - dataLock.writeLock().lock(); - try { - // Remove claimed claims - claims.entrySet().removeIf(entry -> entry.getValue().claimed); + public void clearEndedListings() { + // Remove ended/cancelled listings (claims are in PlayerData now) + listings.entrySet().removeIf(entry -> + entry.getValue().status == EnumAuctionStatus.CLAIMED || + entry.getValue().status == EnumAuctionStatus.ENDED || + entry.getValue().status == EnumAuctionStatus.CANCELLED); - // Remove ended listings that have no unclaimed claims - listings.entrySet().removeIf(entry -> - entry.getValue().status == EnumAuctionStatus.CLAIMED || - (entry.getValue().status == EnumAuctionStatus.ENDED && - !hasUnclaimedClaims(entry.getValue().id))); - - // Rebuild indices - rebuildIndices(); - markDirty(); - } finally { - dataLock.writeLock().unlock(); - } - } - - private boolean hasUnclaimedClaims(String listingId) { - for (AuctionClaim claim : claims.values()) { - if (listingId.equals(claim.listingId) && !claim.claimed) { - return true; - } - } - return false; + // Rebuild indices + rebuildIndices(); + markDirty(); } /** @@ -1240,4 +1421,18 @@ public class AuctionController { AuctionListing listing = listings.get(listingId); return listing != null && listing.isSeller(playerUUID); } + + /** + * Reload the item blacklist from config. + */ + public void reloadBlacklist() { + AuctionBlacklist.reload(); + } + + /** + * Check if an item is blacklisted. + */ + public boolean isItemBlacklisted(ItemStack item) { + return AuctionBlacklist.isBlacklisted(item); + } } diff --git a/src/main/java/noppes/npcs/controllers/PlayerDataController.java b/src/main/java/noppes/npcs/controllers/PlayerDataController.java index c8964b31d..235e2a9f8 100644 --- a/src/main/java/noppes/npcs/controllers/PlayerDataController.java +++ b/src/main/java/noppes/npcs/controllers/PlayerDataController.java @@ -364,6 +364,17 @@ public class PlayerDataController { return data; } + public PlayerData getData(UUID uuid) { + EntityPlayer player = getPlayerFromUUID(uuid); + PlayerData data; + if (player == null) { + data = new PlayerData(); + data.setNBT(PlayerDataController.Instance.loadPlayerData(uuid.toString())); + } else + data = PlayerData.get(player); + return data; + } + public void addPlayerMessage(String username, PlayerMail mail) { mail.time = System.currentTimeMillis(); diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java new file mode 100644 index 000000000..75a1a714a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java @@ -0,0 +1,237 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.registry.GameRegistry; +import kamkeel.npcs.util.AttributeItemUtil; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public class AuctionBlacklist { + private static Set blacklistedItems = new HashSet<>(); + private static Set wildcardPatterns = new HashSet<>(); + private static Set blacklistedMods = new HashSet<>(); + private static Set blacklistedNBTTags = new HashSet<>(); + + // Built-in bound item requirement keys (always blocked) + private static final String SOULBIND_KEY = "cnpc_soulbind"; + private static final String PROFILE_SLOT_KEY = "cnpc_profile_slot"; + + // Bypass permission + private static final String BYPASS_PERMISSION = "customnpcs.auction.blacklist.bypass"; + + public static void reload() { + blacklistedItems.clear(); + wildcardPatterns.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + + // Load blacklisted items + for (String item : ConfigMarket.BlacklistedItems) { + if (item == null || item.isEmpty()) continue; + item = item.toLowerCase().trim(); + + if (item.contains("*")) { + String regex = "^" + item.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(item); + } + } + + // Load blacklisted mods + for (String mod : ConfigMarket.BlacklistedMods) { + if (mod == null || mod.isEmpty()) continue; + blacklistedMods.add(mod.toLowerCase().trim()); + } + + // Load blacklisted NBT tags + for (String tag : ConfigMarket.BlacklistedNBTTags) { + if (tag == null || tag.isEmpty()) continue; + blacklistedNBTTags.add(tag.trim()); + } + } + + /** + * Check if player can bypass the blacklist (OP or has permission) + */ + public static boolean canBypass(EntityPlayer player) { + if (player == null) return false; + + // Check OP status + if (player.canCommandSenderUseCommand(2, BYPASS_PERMISSION)) { + return true; + } + + // Check custom permission + return CustomNpcsPermissions.hasCustomPermission(player, BYPASS_PERMISSION); + } + + /** + * Check if item is blacklisted (without bypass check) + */ + public static boolean isBlacklisted(ItemStack item) { + if (!ConfigMarket.BlacklistEnabled) return false; + if (item == null || item.getItem() == null) return true; + + // Check bound items (always blocked, even if blacklist is "disabled") + if (hasBoundRequirement(item)) { + return true; + } + + // Get registry name + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return true; + + String registryName = (uid.modId + ":" + uid.name).toLowerCase(); + String modId = uid.modId.toLowerCase(); + + // Check exact item match + if (blacklistedItems.contains(registryName)) { + return true; + } + + // Check wildcard patterns + for (Pattern pattern : wildcardPatterns) { + if (pattern.matcher(registryName).matches()) { + return true; + } + } + + // Check mod blacklist + if (blacklistedMods.contains(modId)) { + return true; + } + + // Check NBT tags + if (item.hasTagCompound() && !blacklistedNBTTags.isEmpty()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtKey : blacklistedNBTTags) { + if (tag.hasKey(nbtKey)) { + return true; + } + } + } + + return false; + } + + /** + * Check if item is blacklisted for a specific player (with bypass check) + */ + public static boolean isBlacklistedForPlayer(ItemStack item, EntityPlayer player) { + if (canBypass(player)) return false; + return isBlacklisted(item); + } + + /** + * Get the registry name of an item + */ + public static String getRegistryName(ItemStack item) { + if (item == null || item.getItem() == null) return null; + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return null; + return uid.modId + ":" + uid.name; + } + + // ========================================= + // Bound Item Checking (RPGCore/Requirements) + // ========================================= + + /** + * Check if item has soulbind or profile slot requirement. + * Requirements are stored under: item.stackTagCompound/RPGCore/Requirements + */ + private static boolean hasBoundRequirement(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + + NBTTagCompound root = item.getTagCompound(); + if (!root.hasKey(AttributeItemUtil.TAG_RPGCORE)) return false; + + NBTTagCompound rpgCore = root.getCompoundTag(AttributeItemUtil.TAG_RPGCORE); + if (!rpgCore.hasKey(AttributeItemUtil.TAG_REQUIREMENTS)) return false; + + NBTTagCompound requirements = rpgCore.getCompoundTag(AttributeItemUtil.TAG_REQUIREMENTS); + return requirements.hasKey(SOULBIND_KEY) || requirements.hasKey(PROFILE_SLOT_KEY); + } + + // ========================================= + // Runtime Blacklist Management + // ========================================= + + public static boolean addItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(registryName); + } + return true; + } + + public static boolean removeItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + return wildcardPatterns.removeIf(p -> p.pattern().equals(regex)); + } else { + return blacklistedItems.remove(registryName); + } + } + + public static boolean addMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.add(modId.toLowerCase().trim()); + } + + public static boolean removeMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.remove(modId.toLowerCase().trim()); + } + + public static boolean addNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.add(tag.trim()); + } + + public static boolean removeNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.remove(tag.trim()); + } + + // ========================================= + // Getters for listing + // ========================================= + + public static List getBlacklistedItems() { + List result = new ArrayList<>(blacklistedItems); + for (Pattern p : wildcardPatterns) { + // Convert regex back to wildcard format + String pattern = p.pattern(); + pattern = pattern.substring(1, pattern.length() - 1); // Remove ^ and $ + pattern = pattern.replace("\\.", ".").replace(".*", "*"); + result.add(pattern); + } + return result; + } + + public static List getBlacklistedMods() { + return new ArrayList<>(blacklistedMods); + } + + public static List getBlacklistedNBTTags() { + return new ArrayList<>(blacklistedNBTTags); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java index 7a4e45a5d..ad4ae3238 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -2,28 +2,39 @@ package noppes.npcs.controllers.data; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.scripted.NpcAPI; import java.util.UUID; -public class AuctionClaim { +public class AuctionClaim implements IAuctionClaim { public String id; public UUID playerUUID; public String playerName; public String listingId; public EnumClaimType type; - public ItemStack item; + public ItemStack item; // The item (for ITEM claims only) + public String itemName; // Display name of item (for CURRENCY/REFUND tooltip) + public String otherPlayerName; // Buyer name (CURRENCY) or who outbid (REFUND) public long currency; public long createdTime; public boolean claimed; + public boolean isReturned; // For ITEM claims: true if returned to seller, false if won by buyer public AuctionClaim() { this.id = UUID.randomUUID().toString(); this.createdTime = System.currentTimeMillis(); this.claimed = false; + this.isReturned = false; + this.itemName = ""; + this.otherPlayerName = ""; } - public static AuctionClaim createItemClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { + /** Create claim for winning an item (buyer won auction or buyout) */ + public static AuctionClaim createItemWonClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { AuctionClaim claim = new AuctionClaim(); claim.playerUUID = playerUUID; claim.playerName = playerName; @@ -31,28 +42,50 @@ public class AuctionClaim { claim.type = EnumClaimType.ITEM; claim.item = item != null ? item.copy() : null; claim.currency = 0; + claim.isReturned = false; return claim; } - public static AuctionClaim createCurrencyClaim(UUID playerUUID, String playerName, String listingId, long currency) { + /** Create claim for returned item (seller's expired/cancelled listing) */ + public static AuctionClaim createItemReturnedClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.ITEM; + claim.item = item != null ? item.copy() : null; + claim.currency = 0; + claim.isReturned = true; + return claim; + } + + /** Create claim for seller's proceeds (stores item name for tooltip, not full item) */ + public static AuctionClaim createCurrencyClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String buyerName) { AuctionClaim claim = new AuctionClaim(); claim.playerUUID = playerUUID; claim.playerName = playerName; claim.listingId = listingId; claim.type = EnumClaimType.CURRENCY; - claim.item = null; + claim.item = null; // No item stored for currency claims + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = buyerName != null ? buyerName : ""; claim.currency = currency; + claim.isReturned = false; return claim; } - public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency) { + /** Create claim for outbid refund (stores item for display and rebid option) */ + public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String outbidderName, ItemStack item) { AuctionClaim claim = new AuctionClaim(); claim.playerUUID = playerUUID; claim.playerName = playerName; claim.listingId = listingId; claim.type = EnumClaimType.REFUND; - claim.item = null; + claim.item = item != null ? item.copy() : null; // Store item for display and rebid + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = outbidderName != null ? outbidderName : ""; claim.currency = currency; + claim.isReturned = false; return claim; } @@ -71,6 +104,95 @@ public class AuctionClaim { return playerUUID != null && playerUUID.equals(uuid); } + // ========================================= + // IAuctionClaim Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getPlayerUUID() { + return playerUUID != null ? playerUUID.toString() : null; + } + + @Override + public String getPlayerName() { + return playerName; + } + + @Override + public String getListingId() { + return listingId; + } + + @Override + public int getType() { + return type.ordinal(); + } + + @Override + public boolean isItemClaim() { + return type == EnumClaimType.ITEM; + } + + @Override + public boolean isCurrencyClaim() { + return type == EnumClaimType.CURRENCY; + } + + @Override + public boolean isRefundClaim() { + return type == EnumClaimType.REFUND; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public String getItemName() { + return itemName; + } + + @Override + public long getCurrency() { + return currency; + } + + @Override + public String getOtherPlayerName() { + return otherPlayerName; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public boolean isClaimed() { + return claimed; + } + + @Override + public boolean isReturnedItem() { + return isReturned; + } + + @Override + public long getDaysUntilExpiration() { + return getDaysUntilExpiration(ConfigMarket.ClaimExpirationDays); + } + + @Override + public boolean isExpired() { + return isExpired(ConfigMarket.ClaimExpirationDays); + } + public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ID", id); compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); @@ -84,9 +206,12 @@ public class AuctionClaim { compound.setTag("Item", itemTag); } + compound.setString("ItemName", itemName != null ? itemName : ""); + compound.setString("OtherPlayerName", otherPlayerName != null ? otherPlayerName : ""); compound.setLong("Currency", currency); compound.setLong("CreatedTime", createdTime); compound.setBoolean("Claimed", claimed); + compound.setBoolean("IsReturned", isReturned); return compound; } @@ -107,9 +232,12 @@ public class AuctionClaim { item = null; } + itemName = compound.getString("ItemName"); + otherPlayerName = compound.getString("OtherPlayerName"); currency = compound.getLong("Currency"); createdTime = compound.getLong("CreatedTime"); claimed = compound.getBoolean("Claimed"); + isReturned = compound.getBoolean("IsReturned"); } public static AuctionClaim fromNBT(NBTTagCompound compound) { diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java index 49ed0a29c..95bf2ed24 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -8,7 +8,7 @@ import java.util.regex.Pattern; /** * Filter for auction listings. - * Supports search by item name and sorting options. + * Supports search by item name or seller name and sorting options. */ public class AuctionFilter { private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE); @@ -37,26 +37,42 @@ public class AuctionFilter { return stripped.toLowerCase().trim(); } - /** Check if item name matches search text */ - public boolean matchesSearch(String itemName) { + /** Check if item name or seller name matches search text */ + public boolean matchesSearch(String itemName, String sellerName) { if (!hasSearchText()) return true; - String normalized = normalizeForSearch(itemName); - return normalized.contains(normalizedSearch); + String normalizedItem = normalizeForSearch(itemName); + String normalizedSeller = normalizeForSearch(sellerName); + // Match if search text is found in either item name or seller name + return normalizedItem.contains(normalizedSearch) || normalizedSeller.contains(normalizedSearch); } - /** Advanced search - all words must be present */ - public boolean matchesSearchAdvanced(String itemName) { + /** Legacy method - only checks item name */ + public boolean matchesSearch(String itemName) { + return matchesSearch(itemName, ""); + } + + /** Advanced search - all words must be present in item name OR seller name */ + public boolean matchesSearchAdvanced(String itemName, String sellerName) { if (!hasSearchText()) return true; - String normalized = normalizeForSearch(itemName); + String normalizedItem = normalizeForSearch(itemName); + String normalizedSeller = normalizeForSearch(sellerName); String[] words = normalizedSearch.split("\\s+"); for (String word : words) { - if (!word.isEmpty() && !normalized.contains(word)) { - return false; + if (!word.isEmpty()) { + // Word must be in item name OR seller name + if (!normalizedItem.contains(word) && !normalizedSeller.contains(word)) { + return false; + } } } return true; } + /** Legacy method - only checks item name */ + public boolean matchesSearchAdvanced(String itemName) { + return matchesSearchAdvanced(itemName, ""); + } + public void reset() { searchText = ""; sortBy = EnumAuctionSort.NEWEST; diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index 3106a9fb2..6d57942c1 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -2,11 +2,15 @@ package noppes.npcs.controllers.data; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.scripted.NpcAPI; import java.util.UUID; -public class AuctionListing { +public class AuctionListing implements IAuctionListing { public String id; public UUID sellerUUID; public String sellerName; @@ -40,22 +44,6 @@ public class AuctionListing { this.endTime = this.createdTime + durationMs; } - public boolean hasBids() { - return bidCount > 0 && highBidderUUID != null; - } - - public boolean hasBuyout() { - return buyoutPrice > 0; - } - - public boolean isExpired() { - return System.currentTimeMillis() >= endTime; - } - - public boolean isActive() { - return status == EnumAuctionStatus.ACTIVE && !isExpired(); - } - public long getTimeRemaining() { return Math.max(0, endTime - System.currentTimeMillis()); } @@ -88,6 +76,136 @@ public class AuctionListing { } } + // ========================================= + // IAuctionListing Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getSellerUUID() { + return sellerUUID != null ? sellerUUID.toString() : null; + } + + @Override + public String getSellerName() { + return sellerName; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public long getStartingPrice() { + return startingPrice; + } + + @Override + public long getBuyoutPrice() { + return buyoutPrice; + } + + @Override + public long getCurrentBid() { + return currentBid; + } + + @Override + public String getHighBidderUUID() { + return highBidderUUID != null ? highBidderUUID.toString() : null; + } + + @Override + public String getHighBidderName() { + return highBidderName; + } + + @Override + public int getBidCount() { + return bidCount; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public long getEndTime() { + return endTime; + } + + @Override + public long getRemainingTime() { + return getTimeRemaining(); + } + + @Override + public int getStatus() { + return status.ordinal(); + } + + @Override + public long getMinimumBid() { + return getMinimumBid(ConfigMarket.MinBidIncrementPercent); + } + + @Override + public boolean isSeller(String playerUUID) { + if (playerUUID == null || sellerUUID == null) return false; + return sellerUUID.toString().equals(playerUUID); + } + + @Override + public boolean isHighBidder(String playerUUID) { + if (playerUUID == null || highBidderUUID == null) return false; + return highBidderUUID.toString().equals(playerUUID); + } + + @Override + public String getRemainingTimeFormatted() { + long remaining = getTimeRemaining(); + if (remaining <= 0) return "Ended"; + + long hours = remaining / (60 * 60 * 1000); + long minutes = (remaining % (60 * 60 * 1000)) / (60 * 1000); + + if (hours > 0) { + return hours + "h " + minutes + "m"; + } else if (minutes > 0) { + return minutes + "m"; + } else { + long seconds = (remaining % (60 * 1000)) / 1000; + return seconds + "s"; + } + } + + // Override existing methods with @Override annotation + @Override + public boolean hasBids() { + return bidCount > 0 && highBidderUUID != null; + } + + @Override + public boolean hasBuyout() { + return buyoutPrice > 0; + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() >= endTime; + } + + @Override + public boolean isActive() { + return status == EnumAuctionStatus.ACTIVE && !isExpired(); + } + public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ID", id); compound.setString("SellerUUID", sellerUUID != null ? sellerUUID.toString() : ""); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java deleted file mode 100644 index b2b8c4b57..000000000 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ /dev/null @@ -1,232 +0,0 @@ -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 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 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(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"); - } else { - this.balance = ConfigMarket.StartingBalance; - } - this.lifetimeEarned = compound.getLong("CurrencyLifetimeEarned"); - this.lifetimeSpent = compound.getLong("CurrencyLifetimeSpent"); - } - - 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); - } - - /** - * 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; - } - if (balance > ConfigMarket.MaxBalance) { - balance = ConfigMarket.MaxBalance; - } - - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - VaultUtil.setBalance(playerName, balance); - return; - } - } - this.balance = balance; - } - - /** - * Add currency to balance (deposit) - * - * @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; - } - 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 - */ - @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; - } - this.balance -= amount; - this.lifetimeSpent += amount; - return true; - } - - /** - * 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; - } - - /** - * Get lifetime spent currency - */ - @Override - public long getLifetimeSpent() { - return lifetimeSpent; - } - - /** - * Format the balance for display - */ - @Override - public String formatBalance() { - if (shouldUseVault()) { - return VaultUtil.format(getBalance()); - } - return formatAmount(this.balance); - } - - /** - * Format an amount for display - */ - 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 d2a949074..04e06a41e 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -17,7 +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.IPlayerTradeData; import noppes.npcs.api.handler.IPlayerData; import noppes.npcs.api.handler.IPlayerDialogData; import noppes.npcs.api.handler.IPlayerFactionData; @@ -46,6 +46,8 @@ import java.io.FileOutputStream; import java.util.HashSet; import java.util.UUID; +import static noppes.npcs.util.CustomNPCsThreader.customNPCThread; + public class PlayerData implements IExtendedEntityProperties, IPlayerData { public PlayerDialogData dialogData = new PlayerDialogData(this); public PlayerBankData bankData = new PlayerBankData(this); @@ -60,8 +62,8 @@ 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(this); + // Trade data (currency + auction claims) - shared across all profile slots + public PlayerTradeData tradeData = new PlayerTradeData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -134,6 +136,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { animationData.readFromNBT(data); effectData.readFromNBT(data); magicData.readToNBT(data); + tradeData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); @@ -176,6 +179,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { animationData.writeToNBT(compound); effectData.writeToNBT(compound); magicData.writeToNBT(compound); + tradeData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); @@ -228,6 +232,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { transportData.saveNBTData(compound); factionData.saveNBTData(compound); mailData.saveNBTData(compound); + tradeData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); DBCAddon.instance.writeToNBT(this, compound); @@ -241,6 +246,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { transportData.loadNBTData(data); factionData.loadNBTData(data); mailData.loadNBTData(data); + tradeData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); uuid = player.getPersistentID().toString(); @@ -401,8 +407,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return mailData; } - public IPlayerCurrencyData getCurrencyData() { - return currencyData; + public IPlayerTradeData getTradeData() { + return tradeData; } public synchronized void save() { @@ -414,9 +420,6 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); - // Currency data is saved at player level, NOT per profile slot - currencyData.writeToNBT(compound); - final String filename; if (ConfigMain.DatFormat) { filename = uuid + ".dat"; @@ -453,10 +456,6 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (data.hasNoTags()) { data = getNBT(); } - - // 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/PlayerTradeData.java b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java new file mode 100644 index 000000000..a0a8e0bb7 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java @@ -0,0 +1,414 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.util.VaultUtil; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.api.handler.IPlayerTradeData; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Stores player's trade-related data: currency and auction claims. + * This data is shared across all profile slots. + * Uses CopyOnWriteArrayList for thread-safety when adding claims asynchronously. + */ +public class PlayerTradeData implements IPlayerTradeData { + // NBT key used by trade data - excluded from per-slot profile saving + public static final String NBT_KEY = "TradeData"; + + private final PlayerData playerData; + + // ========================================= + // Currency Data + // ========================================= + + // Built-in CNPC+ currency (always saved, used as fallback) + private long balance; + private long lifetimeEarned; + private long lifetimeSpent; + + // ========================================= + // Auction Claims Data + // ========================================= + + // Thread-safe list for claims - can be modified from CNPC+ thread + private final CopyOnWriteArrayList claims = new CopyOnWriteArrayList<>(); + + public PlayerTradeData(PlayerData playerData) { + this.playerData = playerData; + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + } + + // ========================================= + // Currency Methods + // ========================================= + + /** + * 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; + } + + /** + * 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; + } + if (balance > ConfigMarket.MaxBalance) { + balance = ConfigMarket.MaxBalance; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + VaultUtil.setBalance(playerName, balance); + return; + } + } + this.balance = balance; + } + + /** + * Add currency to balance (deposit) + * + * @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; + } + 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 + */ + @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; + } + this.balance -= amount; + this.lifetimeSpent += amount; + return true; + } + + /** + * 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; + } + + /** + * Get lifetime spent currency + */ + @Override + public long getLifetimeSpent() { + return lifetimeSpent; + } + + /** + * Format the balance for display + */ + @Override + public String formatBalance() { + if (shouldUseVault()) { + return VaultUtil.format(getBalance()); + } + return formatAmount(this.balance); + } + + /** + * Format an amount for display + */ + 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; + } + + // ========================================= + // Auction Claims Methods + // ========================================= + + /** + * Add a claim to this player's pending claims. + * Thread-safe - can be called from any thread. + */ + public void addClaim(AuctionClaim claim) { + if (claim != null && !claim.claimed) { + claims.add(claim); + } + } + + /** + * Get all unclaimed claims for this player (internal use). + * Returns a copy of the list for safe iteration. + */ + public List getClaimsList() { + List result = new ArrayList<>(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + result.add(claim); + } + } + return result; + } + + /** + * Get all unclaimed claims for this player (API). + */ + @Override + public IAuctionClaim[] getClaims() { + List result = getClaimsList(); + return result.toArray(new IAuctionClaim[0]); + } + + /** + * Get a specific claim by ID (internal use). + */ + public AuctionClaim getClaimInternal(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + return claim; + } + } + return null; + } + + /** + * Get a specific claim by ID (API). + */ + @Override + public IAuctionClaim getClaim(String claimId) { + return getClaimInternal(claimId); + } + + /** + * Mark a claim as claimed and remove it. + */ + public boolean claimAndRemove(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + claim.claimed = true; + claims.remove(claim); + return true; + } + } + return false; + } + + /** + * Get count of unclaimed claims. + */ + @Override + public int getClaimCount() { + int count = 0; + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + count++; + } + } + return count; + } + + /** + * Check if player has any unclaimed claims. + */ + @Override + public boolean hasClaims() { + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + return true; + } + } + return false; + } + + /** + * Process expired claims - removes claims older than expiration days. + * Returns number of claims removed. + */ + public int processExpiredClaims() { + int expirationDays = ConfigMarket.ClaimExpirationDays; + int removed = 0; + + Iterator iterator = claims.iterator(); + while (iterator.hasNext()) { + AuctionClaim claim = iterator.next(); + if (!claim.claimed && claim.isExpired(expirationDays)) { + claims.remove(claim); + removed++; + } + } + + return removed; + } + + // ========================================= + // NBT Serialization + // ========================================= + + public void readFromNBT(NBTTagCompound compound) { + NBTTagCompound tradeData = compound.getCompoundTag(NBT_KEY); + if (tradeData == null || tradeData.hasNoTags()) { + // No trade data found, use defaults + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + claims.clear(); + return; + } + + // Currency + if (tradeData.hasKey("CurrencyBalance")) { + this.balance = tradeData.getLong("CurrencyBalance"); + } else { + this.balance = ConfigMarket.StartingBalance; + } + this.lifetimeEarned = tradeData.getLong("CurrencyLifetimeEarned"); + this.lifetimeSpent = tradeData.getLong("CurrencyLifetimeSpent"); + + // Claims + claims.clear(); + if (tradeData.hasKey("AuctionClaims")) { + NBTTagList claimsList = tradeData.getTagList("AuctionClaims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); + if (!claim.claimed) { + claims.add(claim); + } + } + } + } + + public void writeToNBT(NBTTagCompound compound) { + NBTTagCompound tradeData = new NBTTagCompound(); + + // Currency - always save built-in currency data (preserved even when using Vault) + tradeData.setLong("CurrencyBalance", this.balance); + tradeData.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); + tradeData.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); + + // Claims + NBTTagList claimsList = new NBTTagList(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + claimsList.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + } + tradeData.setTag("AuctionClaims", claimsList); + + compound.setTag(NBT_KEY, tradeData); + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 91a116efc..6aa69d357 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -426,7 +426,7 @@ public class RoleTrader extends RoleInterface { // Player balance PlayerData data = PlayerData.get(player); - compound.setLong("Balance", data.currencyData.getBalance()); + compound.setLong("Balance", data.tradeData.getBalance()); // Stock info compound.setBoolean("StockEnabled", stock.enableStock); diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index ecf4b8ba0..c80c18d28 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.eventhandler.EventBus; import foxz.command.ScriptedCommand; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.TelegraphController; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.block.Block; import net.minecraft.command.CommandHandler; @@ -52,6 +53,7 @@ import noppes.npcs.api.IContainer; import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.IParticle; +import noppes.npcs.api.ITelegraph; import noppes.npcs.api.IPos; import noppes.npcs.api.ISkinOverlay; import noppes.npcs.api.ITileEntity; @@ -63,6 +65,7 @@ import noppes.npcs.api.gui.ICustomGui; import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.api.handler.IActionManager; import noppes.npcs.api.handler.IAnimationHandler; +import noppes.npcs.api.handler.IAuctionHandler; import noppes.npcs.api.handler.IAttributeHandler; import noppes.npcs.api.handler.ICloneHandler; import noppes.npcs.api.handler.ICustomEffectHandler; @@ -75,6 +78,7 @@ import noppes.npcs.api.handler.IProfileHandler; import noppes.npcs.api.handler.IQuestHandler; import noppes.npcs.api.handler.IRecipeHandler; import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.api.handler.ITelegraphHandler; import noppes.npcs.api.handler.ITransportHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; @@ -87,6 +91,7 @@ import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.containers.ContainerNpcInterface; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.ChunkController; import noppes.npcs.controllers.CustomEffectController; import noppes.npcs.controllers.DialogController; @@ -370,6 +375,27 @@ public class NpcAPI extends AbstractNpcAPI { return AbilityController.Instance; } + @Override + public ITelegraphHandler getTelegraphs() { + this.checkWorld(); + return TelegraphController.Instance; + } + + @Override + public ITelegraph createTelegraph(String type) { + this.checkWorld(); + return TelegraphController.Instance.create(type); + } + + @Override + public IAuctionHandler getAuctions() { + this.checkWorld(); + if (!noppes.npcs.config.ConfigMarket.AuctionEnabled) { + return null; + } + return AuctionController.getInstance(); + } + @Override public String[] getAllBiomeNames() { List biomes = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java b/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java new file mode 100644 index 000000000..f03be5033 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java @@ -0,0 +1,210 @@ +package noppes.npcs.scripted; + +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import net.minecraft.entity.Entity; +import net.minecraft.world.World; +import noppes.npcs.api.ITelegraph; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntity; + +/** + * Script wrapper for Telegraph configuration. + * Implements ITelegraph API interface. + */ +public class ScriptTelegraph implements ITelegraph { + + private final Telegraph telegraph; + + public ScriptTelegraph(Telegraph telegraph) { + this.telegraph = telegraph; + } + + /** + * Get the underlying Telegraph object. + */ + public Telegraph getMCTelegraph() { + return telegraph; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TYPE + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public String getType() { + return telegraph.getType().name(); + } + + @Override + public void setType(String type) { + if (type == null) return; + try { + telegraph.setType(TelegraphType.valueOf(type.toUpperCase())); + } catch (IllegalArgumentException ignored) { + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SHAPE PARAMETERS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public float getRadius() { + return telegraph.getRadius(); + } + + @Override + public void setRadius(float radius) { + telegraph.setRadius(radius); + } + + @Override + public float getInnerRadius() { + return telegraph.getInnerRadius(); + } + + @Override + public void setInnerRadius(float innerRadius) { + telegraph.setInnerRadius(innerRadius); + } + + @Override + public float getLength() { + return telegraph.getLength(); + } + + @Override + public void setLength(float length) { + telegraph.setLength(length); + } + + @Override + public float getWidth() { + return telegraph.getWidth(); + } + + @Override + public void setWidth(float width) { + telegraph.setWidth(width); + } + + @Override + public float getAngle() { + return telegraph.getAngle(); + } + + @Override + public void setAngle(float angle) { + telegraph.setAngle(angle); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TIMING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getDuration() { + return telegraph.getDurationTicks(); + } + + @Override + public void setDuration(int ticks) { + telegraph.setDurationTicks(ticks); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // COLORS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getColor() { + return telegraph.getColor(); + } + + @Override + public void setColor(int argb) { + telegraph.setColor(argb); + } + + @Override + public int getWarningColor() { + return telegraph.getWarningColor(); + } + + @Override + public void setWarningColor(int argb) { + telegraph.setWarningColor(argb); + } + + @Override + public int getWarningStartTick() { + return telegraph.getWarningStartTick(); + } + + @Override + public void setWarningStartTick(int tick) { + telegraph.setWarningStartTick(tick); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public boolean isAnimated() { + return telegraph.isAnimated(); + } + + @Override + public void setAnimated(boolean animated) { + telegraph.setAnimated(animated); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // POSITIONING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public float getHeightOffset() { + return telegraph.getHeightOffset(); + } + + @Override + public void setHeightOffset(float offset) { + telegraph.setHeightOffset(offset); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SPAWN METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public ITelegraphInstance spawn(IWorld world, double x, double y, double z) { + return spawn(world, x, y, z, 0); + } + + @Override + public ITelegraphInstance spawn(IWorld world, double x, double y, double z, float yaw) { + if (world == null || world.getMCWorld() == null) return null; + World mcWorld = (World) world.getMCWorld(); + TelegraphInstance instance = TelegraphController.Instance.spawn(telegraph, mcWorld, x, y, z, yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraphInstance spawn(IEntity entity) { + return spawn(entity, 0); + } + + @Override + public ITelegraphInstance spawn(IEntity entity, float yaw) { + if (entity == null || entity.getMCEntity() == null) return null; + Entity mcEntity = (Entity) entity.getMCEntity(); + TelegraphInstance instance = TelegraphController.Instance.spawn(telegraph, mcEntity, yaw); + return new ScriptTelegraphInstance(instance); + } +} diff --git a/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java b/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java new file mode 100644 index 000000000..b307a6dc7 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java @@ -0,0 +1,122 @@ +package noppes.npcs.scripted; + +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import net.minecraft.entity.Entity; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.entity.IEntity; + +/** + * Script wrapper for TelegraphInstance. + * Implements ITelegraphInstance API interface. + */ +public class ScriptTelegraphInstance implements ITelegraphInstance { + + private final TelegraphInstance instance; + + public ScriptTelegraphInstance(TelegraphInstance instance) { + this.instance = instance; + } + + /** + * Get the underlying TelegraphInstance object. + */ + public TelegraphInstance getMCInstance() { + return instance; + } + + @Override + public String getInstanceId() { + return instance.getInstanceId(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // POSITION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public double getX() { + return instance.getX(); + } + + @Override + public double getY() { + return instance.getY(); + } + + @Override + public double getZ() { + return instance.getZ(); + } + + @Override + public float getYaw() { + return instance.getYaw(); + } + + @Override + public void setPosition(double x, double y, double z) { + instance.setX(x); + instance.setY(y); + instance.setZ(z); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ENTITY FOLLOWING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void followEntity(IEntity entity) { + if (entity == null || entity.getMCEntity() == null) return; + Entity mcEntity = (Entity) entity.getMCEntity(); + instance.setEntityIdToFollow(mcEntity.getEntityId()); + } + + @Override + public void stopFollowing() { + instance.lockPosition(); + } + + @Override + public boolean isFollowing() { + return instance.getEntityIdToFollow() >= 0; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TIMING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getRemainingTicks() { + return instance.getRemainingTicks(); + } + + @Override + public int getTotalTicks() { + return instance.getTotalTicks(); + } + + @Override + public float getProgress() { + return instance.getProgress(); + } + + @Override + public boolean isWarning() { + return instance.isWarning(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CONTROL + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void remove() { + TelegraphController.Instance.remove(instance); + } + + @Override + public void lockPosition() { + instance.lockPosition(); + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index f2cfc9a34..a1b91e1db 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -901,36 +901,36 @@ public class ScriptPlayer extends ScriptLivingBase @Override public long getCurrencyBalance() { - return getData().currencyData.getBalance(); + return getData().tradeData.getBalance(); } @Override public void setCurrencyBalance(long amount) { - getData().currencyData.setBalance(amount); + getData().tradeData.setBalance(amount); } @Override public boolean depositCurrency(long amount) { - return getData().currencyData.deposit(amount); + return getData().tradeData.deposit(amount); } @Override public boolean withdrawCurrency(long amount) { - return getData().currencyData.withdraw(amount); + return getData().tradeData.withdraw(amount); } @Override public boolean canAffordCurrency(long amount) { - return getData().currencyData.canAfford(amount); + return getData().tradeData.canAfford(amount); } @Override public boolean isUsingVaultCurrency() { - return getData().currencyData.isUsingVault(); + return getData().tradeData.isUsingVault(); } @Override public String getFormattedCurrencyBalance() { - return getData().currencyData.formatBalance(); + return getData().tradeData.formatBalance(); } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 6862e5455..7f0618786 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -157,6 +157,9 @@ gui.create=Create gui.rename=Rename gui.playerMap=Regen PlayerMap gui.unused=Unused +gui.apply=Apply +gui.cancel=Cancel +gui.yes=Yes gui.lineColor1=Upper line color gui.lineColor2=Middle line color @@ -1647,7 +1650,7 @@ ability.healSelf=Heal Self ability.healAllies=Heal Allies # Hazard Settings -ability.dmgPerTick=Dmg/Tick +ability.dmgPerSec=Dmg/Sec ability.debuffDur=Debuff Dur ability.shape=Shape ability.placement=Placement @@ -1737,17 +1740,6 @@ auction.notify.claim=You have items to claim. # Main GUI auction.title=Auction House -auction.browse=Browse -auction.sell=Sell -auction.trades=My Trades -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 @@ -1758,50 +1750,27 @@ auction.bids=%d bids auction.noBids=No bids auction.startingPrice=Starting: %s -# Create Listing -auction.sell.title=Create Listing +# Sell Page 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 -# Trades -auction.trades.title=My Trades -auction.trades.active=Active Listings -auction.trades.activeBids=Active Bids -auction.trades.pending=Pending Claims -auction.trades.claimAll=Claim All -auction.trades.cancel=Cancel Listing -auction.trades.empty=No active trades - # 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 -auction.filter.reset=Reset # Bid Dialog -auction.bid.title=Place Bid -auction.bid.current=Current Bid: %s auction.bid.minimum=Minimum Bid: %s auction.bid.yourBid=Your Bid: auction.bid.place=Place Bid +auction.bid.placeBid=Place Bid auction.bid.buyout=Buyout Now # 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. -auction.error.inventoryFull=Your inventory is full. -auction.error.cannotAfford=You cannot afford this. +auction.error.blacklisted=This item cannot be listed on the Auction House. +auction.error.boundItem=Bound items cannot be listed on the Auction House. # Navigation auction.nav.listings=Browse Auctions @@ -1815,9 +1784,6 @@ auction.claim.expires=Expires in %s days auction.claim.expiresSoon=Expires soon! auction.claim.clickToClaim=Click to claim -# Create Listing -auction.createListing=Create Listing - # Filter auction.filter.sort=Sort By auction.filter.none=(none) @@ -1827,7 +1793,6 @@ auction.filter.rightClick=Right-click: Cycle Sort # Pagination auction.page.prev=Previous Page auction.page.next=Next Page -auction.listings.count=%d Listings # Sell Page auction.sell.confirm=Confirm Listing @@ -1846,14 +1811,32 @@ auction.trades.activeBid=Active Bid auction.trades.unavailable=Slot Unavailable auction.trades.rightClickToCancel=Right-click to cancel auction.trades.leftClickToClaim=Left-click to claim +auction.trades.leftClickToIncrease=Left-click to increase bid auction.trades.winningBid=You are the highest bidder -auction.trades.pendingClaim=Pending Claim -auction.trades.refundClaim=Bid Refund +auction.trades.soldClaim=Item Sold! +auction.trades.wonClaim=Auction Won! +auction.trades.expiredClaim=Auction Expired +auction.trades.refundClaim=Outbid - Refund Available auction.trades.confirmCancel=Cancel Listing? auction.trades.confirmClaim=Claim Item? auction.trades.rightClickConfirm=Right-click again to confirm auction.trades.leftClickConfirm=Left-click again to confirm auction.trades.otherClickCancel=Any other click to cancel +auction.trades.buyer=Buyer +auction.trades.outbidBy=Outbid by +auction.trades.selling=Selling +auction.trades.bidding=Bidding +auction.trades.sold=Sold +auction.trades.won=Won +auction.trades.expired=Expired +auction.trades.outbid=Outbid +auction.trades.clickToRebid=Click to increase bid +auction.trades.leftClickRebid=Left-click to rebid +auction.trades.rightClickRefund=Right-click to claim refund +auction.claim.soldItem=Item: %s +auction.claim.buyer=Buyer: %s +auction.claim.outbidItem=Item: %s +auction.claim.outbidBy=Outbid by: %s # Sell Page - Buyout Toggle auction.sell.allowBuyout=Allow Buy Now diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png b/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png index b4ecd939e..07c9335ff 100644 Binary files a/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png and b/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png index 90253fc9a..a801d21ae 100644 Binary files a/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png and b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png differ