diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md new file mode 100644 index 000000000..304965950 --- /dev/null +++ b/1.11_Market_Update.md @@ -0,0 +1,834 @@ +# CustomNPC+ 1.11 Market Update - Implementation Plan + +## Overview + +The 1.11 Market Update introduces a comprehensive economy system for CustomNPC+: + +1. **Currency System** - Built-in CNPC+ currency with optional Vault integration +2. **Trader Enhancements** - Stock system, currency-based trades, reset timers +3. **Auction House** - Global auction system with bidding, buyout, and claims + +--- + +## 1. Currency System + +### Status: IMPLEMENTED + +The currency system provides a unified economy for CNPC+ features. + +### Features +- **Built-in CNPC+ Currency** - Stored in PlayerData, persists across profile slots +- **Vault Integration** - Optional, when enabled all operations use Vault instead +- **Scripting API** - Full access via `IPlayer` methods + +### Key Files +| File | Purpose | +|------|---------| +| `PlayerCurrencyData.java` | Per-player currency storage, Vault delegation | +| `IPlayerCurrencyData.java` | API interface for currency operations | +| `ConfigMarket.java` | Currency configuration (UseVault, CurrencyName, StartingBalance, MaxBalance) | +| `VaultUtil.java` | Vault API integration via reflection | + +### Configuration (ConfigMarket.java) +``` +Market.Currency: + - Use Vault: false + - Currency Name: "Coins" + - Starting Balance: 0 + - Max Balance: 2147483647 +``` + +### Scripting API (IPlayer) +```javascript +player.getCurrencyBalance() // Get balance +player.setCurrencyBalance(amount) // Set balance +player.depositCurrency(amount) // Add currency (returns success) +player.withdrawCurrency(amount) // Remove currency (returns success) +player.canAffordCurrency(amount) // Check if can afford +player.isUsingVaultCurrency() // Check if Vault is active +player.getFormattedCurrencyBalance() // Get formatted string +``` + +--- + +## 2. Trader Enhancements + +### Status: COMPLETE + +--- + +### 2.1 Stock System - IMPLEMENTED + +Traders can have limited stock that resets on configurable schedules. + +#### Stock Modes +| Mode | Description | Status | +|------|-------------|--------| +| **Global** | All players share the same stock pool | ✅ Working | +| **Per-Player** | Each player has their own stock counts | ✅ Working | + +#### Stock Reset Types (EnumStockReset) +| Type | Description | Status | +|------|-------------|--------| +| `NONE` | Stock never resets | ✅ Working | +| `MCDAILY` | Resets every MC day (24000 ticks) | ✅ Working | +| `MCWEEKLY` | Resets every 7 MC days (168000 ticks) | ✅ Working | +| `MCCUSTOM` | Custom MC tick interval | ✅ Working | +| `RLDAILY` | Resets every 24 real-life hours | ✅ Working | +| `RLWEEKLY` | Resets every 7 real-life days | ✅ Working | +| `RLCUSTOM` | Custom real-time interval | ✅ Working | + +#### Stock UI Features - IMPLEMENTED +| Feature | Status | Notes | +|---------|--------|-------| +| Stock count per item | ✅ | Shows to right of output slot | +| Out of stock indicator | ✅ | Dark overlay + red count | +| Reset timer countdown | ✅ | Real-time client-side countdown in top-right | +| "Reopen Trader" message | ✅ | Shows when countdown reaches 0 | +| Admin stock configuration | ✅ | SubGuiNpcTraderStock | +| Per-slot stock limits | ✅ | SubGuiNpcTraderStockSlots | +| Reset stock button | ✅ | With confirmation dialog | +| Reset cooldown button | ✅ | With confirmation dialog | + +#### Key Files +| File | Purpose | +|------|---------| +| `TraderStock.java` | Core stock data class with per-player/global modes | +| `TraderStock.PlayerTraderStock` | Inner class for per-player tracking | +| `EnumStockReset.java` | Reset type enum with intervals | +| `SubGuiNpcTraderStock.java` | Main stock configuration GUI | +| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limit editor | +| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | +| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | + +--- + +### 2.2 Trader Currency Support - IMPLEMENTED + +Trades can require BOTH items AND currency (additive costs). + +#### Trade Entry Structure +Each trade slot can have: +- **Sold Item** - What the player receives ✅ +- **Currency 1** - First item cost (existing) ✅ +- **Currency 2** - Second item cost (existing) ✅ +- **Currency Cost** - CNPC+ Currency / Vault cost ✅ NEW + +#### Example Trades +| Sold Item | Item Cost | Currency Cost | +|-----------|-----------|---------------| +| Diamond | 2 Emeralds | $0 | +| Diamond Sword | 5 Diamonds | $100 | +| Special Armor | None | $500 | +| Enchanted Book | 1 Book + 10 Lapis | $50 | + +#### Implementation Details +- Currency cost stored as `long[] currencyCost[18]` in RoleTrader +- Default to 0 (no currency cost) for legacy compatibility ✅ +- Check both item AND currency requirements before purchase ✅ +- Withdraw currency on successful purchase ✅ +- NBT serialization for persistence ✅ + +--- + +### 2.3 Player Trader GUI - IMPLEMENTED + +#### Layout (256px wide) +- 3 columns × 6 rows = 18 trade slots +- Column width: 80px +- Per slot: [Currency1][Currency2] = [Output] +- Player inventory positioned below + +#### Display Features +| Element | Position | Status | +|---------|----------|--------| +| "Trader" label | Top-left | ✅ | +| Reset countdown | Top-right | ✅ Real-time ticking | +| Stock count | Right of each trade | ✅ Green/Red | +| Player balance | Bottom-right | ✅ | +| Status message | Bottom-left | ✅ On hover | + +#### Status Messages (on hover) +| Condition | Message | Color | +|-----------|---------|-------| +| Out of stock | "Out of Stock ($COST)" | Red | +| Can't afford balance | "Insufficient Balance ($COST)" | Red | +| Missing item currency | "Unavailable ($COST)" | Red | +| Slot disabled | "Slot Disabled ($COST)" | Orange | +| Can purchase | "Available ($COST)" | Green | + +Cost suffix only appears if trade has currency cost > 0. + +--- + +### 2.4 Multi-Player Sync - IMPLEMENTED + +#### MarketRegistry System +Central registry for linked market synchronization across all NPCs sharing the same market. + +| Component | Purpose | +|-----------|---------| +| `MarketRegistry.java` | Central registry tracking traders and viewers per market | +| `marketTraders` | `Map>` - All traders using each market | +| `marketViewers` | `Map>` - All players viewing each market | + +#### Viewer Tracking System +| Feature | Status | Notes | +|---------|--------|-------| +| Register viewer on GUI open | ✅ | For shared stock mode (linked or local) | +| Unregister on GUI close | ✅ | Via onContainerClosed | +| Sync after purchase | ✅ | All viewers updated market-wide | +| Per-player mode optimization | ✅ | Only sync to purchasing player | +| Disconnected player cleanup | ✅ | Removed before sync | +| NPC unload cleanup | ✅ | Via delete() → onUnload() | + +#### Sync Behavior by Mode +| Mode | Linked Market | Behavior | +|------|---------------|----------| +| Shared Stock | Yes | All viewers across ALL NPCs with same market see update immediately | +| Shared Stock | No | All viewers of THIS NPC see update immediately | +| Per-Player Stock | Yes/No | Only purchasing player receives update | + +#### Data Flow +``` +Purchase on NPC1 (market="shop1") + ↓ +consumeStock() → Market.save() → MarketRegistry.syncMarket("shop1") + ↓ +┌──────────────────────────────────────────────┐ +│ 1. Update ALL traders' stock data │ +│ (NPC1, NPC2, NPC3... all with "shop1") │ +│ 2. Sync ALL viewers across ALL those NPCs │ +└──────────────────────────────────────────────┘ +``` + +--- + +### 2.5 Key Files Modified/Created + +| File | Changes | +|------|---------| +| `MarketRegistry.java` | NEW - Central registry for linked market sync | +| `RoleTrader.java` | currencyCost[], viewer tracking (local + registry), syncAllViewers(), resetCooldown(), delete() cleanup | +| `ContainerNPCTrader.java` | Layout constants, currency/stock validation, viewer registration | +| `GuiNPCTrader.java` | Client-side countdown, stock display, status messages | +| `GetTraderData.java` | Network packet for requesting trader data (IGuiData pattern) | +| `TraderStock.java` | Stock management with per-player/global modes, thread-safe methods | +| `EnumStockReset.java` | Reset type definitions with lang key support | +| `SubGuiNpcTraderStock.java` | Stock configuration dialog, custom time validation | +| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limits | +| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | +| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | +| `GuiNPCInterface.java` | Fixed SubGui background tiling for smaller widths | +| `Market.java` | Fixed save logic for linked markets | +| `IRoleTrader.java` | Extended API with stock manipulation methods | +| `ScriptRoleTrader.java` | Implemented new API methods | + +--- + +### 2.6 Resolved Issues + +All previously identified issues have been fixed: + +#### Critical Issues - FIXED +| Issue | Fix | +|-------|-----| +| Market load overwrites reset | Reordered `interact()` to load market BEFORE stock reset check | +| recordHistory flag inverted | Removed inverted early return in `Market.save()` | +| API returns wrong item | Fixed `getSellOption()` to return `inventorySold` instead of `inventoryCurrency` | + +#### Medium Priority - FIXED +| Issue | Fix | +|-------|-----| +| Thread safety | Added `synchronized` to `getAvailableStock()`, `consumeStock()`, `resetStock()` | +| Per-player cleanup | `playerStock.clear()` called on stock reset | +| Linked market sync | Implemented `MarketRegistry` for market-wide sync | +| Custom time validation | Added min validation (1 tick MC, 1000ms RL) in `SubGuiNpcTraderStock` | + +#### Low Priority - FIXED +| Issue | Fix | +|-------|-----| +| lastPurchaseTime unused | Removed from `PlayerTraderStock` | +| Currency format | Verified working correctly for all ranges | +| Hardcoded reset types | Now uses `EnumStockReset.getDisplayNames()` with lang keys | + +--- + +### 2.7 Scripting API - IMPLEMENTED + +Full API available via `IRoleTrader` interface: + +```javascript +// Stock Configuration +trader.isStockEnabled() +trader.setStockEnabled(boolean) +trader.isPerPlayerStock() +trader.setPerPlayerStock(boolean) +trader.getStockResetType() // Returns int (0-6 for EnumStockReset) +trader.setStockResetType(int) +trader.getCustomResetTime() +trader.setCustomResetTime(long) + +// Stock Limits +trader.getMaxStock(slot) // Max stock for slot (-1 = unlimited) +trader.setMaxStock(slot, amount) + +// Stock Queries +trader.getAvailableStock(slot) // Global mode +trader.getAvailableStock(slot, player) // Per-player mode +trader.getCurrentStock(slot) // Current stock (global) +trader.getPlayerPurchased(slot, player) // Amount purchased by player + +// Stock Manipulation +trader.setCurrentStock(slot, amount) // Set current stock (global) +trader.setPlayerPurchased(slot, player, amount) // Set player's purchased amount +trader.resetStock() // Reset all stock to max +trader.resetCooldown() // Reset cooldown timer + +// Time Queries +trader.getLastResetTime() // Timestamp of last reset +trader.getTimeUntilReset() // Time remaining until next reset + +// Currency Cost System +trader.getCurrencyCost(slot) +trader.setCurrencyCost(slot, amount) +trader.hasCurrencyCost(slot) +``` + +--- + +## 3. Auction House System + +### Status: NOT STARTED (Fresh implementation) + +### 3.1 Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ AuctionController │ +│ - Manages all auction data │ +│ - Handles listing/bidding/buyout logic │ +│ - Processes claims │ +│ - Saves/loads auction data │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ AuctionNotification │ +│ - Tracks pending notifications per player │ +│ - Sends notifications on login │ +│ - Types: Won, Outbid, Sold, Expired, Claimed │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 GUI Structure + +``` +GuiAuctionHouse (Base GUI - opened via Auctioneer NPC) + │ + ├── GuiAuction (Browse/Search auctions) + │ - Paginated item display (ContainerAuction) + │ - Next/Back page buttons + │ - Filter button → opens GuiAuctionFilter + │ - My Auctions button → opens GuiAuctionClaims + │ - Sell button → opens GuiAuctionSell + │ - Click item → SubGuiAuctionDetails + │ + ├── GuiAuctionSell (Create new listing) + │ - Item slot for item to sell + │ - Starting price input + │ - Buyout price input (0 = no buyout) + │ - Listing fee display + │ - Create button + │ + ├── GuiAuctionClaims (My Auctions / Claims) + │ - Active listings tab + │ - Pending claims tab (items + currency to collect) + │ - Cancel listing button + │ - Claim all button + │ + └── GuiAuctionFilter (Filter settings) + - Search text + - Sort options (EnumAuctionSort) + - Price range (min/max) + - Show my listings only toggle +``` + +### 3.3 Admin GUI + +``` +GuiManageAuction (Admin tool - like ManageMagic, ManageQuests) + │ + ├── Active Auctions Tab + │ - View all auctions + │ - Cancel any auction (with reason) + │ - View auction details + │ + ├── Blacklist Tab (Stub for future) + │ - Add/remove blacklisted items + │ - Add/remove blacklisted mods + │ + └── Settings Tab + - View current config values + - Quick reference for config file +``` + +### 3.4 Data Structures + +#### AuctionListing +```java +public class AuctionListing { + int id; // Unique listing ID + UUID sellerUUID; // Seller's UUID + String sellerName; // Seller's display name + ItemStack item; // Item being sold + long startingPrice; // Minimum starting bid + long buyoutPrice; // Instant buy price (0 = no buyout) + long currentBid; // Current highest bid + UUID highBidderUUID; // Current high bidder (null if no bids) + String highBidderName; // High bidder display name + int bidCount; // Number of bids placed + long createdTime; // When listing was created + long endTime; // When auction ends + EnumAuctionStatus status; // ACTIVE, ENDED, CANCELLED, CLAIMED +} +``` + +#### AuctionClaim +```java +public class AuctionClaim { + int id; // Unique claim ID + UUID playerUUID; // Player who can claim + int listingId; // Related auction listing + EnumClaimType type; // ITEM, CURRENCY, REFUND + ItemStack item; // Item to claim (if type = ITEM) + long currency; // Currency to claim (if type = CURRENCY/REFUND) + long createdTime; // When claim was created + boolean claimed; // Whether claimed +} +``` + +#### AuctionFilter +```java +public class AuctionFilter { + String searchText; // Item name search + EnumAuctionSort sortBy; // Sort order + long minPrice; // Minimum price filter + long maxPrice; // Maximum price filter (0 = no max) + boolean myListingsOnly; // Show only my listings +} +``` + +#### Enums +```java +public enum EnumAuctionStatus { + ACTIVE, // Auction is live + ENDED, // Auction ended (winner determined) + CANCELLED, // Cancelled by seller or admin + CLAIMED // All claims processed +} + +public enum EnumAuctionSort { + NEWEST, // Most recently listed + ENDING_SOON, // Ending soonest + PRICE_LOW, // Lowest current price + PRICE_HIGH, // Highest current price + MOST_BIDS // Most bid activity +} + +public enum EnumClaimType { + ITEM, // Winner claims item + CURRENCY, // Seller claims sale currency + REFUND // Outbid player claims refund +} +``` + +### 3.5 AuctionController + +Main controller handling all auction logic. + +#### Core Methods +```java +// Listing Management +int createListing(EntityPlayer seller, ItemStack item, long startingPrice, long buyoutPrice) +void cancelListing(int listingId, EntityPlayer player, boolean isAdmin) +List getActiveListings(AuctionFilter filter, int page, int pageSize) +AuctionListing getListing(int listingId) +int getPlayerListingCount(UUID playerUUID) + +// Bidding +boolean placeBid(int listingId, EntityPlayer bidder, long bidAmount) +boolean buyout(int listingId, EntityPlayer buyer) + +// Claims +List getPlayerClaims(UUID playerUUID) +boolean claimItem(int claimId, EntityPlayer player) +boolean claimCurrency(int claimId, EntityPlayer player) +void claimAll(EntityPlayer player) + +// Lifecycle +void processEndedAuctions() // Called on tick, creates claims for ended auctions +void onPlayerLogin(EntityPlayer player) // Send pending notifications + +// Persistence +void save() +void load() +``` + +#### Auction Flow + +**Creating a Listing:** +1. Validate item not blacklisted +2. Check player has listing slots available +3. Deduct listing fee from player +4. Remove item from player inventory +5. Create AuctionListing with ACTIVE status +6. Save + +**Placing a Bid:** +1. Validate auction is ACTIVE +2. Validate bid > current bid + minimum increment +3. Validate bidder has enough currency +4. Deduct bid amount from bidder +5. If previous bidder exists, create REFUND claim for them +6. Update listing with new bid/bidder +7. Apply snipe protection if needed +8. Save + +**Buyout:** +1. Validate auction is ACTIVE and has buyout price +2. Validate buyer has enough currency +3. Deduct buyout amount from buyer +4. If previous bidder exists, create REFUND claim +5. Create ITEM claim for buyer +6. Create CURRENCY claim for seller (minus sales tax) +7. Set status to ENDED +8. Save + +**Auction Ends (no buyout):** +1. If bids exist: + - Create ITEM claim for high bidder + - Create CURRENCY claim for seller (minus sales tax) +2. If no bids: + - Create ITEM claim for seller (item returned) +3. Set status to ENDED +4. Save + +### 3.6 Snipe Protection + +When a bid is placed with less than `SnipeProtectionMinutes` remaining: +- Extend the auction end time TO `SnipeProtectionMinutes` from now +- Never extend beyond `SnipeProtectionMinutes` +- Default: 2 minutes + +Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 + +### 3.7 Notifications + +#### AuctionNotification Class +```java +public class AuctionNotification { + UUID playerUUID; + EnumNotificationType type; + int listingId; + String message; + long timestamp; +} + +public enum EnumNotificationType { + AUCTION_WON, // You won an auction + AUCTION_OUTBID, // You were outbid + AUCTION_SOLD, // Your item sold + AUCTION_EXPIRED, // Your auction expired (no bids) + CLAIM_READY // You have items/currency to claim +} +``` + +Notifications are queued and sent when player logs in. + +### 3.8 Permissions + +| Permission | Default | Description | +|------------|---------|-------------| +| `customnpcs.auction.use` | true | Can use auction house | +| `customnpcs.auction.sell` | true | Can create listings | +| `customnpcs.auction.slots.X` | 5 | Max listing slots (X = number) | +| `customnpcs.auction.admin` | op | Admin auction management | + +--- + +## 4. Configuration + +### ConfigMarket.java Structure + +```java +// ========================================= +// Market General +// ========================================= +public static boolean MarketEnabled = true; + +// ========================================= +// Currency (Market.Currency) +// ========================================= +public static boolean UseVault = false; +public static String CurrencyName = "Coins"; +public static long StartingBalance = 0; +public static long MaxBalance = Long.MAX_VALUE; + +// ========================================= +// Trader (Market.Trader) +// ========================================= +public static boolean EnableStockByDefault = false; +public static String DefaultResetType = "NONE"; + +// ========================================= +// Auction (Market.Auction) +// ========================================= +public static boolean AuctionEnabled = true; +public static int AuctionDuration = 24; // Hours +public static long ListingFee = 10; // Flat fee to create listing +public static double SalesTaxPercent = 0.05; // 5% tax on sales +public static int DefaultMaxListings = 5; // Per player +public static int SnipeProtectionMinutes = 2; // Snipe protection timer +public static int ClaimExpirationDays = 30; // Days before unclaimed items deleted +public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase +``` + +--- + +## 5. File Structure + +### New Files to Create + +``` +constants/ + EnumAuctionStatus.java + EnumAuctionSort.java + EnumClaimType.java + EnumNotificationType.java + +controllers/ + AuctionController.java + AuctionNotification.java + +controllers/data/ + AuctionListing.java + AuctionClaim.java + AuctionFilter.java + +roles/ + RoleAuctioneer.java + +containers/ + ContainerAuction.java + +client/gui/player/ + GuiAuctionHouse.java (Base GUI) + GuiAuction.java (Browse auctions) + GuiAuctionSell.java (Create listing) + GuiAuctionClaims.java (My auctions/claims) + GuiAuctionFilter.java (Filter settings) + SubGuiAuctionDetails.java (View listing details) + +client/gui/global/ + GuiManageAuction.java (Admin management) + +network/packets/ + AuctionDataPacket.java (Server → Client data sync) + AuctionActionPacket.java (Client → Server actions) +``` + +### Files to Modify + +``` +CustomNpcs.java - Initialize AuctionController +ServerTickHandler.java - Process ended auctions, login notifications +EnumRoleType.java - Add Auctioneer +EnumGuiType.java - Add auction GUI types +DataAdvanced.java - Handle Auctioneer role +PacketHandler.java - Register auction packets +CustomNpcsPermissions.java - Auction permissions +ConfigMarket.java - Add Auction section +RoleTrader.java - Add currency cost per trade +ContainerNPCTrader.java - Currency + stock integration +en_US.lang - All auction lang keys +``` + +--- + +## 6. Data Storage + +### Auction Data File +Location: `customnpcs/auction.dat` + +Structure: +``` +NBTTagCompound root + ├── NextListingId: int + ├── NextClaimId: int + ├── Listings: NBTTagList + │ └── [AuctionListing NBT...] + ├── Claims: NBTTagList + │ └── [AuctionClaim NBT...] + └── PendingNotifications: NBTTagList + └── [AuctionNotification NBT...] +``` + +--- + +## 7. Lang Keys + +```properties +# Auction House +auction.title=Auction House +auction.browse=Browse +auction.sell=Sell +auction.claims=My Auctions +auction.filter=Filter + +# Browse +auction.nextPage=Next > +auction.prevPage=< Prev +auction.page=Page %d of %d +auction.noResults=No auctions found +auction.refresh=Refresh + +# Listing Display +auction.seller=Seller: %s +auction.currentBid=Current Bid: %s +auction.buyout=Buyout: %s +auction.timeLeft=Time Left: %s +auction.bids=%d bids +auction.noBids=No bids + +# Create Listing +auction.sell.title=Create Listing +auction.sell.startingPrice=Starting Price +auction.sell.buyoutPrice=Buyout Price +auction.sell.noBuyout=(0 = no buyout) +auction.sell.fee=Listing Fee: %s +auction.sell.create=Create Listing +auction.sell.noItem=Place an item to sell + +# Claims +auction.claims.title=My Auctions +auction.claims.active=Active Listings +auction.claims.pending=Pending Claims +auction.claims.claimAll=Claim All +auction.claims.cancel=Cancel Listing +auction.claims.empty=No pending claims + +# Filter +auction.filter.title=Filter Auctions +auction.filter.search=Search +auction.filter.minPrice=Min Price +auction.filter.maxPrice=Max Price +auction.filter.myOnly=My Listings Only +auction.filter.apply=Apply + +# Sort Options +auction.sort.newest=Newest +auction.sort.endingSoon=Ending Soon +auction.sort.priceLow=Price: Low to High +auction.sort.priceHigh=Price: High to Low +auction.sort.mostBids=Most Bids + +# Notifications +auction.notify.won=You won the auction for %s! +auction.notify.outbid=You were outbid on %s! +auction.notify.sold=Your %s sold for %s! +auction.notify.expired=Your auction for %s expired with no bids. +auction.notify.claim=You have items to claim at the Auction House. + +# Errors +auction.error.notEnoughCurrency=Not enough currency! +auction.error.auctionEnded=This auction has ended. +auction.error.bidTooLow=Bid must be at least %s. +auction.error.maxListings=You have reached your maximum listings. +auction.error.blacklisted=This item cannot be auctioned. +auction.error.noAuctioneer=You must visit an Auctioneer NPC. + +# Trader Currency +trader.currencyCost=Currency Cost +trader.outOfStock=Out of Stock +trader.stock=Stock: %d +``` + +--- + +## 8. Implementation Order + +### Phase 1: Trader Currency Integration - COMPLETE ✅ +1. ~~Add `currencyCost` field to RoleTrader trades~~ +2. ~~Update ContainerNPCTrader to check/deduct currency~~ +3. ~~Update trader setup GUI with currency input~~ +4. ~~Test with existing item trades (backward compatibility)~~ + +### Phase 2: Trader Stock UI - COMPLETE ✅ +1. ~~Add stock display to player trader GUI~~ +2. ~~Show "Out of Stock" indicator~~ +3. ~~Verify reset timers working~~ +4. ~~Real-time client-side countdown~~ +5. ~~Status messages with costs~~ +6. ~~Reset cooldown button with confirmation~~ + +### Phase 2.5: Trader Sync System - COMPLETE ✅ +1. ~~Implement MarketRegistry for linked market sync~~ +2. ~~Viewer tracking (market-wide and local)~~ +3. ~~NPC unload cleanup~~ +4. ~~Thread safety for stock operations~~ + +### Phase 3: Auction Core +1. Create enums (EnumAuctionStatus, EnumAuctionSort, etc.) +2. Create data classes (AuctionListing, AuctionClaim, AuctionFilter) +3. Create AuctionController with core logic +4. Create RoleAuctioneer +5. Add to CustomNpcs initialization + +### Phase 4: Auction GUI +1. Create GuiAuctionHouse base +2. Create GuiAuction (browse) +3. Create GuiAuctionSell +4. Create GuiAuctionClaims +5. Create GuiAuctionFilter +6. Create SubGuiAuctionDetails +7. Create ContainerAuction + +### Phase 5: Auction Network +1. Create AuctionDataPacket +2. Create AuctionActionPacket +3. Register packets + +### Phase 6: Admin & Polish +1. Create GuiManageAuction +2. Add permissions +3. Add notifications system +4. Add lang keys +5. Testing + +--- + +## 9. Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Claims System | Everything to claims | Prevents offline handling issues | +| Auction Duration | Single config value | Simplicity over flexibility | +| Snipe Protection | Reset to X minutes | Fair bidding, prevents last-second sniping | +| Listing Fees | Flat fee only | Simple, predictable | +| Sales Tax | Percentage of sale | Scales with item value | +| Data Storage | Single .dat file | Consistent with quests/dialogs | +| Blacklist | Stub for now | Complex feature for later | +| Currency per Trade | Additive with items | Backward compatible, flexible | + +--- + +## 10. Future Considerations (Not in 1.11) + +- Category-based filtering +- Advanced blacklist system (items, mods, NBT patterns) +- Auction history/statistics +- Command-based auction access +- Auction house tax sink +- Reserve prices +- Private auctions diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md deleted file mode 100644 index 4d62a7772..000000000 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ /dev/null @@ -1,324 +0,0 @@ -# CustomNPC+ 1.11 Market Update - Implementation Status - -## Overview - -This document tracks the implementation status of the CustomNPC+ 1.11 Market Update, which includes: - -1. **Auction House** - Global auction system with bidding, buyout, and claims -2. **Currency System** - Built-in currency with VaultAPI integration for Bukkit hybrid servers -3. **Trader Enhancements** - Stock/inventory system with reset timers - ---- - -## Implementation Status Summary - -| Phase | Feature | Status | -|-------|---------|--------| -| Phase 1 | Currency System Foundation | **NOT STARTED** | -| Phase 2 | Trader Enhancements | **COMPLETED** | -| Phase 3 | Item Attributes & Restrictions | **COMPLETED** | -| Phase 4 | Auction House Core | **COMPLETED** | -| Phase 5 | Network Packets & Permissions | **COMPLETED** | -| Phase 6 | Auction GUI & Notifications | **COMPLETED** | -| Phase 7 | Admin Management | **NOT STARTED** | - ---- - -## Completed Features - -### Phase 2: Trader Enhancements (COMPLETED) -- `EnumStockReset.java` - Stock reset timing options -- `TraderStock.java` - Stock tracking with per-player and per-server modes -- Modified `RoleTrader.java` with stock and currency support -- Modified `ContainerNPCTrader.java` for stock/currency transactions -- Stock persistence via NBT - -### Phase 3: Item Attributes & Restrictions (COMPLETED) -- `ItemTradeAttribute.java` - Untradeable, ProfileSlot Bound, Soulbound checks -- `AuctionBlacklist.java` - Item/mod/NBT blacklist system -- Integration with AuctionListing validation - -### Phase 4: Auction House Core (COMPLETED) -- `EnumAuctionDuration.java` - Configurable durations with lang keys -- `EnumAuctionStatus.java` - Auction state management -- `EnumAuctionSort.java` - Sort options with lang keys -- `AuctionListing.java` - Complete listing data structure with bidding logic -- `AuctionFilter.java` - Search/filter criteria (categories removed) -- `AuctionController.java` - Consolidated controller with: - - Listing creation/management - - Bidding and buyout logic - - Claim system for items and currency refunds - - Inline logging (via LogWriter) - - Inline player notifications (via chat messages) - - Pending notification queue for offline players - - Snipe protection - - Sales tax and listing fees -- `AuctionClaim.java` - Claim data for items/money -- `RoleAuctioneer.java` - Auctioneer NPC role -- `ContainerAuction.java` - Auction container -- `ConfigMarket.java` - All market configuration options - -### Phase 5: Network Packets & Permissions (COMPLETED) -- `AuctionActionPacket.java` - Client-to-server auction actions -- `AuctionDataPacket.java` - Server-to-client data sync -- Packet registration in `PacketHandler.java` -- Permission nodes added to `CustomNpcsPermissions.java` -- API methods added to `ICustomNpc.java` - -### Phase 6: Auction GUI & Notifications (COMPLETED) -- `GuiAuction.java` - Main auction browser with tabs -- `SubGuiAuctionCreate.java` - Create listing sub-GUI -- `SubGuiAuctionBid.java` - Place bid sub-GUI -- `SubGuiAuctionDetails.java` - View listing details sub-GUI -- All GUI strings use lang keys via `StatCollector.translateToLocal()` -- Localization keys added to `en_US.lang` - ---- - -## Removed/Changed Features - -### Removed: Category System -Originally planned to have item categories (Weapons, Armor, Tools, etc.) for filtering. This was **removed** to simplify the initial implementation: -- Deleted `EnumAuctionCategory.java` -- Removed category field from `AuctionListing.java` -- Removed category filtering from `AuctionFilter.java` -- Removed category buttons from `GuiAuction.java` - -**Rationale:** Categories can be added in a future update. Current implementation uses search-based filtering. - -### Consolidated: Controller Architecture -Originally planned separate controllers: -- ~~`AuctionLogger.java`~~ - **CONSOLIDATED** into `AuctionController.logEvent()` -- ~~`AuctionNotificationController.java`~~ - **CONSOLIDATED** into `AuctionController.notifyPlayer()` - -**Rationale:** Reduces complexity, all auction logic in one place. - -### Changed: Localization Approach -- All client GUI strings now use `StatCollector.translateToLocal()` with lang keys -- `EnumAuctionSort` uses lang keys for display names -- `EnumAuctionDuration` uses lang keys for display names - ---- - -## Not Yet Implemented - -### Phase 1: Currency System (NOT STARTED) -Requires implementation: -- [ ] `VaultHelper.java` - VaultAPI integration via reflection -- [ ] `CurrencyController.java` - Currency management -- [ ] `PlayerCurrencyData.java` - Per-player currency (shared across profile slots) -- [ ] Vault dependency in build files -- [ ] Currency configuration GUI - -**Note:** Currently auctions use `long` for prices but the actual currency deduction/deposit is not connected to any economy system. - -### Phase 7: Admin Management (NOT STARTED) -Requires implementation: -- [ ] `GuiAuctionAdmin.java` - Admin auction management GUI -- [ ] `GuiAuctionBlacklist.java` - Blacklist management GUI -- [ ] Admin methods in `AuctionController.java` (adminCancelListing, adminEditListing, etc.) -- [ ] Audit logging for admin actions -- [ ] `/cnpc auction admin` command - -### Additional Missing Features -- [ ] Auctioneer NPC setup GUI (`GuiNpcAuctioneerSetup.java`) -- [ ] Trader stock reset timer banner display -- [ ] Comprehensive testing -- [ ] Player balance display (requires Currency System) - ---- - -## Configuration Reference - -All settings are in `ConfigMarket.java`: - -```java -// Currency -public static String CurrencyName = "Gold"; - -// Auction Durations (in hours) -public static int AuctionDurationShort = 12; -public static int AuctionDurationMedium = 24; -public static int AuctionDurationLong = 48; -public static int AuctionDurationVeryLong = 72; - -// Auction Fees (per duration) -public static long AuctionFeeShort = 10; -public static long AuctionFeeMedium = 25; -public static long AuctionFeeLong = 50; -public static long AuctionFeeVeryLong = 100; - -// Listing Configuration -public static int DefaultMaxListings = 5; -public static double ListingFeePercent = 0.0; -public static double SalesTaxPercent = 0.05; -public static double MinBidIncrementPercent = 0.05; -public static int ClaimExpirationDays = 30; -public static boolean RequireAuctioneerNPC = true; - -// Snipe Protection -public static int SnipeProtectionMinutes = 5; -public static int SnipeProtectionThreshold = 5; -``` - ---- - -## File Inventory - -### New Files Created - -| File | Purpose | -|------|---------| -| `constants/EnumAuctionDuration.java` | Auction duration options | -| `constants/EnumAuctionSort.java` | Sort options with lang keys | -| `constants/EnumAuctionStatus.java` | Auction state enum | -| `constants/EnumStockReset.java` | Trader stock reset types | -| `controllers/AuctionController.java` | Main auction controller | -| `controllers/AuctionBlacklist.java` | Item blacklist system | -| `controllers/data/AuctionListing.java` | Auction listing data | -| `controllers/data/AuctionFilter.java` | Search/filter criteria | -| `controllers/data/AuctionClaim.java` | Claim data structure | -| `controllers/data/TraderStock.java` | Trader stock system | -| `controllers/data/attribute/ItemTradeAttribute.java` | Trade restriction attributes | -| `roles/RoleAuctioneer.java` | Auctioneer NPC role | -| `containers/ContainerAuction.java` | Auction GUI container | -| `client/gui/player/GuiAuction.java` | Main auction GUI | -| `client/gui/player/SubGuiAuctionCreate.java` | Create listing GUI | -| `client/gui/player/SubGuiAuctionBid.java` | Place bid GUI | -| `client/gui/player/SubGuiAuctionDetails.java` | Listing details GUI | -| `config/ConfigMarket.java` | Market configuration | -| `network/packets/player/AuctionActionPacket.java` | Auction action packet | -| `network/packets/data/AuctionDataPacket.java` | Auction data sync packet | - -### Files Deleted -| File | Reason | -|------|--------| -| ~~`constants/EnumAuctionCategory.java`~~ | Categories removed | -| ~~`controllers/AuctionLogger.java`~~ | Consolidated into AuctionController | -| ~~`controllers/AuctionNotificationController.java`~~ | Consolidated into AuctionController | - -### Files Modified - -| File | Changes | -|------|---------| -| `CustomNpcs.java` | Initialize AuctionController, AuctionBlacklist | -| `ServerTickHandler.java` | Call AuctionController.onPlayerLogin() | -| `constants/EnumRoleType.java` | Added Auctioneer | -| `constants/EnumGuiType.java` | Added auction GUI types | -| `DataAdvanced.java` | Handle Auctioneer role creation | -| `roles/RoleTrader.java` | Added stock system | -| `containers/ContainerNPCTrader.java` | Stock handling | -| `CustomNpcsPermissions.java` | Auction permission nodes | -| `api/ICustomNpc.java` | Auction API methods | -| `PacketHandler.java` | Register auction packets | -| `en_US.lang` | All auction lang keys | - ---- - -## Localization Keys Added - -```properties -# Main GUI -auction.title=Auction House -auction.browse=Browse -auction.myListings=My Listings -auction.myBids=My Bids -auction.claims=Claims -auction.sell=Sell -auction.search=Search -auction.refresh=Refresh -auction.viewDetails=View Details -auction.placeBid=Place Bid -auction.buyout=Buyout -auction.cancel=Cancel -auction.prevPage=< Prev -auction.nextPage=Next > -auction.noResults=No results -auction.page=Page %d of %d -auction.balance=Balance: %s -auction.listings=%d listings - -# Tab Titles -auction.browseTitle=Browse Auctions -auction.myListingsTitle=My Listings -auction.myBidsTitle=My Active Bids -auction.claimsTitle=Claim Items -auction.sellTitle=Create Listing - -# Listing Details -auction.item=Item: -auction.seller=Seller: -auction.startingPrice=Starting Price: -auction.buyoutPrice=Buyout Price: -auction.currentPrice=Current Price: -auction.currentBid=Current Bid: -auction.minimumBid=Minimum Bid: -auction.highBidder=High Bidder: -auction.totalBids=Total Bids: -auction.quantity=Quantity: -auction.timeLeft=Time Left: -auction.yourBid=Your Bid: -auction.yourBalance=Your Balance: -auction.ended=Ended -auction.noBidsYet=No bids yet - -# Create Listing -auction.createListing=Create Listing -auction.placeItemBelow=Place item in slot below: -auction.noItemSelected=(No item selected) -auction.noBuyoutHint=(0 = no buyout) -auction.duration=Duration: -auction.listingFee=Listing Fee: -auction.create=Create - -# Bid GUI -auction.minBid=Min Bid -auction.bidPlus10=+10% -auction.bidPlus25=+25% -auction.insufficientFunds=Insufficient funds! -auction.auctionEnded=Auction has ended! -auction.auctionDetails=Auction Details - -# Sort Options -auction.sort.newest=Newest First -auction.sort.oldest=Oldest First -auction.sort.priceHigh=Most Expensive -auction.sort.priceLow=Least Expensive -auction.sort.endingSoon=Ending Soon -auction.sort.mostTime=Most Time Left -auction.sort.nameAZ=Name: A to Z -auction.sort.nameZA=Name: Z to A -auction.sort.mostBids=Most Bids -auction.sort.leastBids=Least Bids - -# Duration Options -auction.duration.short=Short (12h) -auction.duration.medium=Medium (24h) -auction.duration.long=Long (48h) -auction.duration.veryLong=Very Long (72h) -``` - ---- - -## Design Decisions (Confirmed) - -1. **Outbid Refunds** → Go to claims (not direct to balance) -2. **Currency** → Shared across all profile slots per player (NOT slot-bound) -3. **Permissions** → Uses existing CustomNPC+ permission pattern (default: 5 slots) -4. **Categories** → Removed for initial implementation (can add later) -5. **Controller Architecture** → Single consolidated AuctionController -6. **Localization** → All client strings use StatCollector with lang keys -7. **StatCollector Usage** → Safe - only called from client-side GUI code -8. **Claim Expiration** → 30 days default (configurable) -9. **Snipe Protection** → Extend TO 5 minutes when bid placed near end -10. **Logging** → Inline in AuctionController using LogWriter - ---- - -## Next Steps - -1. **Currency System** - Implement VaultHelper and CurrencyController to connect actual economy -2. **Admin Management** - Implement admin GUI and commands -3. **Testing** - Comprehensive testing of all auction flows -4. **Auctioneer Setup GUI** - Create NPC configuration interface -5. **Trader Banner** - Add stock reset timer display above trader GUI diff --git a/src/api b/src/api index 866be1fef..339e0f164 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 866be1fef5bf37a002379140771096cebe29f286 +Subproject commit 339e0f164e8c7b08c3469f61027fa25aa9e5c118 diff --git a/src/main/java/foxz/utils/Market.java b/src/main/java/foxz/utils/Market.java index ca260b90b..54905423c 100644 --- a/src/main/java/foxz/utils/Market.java +++ b/src/main/java/foxz/utils/Market.java @@ -31,9 +31,6 @@ public class Market { NBTTagCompound roleCompound = r.writeNBT(new NBTTagCompound()); roleCompound.setString("SaveName", name); putMarketCache(name, roleCompound); - if (r.recordHistory) - return; - saveFile(roleCompound); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java b/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java deleted file mode 100644 index 180f41d02..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java +++ /dev/null @@ -1,308 +0,0 @@ -package kamkeel.npcs.controllers.data.attribute; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.controllers.data.PlayerData; - -import java.util.UUID; - -/** - * Utility class for managing item trade restrictions. - * Checks and sets attributes that control whether items can be traded/auctioned. - */ -public class ItemTradeAttribute { - - // New untradeable attribute - prevents all trading - public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; - - // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) - public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; - public static final String TAG_SOULBIND = "cnpc_soulbind"; - - // ==================== Trade Check Methods ==================== - - /** - * Check if item can be traded/auctioned by the given player. - * An item cannot be traded if: - * - It has the Untradeable flag - * - It is bound to a specific profile slot (ProfileSlot Bound) - * - It is soulbound to a different player - * - * @param item The item to check - * @param player The player trying to trade (null to check without player context) - * @return true if the item can be traded - */ - public static boolean canTrade(ItemStack item, EntityPlayer player) { - if (item == null) { - return true; - } - if (!item.hasTagCompound()) { - return true; - } - - NBTTagCompound tag = item.getTagCompound(); - - // Check untradeable flag - always blocks trading - if (tag.getBoolean(TAG_UNTRADEABLE)) { - return false; - } - - // Check profile slot bound - blocks trading as it's tied to a slot - if (tag.hasKey(TAG_PROFILE_SLOT)) { - return false; - } - - // Check soulbound - blocks trading as it's tied to a player - if (tag.hasKey(TAG_SOULBIND)) { - // If we have a player context, check if they're the owner - if (player != null) { - String soulbindUUID = tag.getString(TAG_SOULBIND); - if (soulbindUUID != null && !soulbindUUID.isEmpty()) { - // Even if bound to this player, still can't trade - return false; - } - } - return false; - } - - return true; - } - - /** - * Check if item can be traded (without player context) - */ - public static boolean canTrade(ItemStack item) { - return canTrade(item, null); - } - - /** - * Get the reason why an item cannot be traded. - * Returns a translation key for localization. - * - * @param item The item to check - * @return Translation key for the block reason, or null if tradeable - */ - public static String getTradeBlockReason(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return null; - } - - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) { - return "gui.auction.untradeable"; - } - if (tag.hasKey(TAG_PROFILE_SLOT)) { - return "gui.auction.profileslotbound"; - } - if (tag.hasKey(TAG_SOULBIND)) { - return "gui.auction.soulbound"; - } - - return null; - } - - // ==================== Untradeable Methods ==================== - - /** - * Check if item is marked as untradeable - */ - public static boolean isUntradeable(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); - } - - /** - * Set the untradeable flag on an item - */ - public static void setUntradeable(ItemStack item, boolean untradeable) { - if (item == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (untradeable) { - item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); - } else { - item.getTagCompound().removeTag(TAG_UNTRADEABLE); - } - } - - // ==================== Profile Slot Bound Methods ==================== - - /** - * Check if item is bound to a profile slot - */ - public static boolean isProfileSlotBound(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); - } - - /** - * Get the profile slot an item is bound to - * @return The profile slot ID, or -1 if not bound - */ - public static int getProfileSlot(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return -1; - } - if (item.getTagCompound().hasKey(TAG_PROFILE_SLOT)) { - return item.getTagCompound().getInteger(TAG_PROFILE_SLOT); - } - return -1; - } - - /** - * Bind an item to a specific profile slot - */ - public static void setProfileSlotBound(ItemStack item, int slot) { - if (item == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (slot >= 0) { - item.getTagCompound().setInteger(TAG_PROFILE_SLOT, slot); - } else { - item.getTagCompound().removeTag(TAG_PROFILE_SLOT); - } - } - - /** - * Bind an item to the player's current profile slot - */ - public static void bindToCurrentSlot(ItemStack item, EntityPlayer player) { - if (item == null || player == null) { - return; - } - PlayerData data = PlayerData.get(player); - if (data != null) { - setProfileSlotBound(item, data.profileSlot); - } - } - - // ==================== Soulbind Methods ==================== - - /** - * Check if item is soulbound to any player - */ - public static boolean isSoulbound(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().hasKey(TAG_SOULBIND); - } - - /** - * Check if item is soulbound to a specific player - */ - public static boolean isSoulboundTo(ItemStack item, EntityPlayer player) { - if (item == null || player == null || !item.hasTagCompound()) { - return false; - } - if (!item.getTagCompound().hasKey(TAG_SOULBIND)) { - return false; - } - String soulbindUUID = item.getTagCompound().getString(TAG_SOULBIND); - return player.getUniqueID().toString().equals(soulbindUUID); - } - - /** - * Get the UUID of the player the item is soulbound to - * @return The UUID string, or null if not soulbound - */ - public static String getSoulbindOwner(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return null; - } - if (item.getTagCompound().hasKey(TAG_SOULBIND)) { - return item.getTagCompound().getString(TAG_SOULBIND); - } - return null; - } - - /** - * Soulbind an item to a player - */ - public static void setSoulbound(ItemStack item, EntityPlayer player) { - if (item == null || player == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - item.getTagCompound().setString(TAG_SOULBIND, player.getUniqueID().toString()); - } - - /** - * Soulbind an item to a player by UUID - */ - public static void setSoulbound(ItemStack item, UUID playerUUID) { - if (item == null || playerUUID == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - item.getTagCompound().setString(TAG_SOULBIND, playerUUID.toString()); - } - - /** - * Remove soulbind from an item - */ - public static void removeSoulbind(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return; - } - item.getTagCompound().removeTag(TAG_SOULBIND); - } - - // ==================== Utility Methods ==================== - - /** - * Clear all trade restriction attributes from an item - */ - public static void clearAllRestrictions(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return; - } - NBTTagCompound tag = item.getTagCompound(); - tag.removeTag(TAG_UNTRADEABLE); - tag.removeTag(TAG_PROFILE_SLOT); - tag.removeTag(TAG_SOULBIND); - } - - /** - * Get a display string describing all trade restrictions on an item - * @return Human-readable string of restrictions, or empty if none - */ - public static String getRestrictionSummary(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return ""; - } - - StringBuilder summary = new StringBuilder(); - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) { - summary.append("Untradeable"); - } - if (tag.hasKey(TAG_PROFILE_SLOT)) { - if (summary.length() > 0) summary.append(", "); - summary.append("Profile Slot Bound (Slot ").append(tag.getInteger(TAG_PROFILE_SLOT)).append(")"); - } - if (tag.hasKey(TAG_SOULBIND)) { - if (summary.length() > 0) summary.append(", "); - summary.append("Soulbound"); - } - - return summary.toString(); - } -} diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index b6d30d62d..3a4c35d05 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -36,7 +36,6 @@ import kamkeel.npcs.network.packets.data.gui.GuiWaypointPacket; import kamkeel.npcs.network.packets.data.gui.IsGuiOpenPacket; import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.network.packets.data.large.PartyDataPacket; -import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollListPacket; import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; @@ -52,13 +51,13 @@ import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; import kamkeel.npcs.network.packets.player.DialogSelectPacket; import kamkeel.npcs.network.packets.player.FollowerPacket; import kamkeel.npcs.network.packets.player.GetNPCRole; +import kamkeel.npcs.network.packets.player.GetTraderData; import kamkeel.npcs.network.packets.player.InputDevicePacket; import kamkeel.npcs.network.packets.player.MailActionPacket; import kamkeel.npcs.network.packets.player.SaveBookPacket; @@ -566,9 +565,6 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); - - // Auction Data - DATA_PACKET.registerPacket(new AuctionDataPacket()); } public void registerPlayerPackets() { @@ -591,6 +587,7 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new CompanionActionPacket()); PLAYER_PACKET.registerPacket(new GetNPCRole()); + PLAYER_PACKET.registerPacket(new GetTraderData()); PLAYER_PACKET.registerPacket(new InputDevicePacket()); @@ -630,9 +627,6 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new PartyAcceptInvitePacket()); PLAYER_PACKET.registerPacket(new PartyIgnoreInvitePacket()); PLAYER_PACKET.registerPacket(new PartyLogToServerPacket()); - - // Auction Packets - PLAYER_PACKET.registerPacket(new AuctionActionPacket()); } public void registerChannels() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index ec8eabf41..ebcbdfc59 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,7 +62,4 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, - - // Auction - AUCTION_DATA, } diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 15c90fce5..48f400fb2 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -13,6 +13,7 @@ public enum EnumPlayerPacket { SaveBook, CompanionAction, GetRole, + GetTraderData, InputDevice, SpecialKeyState, ScreenSize, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java deleted file mode 100644 index f7ca16dd7..000000000 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java +++ /dev/null @@ -1,218 +0,0 @@ -package kamkeel.npcs.network.packets.data.large; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import kamkeel.npcs.network.LargeAbstractPacket; -import kamkeel.npcs.network.PacketChannel; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumDataPacket; -import kamkeel.npcs.util.ByteBufUtils; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; - -import java.io.IOException; -import java.util.List; - -/** - * Large packet for sending auction listing data from server to client. - * Used for initial load and search/filter queries. - */ -public final class AuctionDataPacket extends LargeAbstractPacket { - public static final String packetName = "Large|AuctionData"; - - private DataType dataType; - private List listings; - private int totalResults; - private int currentPage; - private int totalPages; - private long playerBalance; - - public AuctionDataPacket() { - } - - private AuctionDataPacket(DataType type, List listings, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - this.dataType = type; - this.listings = listings; - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - this.playerBalance = playerBalance; - } - - /** - * Send filtered listing results to client - */ - public static void sendFilteredListings(EntityPlayerMP player, AuctionFilter filter) { - if (AuctionController.Instance == null) { - return; - } - - AuctionController.FilterResult result = AuctionController.Instance.getFilteredListings(filter); - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.FILTERED_LISTINGS, - result.listings, - result.totalResults, - result.currentPage, - result.totalPages, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's own listings - */ - public static void sendMyListings(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List myListings = AuctionController.Instance - .getListingsBySeller(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.MY_LISTINGS, - myListings, - myListings.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's active bids - */ - public static void sendMyBids(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List myBids = AuctionController.Instance - .getListingsAsBidder(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.MY_BIDS, - myBids, - myBids.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's claimable items - */ - public static void sendClaimable(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List claimable = AuctionController.Instance - .getClaimableListings(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.CLAIMABLE, - claimable, - claimable.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - @Override - public Enum getType() { - return EnumDataPacket.AUCTION_DATA; - } - - @Override - public PacketChannel getChannel() { - return PacketHandler.DATA_PACKET; - } - - @Override - protected byte[] getData() throws IOException { - ByteBuf buffer = Unpooled.buffer(); - - buffer.writeInt(dataType.ordinal()); - buffer.writeInt(totalResults); - buffer.writeInt(currentPage); - buffer.writeInt(totalPages); - buffer.writeLong(playerBalance); - - // Write listings as NBT - NBTTagList listingList = new NBTTagList(); - if (listings != null) { - for (AuctionListing listing : listings) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - } - - NBTTagCompound compound = new NBTTagCompound(); - compound.setTag("Listings", listingList); - ByteBufUtils.writeNBT(buffer, compound); - - byte[] bytes = new byte[buffer.readableBytes()]; - buffer.readBytes(bytes); - return bytes; - } - - @Override - protected void handleCompleteData(ByteBuf data, EntityPlayer player) throws IOException { - // Client-side handling will be done in a client util class - // Similar to NoppesUtil.setScrollData() - int typeOrdinal = data.readInt(); - int totalResults = data.readInt(); - int currentPage = data.readInt(); - int totalPages = data.readInt(); - long playerBalance = data.readLong(); - - NBTTagCompound compound = ByteBufUtils.readNBT(data); - - DataType type = DataType.values()[typeOrdinal]; - - // Store data for client GUI to access - // This will be implemented in the client-side GUI handler - noppes.npcs.client.NoppesUtil.setAuctionData(type.ordinal(), compound, - totalResults, currentPage, totalPages, playerBalance); - } - - public enum DataType { - FILTERED_LISTINGS, - MY_LISTINGS, - MY_BIDS, - CLAIMABLE - } -} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java deleted file mode 100644 index a63312d80..000000000 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ /dev/null @@ -1,579 +0,0 @@ -package kamkeel.npcs.network.packets.player; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.network.AbstractPacket; -import kamkeel.npcs.network.PacketChannel; -import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumPlayerPacket; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.ChatComponentText; -import noppes.npcs.CustomNpcsPermissions; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumRoleType; -import noppes.npcs.containers.ContainerAuction; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.CurrencyController; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.util.NBTJsonUtil; -import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; - -import java.io.IOException; - -/** - * Packet for all auction-related player actions. - * Client -> Server for actions, Server -> Client for responses. - */ -public class AuctionActionPacket extends AbstractPacket { - public static final String packetName = "Player|Auction"; - - private Action action; - private int listingId; - private long amount; - private long buyoutPrice; - private int durationOrdinal; - private NBTTagCompound itemData; - private NBTTagCompound filterData; // For query operations - private String responseMessage; - private boolean success; - - public AuctionActionPacket() { - } - - private AuctionActionPacket(Action action) { - this.action = action; - } - - @Override - public boolean needsNPC() { - return true; - } - - // ==================== Client -> Server Actions ==================== - - /** - * Create a new auction listing - */ - @SideOnly(Side.CLIENT) - public static void CreateListing(ItemStack item, long startingPrice, long buyoutPrice, EnumAuctionDuration duration) { - AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); - packet.amount = startingPrice; - packet.buyoutPrice = buyoutPrice; - packet.durationOrdinal = duration.ordinal(); - packet.itemData = item.writeToNBT(new NBTTagCompound()); - PacketClient.sendClient(packet); - } - - /** - * Place a bid on an auction - */ - @SideOnly(Side.CLIENT) - public static void PlaceBid(int listingId, long bidAmount) { - AuctionActionPacket packet = new AuctionActionPacket(Action.PlaceBid); - packet.listingId = listingId; - packet.amount = bidAmount; - PacketClient.sendClient(packet); - } - - /** - * Execute buyout on an auction - */ - @SideOnly(Side.CLIENT) - public static void Buyout(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Cancel own listing - */ - @SideOnly(Side.CLIENT) - public static void CancelListing(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.CancelListing); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Claim item from won/expired/cancelled auction - */ - @SideOnly(Side.CLIENT) - public static void ClaimItem(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Claim proceeds from sold auction - */ - @SideOnly(Side.CLIENT) - public static void ClaimProceeds(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimProceeds); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Query filtered listings - */ - @SideOnly(Side.CLIENT) - public static void QueryListings(AuctionFilter filter) { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryListings); - packet.filterData = filter.writeToNBT(new NBTTagCompound()); - PacketClient.sendClient(packet); - } - - /** - * Request player's own listings - */ - @SideOnly(Side.CLIENT) - public static void QueryMyListings() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyListings); - PacketClient.sendClient(packet); - } - - /** - * Request player's active bids - */ - @SideOnly(Side.CLIENT) - public static void QueryMyBids() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyBids); - PacketClient.sendClient(packet); - } - - /** - * Request player's claimable items - */ - @SideOnly(Side.CLIENT) - public static void QueryClaimable() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryClaimable); - PacketClient.sendClient(packet); - } - - @Override - public Enum getType() { - return EnumPlayerPacket.AuctionAction; - } - - @Override - public PacketChannel getChannel() { - return PacketHandler.PLAYER_PACKET; - } - - @Override - @SideOnly(Side.CLIENT) - public void sendData(ByteBuf out) throws IOException { - out.writeInt(action.ordinal()); - - switch (action) { - case CreateListing: - out.writeLong(amount); - out.writeLong(buyoutPrice); - out.writeInt(durationOrdinal); - String itemJson = NBTJsonUtil.Convert(itemData); - byte[] itemBytes = itemJson.getBytes("UTF-8"); - out.writeInt(itemBytes.length); - out.writeBytes(itemBytes); - break; - - case PlaceBid: - out.writeInt(listingId); - out.writeLong(amount); - break; - - case Buyout: - case CancelListing: - case ClaimItem: - case ClaimProceeds: - out.writeInt(listingId); - break; - - case QueryListings: - String filterJson = NBTJsonUtil.Convert(filterData); - byte[] filterBytes = filterJson.getBytes("UTF-8"); - out.writeInt(filterBytes.length); - out.writeBytes(filterBytes); - break; - - case QueryMyListings: - case QueryMyBids: - case QueryClaimable: - // No additional data needed - break; - } - } - - @Override - public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { - Action requestedAction = Action.values()[in.readInt()]; - - if (!(player instanceof EntityPlayerMP)) { - return; - } - - EntityPlayerMP playerMP = (EntityPlayerMP) player; - - // Verify NPC is an Auctioneer - if (npc == null || npc.advanced.role != EnumRoleType.Auctioneer) { - return; - } - - // Check if auction system is enabled - if (!ConfigMarket.AuctionEnabled) { - sendResponse(playerMP, false, "Auction system is disabled"); - return; - } - - // Check controller exists - if (AuctionController.Instance == null) { - sendResponse(playerMP, false, "Auction controller not initialized"); - return; - } - - switch (requestedAction) { - case CreateListing: - handleCreateListing(in, playerMP); - break; - case PlaceBid: - handlePlaceBid(in, playerMP); - break; - case Buyout: - handleBuyout(in, playerMP); - break; - case CancelListing: - handleCancelListing(in, playerMP); - break; - case ClaimItem: - handleClaimItem(in, playerMP); - break; - case ClaimProceeds: - handleClaimProceeds(in, playerMP); - break; - case QueryListings: - handleQueryListings(in, playerMP); - break; - case QueryMyListings: - handleQueryMyListings(playerMP); - break; - case QueryMyBids: - handleQueryMyBids(playerMP); - break; - case QueryClaimable: - handleQueryClaimable(playerMP); - break; - } - } - - private void handleCreateListing(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_LIST)) { - sendResponse(player, false, "You don't have permission to create listings"); - return; - } - - long startingPrice = in.readLong(); - long buyoutPrice = in.readLong(); - int durationOrdinal = in.readInt(); - - int itemBytesLength = in.readInt(); - byte[] itemBytes = new byte[itemBytesLength]; - in.readBytes(itemBytes); - String itemJson = new String(itemBytes, "UTF-8"); - - try { - NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); - ItemStack requestedItem = ItemStack.loadItemStackFromNBT(itemNBT); - - if (!(player.openContainer instanceof ContainerAuction)) { - sendResponse(player, false, "Open the auction listing screen to create a listing"); - return; - } - - ContainerAuction container = (ContainerAuction) player.openContainer; - ItemStack slotItem = container.getListingItem(); - - if (requestedItem == null || slotItem == null) { - sendResponse(player, false, "Place an item in the listing slot"); - return; - } - - if (!ItemStack.areItemStacksEqual(slotItem, requestedItem) || slotItem.stackSize != requestedItem.stackSize) { - sendResponse(player, false, "Listing item mismatch"); - return; - } - - // Validate duration - EnumAuctionDuration duration; - if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { - duration = EnumAuctionDuration.values()[durationOrdinal]; - } else { - duration = EnumAuctionDuration.MEDIUM; - } - - // Validate prices - if (startingPrice < 1) { - sendResponse(player, false, "Starting price must be at least 1"); - return; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - sendResponse(player, false, "Buyout price must be higher than starting price"); - return; - } - - // Create the listing - AuctionListing listing = AuctionController.Instance.createListing( - player, slotItem, startingPrice, buyoutPrice, duration); - - if (listing != null) { - container.clearListingSlot(); - sendResponse(player, true, "Listing created successfully"); - } else { - sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); - } - } catch (Exception e) { - sendResponse(player, false, "Error creating listing: " + e.getMessage()); - } - } - - private void handlePlaceBid(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BID)) { - sendResponse(player, false, "You don't have permission to bid"); - return; - } - - int listingId = in.readInt(); - long bidAmount = in.readLong(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (!listing.isActive()) { - sendResponse(player, false, "This auction has ended"); - return; - } - - if (bidAmount < listing.getMinimumBid()) { - sendResponse(player, false, "Bid must be at least " + listing.getMinimumBid()); - return; - } - - if (!CurrencyController.Instance.canAfford(player, bidAmount)) { - sendResponse(player, false, "You cannot afford this bid"); - return; - } - - if (AuctionController.Instance.placeBid(listingId, player, bidAmount)) { - sendResponse(player, true, "Bid placed successfully"); - } else { - sendResponse(player, false, "Failed to place bid"); - } - } - - private void handleBuyout(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BUYOUT)) { - sendResponse(player, false, "You don't have permission to use buyout"); - return; - } - - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (!listing.isActive()) { - sendResponse(player, false, "This auction has ended"); - return; - } - - if (listing.buyoutPrice <= 0) { - sendResponse(player, false, "This listing has no buyout option"); - return; - } - - if (!CurrencyController.Instance.canAfford(player, listing.buyoutPrice)) { - sendResponse(player, false, "You cannot afford the buyout price"); - return; - } - - if (AuctionController.Instance.buyout(listingId, player)) { - sendResponse(player, true, "Buyout successful! Claim your item."); - } else { - sendResponse(player, false, "Failed to execute buyout"); - } - } - - private void handleCancelListing(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - // Admin can cancel any listing - boolean isAdmin = CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_ADMIN); - - if (!listing.sellerUUID.equals(player.getUniqueID()) && !isAdmin) { - sendResponse(player, false, "You can only cancel your own listings"); - return; - } - - if (listing.hasBids() && !isAdmin) { - sendResponse(player, false, "Cannot cancel listing with active bids"); - return; - } - - if (AuctionController.Instance.cancelListing(listingId, player, isAdmin)) { - sendResponse(player, true, "Listing cancelled. Claim your item."); - } else { - sendResponse(player, false, "Failed to cancel listing"); - } - } - - private void handleClaimItem(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - ItemStack claimed = null; - - // Try buyer claim first - if (listing.highBidderUUID != null && listing.highBidderUUID.equals(player.getUniqueID())) { - claimed = AuctionController.Instance.claimBuyerItem(listingId, player); - } - // Then try seller reclaim (for expired/cancelled) - else if (listing.sellerUUID.equals(player.getUniqueID())) { - claimed = AuctionController.Instance.claimExpiredItem(listingId, player); - } - - if (claimed != null) { - // Give item to player - if (!player.inventory.addItemStackToInventory(claimed)) { - // Drop if inventory full - player.entityDropItem(claimed, 0.5f); - } - sendResponse(player, true, "Item claimed"); - } else { - sendResponse(player, false, "Cannot claim item"); - } - } - - private void handleClaimProceeds(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (AuctionController.Instance.claimSellerProceeds(listingId, player)) { - long proceeds = listing.getSellerProceeds(); - sendResponse(player, true, "Claimed " + proceeds + " " + ConfigMarket.CurrencyName); - } else { - sendResponse(player, false, "Cannot claim proceeds"); - } - } - - // ==================== Query Handlers ==================== - - private void handleQueryListings(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - int filterBytesLength = in.readInt(); - byte[] filterBytes = new byte[filterBytesLength]; - in.readBytes(filterBytes); - String filterJson = new String(filterBytes, "UTF-8"); - - try { - NBTTagCompound filterNBT = NBTJsonUtil.Convert(filterJson); - AuctionFilter filter = new AuctionFilter(); - filter.readFromNBT(filterNBT); - - // Send filtered listings to client - AuctionDataPacket.sendFilteredListings(player, filter); - } catch (Exception e) { - sendResponse(player, false, "Error parsing filter"); - } - } - - private void handleQueryMyListings(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendMyListings(player); - } - - private void handleQueryMyBids(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendMyBids(player); - } - - private void handleQueryClaimable(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendClaimable(player); - } - - /** - * Send a response message to the player - */ - private void sendResponse(EntityPlayerMP player, boolean success, String message) { - // Use chat message for now; can be extended to use GUI updates - if (message != null && !message.isEmpty()) { - player.addChatMessage(new ChatComponentText("[Auction] " + message)); - } - } - - private enum Action { - CreateListing, - PlaceBid, - Buyout, - CancelListing, - ClaimItem, - ClaimProceeds, - QueryListings, - QueryMyListings, - QueryMyBids, - QueryClaimable - } -} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java new file mode 100644 index 000000000..5b7cb7dfe --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java @@ -0,0 +1,77 @@ +package kamkeel.npcs.network.packets.player; + +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.roles.RoleTrader; + +import java.io.IOException; + +/** + * Client requests trader-specific GUI data from server. + * Server responds with player balance, stock info, currency costs via GuiDataPacket. + */ +public class GetTraderData extends AbstractPacket { + public static final String packetName = "Player|GetTraderData"; + + @Override + public Enum getType() { + return EnumPlayerPacket.GetTraderData; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + public boolean needsNPC() { + return true; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + // No data needed for request + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!(npc.roleInterface instanceof RoleTrader)) + return; + + RoleTrader role = (RoleTrader) npc.roleInterface; + NBTTagCompound compound = new NBTTagCompound(); + + // Player balance + PlayerData data = PlayerData.get(player); + compound.setLong("Balance", data.currencyData.getBalance()); + + // Stock info + compound.setBoolean("StockEnabled", role.stock.enableStock); + compound.setLong("ResetTimeMillis", role.getResetTimeRemainingMillis()); + + // Available stock per slot (player-specific if perPlayer mode) + String playerName = player.getCommandSenderName(); + int[] stock = new int[18]; + for (int i = 0; i < 18; i++) { + stock[i] = role.getAvailableStock(i, playerName); + } + compound.setIntArray("Stock", stock); + + // Currency cost per slot + for (int i = 0; i < 18; i++) { + compound.setLong("Cost" + i, role.getCurrencyCost(i)); + } + + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } +} diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index a73c179e4..58f931ab8 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -15,7 +15,6 @@ import noppes.npcs.api.IWorld; import noppes.npcs.blocks.tiles.TileNpcContainer; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; -import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -132,9 +131,6 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.CompanionInv) return new ContainerNPCCompanion(npc, player); - if (gui == EnumGuiType.PlayerAuction) - return new ContainerAuction(npc, player); - return null; } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index b29a6cd0b..41cd8770e 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -59,10 +59,6 @@ import noppes.npcs.controllers.GlobalDataController; import noppes.npcs.controllers.LinkedItemController; import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; -import noppes.npcs.controllers.VaultHelper; -import noppes.npcs.controllers.CurrencyController; -import noppes.npcs.controllers.AuctionBlacklist; -import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -314,10 +310,6 @@ public class CustomNpcs { //Loading items in the about to start event was corrupting items with a damage value @EventHandler public void started(FMLServerStartedEvent event) { - new VaultHelper(); - new CurrencyController(); - new AuctionBlacklist(); - new AuctionController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 34a5fbf50..c83e92c22 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -92,13 +92,6 @@ public class CustomNpcsPermissions { public static final Permission SOULSTONE_ALL = new Permission("customnpcs.soulstone.all"); - // Auction System - public static final Permission AUCTION_USE = new Permission("customnpcs.auction.use"); - public static final Permission AUCTION_LIST = new Permission("customnpcs.auction.list"); - public static final Permission AUCTION_BID = new Permission("customnpcs.auction.bid"); - public static final Permission AUCTION_BUYOUT = new Permission("customnpcs.auction.buyout"); - public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); - // Modify / Save Scripts public static final Permission SCRIPT = new Permission("customnpcs.script"); public static final Permission SCRIPT_NPC = new Permission("customnpcs.script.npc"); diff --git a/src/main/java/noppes/npcs/DataAdvanced.java b/src/main/java/noppes/npcs/DataAdvanced.java index f03714506..45ecfe4a8 100644 --- a/src/main/java/noppes/npcs/DataAdvanced.java +++ b/src/main/java/noppes/npcs/DataAdvanced.java @@ -18,7 +18,6 @@ import noppes.npcs.roles.JobGuard; import noppes.npcs.roles.JobHealer; import noppes.npcs.roles.JobItemGiver; import noppes.npcs.roles.JobSpawner; -import noppes.npcs.roles.RoleAuctioneer; import noppes.npcs.roles.RoleBank; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; @@ -219,8 +218,6 @@ public class DataAdvanced { npc.roleInterface = new RoleCompanion(npc); else if (role == EnumRoleType.Mount && !(npc.roleInterface instanceof RoleMount)) npc.roleInterface = new RoleMount(npc); - else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) - npc.roleInterface = new RoleAuctioneer(npc); } public void setJob(int i) { diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 89bdf58c8..08c04f6f4 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -384,6 +384,7 @@ public class NoppesUtilServer { if (gui == EnumGuiType.PlayerFollower || gui == EnumGuiType.PlayerFollowerHire || gui == EnumGuiType.PlayerTrader || gui == EnumGuiType.PlayerTransporter) { sendRoleData(player, npc); } + // Note: Trader-specific data (balance, stock, costs) is requested by client via GetTraderData packet } private static ArrayList getScrollData(EntityPlayer player, EnumGuiType gui, EntityNPCInterface npc) { diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index fe846df19..3648f00d7 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,7 +10,6 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; -import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; @@ -66,11 +65,6 @@ public class ServerTickHandler { ProfileController.Instance.login(player); SyncController.beginLogin(player); SyncController.syncEffects(player); - - // Send pending auction notifications - if (AuctionController.Instance != null) { - AuctionController.Instance.onPlayerLogin(player); - } } @SubscribeEvent diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index a31b57a7f..4e64f8351 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -104,7 +104,6 @@ import noppes.npcs.client.gui.player.GuiCrate; import noppes.npcs.client.gui.player.GuiMailbox; import noppes.npcs.client.gui.player.GuiMailmanWrite; import noppes.npcs.client.gui.player.GuiNPCBankChest; -import noppes.npcs.client.gui.player.GuiAuction; import noppes.npcs.client.gui.player.GuiNPCTrader; import noppes.npcs.client.gui.player.GuiNpcAnvil; import noppes.npcs.client.gui.player.GuiNpcCarpentryBench; @@ -180,7 +179,6 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.config.StringCache; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; -import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -576,9 +574,6 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.MagicBook) return new GuiNpcMagicBook(); - else if (gui == EnumGuiType.PlayerAuction) - return new GuiAuction(npc, (ContainerAuction) container); - else if (gui == EnumGuiType.GlobalRemote) return new GuiNPCGlobalMainMenu(null); diff --git a/src/main/java/noppes/npcs/client/NoppesUtil.java b/src/main/java/noppes/npcs/client/NoppesUtil.java index 64b26fa5d..620142564 100644 --- a/src/main/java/noppes/npcs/client/NoppesUtil.java +++ b/src/main/java/noppes/npcs/client/NoppesUtil.java @@ -182,67 +182,6 @@ public class NoppesUtil { data = new HashMap(); } - // ==================== Auction Data ==================== - - // Static storage for auction data that the GUI can access - private static NBTTagCompound auctionListingData; - private static int auctionDataType; - private static int auctionTotalResults; - private static int auctionCurrentPage; - private static int auctionTotalPages; - private static long auctionPlayerBalance; - - public static void setAuctionData(int dataType, NBTTagCompound compound, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - auctionDataType = dataType; - auctionListingData = compound; - auctionTotalResults = totalResults; - auctionCurrentPage = currentPage; - auctionTotalPages = totalPages; - auctionPlayerBalance = playerBalance; - - // Notify the current GUI if it's an auction GUI - GuiScreen gui = Minecraft.getMinecraft().currentScreen; - if (gui instanceof IAuctionData) { - ((IAuctionData) gui).onAuctionDataReceived(dataType, compound, - totalResults, currentPage, totalPages, playerBalance); - } - } - - public static NBTTagCompound getAuctionListingData() { - return auctionListingData; - } - - public static int getAuctionDataType() { - return auctionDataType; - } - - public static int getAuctionTotalResults() { - return auctionTotalResults; - } - - public static int getAuctionCurrentPage() { - return auctionCurrentPage; - } - - public static int getAuctionTotalPages() { - return auctionTotalPages; - } - - public static long getAuctionPlayerBalance() { - return auctionPlayerBalance; - } - - /** - * Interface for GUIs that receive auction data - */ - public interface IAuctionData { - void onAuctionDataReceived(int dataType, NBTTagCompound data, - int totalResults, int currentPage, int totalPages, - long playerBalance); - } - public static void guiQuestCompletion(EntityPlayer player, NBTTagCompound read) { Quest quest = new Quest(); quest.readNBT(read); diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 7ee5e8881..885c61d94 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -26,7 +26,6 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; import noppes.npcs.client.gui.roles.GuiNpcSpawner; -import noppes.npcs.client.gui.roles.GuiNpcAuctioneer; import noppes.npcs.client.gui.roles.GuiNpcMount; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; @@ -158,8 +157,6 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { displayGuiScreen(new GuiNpcCompanion(npc)); else if (npc.advanced.role == EnumRoleType.Mount) displayGuiScreen(new GuiNpcMount(npc)); - else if (npc.advanced.role == EnumRoleType.Auctioneer) - displayGuiScreen(new GuiNpcAuctioneer(npc)); } else if (compound.hasKey("JobData")) { if (npc.jobInterface != null) npc.jobInterface.readFromNBT(compound); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java deleted file mode 100644 index 30e857e7a..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ /dev/null @@ -1,841 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.inventory.Slot; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.CustomNpcResourceListener; -import noppes.npcs.client.NoppesUtil; -import noppes.npcs.client.gui.util.*; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.containers.ContainerAuction; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.entity.EntityNPCInterface; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Main auction house browse GUI. - * Displays listings with search, sort, and pagination. - */ -public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, - ICustomScrollListener, NoppesUtil.IAuctionData { - - private static final ResourceLocation BG_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/standardbg.png"); - private static final int LEFT_PANEL_X = 8; - private static final int LEFT_PANEL_WIDTH = 190; - private static final int RIGHT_PANEL_X = 210; - private static final int ACTION_BUTTON_WIDTH = 98; - private static final int LISTING_SCROLL_HEIGHT = 100; - private static final int LISTING_SCROLL_WIDTH = 190; - private static final int PAGE_BUTTON_WIDTH = 60; - private static final int SEARCH_FIELD_WIDTH = 140; - private static final int SEARCH_BUTTON_WIDTH = 46; - private static final int HIDDEN_SLOT_POSITION = -1000; - private static final int INVENTORY_START_X = 8; - private static final int INVENTORY_START_Y = 166; - private static final int HOTBAR_Y_OFFSET = 58; - - // GUI Components - private GuiNpcTextField searchField; - private GuiNpcTextField startingPriceField; - private GuiNpcTextField buyoutPriceField; - private GuiCustomScroll listingScroll; - - // Current state - private AuctionFilter currentFilter = new AuctionFilter(); - private List listings = new ArrayList<>(); - private int totalResults = 0; - private int currentPage = 0; - private int totalPages = 0; - private long playerBalance = 0; - - // Tab state (0=Browse, 1=My Listings, 2=My Bids, 3=Claims, 4=Create) - private int currentTab = 0; - private int selectedDuration = 1; - - // Selected listing for details - private int selectedListingIndex = -1; - - // Countdown update timer - private long lastCountdownUpdate = 0; - - private enum ActionMode { - BID, - CANCEL, - CLAIM - } - - public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { - super(npc, container); - this.title = ""; - this.xSize = 319; - this.ySize = 238; - this.closeOnEsc = true; - this.drawDefaultBackground = true; - - // Request initial data - AuctionActionPacket.QueryListings(currentFilter); - } - - @Override - public void initGui() { - super.initGui(); - - int leftX = guiLeft + LEFT_PANEL_X; - int rightX = guiLeft + RIGHT_PANEL_X; - int actionY = guiTop + 44; - boolean isCreateTab = currentTab == 4; - boolean isBrowseTab = currentTab == 0; - - updateListingSlotPosition(isCreateTab); - - // Tab buttons at top - GuiMenuTopButton browseButton = new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse")); - addTopButton(browseButton); - GuiMenuTopButton myListingsButton = new GuiMenuTopButton(1, browseButton, StatCollector.translateToLocal("auction.myListings")); - addTopButton(myListingsButton); - GuiMenuTopButton myBidsButton = new GuiMenuTopButton(2, myListingsButton, StatCollector.translateToLocal("auction.myBids")); - addTopButton(myBidsButton); - GuiMenuTopButton claimsButton = new GuiMenuTopButton(3, myBidsButton, StatCollector.translateToLocal("auction.claims")); - addTopButton(claimsButton); - GuiMenuTopButton sellButton = new GuiMenuTopButton(4, claimsButton, StatCollector.translateToLocal("auction.sell")); - addTopButton(sellButton); - getTopButton(currentTab).setEnabled(false); - - if (isCreateTab) { - addCreateControls(leftX, rightX); - } else { - if (isBrowseTab) { - // Search field - searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); - searchField.setMaxStringLength(50); - addTextField(searchField); - - // Search button - addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.search"))); - - // Sort button - addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); - } - - // Listing scroll area - listingScroll = new GuiCustomScroll(this, 0); - listingScroll.guiLeft = leftX; - listingScroll.guiTop = guiTop + 44; - listingScroll.setSize(LISTING_SCROLL_WIDTH, LISTING_SCROLL_HEIGHT); - addScroll(listingScroll); - updateListingDisplay(); - - if (isBrowseTab) { - // Pagination buttons - addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.prevPage"))); - addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.nextPage"))); - } - - // Action buttons (right side) - addActionButtons(rightX, actionY); - - // Balance display - addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), - rightX, guiTop + 170, 0x00AA00)); - } - - updateButtonStates(); - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update countdowns every second - long now = System.currentTimeMillis(); - if (now - lastCountdownUpdate >= 1000) { - lastCountdownUpdate = now; - updateListingDisplay(); - } - - if (currentTab == 4 && inventorySlots instanceof ContainerAuction) { - ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); - GuiNpcLabel itemLabel = getLabel(102); - GuiNpcButton createButton = getButton(71); - if (item != null) { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); - itemLabel.color = 0x008800; - } - if (createButton != null) { - createButton.setEnabled(true); - } - } else { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); - itemLabel.color = 0x888888; - } - if (createButton != null) { - createButton.setEnabled(false); - } - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - super.actionPerformed(button); - int id = button.id; - - // Tab buttons - if (id >= 0 && id <= 4) { - switchTab(id); - } - // Search button - else if (id == 11) { - if (currentTab == 0) { - performSearch(); - } - } - // Sort button - else if (id == 30) { - if (currentTab == 0) { - cycleSortOrder(); - } - } - // Pagination - else if (id == 40) { - if (currentTab == 0) { - previousPage(); - } - } else if (id == 42) { - if (currentTab == 0) { - nextPage(); - } - } - // Actions - else if (id == 50) { - viewDetails(); - } else if (id == 51) { - ActionMode mode = getActionMode(); - if (mode == ActionMode.BID) { - placeBid(); - } else if (mode == ActionMode.CANCEL) { - cancelListing(); - } else if (mode == ActionMode.CLAIM) { - claimListing(); - } - } else if (id == 52) { - if (getActionMode() == ActionMode.BID) { - buyout(); - } - } else if (id == 53) { - refresh(); - } else if (id == 70) { - selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; - initGui(); - } else if (id == 71) { - createListing(); - } else if (id == 72) { - switchTab(0); - } - } - - private void switchTab(int tab) { - currentTab = tab; - currentPage = 0; - selectedListingIndex = -1; - - switch (tab) { - case 0: // Browse - AuctionActionPacket.QueryListings(currentFilter); - break; - case 1: // My Listings - AuctionActionPacket.QueryMyListings(); - break; - case 2: // My Bids - AuctionActionPacket.QueryMyBids(); - break; - case 3: // Claims - AuctionActionPacket.QueryClaimable(); - break; - case 4: - break; - } - - initGui(); - } - - private void performSearch() { - currentFilter.searchText = searchField.getText(); - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - } - - private void cycleSortOrder() { - currentFilter.sortOrder = currentFilter.sortOrder.next(); - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - initGui(); - } - - private void previousPage() { - if (currentPage > 0) { - currentPage--; - currentFilter.page = currentPage; - AuctionActionPacket.QueryListings(currentFilter); - } - } - - private void nextPage() { - if (currentPage < totalPages - 1) { - currentPage++; - currentFilter.page = currentPage; - AuctionActionPacket.QueryListings(currentFilter); - } - } - - private void viewDetails() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.item == null) { - return; - } - setSubGui(new SubGuiAuctionDetails(this, listing)); - } - } - - private void placeBid() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.item == null) { - return; - } - setSubGui(new SubGuiAuctionBid(this, listing, playerBalance)); - } - } - - private void buyout() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice) { - AuctionActionPacket.Buyout(listing.id); - refresh(); - } - } - } - - private void refresh() { - switch (currentTab) { - case 0: - AuctionActionPacket.QueryListings(currentFilter); - break; - case 1: - AuctionActionPacket.QueryMyListings(); - break; - case 2: - AuctionActionPacket.QueryMyBids(); - break; - case 3: - AuctionActionPacket.QueryClaimable(); - break; - } - } - - private void updateListingDisplay() { - if (listingScroll == null) return; - - List displayList = new ArrayList<>(); - for (AuctionListing listing : listings) { - String line = formatListingLine(listing); - displayList.add(line); - } - listingScroll.setList(displayList, true, false); - - if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { - listingScroll.selected = selectedListingIndex; - } else { - selectedListingIndex = -1; - listingScroll.selected = -1; - } - } - - private String formatListingLine(AuctionListing listing) { - String itemName = listing.item == null - ? "Unknown" - : listing.item.getDisplayName(); - if (itemName.length() > 15) { - itemName = itemName.substring(0, 12) + "..."; - } - - long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - String priceStr = formatCurrency(price); - - String timeStr = formatTimeRemaining(listing.getTimeRemaining()); - - return String.format("%s - %s (%s)", itemName, priceStr, timeStr); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return days + "d " + (hours % 24) + "h"; - } else if (hours > 0) { - return hours + "h " + (minutes % 60) + "m"; - } else if (minutes > 0) { - return minutes + "m " + (seconds % 60) + "s"; - } else { - return seconds + "s"; - } - } - - private String formatCurrency(long amount) { - if (amount >= 1000000) { - return String.format("%.1fM", amount / 1000000.0); - } else if (amount >= 1000) { - return String.format("%.1fK", amount / 1000.0); - } - return String.valueOf(amount); - } - - private String getPageLabel() { - if (totalPages == 0) { - return StatCollector.translateToLocal("auction.noResults"); - } - return String.format(StatCollector.translateToLocal("auction.page"), currentPage + 1, totalPages); - } - - private void updateButtonStates() { - ActionMode mode = getActionMode(); - AuctionListing listing = getSelectedListing(); - boolean hasSelection = listing != null; - - // Pagination buttons - if (getButton(40) != null) getButton(40).setEnabled(currentPage > 0); - if (getButton(42) != null) getButton(42).setEnabled(currentPage < totalPages - 1); - - // Action buttons based on selection - if (getButton(50) != null) getButton(50).setEnabled(hasSelection); - if (getButton(51) != null) { - if (!hasSelection) { - getButton(51).setEnabled(false); - } else if (mode == ActionMode.BID) { - getButton(51).setEnabled(canPlaceBid(listing)); - } else if (mode == ActionMode.CANCEL) { - getButton(51).setEnabled(canCancelListing(listing)); - } else { - getButton(51).setEnabled(getClaimAction(listing) != ClaimAction.NONE); - } - } - - if (getButton(52) != null) { - if (!hasSelection || mode != ActionMode.BID) { - getButton(52).setEnabled(false); - } else { - getButton(52).setEnabled(canBuyout(listing)); - } - } - } - - private void updateListingSlotPosition(boolean showInventory) { - if (!(inventorySlots instanceof ContainerAuction)) { - return; - } - - ContainerAuction container = (ContainerAuction) inventorySlots; - if (showInventory) { - container.listingSlot.xDisplayPosition = RIGHT_PANEL_X + 41; - container.listingSlot.yDisplayPosition = 64; - - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 9; col++) { - int slotIndex = 1 + col + row * 9; - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = INVENTORY_START_X + col * 18; - slot.yDisplayPosition = INVENTORY_START_Y + row * 18; - } - } - - for (int col = 0; col < 9; col++) { - int slotIndex = 28 + col; - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = INVENTORY_START_X + col * 18; - slot.yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; - } - } else { - container.listingSlot.xDisplayPosition = HIDDEN_SLOT_POSITION; - container.listingSlot.yDisplayPosition = HIDDEN_SLOT_POSITION; - - for (int slotIndex = 1; slotIndex < container.inventorySlots.size(); slotIndex++) { - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = HIDDEN_SLOT_POSITION; - slot.yDisplayPosition = HIDDEN_SLOT_POSITION; - } - } - } - - private void addCreateControls(int leftX, int rightX) { - addLabel(new GuiNpcLabel(100, StatCollector.translateToLocal("auction.createListing"), leftX, guiTop + 8, 0x404040)); - addLabel(new GuiNpcLabel(101, StatCollector.translateToLocal("auction.placeItemBelow"), rightX, guiTop + 32, 0x404040)); - - ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); - if (item != null) { - addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), - rightX, guiTop + 86, 0x008800)); - } else { - addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.noItemSelected"), rightX, guiTop + 86, 0x888888)); - } - - addLabel(new GuiNpcLabel(110, StatCollector.translateToLocal("auction.startingPrice"), leftX, guiTop + 36, 0x404040)); - startingPriceField = new GuiNpcTextField(111, this, leftX + 120, guiTop + 34, 90, 14, "100"); - startingPriceField.setIntegersOnly(); - startingPriceField.setMinMaxDefault(1, 999999999, 100); - addTextField(startingPriceField); - - addLabel(new GuiNpcLabel(120, StatCollector.translateToLocal("auction.buyoutPrice"), leftX, guiTop + 58, 0x404040)); - buyoutPriceField = new GuiNpcTextField(121, this, leftX + 120, guiTop + 56, 90, 14, "0"); - buyoutPriceField.setIntegersOnly(); - buyoutPriceField.setMinMaxDefault(0, 999999999, 0); - addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(122, StatCollector.translateToLocal("auction.noBuyoutHint"), leftX + 214, guiTop + 58, 0x888888)); - - addLabel(new GuiNpcLabel(130, StatCollector.translateToLocal("auction.duration"), leftX, guiTop + 80, 0x404040)); - addButton(new GuiNpcButton(70, leftX + 120, guiTop + 76, 110, 20, getDurationDisplay())); - - long fee = getListingFee(); - addLabel(new GuiNpcLabel(140, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - leftX, guiTop + 104, fee > 0 ? 0xAA0000 : 0x008800)); - - addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.create"))); - addButton(new GuiNpcButton(72, leftX + 100, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.cancel"))); - - if (getButton(71) != null) { - getButton(71).setEnabled(item != null); - } - } - - private String getDurationDisplay() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getDisplayName(); - } - - private long getListingFee() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getListingFee(); - } - - private void createListing() { - ContainerAuction container = (ContainerAuction) inventorySlots; - ItemStack item = container.getListingItem(); - if (item == null) { - return; - } - - long startingPrice = parsePrice(startingPriceField, 1); - long buyoutPrice = parsePrice(buyoutPriceField, 0); - - if (startingPrice < 1) { - startingPrice = 1; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - return; - } - - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); - container.clearListingSlot(); - switchTab(0); - } - - private void cancelListing() { - AuctionListing listing = getSelectedListing(); - if (!canCancelListing(listing)) { - return; - } - - AuctionActionPacket.CancelListing(listing.id); - refresh(); - } - - private void claimListing() { - AuctionListing listing = getSelectedListing(); - if (listing == null) { - return; - } - - ClaimAction action = getClaimAction(listing); - if (action == ClaimAction.ITEM) { - AuctionActionPacket.ClaimItem(listing.id); - } else if (action == ClaimAction.PROCEEDS) { - AuctionActionPacket.ClaimProceeds(listing.id); - } else { - return; - } - - refresh(); - } - - private ActionMode getActionMode() { - if (currentTab == 1) { - return ActionMode.CANCEL; - } - if (currentTab == 3) { - return ActionMode.CLAIM; - } - return ActionMode.BID; - } - - private String getPrimaryActionLabel(ActionMode mode) { - if (mode == ActionMode.CANCEL) { - return StatCollector.translateToLocal("auction.cancel"); - } - if (mode == ActionMode.CLAIM) { - return StatCollector.translateToLocal("auction.claim"); - } - return StatCollector.translateToLocal("auction.placeBid"); - } - - private void addActionButtons(int rightX, int actionY) { - ActionMode mode = getActionMode(); - addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.viewDetails"))); - addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, - getPrimaryActionLabel(mode))); - if (mode == ActionMode.BID) { - addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.buyout"))); - addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.refresh"))); - } else { - addButton(new GuiNpcButton(53, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.refresh"))); - } - } - - private AuctionListing getSelectedListing() { - if (selectedListingIndex < 0 || selectedListingIndex >= listings.size()) { - return null; - } - AuctionListing listing = listings.get(selectedListingIndex); - if (listing == null || listing.item == null) { - return null; - } - return listing; - } - - private boolean canPlaceBid(AuctionListing listing) { - if (listing == null || !listing.isActive()) { - return false; - } - if (isPlayerSeller(listing)) { - return false; - } - return playerBalance >= listing.getMinimumBid(); - } - - private boolean canBuyout(AuctionListing listing) { - if (listing == null || !listing.isActive()) { - return false; - } - if (listing.buyoutPrice <= 0) { - return false; - } - if (isPlayerSeller(listing)) { - return false; - } - return playerBalance >= listing.buyoutPrice; - } - - private boolean canCancelListing(AuctionListing listing) { - if (listing == null || listing.item == null) { - return false; - } - return listing.status == EnumAuctionStatus.ACTIVE && !listing.hasBids(); - } - - private boolean isPlayerSeller(AuctionListing listing) { - if (mc == null || mc.thePlayer == null || listing == null || listing.sellerUUID == null) { - return false; - } - UUID playerId = mc.thePlayer.getUniqueID(); - return listing.sellerUUID.equals(playerId); - } - - private enum ClaimAction { - ITEM, - PROCEEDS, - NONE - } - - private ClaimAction getClaimAction(AuctionListing listing) { - if (listing == null || mc == null || mc.thePlayer == null) { - return ClaimAction.NONE; - } - - UUID playerId = mc.thePlayer.getUniqueID(); - if (listing.sellerUUID != null && listing.sellerUUID.equals(playerId) && !listing.sellerClaimed) { - if (listing.status == EnumAuctionStatus.SOLD) { - return ClaimAction.PROCEEDS; - } - if (listing.status == EnumAuctionStatus.CANCELLED || listing.status == EnumAuctionStatus.EXPIRED) { - return ClaimAction.ITEM; - } - } - - if (listing.highBidderUUID != null && listing.highBidderUUID.equals(playerId) - && listing.status == EnumAuctionStatus.SOLD && !listing.buyerClaimed) { - return ClaimAction.ITEM; - } - - return ClaimAction.NONE; - } - - private long parsePrice(GuiNpcTextField field, long defaultValue) { - if (field == null) { - return defaultValue; - } - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - @Override - protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { - drawDefaultBackground(); - GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - - // Draw background - mc.renderEngine.bindTexture(BG_TEXTURE); - drawTexturedModalRect(guiLeft, guiTop, 0, 0, 252, ySize); - drawTexturedModalRect(guiLeft + 252, guiTop, 188, 0, xSize - 252, ySize); - - // Draw selected item preview - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - drawItemPreview(listing, guiLeft + RIGHT_PANEL_X + 41, guiTop + 192); - } - - super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); - } - - private void drawItemPreview(AuctionListing listing, int x, int y) { - if (listing == null || listing.item == null) return; - - RenderHelper.enableGUIStandardItemLighting(); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_COLOR_MATERIAL); - GL11.glEnable(GL11.GL_LIGHTING); - - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); - - RenderHelper.disableStandardItemLighting(); - } - - @Override - protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { - // Title - String titleText = getTabTitle(); - fontRendererObj.drawString(titleText, (xSize - fontRendererObj.getStringWidth(titleText)) / 2, -8, - CustomNpcResourceListener.DefaultTextColor); - - if (currentTab != 4) { - String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); - fontRendererObj.drawString(listingsLabel, LEFT_PANEL_X, 36, 0x666666); - } - } - - private String getTabTitle() { - switch (currentTab) { - case 0: return StatCollector.translateToLocal("auction.browseTitle"); - case 1: return StatCollector.translateToLocal("auction.myListingsTitle"); - case 2: return StatCollector.translateToLocal("auction.myBidsTitle"); - case 3: return StatCollector.translateToLocal("auction.claimsTitle"); - case 4: return StatCollector.translateToLocal("auction.sellTitle"); - default: return StatCollector.translateToLocal("auction.title"); - } - } - - // ICustomScrollListener - @Override - public void customScrollClicked(int mouseX, int mouseY, int mouseButton, GuiCustomScroll scroll) { - int index = scroll.selected; - selectedListingIndex = index; - updateButtonStates(); - } - - @Override - public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - viewDetails(); - } - } - - // IGuiData - @Override - public void setGuiData(NBTTagCompound compound) { - // This can be used for initial setup data - } - - // IAuctionData - @Override - public void onAuctionDataReceived(int dataType, NBTTagCompound data, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - this.playerBalance = playerBalance; - - // Parse listings from NBT - listings.clear(); - NBTTagList listingList = data.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - NBTTagCompound listingNBT = listingList.getCompoundTagAt(i); - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingNBT); - listings.add(listing); - } - - selectedListingIndex = -1; - updateListingDisplay(); - updateButtonStates(); - - // Update labels - if (getLabel(41) != null) { - getLabel(41).label = getPageLabel(); - } - if (getLabel(60) != null) { - getLabel(60).label = String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)); - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index d23b15398..f11bbb2e6 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -1,136 +1,335 @@ package noppes.npcs.client.gui.player; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.player.GetTraderData; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.util.GuiContainerNPCInterface; +import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.containers.ContainerNPCTrader; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; -public class GuiNPCTrader extends GuiContainerNPCInterface { - private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/trader.png"); +public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { + private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/tradersetup.png"); private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; private ContainerNPCTrader container; + // Layout constants matching ContainerNPCTrader + private static final int COLUMN_WIDTH = ContainerNPCTrader.COLUMN_WIDTH; + private static final int COLUMN_START_X = ContainerNPCTrader.COLUMN_START_X; + private static final int ROW_HEIGHT = ContainerNPCTrader.ROW_HEIGHT; + private static final int ROW_START_Y = ContainerNPCTrader.ROW_START_Y; + private static final int CURRENCY1_OFFSET = ContainerNPCTrader.CURRENCY1_OFFSET; + private static final int CURRENCY2_OFFSET = ContainerNPCTrader.CURRENCY2_OFFSET; + private static final int OUTPUT_OFFSET = ContainerNPCTrader.OUTPUT_OFFSET; + + // Data received from server via IGuiData + private long playerBalance = 0; + private int[] availableStock = new int[18]; + private long[] currencyCost = new long[18]; + private boolean stockEnabled = false; + + // Client-side countdown: target time when stock resets (System.currentTimeMillis()) + private long resetTargetTime = -1; + + // Current hover status for bottom display + private String statusMessage = ""; + private int statusColor = 0xFFFFFF; + public GuiNPCTrader(EntityNPCInterface npc, ContainerNPCTrader container) { super(npc, container); this.container = container; role = (RoleTrader) npc.roleInterface; closeOnEsc = true; - ySize = 224; - xSize = 223; - this.title = "role.trader"; + ySize = 216; + xSize = 256; + this.title = ""; + + // Initialize stock to unlimited + for (int i = 0; i < 18; i++) { + availableStock[i] = Integer.MAX_VALUE; + currencyCost[i] = 0; + } + + // Request trader data from server (balance, stock, currency costs) + PacketClient.sendClient(new GetTraderData()); } @Override - protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { + public void setGuiData(NBTTagCompound compound) { + playerBalance = compound.getLong("Balance"); + stockEnabled = compound.getBoolean("StockEnabled"); + + // Calculate target time for client-side countdown + long remainingMillis = compound.getLong("ResetTimeMillis"); + if (remainingMillis > 0) { + resetTargetTime = System.currentTimeMillis() + remainingMillis; + } else { + resetTargetTime = -1; + } + + int[] stock = compound.getIntArray("Stock"); + if (stock != null && stock.length == 18) { + availableStock = stock; + } + + for (int i = 0; i < 18; i++) { + currencyCost[i] = compound.getLong("Cost" + i); + } + } + + @Override + protected void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) { + // Reset status each frame + statusMessage = ""; + statusColor = 0xFFFFFF; + this.drawWorldBackground(0); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(resource); drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + + // Draw "Trader" label in TOP LEFT (above the GUI) + String traderLabel = StatCollector.translateToLocal("role.trader"); + fontRendererObj.drawString(traderLabel, guiLeft + 4, guiTop - 8, 0xFFFFFF); + + // Draw reset timer in TOP RIGHT (above the GUI) + if (stockEnabled && resetTargetTime > 0) { + String timerText = getResetCountdownText(); + int timerWidth = fontRendererObj.getStringWidth(timerText); + int timerColor = timerText.equals(StatCollector.translateToLocal("trader.reopentrader")) ? 0xFFAA00 : 0xFFFFFF; + fontRendererObj.drawString(timerText, guiLeft + xSize - timerWidth - 4, guiTop - 8, timerColor); + } + RenderHelper.enableGUIStandardItemLighting(); GL11.glEnable(GL12.GL_RESCALE_NORMAL); GL11.glEnable(GL11.GL_COLOR_MATERIAL); GL11.glEnable(GL11.GL_LIGHTING); - mc.renderEngine.bindTexture(slot); - for (int slot = 0; slot < 18; slot++) { - int x = guiLeft + slot % 3 * 72 + 10; - int y = guiTop + slot / 3 * 21 + 6; + // Draw trade slots (3 columns x 6 rows, 80px column width) + for (int slotIdx = 0; slotIdx < 18; slotIdx++) { + int col = slotIdx % 3; + int row = slotIdx / 3; + int x = guiLeft + COLUMN_START_X + col * COLUMN_WIDTH; + int y = guiTop + ROW_START_Y + row * ROW_HEIGHT; - ItemStack item = role.inventoryCurrency.items.get(slot); - ItemStack item2 = role.inventoryCurrency.items.get(slot + 18); + // Get item currencies + ItemStack item = role.inventoryCurrency.items.get(slotIdx); + ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18); if (item == null) { item = item2; item2 = null; } - if (NoppesUtilPlayer.compareItems(item, item2, false, false)) { + if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { item = item.copy(); item.stackSize += item2.stackSize; item2 = null; } - ItemStack sold = role.inventorySold.items.get(slot); + ItemStack sold = role.inventorySold.items.get(slotIdx); + + // Draw slot backgrounds (18x18 each) GL11.glColor4f(1, 1, 1, 1); mc.renderEngine.bindTexture(this.slot); - drawTexturedModalRect(x + 42, y, 0, 0, 18, 18); - if (item != null && sold != null) { + + // Currency slot 1 + drawTexturedModalRect(x + CURRENCY1_OFFSET, y, 0, 0, 18, 18); + // Currency slot 2 + drawTexturedModalRect(x + CURRENCY2_OFFSET, y, 0, 0, 18, 18); + // Output slot + drawTexturedModalRect(x + OUTPUT_OFFSET, y, 0, 0, 18, 18); + + // "=" sign between currency and output + fontRendererObj.drawString("=", x + 40, y + 5, CustomNpcResourceListener.DefaultTextColor); + + // Draw currency items (centered: +1 offset for 16x16 item in 18x18 slot) + if (sold != null) { RenderHelper.enableGUIStandardItemLighting(); + // Draw secondary currency (slot 1) if (item2 != null) { - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1); + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1); } - GL11.glColor4f(1, 1, 1, 1); - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1); + + // Draw primary currency (slot 2) + if (item != null) { + GL11.glColor4f(1, 1, 1, 1); + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1); + } + RenderHelper.disableStandardItemLighting(); - fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor); + // Draw stock count to the RIGHT of the trade (after output slot) + if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) { + String stockText = String.valueOf(availableStock[slotIdx]); + int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000; + fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor); + } + // Draw dark overlay for out of stock items + if (stockEnabled && availableStock[slotIdx] <= 0) { + drawGradientRect(x + OUTPUT_OFFSET + 1, y + 1, x + OUTPUT_OFFSET + 17, y + 17, 0xC0101010, 0xC0101010); + } } } + GL11.glEnable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_DEPTH_TEST); RenderHelper.enableStandardItemLighting(); - super.drawGuiContainerBackgroundLayer(f, i, j); + + super.drawGuiContainerBackgroundLayer(f, mouseX, mouseY); + + // Draw player balance (BOTTOM RIGHT, below GUI) + int infoY = guiTop + ySize + 2; + String balanceText = StatCollector.translateToLocal("trader.balance") + ": $" + formatCurrency(playerBalance); + int balanceWidth = fontRendererObj.getStringWidth(balanceText); + fontRendererObj.drawString(balanceText, guiLeft + xSize - balanceWidth - 4, infoY, 0x36e82a); } @Override - protected void drawGuiContainerForegroundLayer(int par1, int par2) { - for (int slot = 0; slot < 18; slot++) { - int x = slot % 3 * 72 + 10; - int y = slot / 3 * 21 + 6; + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + for (int slotIdx = 0; slotIdx < 18; slotIdx++) { + int col = slotIdx % 3; + int row = slotIdx / 3; + int x = COLUMN_START_X + col * COLUMN_WIDTH; + int y = ROW_START_Y + row * ROW_HEIGHT; - ItemStack item = role.inventoryCurrency.items.get(slot); - ItemStack item2 = role.inventoryCurrency.items.get(slot + 18); + ItemStack item = role.inventoryCurrency.items.get(slotIdx); + ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18); if (item == null) { item = item2; item2 = null; } - if (NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { + if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { item = item.copy(); item.stackSize += item2.stackSize; item2 = null; } - ItemStack sold = role.inventorySold.items.get(slot); - if (item == null || sold == null) + + ItemStack sold = role.inventorySold.items.get(slotIdx); + if (sold == null) { continue; - - if (this.func_146978_c(x + 43, y + 1, 16, 16, par1, par2)) { - if (!container.canBuy(slot, player)) { - GL11.glTranslatef(0, 0, 300); - if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT)) - this.drawGradientRect(x + 17, y, x + 35, y + 18, 0x70771010, 0x70771010); - if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT)) - this.drawGradientRect(x - 1, y, x + 17, y + 18, 0x70771010, 0x70771010); - - String title = StatCollector.translateToLocal("trader.insufficient"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xDD0000); - GL11.glTranslatef(0, 0, -300); - } else if (!container.isSlotEnabled(slot, player)) { - String title = StatCollector.translateToLocal("trader.slotdisabled"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xFF4000); - } else { - String title = StatCollector.translateToLocal("trader.sufficient"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0x00DD00); - } } - if (this.func_146978_c(x, y, 16, 16, par1, par2) && item2 != null) { - this.renderToolTip(item2, par1 - guiLeft, par2 - guiTop); + // Check if hovering over output slot (18x18 area) + if (this.func_146978_c(x + OUTPUT_OFFSET, y, 18, 18, mouseX, mouseY)) { + updateSlotStatus(slotIdx, item, item2, x, y); } - if (this.func_146978_c(x + 18, y, 16, 16, par1, par2)) { - this.renderToolTip(item, par1 - guiLeft, par2 - guiTop); + + // Tooltips for currency items + if (this.func_146978_c(x + CURRENCY1_OFFSET, y, 18, 18, mouseX, mouseY) && item2 != null) { + this.renderToolTip(item2, mouseX - guiLeft, mouseY - guiTop); + } + if (this.func_146978_c(x + CURRENCY2_OFFSET, y, 18, 18, mouseX, mouseY) && item != null) { + this.renderToolTip(item, mouseX - guiLeft, mouseY - guiTop); } } + + // Draw status message in BOTTOM LEFT (in foreground coordinates) + if (!statusMessage.isEmpty()) { + fontRendererObj.drawString(statusMessage, 4, ySize + 4, statusColor); + } + } + + /** + * Update status message when hovering over a trade slot + */ + private void updateSlotStatus(int slotIdx, ItemStack item, ItemStack item2, int x, int y) { + long cost = currencyCost[slotIdx]; + String costSuffix = cost > 0 ? " ($" + formatCurrency(cost) + ")" : ""; + + // Check conditions in order of priority + if (stockEnabled && availableStock[slotIdx] <= 0) { + // Out of stock + statusMessage = StatCollector.translateToLocal("trader.outofstock") + costSuffix; + statusColor = 0xDD0000; + } else if (cost > 0 && playerBalance < cost) { + // Can't afford currency cost + statusMessage = StatCollector.translateToLocal("trader.insufficient.currency") + costSuffix; + statusColor = 0xDD0000; + } else if (!container.canBuy(slotIdx, player)) { + // Missing item currency - highlight which ones + GL11.glTranslatef(0, 0, 300); + if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT)) { + this.drawGradientRect(x + CURRENCY2_OFFSET, y, x + CURRENCY2_OFFSET + 18, y + 18, 0x70771010, 0x70771010); + } + if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT)) { + this.drawGradientRect(x + CURRENCY1_OFFSET, y, x + CURRENCY1_OFFSET + 18, y + 18, 0x70771010, 0x70771010); + } + GL11.glTranslatef(0, 0, -300); + statusMessage = StatCollector.translateToLocal("trader.unavailable") + costSuffix; + statusColor = 0xDD0000; + } else if (!container.isSlotEnabled(slotIdx, player)) { + // Slot disabled + statusMessage = StatCollector.translateToLocal("trader.slotdisabled") + costSuffix; + statusColor = 0xFF4000; + } else { + // Can buy! + statusMessage = StatCollector.translateToLocal("trader.available") + costSuffix; + statusColor = 0x00DD00; + } + } + + /** + * Get the countdown text for stock reset timer. + * Calculated client-side each frame for smooth countdown. + */ + private String getResetCountdownText() { + if (resetTargetTime <= 0) { + return ""; + } + + long remaining = resetTargetTime - System.currentTimeMillis(); + if (remaining <= 0) { + return StatCollector.translateToLocal("trader.reopentrader"); + } + + // Format as HH:MM:SS or D days HH:MM:SS + long seconds = (remaining / 1000) % 60; + long minutes = (remaining / 60000) % 60; + long hours = (remaining / 3600000) % 24; + long days = remaining / 86400000; + + StringBuilder sb = new StringBuilder(); + sb.append(StatCollector.translateToLocal("trader.restock")).append(": "); + + if (days > 0) { + sb.append(days).append("d "); + } + sb.append(String.format("%02d:%02d:%02d", hours, minutes, seconds)); + + return sb.toString(); + } + + /** + * Format currency for display (e.g., 1000 -> "1,000") + */ + private String formatCurrency(long amount) { + if (amount < 1000) { + return String.valueOf(amount); + } + StringBuilder sb = new StringBuilder(); + String str = String.valueOf(amount); + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) { + sb.insert(0, ','); + } + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java deleted file mode 100644 index 99f1e00f1..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ /dev/null @@ -1,221 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; - -/** - * Sub GUI for placing a bid on an auction. - */ -public class SubGuiAuctionBid extends SubGuiInterface { - private AuctionListing listing; - private long playerBalance; - private GuiNpcTextField bidField; - private long lastUpdate = 0; - - public SubGuiAuctionBid(GuiAuction parent, AuctionListing listing, long playerBalance) { - this.listing = listing; - this.playerBalance = playerBalance; - setBackground("menubg.png"); - xSize = 240; - ySize = 165; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - int y = guiTop + 10; - int labelColor = 0x404040; - - if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); - addButton(new GuiNpcButton(31, guiLeft + 80, guiTop + 125, 80, 20, StatCollector.translateToLocal("gui.cancel"))); - return; - } - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 90, y, labelColor)); - y += 18; - - // Item name - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item") + " " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); - y += 14; - - // Current price - long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.currentPrice"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 120, y, 0x008800)); - y += 14; - - // Minimum bid - long minBid = listing.getMinimumBid(); - addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 120, y, 0x0066AA)); - y += 14; - - // Your balance - addLabel(new GuiNpcLabel(6, StatCollector.translateToLocal("auction.yourBalance"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 120, y, - playerBalance >= minBid ? 0x008800 : 0xAA0000)); - y += 14; - - // Time remaining - addLabel(new GuiNpcLabel(8, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, - getTimeColor(listing.getTimeRemaining()))); - y += 18; - - // Bid input - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); - bidField = new GuiNpcTextField(11, this, guiLeft + 100, y, 110, 16, String.valueOf(minBid)); - bidField.setIntegersOnly(); - int minBidInt = (int) Math.min(Integer.MAX_VALUE, Math.max(1L, minBid)); - bidField.setMinMaxDefault(minBidInt, Integer.MAX_VALUE, minBidInt); - addTextField(bidField); - y += 24; - - // Quick bid buttons - addButton(new GuiNpcButton(20, guiLeft + 15, y, 60, 16, StatCollector.translateToLocal("auction.minBid"))); - addButton(new GuiNpcButton(21, guiLeft + 79, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus10"))); - addButton(new GuiNpcButton(22, guiLeft + 143, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus25"))); - y += 22; - - // Place bid button - GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 35, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); - bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); - addButton(bidButton); - - // Cancel button - addButton(new GuiNpcButton(31, guiLeft + 125, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); - - // Error message area - if (playerBalance < minBid) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); - } else if (!listing.isActive()) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); - } - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - - if (hours > 0) { - return String.format("%dh %dm", hours, minutes % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - private int getTimeColor(long millis) { - if (millis <= 0) return 0xAA0000; - if (millis < 5 * 60 * 1000) return 0xFF0000; - if (millis < 30 * 60 * 1000) return 0xFF6600; - return 0x008800; - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update time remaining every second - long now = System.currentTimeMillis(); - if (now - lastUpdate >= 1000) { - lastUpdate = now; - - GuiNpcLabel timeLabel = getLabel(9); - if (timeLabel != null) { - long remaining = listing.getTimeRemaining(); - timeLabel.label = formatTimeRemaining(remaining); - timeLabel.color = getTimeColor(remaining); - } - - // Disable bid button if auction ended - GuiNpcButton bidButton = getButton(30); - if (bidButton != null && !listing.isActive()) { - bidButton.setEnabled(false); - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - int id = button.id; - - long minBid = listing.getMinimumBid(); - - if (id == 20) { - // Min bid - bidField.setText(String.valueOf(minBid)); - } else if (id == 21) { - // +10% - long newBid = (long) (minBid * 1.10); - if (newBid <= playerBalance) { - bidField.setText(String.valueOf(newBid)); - } - } else if (id == 22) { - // +25% - long newBid = (long) (minBid * 1.25); - if (newBid <= playerBalance) { - bidField.setText(String.valueOf(newBid)); - } - } else if (id == 30) { - // Place bid - placeBid(); - } else if (id == 31) { - // Cancel - close(); - } - } - - private void placeBid() { - long bidAmount = parseBid(bidField); - long minBid = listing.getMinimumBid(); - - if (bidAmount < minBid) { - return; - } - - if (bidAmount > playerBalance) { - return; - } - - if (!listing.isActive()) { - return; - } - - // Send bid to server - AuctionActionPacket.PlaceBid(listing.id, bidAmount); - close(); - } - - private long parseBid(GuiNpcTextField field) { - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return 0L; - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java deleted file mode 100644 index 01c650bbd..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ /dev/null @@ -1,178 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.item.ItemStack; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.containers.ContainerAuction; - -/** - * Sub GUI for creating a new auction listing. - */ -public class SubGuiAuctionCreate extends SubGuiInterface { - private ContainerAuction container; - - private GuiNpcTextField startingPriceField; - private GuiNpcTextField buyoutPriceField; - private int selectedDuration = 1; // Default to MEDIUM - - public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { - this.container = container; - setBackground("menubg.png"); - xSize = 260; - ySize = 190; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 80, guiTop + 8, 0x404040)); - - // Item slot instructions - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.placeItemBelow"), guiLeft + 20, guiTop + 28, 0x404040)); - - // Item info (shows when item is in slot) - ItemStack item = container.getListingItem(); - if (item != null) { - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); - } else { - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 20, guiTop + 50, 0x888888)); - } - - // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 74, 0x404040)); - startingPriceField = new GuiNpcTextField(11, this, guiLeft + 120, guiTop + 72, 90, 14, "100"); - startingPriceField.setIntegersOnly(); - startingPriceField.setMinMaxDefault(1, 999999999, 100); - addTextField(startingPriceField); - - // Buyout price (optional) - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 96, 0x404040)); - buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 120, guiTop + 94, 90, 14, "0"); - buyoutPriceField.setIntegersOnly(); - buyoutPriceField.setMinMaxDefault(0, 999999999, 0); - addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 215, guiTop + 96, 0x888888)); - - // Duration selection - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 118, 0x404040)); - addButton(new GuiNpcButton(31, guiLeft + 120, guiTop + 114, 110, 20, getDurationDisplay())); - - // Fee display - long fee = getListingFee(); - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - guiLeft + 20, guiTop + 142, fee > 0 ? 0xAA0000 : 0x008800)); - - // Create button - addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.create"))); - getButton(50).setEnabled(item != null); - - // Cancel button - addButton(new GuiNpcButton(51, guiLeft + 140, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.cancel"))); - } - - private String getDurationDisplay() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getDisplayName(); - } - - private long getListingFee() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getListingFee(); - } - - @Override - public void actionPerformed(GuiButton button) { - int id = button.id; - - if (id == 31) { - // Cycle duration - selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; - initGui(); - } else if (id == 50) { - // Create listing - createListing(); - } else if (id == 51) { - // Cancel - close(); - } - } - - private void createListing() { - ItemStack item = container.getListingItem(); - if (item == null) { - return; - } - - long startingPrice = parsePrice(startingPriceField, 1); - long buyoutPrice = parsePrice(buyoutPriceField, 0); - - if (startingPrice < 1) { - startingPrice = 1; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - // Show error - buyout must be higher than starting - return; - } - - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - - // Send to server - AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); - - // Clear slot and close - container.clearListingSlot(); - close(); - } - - private long parsePrice(GuiNpcTextField field, long defaultValue) { - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update item display if it changed - ItemStack item = container.getListingItem(); - GuiNpcLabel itemLabel = getLabel(2); - GuiNpcButton createButton = getButton(50); - - if (item != null) { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); - itemLabel.color = 0x008800; - } - if (createButton != null) { - createButton.setEnabled(true); - } - } else { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); - itemLabel.color = 0x888888; - } - if (createButton != null) { - createButton.setEnabled(false); - } - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java deleted file mode 100644 index e4131fe94..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ /dev/null @@ -1,200 +0,0 @@ -package noppes.npcs.client.gui.player; - -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; - -/** - * Sub GUI for viewing auction listing details. - */ -public class SubGuiAuctionDetails extends SubGuiInterface { - private AuctionListing listing; - private long lastUpdate = 0; - - public SubGuiAuctionDetails(GuiAuction parent, AuctionListing listing) { - this.listing = listing; - setBackground("menubg.png"); - xSize = 280; - ySize = 210; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - int y = guiTop + 8; - int labelColor = 0x404040; - int valueColor = 0x000000; - - if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 80, y, 0xAA0000)); - addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); - return; - } - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 100, y, labelColor)); - y += 16; - - // Item name - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 100, y, valueColor)); - y += 14; - - // Quantity - addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("auction.quantity"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 100, y, valueColor)); - y += 14; - - // Seller - addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("auction.seller"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 100, y, valueColor)); - y += 14; - - // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 120, y, valueColor)); - y += 14; - - // Current bid - if (listing.currentBid > 0) { - addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 120, y, 0x008800)); - y += 14; - - // High bidder - addLabel(new GuiNpcLabel(14, StatCollector.translateToLocal("auction.highBidder"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 120, y, valueColor)); - y += 14; - - // Bid count - addLabel(new GuiNpcLabel(16, StatCollector.translateToLocal("auction.totalBids"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 120, y, valueColor)); - y += 14; - } else { - addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 120, y, 0x888888)); - y += 14; - } - - // Buyout price - if (listing.buyoutPrice > 0) { - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 120, y, 0xAA6600)); - y += 14; - } - - // Minimum bid - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 120, y, 0x0066AA)); - y += 14; - - // Time remaining (will be updated) - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, - getTimeColor(listing.getTimeRemaining()))); - y += 20; - - // Close button - addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return String.format("%dd %dh %dm", days, hours % 24, minutes % 60); - } else if (hours > 0) { - return String.format("%dh %dm %ds", hours, minutes % 60, seconds % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - private int getTimeColor(long millis) { - if (millis <= 0) return 0xAA0000; - if (millis < 5 * 60 * 1000) return 0xFF0000; // < 5 minutes - red - if (millis < 30 * 60 * 1000) return 0xFF6600; // < 30 minutes - orange - if (millis < 60 * 60 * 1000) return 0xAAAA00; // < 1 hour - yellow - return 0x008800; // green - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update time remaining every second - long now = System.currentTimeMillis(); - if (now - lastUpdate >= 1000) { - lastUpdate = now; - - GuiNpcLabel timeLabel = getLabel(31); - if (timeLabel != null) { - long remaining = listing.getTimeRemaining(); - timeLabel.label = formatTimeRemaining(remaining); - timeLabel.color = getTimeColor(remaining); - } - } - } - - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - super.drawScreen(mouseX, mouseY, partialTicks); - - // Draw item icon - if (listing.item != null) { - int itemX = guiLeft + 230; - int itemY = guiTop + 32; - - RenderHelper.enableGUIStandardItemLighting(); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_COLOR_MATERIAL); - GL11.glEnable(GL11.GL_LIGHTING); - GL11.glScalef(2.0f, 2.0f, 2.0f); - - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, - listing.item, itemX / 2, itemY / 2); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, - listing.item, itemX / 2, itemY / 2); - - GL11.glScalef(0.5f, 0.5f, 0.5f); - RenderHelper.disableStandardItemLighting(); - - // Tooltip on hover - if (mouseX >= itemX && mouseX < itemX + 32 && mouseY >= itemY && mouseY < itemY + 32) { - renderToolTip(listing.item, mouseX, mouseY); - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - if (button.id == 50) { - close(); - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java deleted file mode 100644 index d5fb8e962..000000000 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java +++ /dev/null @@ -1,144 +0,0 @@ -package noppes.npcs.client.gui.roles; - -import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.role.RoleSavePacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.client.gui.util.GuiNPCInterface2; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.roles.RoleAuctioneer; - -public class GuiNpcAuctioneer extends GuiNPCInterface2 implements ITextfieldListener { - - private final RoleAuctioneer role; - - public GuiNpcAuctioneer(EntityNPCInterface npc) { - super(npc); - this.role = npc.roleInterface instanceof RoleAuctioneer ? (RoleAuctioneer) npc.roleInterface : null; - } - - @Override - public void initGui() { - super.initGui(); - if (role == null) { - close(); - return; - } - - int y = guiTop + 10; - - // Left column - Tab visibility - addLabel(new GuiNpcLabel(0, "role.auctioneer.tabs", guiLeft + 10, y, 0x404040)); - y += 18; - - addLabel(new GuiNpcLabel(1, "role.auctioneer.showBrowse", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(1, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showActiveListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(2, "role.auctioneer.showMyListings", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(2, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(3, "role.auctioneer.showMyBids", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(3, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyBids ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(4, "role.auctioneer.showClaims", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(4, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showClaims ? 1 : 0)); - y += 30; - - // Actions - addLabel(new GuiNpcLabel(10, "role.auctioneer.actions", guiLeft + 10, y, 0x404040)); - y += 18; - - addLabel(new GuiNpcLabel(11, "role.auctioneer.allowCreate", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(11, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowCreatingListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(12, "role.auctioneer.allowBid", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(12, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBidding ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(13, "role.auctioneer.allowBuyout", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(13, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBuyout ? 1 : 0)); - - // Right column - Messages - int rightX = guiLeft + 210; - int rightY = guiTop + 10; - - addLabel(new GuiNpcLabel(20, "role.auctioneer.messages", rightX, rightY, 0x404040)); - rightY += 18; - - addLabel(new GuiNpcLabel(21, "role.auctioneer.welcome", rightX, rightY + 5, 0x404040)); - rightY += 18; - GuiNpcTextField welcomeField = new GuiNpcTextField(21, this, rightX, rightY, 180, 20, role.welcomeMessage); - welcomeField.setMaxStringLength(200); - addTextField(welcomeField); - rightY += 28; - - addLabel(new GuiNpcLabel(22, "role.auctioneer.noAuction", rightX, rightY + 5, 0x404040)); - rightY += 18; - GuiNpcTextField noAuctionField = new GuiNpcTextField(22, this, rightX, rightY, 180, 20, role.noAuctionMessage); - noAuctionField.setMaxStringLength(200); - addTextField(noAuctionField); - } - - @Override - public void buttonEvent(GuiButton guibutton) { - if (role == null) return; - - if (guibutton instanceof GuiNpcButton) { - GuiNpcButton button = (GuiNpcButton) guibutton; - boolean value = button.getValue() == 1; - - switch (button.id) { - case 1: - role.showActiveListings = value; - break; - case 2: - role.showMyListings = value; - break; - case 3: - role.showMyBids = value; - break; - case 4: - role.showClaims = value; - break; - case 11: - role.allowCreatingListings = value; - break; - case 12: - role.allowBidding = value; - break; - case 13: - role.allowBuyout = value; - break; - } - } - } - - @Override - public void unFocused(GuiNpcTextField textfield) { - if (role == null) return; - - switch (textfield.id) { - case 21: - role.welcomeMessage = textfield.getText(); - break; - case 22: - role.noAuctionMessage = textfield.getText(); - break; - } - } - - @Override - public void save() { - if (role != null) { - PacketClient.sendClient(new RoleSavePacket(role.writeToNBT(new NBTTagCompound()))); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 43c259da7..6c4745d42 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -8,7 +8,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; -import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.ITextfieldListener; @@ -22,6 +22,9 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; + // Currency cost textfield IDs start at 100 (100-117 for slots 0-17) + private static final int CURRENCY_FIELD_ID_START = 100; + public GuiNpcTraderSetup(EntityNPCInterface npc, ContainerNPCTraderSetup container) { super(npc, container); ySize = 220; @@ -34,17 +37,30 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex super.initGui(); buttonList.clear(); setBackground("tradersetup.png"); + + // Linked Market name addLabel(new GuiNpcLabel(0, "role.marketname", guiLeft + 214, guiTop + 150)); addTextField(new GuiNpcTextField(0, this, guiLeft + 214, guiTop + 160, 180, 20, role.marketName)); - addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 260, guiTop + 29)); - addButton(new GuiNpcButtonYesNo(1, guiLeft + 340, guiTop + 24, role.ignoreDamage)); + // Settings button (opens SubGuiNpcTraderSettings) + addButton(new GuiNpcButton(1, guiLeft + 214, guiTop + 184, 88, 20, "gui.settings")); + getButton(1).setHoverText("gui.settings.hover"); - addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 260, guiTop + 51)); - addButton(new GuiNpcButtonYesNo(2, guiLeft + 340, guiTop + 46, role.ignoreNBT)); + // Stock Options button (opens SubGuiNpcTraderStock) + addButton(new GuiNpcButton(2, guiLeft + 306, guiTop + 184, 88, 20, "stock.options")); + getButton(2).setHoverText("stock.options.hover"); - addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 260, guiTop + 73)); - addButton(new GuiNpcButtonYesNo(3, guiLeft + 340, guiTop + 68, role.recordHistory)); + // Currency cost textfields for each slot (FIRST - before item slots) + // Column width: 130px for better spacing + for (int i = 0; i < 18; i++) { + int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label + int y = guiTop + i / 3 * 22 + 14; + long cost = role.getCurrencyCost(i); + GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : ""); + field.setMaxStringLength(8); + field.setIntegersOnly(); + addTextField(field); + } } @Override @@ -57,13 +73,12 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void actionPerformed(GuiButton guibutton) { if (guibutton.id == 1) { - role.ignoreDamage = ((GuiNpcButtonYesNo) guibutton).getBoolean(); + // Open Settings SubGui + setSubGui(new SubGuiNpcTraderSettings(role)); } if (guibutton.id == 2) { - role.ignoreNBT = ((GuiNpcButtonYesNo) guibutton).getBoolean(); - } - if (guibutton.id == 3) { - role.recordHistory = ((GuiNpcButtonYesNo) guibutton).getBoolean(); + // Open Stock Options SubGui + setSubGui(new SubGuiNpcTraderStock(role)); } } @@ -71,19 +86,31 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { super.drawGuiContainerBackgroundLayer(f, i, j); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + // Column width: 130px for better spacing for (int slot = 0; slot < 18; slot++) { - int x = guiLeft + slot % 3 * 94 + 7; + int x = guiLeft + slot % 3 * 130 + 7; int y = guiTop + slot / 3 * 22 + 4; - mc.renderEngine.bindTexture(this.slot); - GL11.glColor4f(1, 1, 1, 1); - drawTexturedModalRect(x - 1, y, 0, 0, 18, 18); - drawTexturedModalRect(x + 17, y, 0, 0, 18, 18); + // "$" label for currency cost + fontRendererObj.drawString("$", x, y + 5, CustomNpcResourceListener.DefaultTextColor); - fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor); + // Currency textfield is at x + 8 (drawn by textfield system) + + // Item Cost 1 slot mc.renderEngine.bindTexture(this.slot); GL11.glColor4f(1, 1, 1, 1); - drawTexturedModalRect(x + 42, y, 0, 0, 18, 18); + drawTexturedModalRect(x + 50, y, 0, 0, 18, 18); + + // Item Cost 2 slot + drawTexturedModalRect(x + 68, y, 0, 0, 18, 18); + + // "=" sign + fontRendererObj.drawString("=", x + 88, y + 5, CustomNpcResourceListener.DefaultTextColor); + + // Output slot + mc.renderEngine.bindTexture(this.slot); + GL11.glColor4f(1, 1, 1, 1); + drawTexturedModalRect(x + 104, y, 0, 0, 18, 18); } } @@ -95,10 +122,23 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void unFocused(GuiNpcTextField guiNpcTextField) { - String name = guiNpcTextField.getText(); - if (!name.equalsIgnoreCase(role.marketName)) { - role.marketName = name; - PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); + int id = guiNpcTextField.id; + + // Handle market name field + if (id == 0) { + String name = guiNpcTextField.getText(); + if (!name.equalsIgnoreCase(role.marketName)) { + role.marketName = name; + PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); + } + return; + } + + // Handle currency cost fields (IDs 100-117) + if (id >= CURRENCY_FIELD_ID_START && id < CURRENCY_FIELD_ID_START + 18) { + int slot = id - CURRENCY_FIELD_ID_START; + long cost = guiNpcTextField.getInteger(); + role.setCurrencyCost(slot, cost); } } } diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java new file mode 100644 index 000000000..4c72e2059 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java @@ -0,0 +1,52 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Confirmation dialog for resetting trader stock cooldown. + */ +public class SubGuiNpcTraderCooldownReset extends SubGuiInterface { + + private final RoleTrader role; + public boolean confirmed = false; + + public SubGuiNpcTraderCooldownReset(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 200; + ySize = 100; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 15; + + addLabel(new GuiNpcLabel(0, "stock.cooldown.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "stock.cooldown.warning", guiLeft + 10, y)); + + y += 35; + + addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes")); + addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + // Reset the cooldown + role.resetCooldown(); + confirmed = true; + close(); + } else if (guibutton.id == 1) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java new file mode 100644 index 000000000..d9e2a0b40 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java @@ -0,0 +1,66 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +public class SubGuiNpcTraderSettings extends SubGuiInterface { + + private final RoleTrader role; + + public SubGuiNpcTraderSettings(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 176; + ySize = 136; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 14; + + addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(1, guiLeft + 120, y, role.ignoreDamage)); + getButton(1).setHoverText("gui.ignoreDamage.hover"); + + y += 28; + + addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(2, guiLeft + 120, y, role.ignoreNBT)); + getButton(2).setHoverText("gui.ignoreNBT.hover"); + + y += 28; + + addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(3, guiLeft + 120, y, role.recordHistory)); + getButton(3).setHoverText("gui.recordHistory.hover"); + + y += 34; + + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, y, 60, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case 0: + close(); + break; + case 1: + role.ignoreDamage = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 2: + role.ignoreNBT = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 3: + role.recordHistory = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java new file mode 100644 index 000000000..2e369b7e6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java @@ -0,0 +1,159 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.client.gui.util.SubGuiNpcCooldownPicker; +import noppes.npcs.constants.EnumStockReset; +import noppes.npcs.roles.RoleTrader; + +public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiListener, ITextfieldListener { + + private final RoleTrader role; + + public SubGuiNpcTraderStock(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 220; + ySize = 220; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + + // Enable Stock + addLabel(new GuiNpcLabel(1, "stock.enable", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(1, guiLeft + 140, y, role.stock.enableStock)); + getButton(1).setHoverText("stock.enable.hover"); + + y += 24; + + // Per-Player Stock + addLabel(new GuiNpcLabel(2, "stock.perplayer", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(2, guiLeft + 140, y, role.stock.perPlayer)); + getButton(2).setHoverText("stock.perplayer.hover"); + + y += 24; + + // Reset Type + addLabel(new GuiNpcLabel(3, "stock.resettype", guiLeft + 10, y + 5)); + addButton(new GuiNpcButton(3, guiLeft + 90, y, 120, 20, EnumStockReset.getDisplayNames(), role.stock.resetType.ordinal())); + getButton(3).setHoverText("stock.resettype.hover"); + + y += 24; + + // Custom Time button (only visible for custom types) + EnumStockReset resetType = role.stock.resetType; + if (resetType == EnumStockReset.MCCUSTOM || resetType == EnumStockReset.RLCUSTOM) { + addLabel(new GuiNpcLabel(4, "stock.customtime", guiLeft + 10, y + 5)); + addButton(new GuiNpcButton(4, guiLeft + 90, y, 120, 20, "gui.edit")); + getButton(4).setHoverText("stock.customtime.hover"); + y += 24; + } + + // Default Max Stock (applies to all slots as default) + addLabel(new GuiNpcLabel(5, "stock.defaultmax", guiLeft + 10, y + 5)); + GuiNpcTextField defaultStock = new GuiNpcTextField(5, this, fontRendererObj, guiLeft + 140, y, 60, 20, getDefaultStockString()); + defaultStock.setIntegersOnly(); + defaultStock.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); + addTextField(defaultStock); + + y += 26; + + // Per-Slot Stock button + addButton(new GuiNpcButton(5, guiLeft + 10, y, 95, 20, "stock.perslot")); + getButton(5).setHoverText("stock.perslot.hover"); + + // Reset Stock button + addButton(new GuiNpcButton(6, guiLeft + 115, y, 95, 20, "stock.reset")); + getButton(6).setHoverText("stock.reset.hover"); + + y += 24; + + // Reset Cooldown button + addButton(new GuiNpcButton(7, guiLeft + 10, y, 200, 20, "stock.resetcooldown")); + getButton(7).setHoverText("stock.resetcooldown.hover"); + + // Done button + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 40, 60, 20, "gui.done")); + } + + private String getDefaultStockString() { + // Check if all stocks are the same, if so return that value + int first = role.stock.maxStock[0]; + for (int i = 1; i < 18; i++) { + if (role.stock.maxStock[i] != first) { + return "-1"; // Mixed values, return unlimited + } + } + return String.valueOf(first); + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case 0: // Done + close(); + break; + case 1: // Enable Stock + role.stock.enableStock = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 2: // Per-Player + role.stock.perPlayer = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 3: // Reset Type + int value = ((GuiNpcButton) button).getValue(); + role.stock.resetType = EnumStockReset.values()[value]; + initGui(); // Refresh to show/hide custom time button + break; + case 4: // Custom Time + boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM; + setSubGui(new SubGuiNpcCooldownPicker(isMC, role.stock.customResetTime)); + break; + case 5: // Per-Slot Stock + setSubGui(new SubGuiNpcTraderStockSlots(role)); + break; + case 6: // Reset Stock + setSubGui(new SubGuiNpcTraderStockReset(role)); + break; + case 7: // Reset Cooldown + setSubGui(new SubGuiNpcTraderCooldownReset(role)); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subGui) { + if (subGui instanceof SubGuiNpcCooldownPicker) { + long value = ((SubGuiNpcCooldownPicker) subGui).cooldownValue; + // Validate minimum: 1 tick for MC time, 1000ms (1 second) for real time + boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM; + long minimum = isMC ? 1 : 1000; + role.stock.customResetTime = Math.max(minimum, value); + } + // Refresh after per-slot editing + if (subGui instanceof SubGuiNpcTraderStockSlots) { + initGui(); + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + if (textField.id == 5) { + int value = textField.getInteger(); + // Apply to all slots + for (int i = 0; i < 18; i++) { + role.stock.setMaxStock(i, value); + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java new file mode 100644 index 000000000..027108f76 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java @@ -0,0 +1,55 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Confirmation dialog for resetting trader stock. + */ +public class SubGuiNpcTraderStockReset extends SubGuiInterface { + + private final RoleTrader role; + public boolean confirmed = false; + + public SubGuiNpcTraderStockReset(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 200; + ySize = 100; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 15; + + addLabel(new GuiNpcLabel(0, "stock.reset.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "stock.reset.warning", guiLeft + 10, y)); + + y += 35; + + addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes")); + addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + // Reset the stock + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (role.npc != null && role.npc.worldObj != null ? role.npc.worldObj.getTotalWorldTime() : 0); + role.stock.resetStock(currentTime); + confirmed = true; + close(); + } else if (guibutton.id == 1) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java new file mode 100644 index 000000000..b4a76ba48 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java @@ -0,0 +1,76 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Per-slot stock management dialog. + * Shows all 18 slots with their max stock values. + */ +public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextfieldListener { + + private final RoleTrader role; + + public SubGuiNpcTraderStockSlots(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 256; + ySize = 216; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + // Title + addLabel(new GuiNpcLabel(0, "stock.perslot.title", guiLeft + 10, guiTop + 6)); + + // 18 slots in 3 columns x 6 rows + int startY = guiTop + 22; + int colWidth = 80; + + for (int i = 0; i < 18; i++) { + int col = i % 3; + int row = i / 3; + + int x = guiLeft + 10 + col * colWidth; + int y = startY + row * 28; + + // Slot label + addLabel(new GuiNpcLabel(i + 1, "Slot " + (i + 1) + ":", x, y + 5)); + + // Max stock textfield + int maxStock = role.stock.maxStock[i]; + String value = maxStock < 0 ? "-1" : String.valueOf(maxStock); + GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value); + field.setIntegersOnly(); + field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); + addTextField(field); + } + + // Done button + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 26, 60, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton button) { + if (button.id == 0) { + close(); + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + int slot = textField.id; + if (slot >= 0 && slot < 18) { + int value = textField.getInteger(); + role.stock.setMaxStock(slot, value); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java index b4049bc8a..54ca9da5d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java @@ -224,6 +224,15 @@ public abstract class GuiContainerNPCInterface extends GuiContainer { RenderHelper.disableStandardItemLighting(); subgui.drawScreen(i, j, f); } + + // Draw hover text for buttons + boolean subGui = hasSubGui(); + for (GuiCustomScroll scroll : new ArrayList(scrolls.values())) { + scroll.drawHover(i, j); + } + for (GuiNpcButton button : buttons.values()) { + button.drawHover(i, j, subGui); + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java index 3455e4986..5e97ec19a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java @@ -61,9 +61,9 @@ public abstract class GuiContainerNPCInterface2 extends GuiContainerNPCInterface GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(defaultBackground2); if (this instanceof GuiNpcTraderSetup) - drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 256, 220); + drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 210, 220); else - drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 256, 220); + drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 230, 220); menu.drawElements(fontRendererObj, i, j, mc, f); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 1d453a395..7e7d2acd8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -401,10 +401,23 @@ public abstract class GuiNPCInterface extends GuiScreen { GL11.glScalef(bgScale * bgScaleX, bgScale * bgScaleY, bgScale * bgScaleZ); mc.renderEngine.bindTexture(background); if (xSize > 256) { + // GUI wider than texture: draw left portion, then right portion from texture edge drawTexturedModalRect(0, 0, 0, 0, 250, ySize); drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, ySize); - } else + } else if (xSize < 256) { + // GUI narrower than texture: stitch left and right edges together + // Left portion (first half of GUI) + int leftWidth = xSize / 2; + // Right portion width (remaining) + int rightWidth = xSize - leftWidth; + // Draw left side from texture start + drawTexturedModalRect(0, 0, 0, 0, leftWidth, ySize); + // Draw right side from texture end (256 - rightWidth) + drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, ySize); + } else { + // GUI exactly 256px: draw full texture drawTexturedModalRect(0, 0, 0, 0, xSize, ySize); + } GL11.glPopMatrix(); } diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index f0f019290..c84ec3604 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -2,7 +2,6 @@ package noppes.npcs.config; import cpw.mods.fml.common.FMLLog; import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.common.config.Property; import org.apache.logging.log4j.Level; import java.io.File; @@ -14,7 +13,6 @@ public class ConfigMarket { public final static String MARKET = "Market"; public final static String CURRENCY = "Market.Currency"; - public final static String AUCTION = "Market.Auction"; public final static String TRADER = "Market.Trader"; // ========================================= @@ -25,37 +23,10 @@ public class ConfigMarket { // ========================================= // Currency Settings // ========================================= - public static boolean UseVaultIfAvailable = true; - public static String CurrencyName = "Gold"; - public static String FallbackCurrencyItem = "customnpcs:npcCoinGold"; - public static long FallbackItemRatio = 1; - public static long StartingBalance = 1000; - public static long MaxBalance = 999999999999L; - - // ========================================= - // Auction Settings - // ========================================= - public static boolean AuctionEnabled = true; - public static int DefaultMaxListings = 5; - public static double ListingFeePercent = 0.0; - public static double SalesTaxPercent = 0.05; - public static boolean RequireAuctioneerNPC = true; - public static int ClaimExpirationDays = 30; - public static double MinBidIncrementPercent = 0.05; - public static int SnipeProtectionMinutes = 5; - public static int SnipeProtectionThreshold = 5; - - // Auction Duration Settings (in hours) - public static int AuctionDurationShort = 2; - public static int AuctionDurationMedium = 12; - public static int AuctionDurationLong = 24; - public static int AuctionDurationVeryLong = 48; - - // Auction Duration Fees (flat currency amount) - public static long AuctionFeeShort = 0; - public static long AuctionFeeMedium = 0; - public static long AuctionFeeLong = 0; - public static long AuctionFeeVeryLong = 0; + public static boolean UseVault = false; + public static String CurrencyName = "Coins"; + public static long StartingBalance = 0; + public static long MaxBalance = Long.MAX_VALUE; // ========================================= // Trader Settings @@ -78,91 +49,19 @@ public class ConfigMarket { // ========================================= // Currency Settings // ========================================= - config.setCategoryComment(CURRENCY, "Currency system settings for the virtual economy"); + config.setCategoryComment(CURRENCY, "CNPC+ Currency system settings. When UseVault is enabled, all currency operations use Vault instead of the built-in system."); - UseVaultIfAvailable = config.get(CURRENCY, "Use Vault If Available", true, - "Use VaultAPI if available on Bukkit hybrid servers").getBoolean(true); + UseVault = config.get(CURRENCY, "Use Vault", false, + "If true, use Vault API for all currency operations instead of CNPC+ built-in currency. The built-in currency data will be preserved but unused.").getBoolean(false); - CurrencyName = config.get(CURRENCY, "Currency Name", "Gold", - "Display name for the currency").getString(); + CurrencyName = config.get(CURRENCY, "Currency Name", "Coins", + "Display name for the currency (e.g. 'Coins', 'Gold', 'Credits')").getString(); - FallbackCurrencyItem = config.get(CURRENCY, "Fallback Currency Item", "customnpcs:npcCoinGold", - "Item registry name to use as currency when Vault is unavailable. Leave empty to disable.").getString(); + StartingBalance = config.get(CURRENCY, "Starting Balance", 0, + "Starting currency balance for new players").getInt(0); - FallbackItemRatio = config.get(CURRENCY, "Fallback Item Ratio", 1, - "Conversion ratio: 1 fallback item = X virtual currency").getInt(1); - - StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, - "Starting balance for new players (only used when Vault is not available)").getInt(1000); - - Property maxBalanceProp = config.get(CURRENCY, "Maximum Balance", String.valueOf(MaxBalance), - "Maximum balance a player can have (only used when Vault is not available)"); - try { - MaxBalance = Long.parseLong(maxBalanceProp.getString()); - } catch (NumberFormatException e) { - MaxBalance = 999999999999L; - } - if (MaxBalance < 0) { - MaxBalance = 0; - } - - // ========================================= - // Auction Settings - // ========================================= - config.setCategoryComment(AUCTION, "Auction house settings"); - - AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, - "Enable/disable the auction house system").getBoolean(true); - - DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, - "Default maximum auction listings per player (without permission override)").getInt(5); - - ListingFeePercent = config.get(AUCTION, "Listing Fee Percent", 0.0, - "Listing fee as percentage of starting price (0.0 = no fee)").getDouble(0.0); - - SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, - "Sales tax as percentage taken from final sale price (0.05 = 5%)").getDouble(0.05); - - RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, - "Require an Auctioneer NPC to access auction house").getBoolean(true); - - ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, - "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); - - MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, - "Minimum bid increment as percentage (0.05 = 5% minimum increase)").getDouble(0.05); - - SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 5, - "Extend auction TO this many minutes when bid placed near end. Set to 0 to disable.").getInt(5); - - SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, - "How many minutes before end triggers snipe protection").getInt(5); - - // Duration settings - AuctionDurationShort = config.get(AUCTION, "Duration Short Hours", 2, - "Short auction duration in hours").getInt(2); - - AuctionDurationMedium = config.get(AUCTION, "Duration Medium Hours", 12, - "Medium auction duration in hours").getInt(12); - - AuctionDurationLong = config.get(AUCTION, "Duration Long Hours", 24, - "Long auction duration in hours").getInt(24); - - AuctionDurationVeryLong = config.get(AUCTION, "Duration Very Long Hours", 48, - "Very long auction duration in hours").getInt(48); - - // Duration fees - AuctionFeeShort = config.get(AUCTION, "Fee Short Duration", 0, - "Flat fee for short duration auctions").getInt(0); - - AuctionFeeMedium = config.get(AUCTION, "Fee Medium Duration", 0, - "Flat fee for medium duration auctions").getInt(0); - - AuctionFeeLong = config.get(AUCTION, "Fee Long Duration", 0, - "Flat fee for long duration auctions").getInt(0); - - AuctionFeeVeryLong = config.get(AUCTION, "Fee Very Long Duration", 0, - "Flat fee for very long duration auctions").getInt(0); + MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE, + "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE); // ========================================= // Trader Settings @@ -181,32 +80,10 @@ public class ConfigMarket { ))); config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( - "Use Vault If Available", + "Use Vault", "Currency Name", - "Fallback Currency Item", - "Fallback Item Ratio", "Starting Balance", - "Maximum Balance" - ))); - - config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( - "Enable Auction House", - "Default Max Listings", - "Listing Fee Percent", - "Sales Tax Percent", - "Require Auctioneer NPC", - "Claim Expiration Days", - "Min Bid Increment Percent", - "Snipe Protection Minutes", - "Snipe Protection Threshold", - "Duration Short Hours", - "Duration Medium Hours", - "Duration Long Hours", - "Duration Very Long Hours", - "Fee Short Duration", - "Fee Medium Duration", - "Fee Long Duration", - "Fee Very Long Duration" + "Max Balance" ))); config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( @@ -214,26 +91,6 @@ public class ConfigMarket { "Default Reset Type" ))); - // Validation - if (DefaultMaxListings < 1) { - DefaultMaxListings = 1; - } - if (ClaimExpirationDays < 0) { - ClaimExpirationDays = 0; - } - if (SnipeProtectionMinutes < 0) { - SnipeProtectionMinutes = 0; - } - if (SnipeProtectionThreshold < 0) { - SnipeProtectionThreshold = 0; - } - if (StartingBalance < 0) { - StartingBalance = 0; - } - if (MaxBalance < 1) { - MaxBalance = 1; - } - } catch (Exception e) { FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration"); } finally { diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java deleted file mode 100644 index c901d21ce..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java +++ /dev/null @@ -1,82 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.util.StatCollector; -import noppes.npcs.config.ConfigMarket; - -/** - * Duration options for auction listings. - * Values are configurable via ConfigMarket. - */ -public enum EnumAuctionDuration { - SHORT("auction.duration.short"), - MEDIUM("auction.duration.medium"), - LONG("auction.duration.long"), - VERY_LONG("auction.duration.veryLong"); - - private final String langKey; - - EnumAuctionDuration(String langKey) { - this.langKey = langKey; - } - - /** - * Get duration in milliseconds - */ - public long getMillis() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort * 60L * 60L * 1000L; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium * 60L * 60L * 1000L; - case LONG: - return ConfigMarket.AuctionDurationLong * 60L * 60L * 1000L; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong * 60L * 60L * 1000L; - default: - return 24 * 60L * 60L * 1000L; - } - } - - /** - * Get duration in hours - */ - public int getHours() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium; - case LONG: - return ConfigMarket.AuctionDurationLong; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong; - default: - return 24; - } - } - - /** - * Get display name for GUI - */ - public String getDisplayName() { - return StatCollector.translateToLocal(langKey); - } - - /** - * Get listing fee for this duration - */ - public long getListingFee() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionFeeShort; - case MEDIUM: - return ConfigMarket.AuctionFeeMedium; - case LONG: - return ConfigMarket.AuctionFeeLong; - case VERY_LONG: - return ConfigMarket.AuctionFeeVeryLong; - default: - return 0; - } - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java deleted file mode 100644 index 75220861c..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java +++ /dev/null @@ -1,39 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.util.StatCollector; - -/** - * Sorting options for auction listings. - */ -public enum EnumAuctionSort { - NEWEST_FIRST("auction.sort.newest"), - OLDEST_FIRST("auction.sort.oldest"), - PRICE_HIGH_TO_LOW("auction.sort.priceHigh"), - PRICE_LOW_TO_HIGH("auction.sort.priceLow"), - ENDING_SOON("auction.sort.endingSoon"), - MOST_TIME_LEFT("auction.sort.mostTime"), - NAME_A_TO_Z("auction.sort.nameAZ"), - NAME_Z_TO_A("auction.sort.nameZA"), - MOST_BIDS("auction.sort.mostBids"), - LEAST_BIDS("auction.sort.leastBids"); - - private final String langKey; - - EnumAuctionSort(String langKey) { - this.langKey = langKey; - } - - public String getDisplayName() { - return StatCollector.translateToLocal(langKey); - } - - public EnumAuctionSort next() { - EnumAuctionSort[] values = values(); - return values[(this.ordinal() + 1) % values.length]; - } - - public EnumAuctionSort previous() { - EnumAuctionSort[] values = values(); - return values[(this.ordinal() - 1 + values.length) % values.length]; - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java deleted file mode 100644 index 9e614e496..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java +++ /dev/null @@ -1,54 +0,0 @@ -package noppes.npcs.constants; - -/** - * Status of an auction listing. - */ -public enum EnumAuctionStatus { - ACTIVE, // Currently listed and accepting bids - SOLD, // Successfully sold (buyout or auction end with bids) - EXPIRED, // Ended without any bids - CANCELLED, // Cancelled by seller (only allowed before first bid) - CLAIMED; // Item/money has been claimed by respective parties - - /** - * Get display name for GUI - */ - public String getDisplayName() { - switch (this) { - case ACTIVE: return "Active"; - case SOLD: return "Sold"; - case EXPIRED: return "Expired"; - case CANCELLED: return "Cancelled"; - case CLAIMED: return "Claimed"; - default: return "Unknown"; - } - } - - /** - * Get display color for GUI (MC color codes) - */ - public char getColorCode() { - switch (this) { - case ACTIVE: return 'a'; // Green - case SOLD: return '6'; // Gold - case EXPIRED: return '7'; // Gray - case CANCELLED: return 'c'; // Red - case CLAIMED: return '8'; // Dark gray - default: return 'f'; // White - } - } - - /** - * Check if listing can still receive bids - */ - public boolean canBid() { - return this == ACTIVE; - } - - /** - * Check if listing needs claiming (item or money) - */ - public boolean needsClaiming() { - return this == SOLD || this == EXPIRED || this == CANCELLED; - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index 6c85cb4f5..f2d911561 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -64,7 +64,5 @@ public enum EnumGuiType { ScriptBlock, Paintbrush, MagicBook, - ManageAbilities, - PlayerAuction, - SetupAuctioneer; + ManageAbilities; } diff --git a/src/main/java/noppes/npcs/constants/EnumRoleType.java b/src/main/java/noppes/npcs/constants/EnumRoleType.java index 33f36941a..624658fb3 100644 --- a/src/main/java/noppes/npcs/constants/EnumRoleType.java +++ b/src/main/java/noppes/npcs/constants/EnumRoleType.java @@ -2,5 +2,5 @@ package noppes.npcs.constants; public enum EnumRoleType { - None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount, Auctioneer + None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount } diff --git a/src/main/java/noppes/npcs/constants/EnumStockReset.java b/src/main/java/noppes/npcs/constants/EnumStockReset.java index cdc56cb95..485318dd5 100644 --- a/src/main/java/noppes/npcs/constants/EnumStockReset.java +++ b/src/main/java/noppes/npcs/constants/EnumStockReset.java @@ -1,17 +1,25 @@ package noppes.npcs.constants; +import net.minecraft.util.StatCollector; + /** * Stock reset types for Trader NPCs. * Mirrors EnumQuestRepeat pattern for consistency. */ public enum EnumStockReset { - NONE, // Never resets - MCDAILY, // Every 24000 MC ticks (1 MC day) - MCWEEKLY, // Every 168000 MC ticks (7 MC days) - MCCUSTOM, // Custom MC tick interval - RLDAILY, // Every 24 real hours - RLWEEKLY, // Every 7 real days - RLCUSTOM; // Custom real-time interval + NONE("stock.reset.never"), + MCDAILY("stock.reset.mcdaily"), + MCWEEKLY("stock.reset.mcweekly"), + MCCUSTOM("stock.reset.mccustom"), + RLDAILY("stock.reset.rldaily"), + RLWEEKLY("stock.reset.rlweekly"), + RLCUSTOM("stock.reset.rlcustom"); + + private final String langKey; + + EnumStockReset(String langKey) { + this.langKey = langKey; + } /** * Get reset interval in milliseconds for real-time types @@ -47,18 +55,21 @@ public enum EnumStockReset { } /** - * Get display name for GUI + * Get display name for GUI (translated) */ public String getDisplayName() { - switch (this) { - case NONE: return "Never"; - case MCDAILY: return "MC Daily"; - case MCWEEKLY: return "MC Weekly"; - case MCCUSTOM: return "MC Custom"; - case RLDAILY: return "Real Daily"; - case RLWEEKLY: return "Real Weekly"; - case RLCUSTOM: return "Real Custom"; - default: return "Unknown"; + return StatCollector.translateToLocal(langKey); + } + + /** + * Get all display names for dropdown menus + */ + public static String[] getDisplayNames() { + EnumStockReset[] values = values(); + String[] names = new String[values.length]; + for (int i = 0; i < values.length; i++) { + names[i] = values[i].getDisplayName(); } + return names; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java deleted file mode 100644 index 2efd2b1b5..000000000 --- a/src/main/java/noppes/npcs/containers/ContainerAuction.java +++ /dev/null @@ -1,193 +0,0 @@ -package noppes.npcs.containers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import noppes.npcs.entity.EntityNPCInterface; - -/** - * Container for auction house GUI. - * Handles player inventory and item slot for creating listings. - */ -public class ContainerAuction extends Container { - public EntityNPCInterface npc; - public EntityPlayer player; - - // Slot for item being listed - public SlotAuctionItem listingSlot; - private IInventory listingInventory; - - public ContainerAuction(EntityNPCInterface npc, EntityPlayer player) { - this.npc = npc; - this.player = player; - - // Create a single-slot inventory for the item being listed - listingInventory = new InventoryAuctionListing(); - listingSlot = new SlotAuctionItem(listingInventory, 0, 251, 146); - addSlotToContainer(listingSlot); - - // Player inventory slots (standard positioning) - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col + row * 9 + 9, - 8 + col * 18, 166 + row * 18)); - } - } - - // Player hotbar - for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 224)); - } - } - - @Override - public boolean canInteractWith(EntityPlayer player) { - return npc != null && npc.isEntityAlive() && - player.getDistanceSqToEntity(npc) < 64.0D; - } - - @Override - public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { - ItemStack itemstack = null; - Slot slot = (Slot) inventorySlots.get(slotIndex); - - if (slot != null && slot.getHasStack()) { - ItemStack stackInSlot = slot.getStack(); - itemstack = stackInSlot.copy(); - - // If from listing slot, move to player inventory - if (slotIndex == 0) { - if (!mergeItemStack(stackInSlot, 1, 37, true)) { - return null; - } - } - // If from player inventory, move to listing slot - else { - if (!mergeItemStack(stackInSlot, 0, 1, false)) { - return null; - } - } - - if (stackInSlot.stackSize == 0) { - slot.putStack(null); - } else { - slot.onSlotChanged(); - } - } - - return itemstack; - } - - @Override - public void onContainerClosed(EntityPlayer player) { - super.onContainerClosed(player); - - // Return any item in the listing slot to player - ItemStack stack = listingInventory.getStackInSlot(0); - if (stack != null) { - if (!player.inventory.addItemStackToInventory(stack)) { - player.entityDropItem(stack, 0.5f); - } - } - } - - /** - * Get the item currently in the listing slot - */ - public ItemStack getListingItem() { - return listingInventory.getStackInSlot(0); - } - - /** - * Clear the listing slot (after successful listing creation) - */ - public void clearListingSlot() { - listingInventory.setInventorySlotContents(0, null); - } - - /** - * Simple inventory for the listing slot - */ - private class InventoryAuctionListing implements IInventory { - private ItemStack item; - - @Override - public int getSizeInventory() { return 1; } - - @Override - public ItemStack getStackInSlot(int slot) { return item; } - - @Override - public ItemStack decrStackSize(int slot, int amount) { - if (item != null) { - ItemStack result; - if (item.stackSize <= amount) { - result = item; - item = null; - return result; - } else { - result = item.splitStack(amount); - if (item.stackSize == 0) { - item = null; - } - return result; - } - } - return null; - } - - @Override - public ItemStack getStackInSlotOnClosing(int slot) { - ItemStack result = item; - item = null; - return result; - } - - @Override - public void setInventorySlotContents(int slot, ItemStack stack) { - item = stack; - } - - @Override - public String getInventoryName() { return "AuctionListing"; } - - @Override - public boolean hasCustomInventoryName() { return false; } - - @Override - public int getInventoryStackLimit() { return 64; } - - @Override - public void markDirty() {} - - @Override - public boolean isUseableByPlayer(EntityPlayer player) { return true; } - - @Override - public void openInventory() {} - - @Override - public void closeInventory() {} - - @Override - public boolean isItemValidForSlot(int slot, ItemStack stack) { return true; } - } - - /** - * Custom slot for auction listings - */ - public class SlotAuctionItem extends Slot { - public SlotAuctionItem(IInventory inv, int index, int x, int y) { - super(inv, index, x, y); - } - - @Override - public boolean isItemValid(ItemStack stack) { - // Check if item can be traded (not untradeable, soulbound, etc.) - return kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute.canTrade(stack); - } - } -} diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index 936868152..e6b4c86c8 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -1,45 +1,71 @@ package noppes.npcs.containers; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilPlayer; -import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; public class ContainerNPCTrader extends ContainerNpcInterface { public RoleTrader role; + private final EntityNPCInterface npc; + + // Layout constants for 256px wide GUI + public static final int COLUMN_WIDTH = 80; + public static final int COLUMN_START_X = 8; + public static final int ROW_HEIGHT = 22; + public static final int ROW_START_Y = 6; + + // Slot offsets within each column (for 18x18 slots, items render centered) + public static final int CURRENCY1_OFFSET = 2; // Primary currency slot + public static final int CURRENCY2_OFFSET = 20; // Secondary currency slot + public static final int OUTPUT_OFFSET = 50; // Output slot public ContainerNPCTrader(EntityNPCInterface npc, EntityPlayer player) { super(player); + this.npc = npc; role = (RoleTrader) npc.roleInterface; - for (int i = 0; i < 18; i++) { - int x = 53; - x += i % 3 * 72; - int y = 7; - y += i / 3 * 21; + // Register this player as viewing the trader (server-side only) + // Needed for shared stock (not per-player) to sync updates to other viewers + // Works for both linked markets (MarketRegistry) and normal traders (local viewers) + if (player instanceof EntityPlayerMP && !role.stock.perPlayer) { + role.registerViewer((EntityPlayerMP) player); + } - ItemStack item = role.inventoryCurrency.items.get(i); - ItemStack item2 = role.inventoryCurrency.items.get(i + 18); - if (item == null) { - item = item2; - item2 = null; - } - addSlotToContainer(new Slot(role.inventorySold, i, x, y)); + // Trade slots: 3 columns x 6 rows, 80px column width + // Layout per column: [Slot1][Slot2] = [Output] + for (int i = 0; i < 18; i++) { + int col = i % 3; + int row = i / 3; + int x = COLUMN_START_X + col * COLUMN_WIDTH; + int y = ROW_START_Y + row * ROW_HEIGHT; + // Output slot position (slot texture is 18x18, item renders centered at +1) + addSlotToContainer(new Slot(role.inventorySold, i, x + OUTPUT_OFFSET + 1, y + 1)); } for (int i1 = 0; i1 < 3; i1++) { for (int l1 = 0; l1 < 9; l1++) { - addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 32 + l1 * 18, 140 + i1 * 18)); + addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 48 + l1 * 18, 137 + i1 * 18)); } - } + // Player hotbar for (int j1 = 0; j1 < 9; j1++) { - addSlotToContainer(new Slot(player.inventory, j1, 32 + j1 * 18, 198)); + addSlotToContainer(new Slot(player.inventory, j1, 48 + j1 * 18, 195)); + } + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + // Unregister this player from viewer tracking (only if registered for shared stock) + if (player instanceof EntityPlayerMP && !role.stock.perPlayer) { + role.unregisterViewer((EntityPlayerMP) player); } } @@ -71,25 +97,27 @@ public class ContainerNPCTrader extends ContainerNpcInterface { return null; // Out of stock } - // Handle currency-based purchase - if (role.hasSlotPrice(i)) { - long price = role.getSlotPrice(i); - if (CurrencyController.Instance == null) { - return null; // Currency system not available + // Item-based currency + if (!canBuy(i, entityplayer)) + return null; + + // Currency cost check (additive to item costs) + long currencyCost = role.getCurrencyCost(i); + if (currencyCost > 0) { + PlayerData data = PlayerData.get(entityplayer); + if (data.currencyData.getBalance() < currencyCost) { + return null; // Can't afford currency cost } - if (!CurrencyController.Instance.canAfford(entityplayer, price)) { - return null; // Can't afford - } - // Withdraw currency - if (!CurrencyController.Instance.withdraw(entityplayer, price)) { - return null; // Withdrawal failed - } - } else { - // Item-based currency - existing logic - if (!canBuy(i, entityplayer)) - return null; - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + } + + // Consume item currency + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + + // Withdraw currency cost (after item consumption to maintain order) + if (currencyCost > 0) { + PlayerData data = PlayerData.get(entityplayer); + data.currencyData.withdraw(currencyCost); } // Consume stock @@ -98,6 +126,18 @@ public class ContainerNPCTrader extends ContainerNpcInterface { ItemStack soldItem = item.copy(); givePlayer(soldItem, entityplayer); role.addPurchase(i, entityplayer.getDisplayName()); + + // Sync updated data after purchase + // For per-player stocks: sync only to the purchasing player + // For shared stocks: consumeStock() handles sync (market-wide or local) + if (entityplayer instanceof EntityPlayerMP) { + if (role.stock.perPlayer) { + // Per-player: only sync to purchasing player (their own stock/balance) + role.syncToPlayer((EntityPlayerMP) entityplayer); + } + // Shared stock sync is already handled in consumeStock() + } + return soldItem; } diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java b/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java index 8209f22a8..f5d347e16 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java @@ -13,15 +13,19 @@ public class ContainerNPCTraderSetup extends Container { public ContainerNPCTraderSetup(EntityNPCInterface npc, EntityPlayer player) { role = (RoleTrader) npc.roleInterface; + // New layout: $[Currency TextField], Item Cost 1, Item Cost 2, =, Output + // Column width: 130px for better spacing for (int i = 0; i < 18; i++) { int x = 7; - x += i % 3 * 94; + x += i % 3 * 130; int y = 15; y += i / 3 * 22; - addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x, y)); - addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 18, y)); - addSlotToContainer(new Slot(role.inventorySold, i, x + 43, y)); - + // Item Cost 1 (primary currency) + addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 51, y)); + // Item Cost 2 (secondary currency) + addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x + 69, y)); + // Output (sold item) + addSlotToContainer(new Slot(role.inventorySold, i, x + 105, y)); } for (int i1 = 0; i1 < 3; i1++) { diff --git a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java deleted file mode 100644 index 020bb0bcb..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java +++ /dev/null @@ -1,419 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.nbt.NBTTagString; -import noppes.npcs.CustomNpcs; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.HashSet; -import java.util.Set; - -/** - * Controller for managing auction house item blacklisting. - * Allows administrators to block specific items, entire mods, - * or items with certain NBT tags from being listed. - */ -public class AuctionBlacklist { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static AuctionBlacklist Instance; - - // Blacklist by item registry name (e.g., "minecraft:diamond_sword") - private Set blacklistedItems = new HashSet<>(); - - // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) - private Set blacklistedMods = new HashSet<>(); - - // Blacklist by NBT tag presence (e.g., items with specific custom data) - private Set blacklistedNBTTags = new HashSet<>(); - - public AuctionBlacklist() { - Instance = this; - load(); - } - - // ==================== Blacklist Checking ==================== - - /** - * Check if an item is blacklisted from the auction house. - * - * @param item The item to check - * @return true if the item is blacklisted - */ - public boolean isBlacklisted(ItemStack item) { - if (item == null || item.getItem() == null) { - return false; - } - - // Check item registry name - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - // Direct item blacklist - if (blacklistedItems.contains(itemName)) { - return true; - } - - // Also check with metadata for specific variants - String itemNameWithMeta = itemName + ":" + item.getItemDamage(); - if (blacklistedItems.contains(itemNameWithMeta)) { - return true; - } - - // Check mod ID - String modId = getModIdFromName(itemName); - if (modId != null && blacklistedMods.contains(modId)) { - return true; - } - } - - // Check NBT tags - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (hasNestedTag(tag, nbtTag)) { - return true; - } - } - } - - return false; - } - - /** - * Get the reason why an item is blacklisted - * @return Description of why blacklisted, or null if not blacklisted - */ - public String getBlacklistReason(ItemStack item) { - if (item == null || item.getItem() == null) { - return null; - } - - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - if (blacklistedItems.contains(itemName)) { - return "Item is blacklisted: " + itemName; - } - - String itemNameWithMeta = itemName + ":" + item.getItemDamage(); - if (blacklistedItems.contains(itemNameWithMeta)) { - return "Item variant is blacklisted: " + itemNameWithMeta; - } - - String modId = getModIdFromName(itemName); - if (modId != null && blacklistedMods.contains(modId)) { - return "Mod is blacklisted: " + modId; - } - } - - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (hasNestedTag(tag, nbtTag)) { - return "Item has blacklisted NBT tag: " + nbtTag; - } - } - } - - return null; - } - - /** - * Extract mod ID from registry name (e.g., "minecraft" from "minecraft:stone") - */ - private String getModIdFromName(String registryName) { - if (registryName == null) { - return null; - } - int colonIndex = registryName.indexOf(':'); - if (colonIndex > 0) { - return registryName.substring(0, colonIndex); - } - return null; - } - - /** - * Check for nested NBT tag (supports dot notation like "display.Name") - */ - private boolean hasNestedTag(NBTTagCompound tag, String path) { - if (tag == null || path == null || path.isEmpty()) { - return false; - } - - String[] parts = path.split("\\."); - NBTTagCompound current = tag; - - for (int i = 0; i < parts.length - 1; i++) { - if (!current.hasKey(parts[i])) { - return false; - } - if (current.hasKey(parts[i], 10)) { // 10 = Compound tag - current = current.getCompoundTag(parts[i]); - } else { - return false; - } - } - - return current.hasKey(parts[parts.length - 1]); - } - - // ==================== Item Blacklist Management ==================== - - /** - * Add an item to the blacklist by registry name - * @param itemName Registry name (e.g., "minecraft:diamond_sword" or "minecraft:wool:14") - */ - public void addItem(String itemName) { - if (itemName != null && !itemName.isEmpty()) { - blacklistedItems.add(itemName); - save(); - logger.info("Added item to auction blacklist: " + itemName); - } - } - - /** - * Add an item to the blacklist from an ItemStack - */ - public void addItem(ItemStack item, boolean includeMeta) { - if (item == null || item.getItem() == null) { - return; - } - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - if (includeMeta && item.getHasSubtypes()) { - addItem(itemName + ":" + item.getItemDamage()); - } else { - addItem(itemName); - } - } - } - - /** - * Remove an item from the blacklist - */ - public void removeItem(String itemName) { - if (blacklistedItems.remove(itemName)) { - save(); - logger.info("Removed item from auction blacklist: " + itemName); - } - } - - /** - * Check if a specific item is blacklisted - */ - public boolean isItemBlacklisted(String itemName) { - return blacklistedItems.contains(itemName); - } - - /** - * Get all blacklisted items - */ - public Set getBlacklistedItems() { - return new HashSet<>(blacklistedItems); - } - - // ==================== Mod Blacklist Management ==================== - - /** - * Add an entire mod to the blacklist - * @param modId The mod ID (e.g., "minecraft", "customnpcs") - */ - public void addMod(String modId) { - if (modId != null && !modId.isEmpty()) { - blacklistedMods.add(modId); - save(); - logger.info("Added mod to auction blacklist: " + modId); - } - } - - /** - * Remove a mod from the blacklist - */ - public void removeMod(String modId) { - if (blacklistedMods.remove(modId)) { - save(); - logger.info("Removed mod from auction blacklist: " + modId); - } - } - - /** - * Check if a mod is blacklisted - */ - public boolean isModBlacklisted(String modId) { - return blacklistedMods.contains(modId); - } - - /** - * Get all blacklisted mods - */ - public Set getBlacklistedMods() { - return new HashSet<>(blacklistedMods); - } - - // ==================== NBT Tag Blacklist Management ==================== - - /** - * Add an NBT tag to the blacklist - * Items containing this tag will be blocked - * @param tagPath The tag path (e.g., "display.Name" or "CustomData") - */ - public void addNBTTag(String tagPath) { - if (tagPath != null && !tagPath.isEmpty()) { - blacklistedNBTTags.add(tagPath); - save(); - logger.info("Added NBT tag to auction blacklist: " + tagPath); - } - } - - /** - * Remove an NBT tag from the blacklist - */ - public void removeNBTTag(String tagPath) { - if (blacklistedNBTTags.remove(tagPath)) { - save(); - logger.info("Removed NBT tag from auction blacklist: " + tagPath); - } - } - - /** - * Check if an NBT tag is blacklisted - */ - public boolean isNBTTagBlacklisted(String tagPath) { - return blacklistedNBTTags.contains(tagPath); - } - - /** - * Get all blacklisted NBT tags - */ - public Set getBlacklistedNBTTags() { - return new HashSet<>(blacklistedNBTTags); - } - - // ==================== Persistence ==================== - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - try { - NBTTagCompound compound = new NBTTagCompound(); - - // Save blacklisted items - NBTTagList itemList = new NBTTagList(); - for (String item : blacklistedItems) { - itemList.appendTag(new NBTTagString(item)); - } - compound.setTag("Items", itemList); - - // Save blacklisted mods - NBTTagList modList = new NBTTagList(); - for (String mod : blacklistedMods) { - modList.appendTag(new NBTTagString(mod)); - } - compound.setTag("Mods", modList); - - // Save blacklisted NBT tags - NBTTagList nbtList = new NBTTagList(); - for (String nbt : blacklistedNBTTags) { - nbtList.appendTag(new NBTTagString(nbt)); - } - compound.setTag("NBTTags", nbtList); - - // Write to file - File file = new File(saveDir, "auction_blacklist.dat"); - File backup = new File(saveDir, "auction_blacklist.dat_old"); - - if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } - file.renameTo(backup); - } - - file = new File(saveDir, "auction_blacklist.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - - } catch (Exception e) { - logger.error("Error saving auction blacklist", e); - } - } - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - blacklistedItems.clear(); - blacklistedMods.clear(); - blacklistedNBTTags.clear(); - - try { - File file = new File(saveDir, "auction_blacklist.dat"); - if (!file.exists()) { - // Try backup - file = new File(saveDir, "auction_blacklist.dat_old"); - if (!file.exists()) { - return; - } - } - - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - // Load blacklisted items - NBTTagList itemList = compound.getTagList("Items", 8); // 8 = String tag - for (int i = 0; i < itemList.tagCount(); i++) { - blacklistedItems.add(itemList.getStringTagAt(i)); - } - - // Load blacklisted mods - NBTTagList modList = compound.getTagList("Mods", 8); - for (int i = 0; i < modList.tagCount(); i++) { - blacklistedMods.add(modList.getStringTagAt(i)); - } - - // Load blacklisted NBT tags - NBTTagList nbtList = compound.getTagList("NBTTags", 8); - for (int i = 0; i < nbtList.tagCount(); i++) { - blacklistedNBTTags.add(nbtList.getStringTagAt(i)); - } - - logger.info("Loaded auction blacklist: {} items, {} mods, {} NBT tags", - blacklistedItems.size(), blacklistedMods.size(), blacklistedNBTTags.size()); - - } catch (Exception e) { - logger.error("Error loading auction blacklist", e); - } - } - - /** - * Clear all blacklist entries - */ - public void clearAll() { - blacklistedItems.clear(); - blacklistedMods.clear(); - blacklistedNBTTags.clear(); - save(); - logger.info("Cleared all auction blacklist entries"); - } - - /** - * Get total number of blacklist entries - */ - public int getTotalEntries() { - return blacklistedItems.size() + blacklistedMods.size() + blacklistedNBTTags.size(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java deleted file mode 100644 index 8333fd01a..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ /dev/null @@ -1,834 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.ChatComponentText; -import net.minecraft.util.EnumChatFormatting; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionSort; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -/** - * Controller for the global auction house. - * Manages all auction listings, bidding, claims, notifications, and logging. - */ -public class AuctionController { - private static final String LOG_PREFIX = "[Auction] "; - - public static AuctionController Instance; - - // All auction listings by ID - private Map listings = new ConcurrentHashMap<>(); - - // Next available listing ID - private int nextId = 1; - - // Indices for fast lookup - private Map> listingsBySeller = new ConcurrentHashMap<>(); - private Map> listingsByBidder = new ConcurrentHashMap<>(); - - // Pending notifications for offline players - private Map> pendingNotifications = new ConcurrentHashMap<>(); - - public AuctionController() { - Instance = this; - load(); - } - - // ==================== Listing Management ==================== - - /** - * Create a new auction listing - * @return The new listing, or null if creation failed - */ - public AuctionListing createListing(EntityPlayer seller, ItemStack item, - long startingPrice, long buyoutPrice, - EnumAuctionDuration duration) { - // Validate item - String validation = AuctionListing.validateItem(item); - if (validation != null) { - LogWriter.info(LOG_PREFIX + "Listing validation failed: " + validation); - return null; - } - - // Check seller's listing limit - int currentListings = getActiveListingsCount(seller.getUniqueID()); - int maxListings = getMaxListings(seller); - if (currentListings >= maxListings) { - return null; - } - - // Calculate and charge listing fee - AuctionListing listing = new AuctionListing( - seller.getUniqueID(), - seller.getCommandSenderName(), - item, - startingPrice, - buyoutPrice, - duration - ); - - long listingFee = listing.getListingFee(); - if (listingFee > 0) { - if (!CurrencyController.Instance.canAfford(seller, listingFee)) { - return null; - } - CurrencyController.Instance.withdraw(seller, listingFee); - } - - // Assign ID and store - listing.id = nextId++; - listings.put(listing.id, listing); - - // Update indices - addToSellerIndex(seller.getUniqueID(), listing.id); - - save(); - - // Log to CNPC+ - logEvent("LISTING_CREATED", String.format("ID=%d, Seller=%s, Item=%s x%d, StartPrice=%d, Buyout=%s", - listing.id, listing.sellerName, item.getDisplayName(), item.stackSize, - listing.startingPrice, listing.buyoutPrice > 0 ? listing.buyoutPrice : "None")); - - return listing; - } - - /** - * Get a listing by ID - */ - public AuctionListing getListing(int id) { - return listings.get(id); - } - - /** - * Get all active listings - */ - public List getActiveListings() { - processExpiredAuctions(); - return listings.values().stream() - .filter(AuctionListing::isActive) - .collect(Collectors.toList()); - } - - /** - * Search listings by item name - */ - public List searchListings(String query) { - String lowerQuery = query.toLowerCase(); - return getActiveListings().stream() - .filter(l -> l.item != null) - .filter(l -> l.item.getDisplayName().toLowerCase().contains(lowerQuery)) - .collect(Collectors.toList()); - } - - // ==================== Filtering & Sorting ==================== - - /** - * Get filtered and sorted auction listings. - * @param filter The filter criteria - * @return FilterResult containing matching listings and metadata - */ - public FilterResult getFilteredListings(AuctionFilter filter) { - processExpiredAuctions(); - - // Get all active listings - List results = listings.values().stream() - .filter(AuctionListing::isActive) - .filter(filter::matches) - .collect(Collectors.toList()); - - // Sort the results - sortListings(results, filter.sortOrder); - - // Calculate pagination - int totalResults = results.size(); - int totalPages = totalResults == 0 - ? 0 - : Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); - int currentPage = Math.max(0, filter.page); - if (totalPages > 0 && currentPage >= totalPages) { - currentPage = totalPages - 1; - } - int startIndex = currentPage * filter.pageSize; - int endIndex = Math.min(startIndex + filter.pageSize, totalResults); - - // Get the page slice - List pageResults; - if (startIndex >= totalResults) { - pageResults = new ArrayList<>(); - } else { - pageResults = new ArrayList<>(results.subList(startIndex, endIndex)); - } - - return new FilterResult(pageResults, totalResults, currentPage, totalPages); - } - - /** - * Sort listings according to the specified sort order - */ - public void sortListings(List listings, EnumAuctionSort sortOrder) { - if (listings == null || listings.isEmpty()) { - return; - } - - Comparator comparator; - - switch (sortOrder) { - case NEWEST_FIRST: - comparator = Comparator.comparingLong(l -> -l.listingTime); - break; - case OLDEST_FIRST: - comparator = Comparator.comparingLong(l -> l.listingTime); - break; - case PRICE_HIGH_TO_LOW: - comparator = Comparator.comparingLong((AuctionListing l) -> - -(l.currentBid > 0 ? l.currentBid : l.startingPrice)); - break; - case PRICE_LOW_TO_HIGH: - comparator = Comparator.comparingLong(l -> - l.currentBid > 0 ? l.currentBid : l.startingPrice); - break; - case ENDING_SOON: - comparator = Comparator.comparingLong(l -> l.expirationTime); - break; - case MOST_TIME_LEFT: - comparator = Comparator.comparingLong(l -> -l.expirationTime); - break; - case NAME_A_TO_Z: - comparator = Comparator.comparing(l -> getListingName(l).toLowerCase()); - break; - case NAME_Z_TO_A: - comparator = Comparator.comparing((AuctionListing l) -> - getListingName(l).toLowerCase()).reversed(); - break; - case MOST_BIDS: - comparator = Comparator.comparingInt(l -> -l.bidCount); - break; - case LEAST_BIDS: - comparator = Comparator.comparingInt(l -> l.bidCount); - break; - default: - comparator = Comparator.comparingLong(l -> l.expirationTime); - } - - listings.sort(comparator); - } - - /** - * Result container for filtered queries - */ - public static class FilterResult { - public final List listings; - public final int totalResults; - public final int currentPage; - public final int totalPages; - - public FilterResult(List listings, int totalResults, - int currentPage, int totalPages) { - this.listings = listings; - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - } - - public boolean hasNextPage() { - return currentPage < totalPages - 1; - } - - public boolean hasPreviousPage() { - return currentPage > 0; - } - - public boolean isEmpty() { - return listings == null || listings.isEmpty(); - } - } - - /** - * Get all listings by a seller - */ - public List getListingsBySeller(UUID sellerUUID) { - List ids = listingsBySeller.get(sellerUUID); - if (ids == null) { - return new ArrayList<>(); - } - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Get active listing count for a seller - */ - public int getActiveListingsCount(UUID sellerUUID) { - return (int) getListingsBySeller(sellerUUID).stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .count(); - } - - /** - * Get listings where player is the high bidder - */ - public List getListingsAsBidder(UUID bidderUUID) { - List ids = listingsByBidder.get(bidderUUID); - if (ids == null) { - return new ArrayList<>(); - } - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .filter(AuctionListing::isActive) - .filter(l -> bidderUUID.equals(l.highBidderUUID)) - .collect(Collectors.toList()); - } - - /** - * Get listings that need claiming by a player - */ - public List getClaimableListings(UUID playerUUID) { - List claimable = new ArrayList<>(); - - for (AuctionListing listing : listings.values()) { - // Seller can claim proceeds from sold items - if (listing.sellerUUID.equals(playerUUID) && - listing.status == EnumAuctionStatus.SOLD && - !listing.sellerClaimed) { - claimable.add(listing); - } - - // Seller can claim back cancelled/expired items - if (listing.sellerUUID.equals(playerUUID) && - (listing.status == EnumAuctionStatus.CANCELLED || - listing.status == EnumAuctionStatus.EXPIRED) && - !listing.sellerClaimed) { - claimable.add(listing); - } - - // Buyer can claim won items - if (listing.highBidderUUID != null && - listing.highBidderUUID.equals(playerUUID) && - listing.status == EnumAuctionStatus.SOLD && - !listing.buyerClaimed) { - claimable.add(listing); - } - } - - return claimable; - } - - // ==================== Bidding ==================== - - /** - * Place a bid on an auction - */ - public boolean placeBid(int listingId, EntityPlayer bidder, long amount) { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return false; - } - - if (!CurrencyController.Instance.canAfford(bidder, amount)) { - return false; - } - - // Store previous bidder for refund - UUID previousBidder = listing.highBidderUUID; - String previousBidderName = listing.highBidderName; - long previousBidAmount = listing.currentBid; - - if (!listing.placeBid(bidder.getUniqueID(), bidder.getCommandSenderName(), amount)) { - return false; - } - - CurrencyController.Instance.withdraw(bidder, amount); - - // Refund previous bidder - if (previousBidder != null) { - CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, - "Auction Outbid", String.valueOf(listingId)); - - // Notify outbid player - notifyPlayer(previousBidder, EnumChatFormatting.RED + - "You have been outbid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + "! New bid: " + formatCurrency(amount)); - - logEvent("OUTBID", String.format("ID=%d, OutbidPlayer=%s, PrevBid=%d, NewBid=%d", - listingId, previousBidderName, previousBidAmount, amount)); - } - - // Notify seller - notifyPlayer(listing.sellerUUID, EnumChatFormatting.AQUA + - bidder.getCommandSenderName() + " placed a bid of " + formatCurrency(amount) + - " on your " + listing.item.getDisplayName() + "!"); - - addToBidderIndex(bidder.getUniqueID(), listingId); - save(); - - logEvent("BID_PLACED", String.format("ID=%d, Bidder=%s, Amount=%d", - listingId, bidder.getCommandSenderName(), amount)); - - return true; - } - - /** - * Execute buyout on an auction - */ - public boolean buyout(int listingId, EntityPlayer buyer) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.buyoutPrice <= 0) { - return false; - } - - if (!CurrencyController.Instance.canAfford(buyer, listing.buyoutPrice)) { - return false; - } - - UUID previousBidder = listing.highBidderUUID; - String previousBidderName = listing.highBidderName; - long previousBidAmount = listing.currentBid; - - if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { - return false; - } - - CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); - - // Refund previous bidder - if (previousBidder != null) { - CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, - "Auction Outbid (Buyout)", String.valueOf(listingId)); - - notifyPlayer(previousBidder, EnumChatFormatting.RED + - "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was outbid by a buyout. Your " + - formatCurrency(previousBidAmount) + " has been refunded."); - } - - addToBidderIndex(buyer.getUniqueID(), listingId); - save(); - - // Notify buyer and seller - notifyPlayer(buyer.getUniqueID(), EnumChatFormatting.GREEN + - "Buyout successful! Claim your " + listing.item.getDisplayName() + " from an Auctioneer."); - - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + - "Your " + listing.item.getDisplayName() + " was bought out for " + - formatCurrency(listing.buyoutPrice) + "! Claim your proceeds."); - - logEvent("BUYOUT", String.format("ID=%d, Buyer=%s, Price=%d, Seller=%s", - listingId, buyer.getCommandSenderName(), listing.buyoutPrice, listing.sellerName)); - - return true; - } - - /** - * Cancel an auction listing - */ - public boolean cancelListing(int listingId, EntityPlayer requester, boolean forceCancel) { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return false; - } - - boolean byAdmin = !listing.sellerUUID.equals(requester.getUniqueID()); - if (byAdmin && !forceCancel) { - return false; - } - if (!forceCancel && listing.hasBids()) { - return false; - } - if (listing.status != EnumAuctionStatus.ACTIVE) { - return false; - } - - if (forceCancel && listing.hasBids() && listing.highBidderUUID != null) { - CurrencyController.Instance.addClaim(listing.highBidderUUID, listing.currentBid, - "Auction Cancelled", String.valueOf(listingId)); - notifyPlayer(listing.highBidderUUID, EnumChatFormatting.RED + - "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was cancelled. Your " + - formatCurrency(listing.currentBid) + " has been refunded."); - } - - listing.status = EnumAuctionStatus.CANCELLED; - - save(); - - if (byAdmin) { - notifyPlayer(listing.sellerUUID, EnumChatFormatting.RED + - "Your auction for " + listing.item.getDisplayName() + - " was cancelled by an administrator."); - } - - logEvent("CANCELLED", String.format("ID=%d, CancelledBy=%s, ByAdmin=%b", - listingId, requester.getCommandSenderName(), byAdmin)); - - return true; - } - - // ==================== Claiming ==================== - - /** - * Claim seller proceeds from a sold auction - */ - public boolean claimSellerProceeds(int listingId, EntityPlayer seller) { - AuctionListing listing = listings.get(listingId); - if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || - listing.status != EnumAuctionStatus.SOLD || listing.sellerClaimed) { - return false; - } - - long proceeds = listing.getSellerProceeds(); - CurrencyController.Instance.deposit(seller, proceeds); - - listing.sellerClaimed = true; - checkAndMarkClaimed(listing); - save(); - - logEvent("PROCEEDS_CLAIMED", String.format("ID=%d, Seller=%s, Amount=%d", - listingId, seller.getCommandSenderName(), proceeds)); - - return true; - } - - /** - * Claim item from won auction - */ - public ItemStack claimBuyerItem(int listingId, EntityPlayer buyer) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.highBidderUUID == null || - !listing.highBidderUUID.equals(buyer.getUniqueID()) || - listing.status != EnumAuctionStatus.SOLD || listing.buyerClaimed) { - return null; - } - - listing.buyerClaimed = true; - checkAndMarkClaimed(listing); - save(); - - logEvent("ITEM_CLAIMED", String.format("ID=%d, Buyer=%s, Item=%s", - listingId, buyer.getCommandSenderName(), listing.item.getDisplayName())); - - return listing.item.copy(); - } - - /** - * Claim item from cancelled or expired auction - */ - public ItemStack claimExpiredItem(int listingId, EntityPlayer seller) { - AuctionListing listing = listings.get(listingId); - if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || - (listing.status != EnumAuctionStatus.CANCELLED && listing.status != EnumAuctionStatus.EXPIRED) || - listing.sellerClaimed) { - return null; - } - - listing.sellerClaimed = true; - listing.status = EnumAuctionStatus.CLAIMED; - save(); - - logEvent("ITEM_RECLAIMED", String.format("ID=%d, Seller=%s, Item=%s", - listingId, seller.getCommandSenderName(), listing.item.getDisplayName())); - - return listing.item.copy(); - } - - private void checkAndMarkClaimed(AuctionListing listing) { - if (listing.status == EnumAuctionStatus.SOLD && - listing.sellerClaimed && listing.buyerClaimed) { - listing.status = EnumAuctionStatus.CLAIMED; - } - } - - // ==================== Expiration Processing ==================== - - /** - * Process all expired auctions - */ - public void processExpiredAuctions() { - boolean changed = false; - - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { - listing.finalize(); - changed = true; - - if (listing.status == EnumAuctionStatus.SOLD) { - // Notify winner and seller - notifyPlayer(listing.highBidderUUID, EnumChatFormatting.GREEN + - "You won the auction for " + listing.item.getDisplayName() + - " for " + formatCurrency(listing.currentBid) + "! Claim your item."); - - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + - "Your " + listing.item.getDisplayName() + " sold for " + - formatCurrency(listing.currentBid) + "! Claim your proceeds."); - - logEvent("AUCTION_SOLD", String.format("ID=%d, Winner=%s, Price=%d, Seller=%s", - listing.id, listing.highBidderName, listing.currentBid, listing.sellerName)); - } else { - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GRAY + - "Your auction for " + listing.item.getDisplayName() + - " has expired with no bids. Claim your item."); - - logEvent("AUCTION_EXPIRED", String.format("ID=%d, Item=%s, Seller=%s", - listing.id, listing.item.getDisplayName(), listing.sellerName)); - } - } - } - - if (changed) { - save(); - } - } - - /** - * Clean up old claimed listings - */ - public void cleanupOldListings() { - long claimExpirationMs = ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L; - if (claimExpirationMs <= 0) return; - - long cutoffTime = System.currentTimeMillis() - claimExpirationMs; - List toRemove = new ArrayList<>(); - - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.CLAIMED && - listing.expirationTime < cutoffTime) { - toRemove.add(listing.id); - } - } - - for (int id : toRemove) { - AuctionListing removed = listings.remove(id); - if (removed != null) { - removeFromSellerIndex(removed.sellerUUID, id); - if (removed.highBidderUUID != null) { - removeFromBidderIndex(removed.highBidderUUID, id); - } - } - } - - if (!toRemove.isEmpty()) { - save(); - logEvent("CLEANUP", "Removed " + toRemove.size() + " old listings"); - } - } - - // ==================== Notifications ==================== - - /** - * Send notification to player (or queue if offline) - */ - private void notifyPlayer(UUID playerUUID, String message) { - if (playerUUID == null) return; - - EntityPlayerMP player = getOnlinePlayer(playerUUID); - String fullMessage = EnumChatFormatting.YELLOW + "[Auction] " + message; - - if (player != null) { - player.addChatMessage(new ChatComponentText(fullMessage)); - } else { - pendingNotifications.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(fullMessage); - // Limit queue size - List queue = pendingNotifications.get(playerUUID); - while (queue.size() > 50) { - queue.remove(0); - } - } - } - - /** - * Send pending notifications on player login - */ - public void onPlayerLogin(EntityPlayerMP player) { - UUID uuid = player.getUniqueID(); - - List notifications = pendingNotifications.remove(uuid); - if (notifications != null && !notifications.isEmpty()) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + - "You have " + notifications.size() + " auction notification(s):")); - - for (String msg : notifications) { - player.addChatMessage(new ChatComponentText(msg)); - } - } - - // Check claimable items - int claimableCount = getClaimableListings(uuid).size(); - if (claimableCount > 0) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "You have " + claimableCount + " item(s) waiting to be claimed!")); - } - } - - private EntityPlayerMP getOnlinePlayer(UUID uuid) { - if (CustomNpcs.getServer() == null) return null; - - for (Object obj : CustomNpcs.getServer().getConfigurationManager().playerEntityList) { - EntityPlayerMP player = (EntityPlayerMP) obj; - if (player.getUniqueID().equals(uuid)) { - return player; - } - } - return null; - } - - // ==================== Index Management ==================== - - private void addToSellerIndex(UUID seller, int listingId) { - listingsBySeller.computeIfAbsent(seller, k -> new ArrayList<>()).add(listingId); - } - - private void removeFromSellerIndex(UUID seller, int listingId) { - List ids = listingsBySeller.get(seller); - if (ids != null) { - ids.remove(Integer.valueOf(listingId)); - } - } - - private void addToBidderIndex(UUID bidder, int listingId) { - listingsByBidder.computeIfAbsent(bidder, k -> new ArrayList<>()).add(listingId); - } - - private void removeFromBidderIndex(UUID bidder, int listingId) { - List ids = listingsByBidder.get(bidder); - if (ids != null) { - ids.remove(Integer.valueOf(listingId)); - } - } - - private void rebuildIndices() { - listingsBySeller.clear(); - listingsByBidder.clear(); - - for (AuctionListing listing : listings.values()) { - addToSellerIndex(listing.sellerUUID, listing.id); - if (listing.highBidderUUID != null) { - addToBidderIndex(listing.highBidderUUID, listing.id); - } - } - } - - // ==================== Helpers ==================== - - public int getMaxListings(EntityPlayer player) { - return ConfigMarket.DefaultMaxListings; - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private void logEvent(String event, String details) { - LogWriter.info(LOG_PREFIX + event + ": " + details); - } - - private String getListingName(AuctionListing listing) { - if (listing == null || listing.item == null) { - return ""; - } - return listing.item.getDisplayName(); - } - - // ==================== Persistence ==================== - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) return; - - try { - NBTTagCompound compound = new NBTTagCompound(); - compound.setInteger("NextId", nextId); - - NBTTagList listingList = new NBTTagList(); - for (AuctionListing listing : listings.values()) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - compound.setTag("Listings", listingList); - - File file = new File(saveDir, "auctions.dat"); - File backup = new File(saveDir, "auctions.dat_old"); - - if (file.exists()) { - if (backup.exists()) backup.delete(); - file.renameTo(backup); - } - - file = new File(saveDir, "auctions.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - - } catch (Exception e) { - LogWriter.error(LOG_PREFIX + "Error saving auctions", e); - } - } - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) return; - - listings.clear(); - - try { - File file = new File(saveDir, "auctions.dat"); - if (!file.exists()) { - file = new File(saveDir, "auctions.dat_old"); - if (!file.exists()) return; - } - - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - nextId = compound.getInteger("NextId"); - if (nextId < 1) nextId = 1; - - NBTTagList listingList = compound.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingList.getCompoundTagAt(i)); - listings.put(listing.id, listing); - } - - rebuildIndices(); - - logEvent("SYSTEM_STARTUP", String.format("Loaded %d listings, %d active", - listings.size(), getActiveListingCount())); - - } catch (Exception e) { - LogWriter.error(LOG_PREFIX + "Error loading auctions", e); - } - } - - // ==================== Statistics ==================== - - public int getActiveListingCount() { - return (int) listings.values().stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .count(); - } - - public int getTotalListingCount() { - return listings.size(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/CurrencyController.java b/src/main/java/noppes/npcs/controllers/CurrencyController.java deleted file mode 100644 index 16d6c99f4..000000000 --- a/src/main/java/noppes/npcs/controllers/CurrencyController.java +++ /dev/null @@ -1,400 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.CustomNpcs; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.PlayerData; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.*; - -/** - * Controller for managing currency operations including: - * - Claims system (for auction refunds, NPC payments, etc.) - * - Transaction history - * - Currency administration - * - * Delegates actual balance operations to VaultHelper. - */ -public class CurrencyController { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static CurrencyController Instance; - - // Pending claims per player UUID - claims that can be collected - private Map> pendingClaims = new HashMap<>(); - - // File path for persistence - private String filePath = ""; - - public CurrencyController() { - Instance = this; - load(); - } - - /** - * Get player's current balance - */ - public long getBalance(EntityPlayer player) { - return VaultHelper.Instance.getBalance(player); - } - - /** - * Deposit currency directly to player's balance - */ - public boolean deposit(EntityPlayer player, long amount) { - return VaultHelper.Instance.deposit(player, amount); - } - - /** - * Withdraw currency from player's balance - */ - public boolean withdraw(EntityPlayer player, long amount) { - return VaultHelper.Instance.withdraw(player, amount); - } - - /** - * Check if player can afford an amount - */ - public boolean canAfford(EntityPlayer player, long amount) { - return VaultHelper.Instance.canAfford(player, amount); - } - - /** - * Transfer currency between players - */ - public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { - return VaultHelper.Instance.transfer(from, to, amount); - } - - /** - * Format amount for display - */ - public String formatAmount(long amount) { - return VaultHelper.Instance.formatAmount(amount); - } - - // ==================== CLAIMS SYSTEM ==================== - - /** - * Add a claim for a player (e.g., auction refund, NPC payment) - * Claims can be collected later by the player. - * - * @param playerUUID The player's UUID - * @param amount The amount to claim - * @param source Description of where this claim came from - * @param sourceId Optional ID of the source (e.g., auction ID) - */ - public void addClaim(UUID playerUUID, long amount, String source, String sourceId) { - if (amount <= 0) { - return; - } - - CurrencyClaim claim = new CurrencyClaim(); - claim.amount = amount; - claim.source = source; - claim.sourceId = sourceId; - claim.timestamp = System.currentTimeMillis(); - claim.expiresAt = claim.timestamp + (ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L); - - List claims = pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()); - claims.add(claim); - - save(); - logger.info("Added claim of {} for player {} from {}", amount, playerUUID, source); - } - - /** - * Get all pending claims for a player - */ - public List getPendingClaims(EntityPlayer player) { - return getPendingClaims(player.getUniqueID()); - } - - /** - * Get all pending claims for a player by UUID - */ - public List getPendingClaims(UUID playerUUID) { - List claims = pendingClaims.get(playerUUID); - if (claims == null) { - return new ArrayList<>(); - } - - // Filter out expired claims - long now = System.currentTimeMillis(); - claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); - - return new ArrayList<>(claims); - } - - /** - * Get total pending claim amount for a player - */ - public long getTotalPendingClaims(EntityPlayer player) { - return getTotalPendingClaims(player.getUniqueID()); - } - - /** - * Get total pending claim amount for a player by UUID - */ - public long getTotalPendingClaims(UUID playerUUID) { - List claims = getPendingClaims(playerUUID); - long total = 0; - for (CurrencyClaim claim : claims) { - total += claim.amount; - } - return total; - } - - /** - * Claim all pending claims for a player - * @return Amount successfully claimed - */ - public long claimAll(EntityPlayer player) { - List claims = getPendingClaims(player); - if (claims.isEmpty()) { - return 0; - } - - long totalClaimed = 0; - - for (CurrencyClaim claim : claims) { - if (deposit(player, claim.amount)) { - totalClaimed += claim.amount; - } - } - - // Clear claims after successful deposit - pendingClaims.remove(player.getUniqueID()); - save(); - - return totalClaimed; - } - - /** - * Claim a specific claim by index - * @return true if successful - */ - public boolean claimSingle(EntityPlayer player, int claimIndex) { - List claims = pendingClaims.get(player.getUniqueID()); - if (claims == null || claimIndex < 0 || claimIndex >= claims.size()) { - return false; - } - - CurrencyClaim claim = claims.get(claimIndex); - if (deposit(player, claim.amount)) { - claims.remove(claimIndex); - if (claims.isEmpty()) { - pendingClaims.remove(player.getUniqueID()); - } - save(); - return true; - } - - return false; - } - - /** - * Check if player has any pending claims - */ - public boolean hasPendingClaims(EntityPlayer player) { - return !getPendingClaims(player).isEmpty(); - } - - // ==================== PERSISTENCE ==================== - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - filePath = saveDir.getAbsolutePath(); - pendingClaims.clear(); - - try { - File file = new File(saveDir, "currency_claims.dat"); - if (file.exists()) { - loadFromFile(file); - } - } catch (Exception e) { - logger.error("Error loading currency claims", e); - try { - File file = new File(saveDir, "currency_claims.dat_old"); - if (file.exists()) { - loadFromFile(file); - } - } catch (Exception ee) { - logger.error("Error loading backup currency claims", ee); - } - } - - // Clean expired claims - cleanExpiredClaims(); - } - - private void loadFromFile(File file) throws Exception { - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - NBTTagList claimsList = compound.getTagList("Claims", 10); - for (int i = 0; i < claimsList.tagCount(); i++) { - NBTTagCompound playerCompound = claimsList.getCompoundTagAt(i); - UUID playerUUID = UUID.fromString(playerCompound.getString("UUID")); - - List playerClaims = new ArrayList<>(); - NBTTagList playerClaimsList = playerCompound.getTagList("PlayerClaims", 10); - for (int j = 0; j < playerClaimsList.tagCount(); j++) { - NBTTagCompound claimCompound = playerClaimsList.getCompoundTagAt(j); - CurrencyClaim claim = new CurrencyClaim(); - claim.readFromNBT(claimCompound); - playerClaims.add(claim); - } - - if (!playerClaims.isEmpty()) { - pendingClaims.put(playerUUID, playerClaims); - } - } - - logger.info("Loaded {} player claim records", pendingClaims.size()); - } - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - try { - File file = new File(saveDir, "currency_claims.dat"); - File backup = new File(saveDir, "currency_claims.dat_old"); - - if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } - file.renameTo(backup); - } - - NBTTagCompound compound = new NBTTagCompound(); - NBTTagList claimsList = new NBTTagList(); - - for (Map.Entry> entry : pendingClaims.entrySet()) { - if (entry.getValue().isEmpty()) { - continue; - } - - NBTTagCompound playerCompound = new NBTTagCompound(); - playerCompound.setString("UUID", entry.getKey().toString()); - - NBTTagList playerClaimsList = new NBTTagList(); - for (CurrencyClaim claim : entry.getValue()) { - NBTTagCompound claimCompound = new NBTTagCompound(); - claim.writeToNBT(claimCompound); - playerClaimsList.appendTag(claimCompound); - } - playerCompound.setTag("PlayerClaims", playerClaimsList); - - claimsList.appendTag(playerCompound); - } - - compound.setTag("Claims", claimsList); - - file = new File(saveDir, "currency_claims.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - } catch (Exception e) { - logger.error("Error saving currency claims", e); - } - } - - /** - * Clean up expired claims - */ - public void cleanExpiredClaims() { - long now = System.currentTimeMillis(); - boolean changed = false; - - Iterator>> it = pendingClaims.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry> entry = it.next(); - List claims = entry.getValue(); - - int before = claims.size(); - claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); - - if (claims.size() != before) { - changed = true; - } - - if (claims.isEmpty()) { - it.remove(); - } - } - - if (changed) { - save(); - } - } - - // ==================== CLAIM DATA CLASS ==================== - - public static class CurrencyClaim { - public long amount; - public String source; // e.g., "Auction Refund", "NPC Payment", "Quest Reward" - public String sourceId; // e.g., auction ID - public long timestamp; // When the claim was created - public long expiresAt; // When the claim expires (0 = never) - - public void writeToNBT(NBTTagCompound compound) { - compound.setLong("Amount", amount); - compound.setString("Source", source != null ? source : ""); - compound.setString("SourceId", sourceId != null ? sourceId : ""); - compound.setLong("Timestamp", timestamp); - compound.setLong("ExpiresAt", expiresAt); - } - - public void readFromNBT(NBTTagCompound compound) { - amount = compound.getLong("Amount"); - source = compound.getString("Source"); - sourceId = compound.getString("SourceId"); - timestamp = compound.getLong("Timestamp"); - expiresAt = compound.getLong("ExpiresAt"); - } - - /** - * Get formatted time remaining until expiration - */ - public String getTimeRemaining() { - if (expiresAt <= 0) { - return "Never"; - } - - long remaining = expiresAt - System.currentTimeMillis(); - if (remaining <= 0) { - return "Expired"; - } - - long days = remaining / (24L * 60L * 60L * 1000L); - long hours = (remaining % (24L * 60L * 60L * 1000L)) / (60L * 60L * 1000L); - - if (days > 0) { - return days + " days, " + hours + " hours"; - } else if (hours > 0) { - return hours + " hours"; - } else { - long minutes = remaining / (60L * 1000L); - return minutes + " minutes"; - } - } - } -} diff --git a/src/main/java/noppes/npcs/controllers/MarketRegistry.java b/src/main/java/noppes/npcs/controllers/MarketRegistry.java new file mode 100644 index 000000000..1110f9e5f --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/MarketRegistry.java @@ -0,0 +1,214 @@ +package noppes.npcs.controllers; + +import foxz.utils.Market; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.roles.RoleTrader; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Central registry for linked market synchronization. + * Tracks which RoleTrader instances use which markets, + * and which players are viewing each market. + * + * This enables immediate sync across all traders sharing + * the same market when a purchase occurs. + */ +public class MarketRegistry { + + // marketName -> Set of traders using this market (WeakHashMap for auto-cleanup) + private static final Map> marketTraders = new ConcurrentHashMap<>(); + + // marketName -> Set of players viewing this market (across all NPCs) + private static final Map> marketViewers = new ConcurrentHashMap<>(); + + // ==================== Trader Registration ==================== + + /** + * Register a trader as using a linked market. + * Should be called when NPC loads or marketName is set. + */ + public static void registerTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (traders) { + traders.add(trader); + } + } + + /** + * Unregister a trader from a linked market. + * Should be called when NPC unloads or marketName changes. + */ + public static void unregisterTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.get(marketName); + if (traders != null) { + synchronized (traders) { + traders.remove(trader); + if (traders.isEmpty()) { + marketTraders.remove(marketName); + } + } + } + } + + /** + * Change a trader's market registration. + * Handles unregistering from old market and registering to new. + */ + public static void updateTraderMarket(String oldMarket, String newMarket, RoleTrader trader) { + if (trader == null) return; + + if (oldMarket != null && !oldMarket.isEmpty()) { + unregisterTrader(oldMarket, trader); + } + if (newMarket != null && !newMarket.isEmpty()) { + registerTrader(newMarket, trader); + } + } + + // ==================== Viewer Registration ==================== + + /** + * Register a player as viewing a market (via any NPC). + * Should be called when player opens trader GUI with linked market. + */ + public static void registerViewer(String marketName, EntityPlayerMP player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (viewers) { + viewers.add(player); + } + } + + /** + * Unregister a player from viewing a market. + * Should be called when player closes trader GUI. + */ + public static void unregisterViewer(String marketName, EntityPlayerMP player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.get(marketName); + if (viewers != null) { + synchronized (viewers) { + viewers.remove(player); + if (viewers.isEmpty()) { + marketViewers.remove(marketName); + } + } + } + } + + // ==================== Synchronization ==================== + + /** + * Sync stock data to all traders and viewers of a market. + * Called after a purchase to update all linked traders and their viewers. + * + * @param marketName The market to sync + */ + public static void syncMarket(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + // Get latest data from cache + NBTTagCompound marketData = Market.getMarketCache(marketName); + if (marketData == null) return; + + // Update all traders' internal stock data + Set traders = marketTraders.get(marketName); + if (traders != null) { + NBTTagCompound stockData = marketData.getCompoundTag("Stock"); + + synchronized (traders) { + // Clean up any null references (WeakHashMap cleanup) + traders.removeIf(t -> t == null || t.npc == null); + + for (RoleTrader trader : traders) { + // Update stock data from cache + if (stockData != null && stockData.hasKey("EnableStock")) { + trader.stock.readFromNBT(stockData); + } + } + } + } + + // Sync all viewers (across all NPCs for this market) + syncMarketViewers(marketName); + } + + /** + * Sync stock data to all players viewing a specific market. + * Each viewer gets data relative to their own player (for per-player stock display). + */ + public static void syncMarketViewers(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + Set viewers = marketViewers.get(marketName); + if (viewers == null || viewers.isEmpty()) return; + + // Find any trader with this market to use for sync (they all have same data now) + Set traders = marketTraders.get(marketName); + if (traders == null || traders.isEmpty()) return; + + RoleTrader syncSource = null; + synchronized (traders) { + for (RoleTrader trader : traders) { + if (trader != null && trader.npc != null) { + syncSource = trader; + break; + } + } + } + + if (syncSource == null) return; + + // Sync to all viewers + synchronized (viewers) { + // Remove disconnected players + viewers.removeIf(p -> p == null || p.playerNetServerHandler == null); + + for (EntityPlayerMP viewer : viewers) { + syncSource.syncToPlayer(viewer); + } + } + } + + // ==================== Utility ==================== + + /** + * Get count of traders registered to a market (for debugging/info). + */ + public static int getTraderCount(String marketName) { + Set traders = marketTraders.get(marketName); + return traders != null ? traders.size() : 0; + } + + /** + * Get count of viewers for a market (for debugging/info). + */ + public static int getViewerCount(String marketName) { + Set viewers = marketViewers.get(marketName); + return viewers != null ? viewers.size() : 0; + } + + /** + * Clear all registrations (for server shutdown/reload). + */ + public static void clear() { + marketTraders.clear(); + marketViewers.clear(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java deleted file mode 100644 index 54b9235ef..000000000 --- a/src/main/java/noppes/npcs/controllers/VaultHelper.java +++ /dev/null @@ -1,272 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import noppes.npcs.CustomNpcs; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.controllers.PlayerDataController; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.lang.reflect.Method; - -/** - * Helper class for VaultAPI integration on Bukkit hybrid servers. - * Uses reflection to integrate with Vault when available. - * Falls back to internal currency system when Vault is not present. - */ -public class VaultHelper { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static VaultHelper Instance; - - // Vault reflection references - private boolean vaultAvailable = false; - private Object economy = null; - private Method getBalance; - private Method depositPlayer; - private Method withdrawPlayer; - private Method hasAccount; - private Method createPlayerAccount; - private Method responseSuccess; - - // Bukkit references for getting offline player - private Class bukkitClass; - private Method getOfflinePlayer; - - public VaultHelper() { - Instance = this; - - if (!ConfigMarket.UseVaultIfAvailable) { - logger.info("Vault integration disabled by config"); - return; - } - - try { - // Check for Bukkit first - bukkitClass = Class.forName("org.bukkit.Bukkit"); - getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); - - // Check for Vault - Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); - - // Get the Economy service from Bukkit's ServicesManager - Method getServicesManager = bukkitClass.getMethod("getServicesManager"); - Object servicesManager = getServicesManager.invoke(null); - - Method getRegistration = servicesManager.getClass().getMethod("getRegistration", Class.class); - Object registration = getRegistration.invoke(servicesManager, vaultClass); - - if (registration != null) { - Method getProvider = registration.getClass().getMethod("getProvider"); - economy = getProvider.invoke(registration); - - if (economy != null) { - // Get Economy methods - using OfflinePlayer versions for broader compatibility - Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); - getBalance = economy.getClass().getMethod("getBalance", offlinePlayerClass); - depositPlayer = economy.getClass().getMethod("depositPlayer", offlinePlayerClass, double.class); - withdrawPlayer = economy.getClass().getMethod("withdrawPlayer", offlinePlayerClass, double.class); - hasAccount = economy.getClass().getMethod("hasAccount", offlinePlayerClass); - createPlayerAccount = economy.getClass().getMethod("createPlayerAccount", offlinePlayerClass); - - // EconomyResponse.transactionSuccess() method - Class economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); - responseSuccess = economyResponseClass.getField("transactionSuccess").getClass().getMethod("booleanValue"); - - vaultAvailable = true; - logger.info("Vault Economy integration enabled - using external economy"); - } - } - - if (!vaultAvailable) { - logger.info("Vault found but no Economy provider registered - using internal currency"); - } - } catch (ClassNotFoundException e) { - // Vault not present - use internal currency - logger.info("Vault not found - using internal currency system"); - } catch (Exception e) { - logger.warn("Error initializing Vault integration - using internal currency: " + e.getMessage()); - } - } - - /** - * Check if Vault economy is available and enabled - */ - public boolean isVaultEnabled() { - return vaultAvailable && ConfigMarket.UseVaultIfAvailable; - } - - /** - * Get player's balance - * Uses Vault if available, otherwise internal currency - */ - public long getBalance(EntityPlayer player) { - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - Double balance = (Double) getBalance.invoke(economy, offlinePlayer); - return balance.longValue(); - } catch (Exception e) { - logger.warn("Vault getBalance failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - return data.currencyData.getBalance(); - } - - /** - * Deposit currency to player's balance - * Uses Vault if available, otherwise internal currency - * @return true if successful - */ - public boolean deposit(EntityPlayer player, long amount) { - if (amount <= 0) { - return false; - } - - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - - // Ensure account exists - Boolean hasAcc = (Boolean) hasAccount.invoke(economy, offlinePlayer); - if (!hasAcc) { - createPlayerAccount.invoke(economy, offlinePlayer); - } - - Object response = depositPlayer.invoke(economy, offlinePlayer, (double) amount); - // Check if transaction succeeded by checking the transactionSuccess field - Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); - - if (success) { - // Also update internal tracking for statistics - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(getBalance(player)); - return true; - } - return false; - } catch (Exception e) { - logger.warn("Vault deposit failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - boolean success = data.currencyData.deposit(amount); - if (success) { - data.save(); - } - return success; - } - - /** - * Withdraw currency from player's balance - * Uses Vault if available, otherwise internal currency - * @return true if successful - */ - public boolean withdraw(EntityPlayer player, long amount) { - if (amount <= 0) { - return false; - } - - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - - // Check if player has enough - Double currentBalance = (Double) getBalance.invoke(economy, offlinePlayer); - if (currentBalance < amount) { - return false; - } - - Object response = withdrawPlayer.invoke(economy, offlinePlayer, (double) amount); - Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); - - if (success) { - // Also update internal tracking for statistics - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(getBalance(player)); - return true; - } - return false; - } catch (Exception e) { - logger.warn("Vault withdraw failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - boolean success = data.currencyData.withdraw(amount); - if (success) { - data.save(); - } - return success; - } - - /** - * Check if player can afford an amount - */ - public boolean canAfford(EntityPlayer player, long amount) { - return getBalance(player) >= amount; - } - - /** - * Transfer currency from one player to another - * @return true if successful - */ - public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { - if (amount <= 0) { - return false; - } - - if (!canAfford(from, amount)) { - return false; - } - - // Withdraw from sender first - if (!withdraw(from, amount)) { - return false; - } - - // Deposit to receiver - if (!deposit(to, amount)) { - // Rollback - deposit back to sender - deposit(from, amount); - return false; - } - - return true; - } - - /** - * Set player's balance directly (admin operation) - * Only works with internal currency - Vault manages its own balances - */ - public void setBalance(EntityPlayer player, long amount) { - if (isVaultEnabled()) { - logger.warn("Cannot set balance directly when using Vault - use Vault's economy commands"); - return; - } - - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(amount); - data.save(); - } - - /** - * Format an amount for display with currency name - */ - public String formatAmount(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - /** - * Get the currency name - */ - public String getCurrencyName() { - return ConfigMarket.CurrencyName; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java deleted file mode 100644 index 176896fae..000000000 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ /dev/null @@ -1,226 +0,0 @@ -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.constants.EnumAuctionSort; - -/** - * Filter/search criteria for auction listings. - * Used to query the auction controller with specific criteria. - */ -public class AuctionFilter { - - /** Search text to match against item names */ - public String searchText = ""; - - /** How to sort results */ - public EnumAuctionSort sortOrder = EnumAuctionSort.ENDING_SOON; - - /** Minimum current bid/price filter */ - public long minPrice = 0; - - /** Maximum current bid/price filter (0 = no max) */ - public long maxPrice = 0; - - /** Only show listings with buyout option */ - public boolean buyoutOnly = false; - - /** Only show listings without any bids yet */ - public boolean noBidsOnly = false; - - /** Page number for pagination (0-indexed) */ - public int page = 0; - - /** Items per page */ - public int pageSize = 20; - - /** Filter by specific seller UUID string (empty = all sellers) */ - public String sellerUUID = ""; - - /** Filter by specific seller name (partial match, empty = all) */ - public String sellerName = ""; - - public AuctionFilter() { - } - - /** - * Create a filter with search text - */ - public static AuctionFilter search(String text) { - AuctionFilter filter = new AuctionFilter(); - filter.searchText = text != null ? text : ""; - return filter; - } - - /** - * Create a filter for a specific seller - */ - public static AuctionFilter seller(String sellerUUID) { - AuctionFilter filter = new AuctionFilter(); - filter.sellerUUID = sellerUUID != null ? sellerUUID : ""; - return filter; - } - - // Builder-style methods for chaining - - public AuctionFilter withSearch(String text) { - this.searchText = text != null ? text : ""; - return this; - } - - public AuctionFilter withSort(EnumAuctionSort sort) { - this.sortOrder = sort; - return this; - } - - public AuctionFilter withPriceRange(long min, long max) { - this.minPrice = min; - this.maxPrice = max; - return this; - } - - public AuctionFilter withBuyoutOnly(boolean buyoutOnly) { - this.buyoutOnly = buyoutOnly; - return this; - } - - public AuctionFilter withNoBidsOnly(boolean noBidsOnly) { - this.noBidsOnly = noBidsOnly; - return this; - } - - public AuctionFilter withPage(int page, int pageSize) { - this.page = Math.max(0, page); - this.pageSize = Math.max(1, Math.min(100, pageSize)); - return this; - } - - public AuctionFilter withSeller(String sellerUUID, String sellerName) { - this.sellerUUID = sellerUUID != null ? sellerUUID : ""; - this.sellerName = sellerName != null ? sellerName : ""; - return this; - } - - /** - * Check if a listing matches this filter's criteria - */ - public boolean matches(AuctionListing listing) { - if (listing == null) { - return false; - } - if (listing.item == null) { - return false; - } - - // Search text filter (case-insensitive) - if (!searchText.isEmpty()) { - String lowerSearch = searchText.toLowerCase(); - String itemName = listing.item.getDisplayName().toLowerCase(); - if (!itemName.contains(lowerSearch)) { - return false; - } - } - - // Price range filter (uses current bid or starting price) - long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - if (minPrice > 0 && price < minPrice) { - return false; - } - if (maxPrice > 0 && price > maxPrice) { - return false; - } - - // Buyout only filter - if (buyoutOnly && listing.buyoutPrice <= 0) { - return false; - } - - // No bids only filter - if (noBidsOnly && listing.hasBids()) { - return false; - } - - // Seller UUID filter - if (!sellerUUID.isEmpty() && !listing.sellerUUID.toString().equals(sellerUUID)) { - return false; - } - - // Seller name filter (partial match) - if (!sellerName.isEmpty()) { - String lowerName = sellerName.toLowerCase(); - if (!listing.sellerName.toLowerCase().contains(lowerName)) { - return false; - } - } - - return true; - } - - /** - * Reset all filters to defaults - */ - public void reset() { - searchText = ""; - sortOrder = EnumAuctionSort.ENDING_SOON; - minPrice = 0; - maxPrice = 0; - buyoutOnly = false; - noBidsOnly = false; - page = 0; - sellerUUID = ""; - sellerName = ""; - } - - /** - * Write filter to NBT for network transmission - */ - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("SearchText", searchText); - compound.setInteger("SortOrder", sortOrder.ordinal()); - compound.setLong("MinPrice", minPrice); - compound.setLong("MaxPrice", maxPrice); - compound.setBoolean("BuyoutOnly", buyoutOnly); - compound.setBoolean("NoBidsOnly", noBidsOnly); - compound.setInteger("Page", page); - compound.setInteger("PageSize", pageSize); - compound.setString("SellerUUID", sellerUUID); - compound.setString("SellerName", sellerName); - return compound; - } - - /** - * Read filter from NBT - */ - public void readFromNBT(NBTTagCompound compound) { - searchText = compound.getString("SearchText"); - - int sortOrdinal = compound.getInteger("SortOrder"); - if (sortOrdinal >= 0 && sortOrdinal < EnumAuctionSort.values().length) { - sortOrder = EnumAuctionSort.values()[sortOrdinal]; - } - - minPrice = compound.getLong("MinPrice"); - maxPrice = compound.getLong("MaxPrice"); - buyoutOnly = compound.getBoolean("BuyoutOnly"); - noBidsOnly = compound.getBoolean("NoBidsOnly"); - page = Math.max(0, compound.getInteger("Page")); - pageSize = compound.getInteger("PageSize"); - if (pageSize <= 0) { - pageSize = 20; - } else { - pageSize = Math.min(100, pageSize); - } - sellerUUID = compound.getString("SellerUUID"); - sellerName = compound.getString("SellerName"); - } - - @Override - public String toString() { - return "AuctionFilter{" + - "search='" + searchText + '\'' + - ", sort=" + sortOrder + - ", price=" + minPrice + "-" + maxPrice + - ", buyoutOnly=" + buyoutOnly + - ", page=" + page + - '}'; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java deleted file mode 100644 index 5366e367b..000000000 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ /dev/null @@ -1,397 +0,0 @@ -package noppes.npcs.controllers.data; - -import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.AuctionBlacklist; - -import java.util.UUID; - -/** - * Represents a single auction listing. - * Contains all data about the item, seller, bidding, and timing. - */ -public class AuctionListing { - // Unique identifier - public int id; - - // Seller info - public UUID sellerUUID; - public String sellerName; - - // Item being sold - public ItemStack item; - - // Pricing - public long startingPrice; - public long buyoutPrice; // 0 = no buyout option - public long currentBid; - - // Current high bidder - public UUID highBidderUUID; - public String highBidderName; - - // Previous bidder (for refund notifications) - public UUID previousBidderUUID; - public String previousBidderName; - public long previousBidAmount; - - // Timing - public long listingTime; // When the auction was created - public long expirationTime; // When the auction ends - public EnumAuctionDuration duration; - - // Status - public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; - - // Claim tracking - public boolean sellerClaimed = false; // Has seller claimed their proceeds? - public boolean buyerClaimed = false; // Has buyer claimed their item? - - // Bid tracking - public int bidCount = 0; // Number of bids placed - - public AuctionListing() { - } - - /** - * Alias for listingTime (when the auction was created) - */ - public long getCreationTime() { - return listingTime; - } - - // For sorting compatibility - public long creationTime() { - return listingTime; - } - - /** - * Create a new auction listing - */ - public AuctionListing(UUID seller, String sellerName, ItemStack item, - long startPrice, long buyout, EnumAuctionDuration duration) { - this.sellerUUID = seller; - this.sellerName = sellerName; - this.item = item.copy(); - this.startingPrice = startPrice; - this.buyoutPrice = buyout; - this.currentBid = 0; // No bids yet - this.duration = duration; - this.listingTime = System.currentTimeMillis(); - this.expirationTime = listingTime + duration.getMillis(); - this.status = EnumAuctionStatus.ACTIVE; - } - - // ==================== Status Checks ==================== - - /** - * Check if auction has expired - */ - public boolean isExpired() { - return System.currentTimeMillis() >= expirationTime; - } - - /** - * Check if auction is still active - */ - public boolean isActive() { - return status == EnumAuctionStatus.ACTIVE && !isExpired(); - } - - /** - * Get time remaining in milliseconds - */ - public long getTimeRemaining() { - return Math.max(0, expirationTime - System.currentTimeMillis()); - } - - /** - * Get formatted time remaining - */ - public String getTimeRemainingFormatted() { - long remaining = getTimeRemaining(); - - if (remaining <= 0) { - return "Ended"; - } - - long hours = remaining / (60L * 60L * 1000L); - long minutes = (remaining % (60L * 60L * 1000L)) / (60L * 1000L); - - if (hours > 24) { - long days = hours / 24; - hours = hours % 24; - return days + "d " + hours + "h"; - } else if (hours > 0) { - return hours + "h " + minutes + "m"; - } else { - long seconds = (remaining % (60L * 1000L)) / 1000L; - return minutes + "m " + seconds + "s"; - } - } - - /** - * Check if auction has any bids - */ - public boolean hasBids() { - return highBidderUUID != null; - } - - // ==================== Bidding Methods ==================== - - /** - * Calculate minimum valid bid amount - */ - public long getMinimumBid() { - if (!hasBids()) { - return startingPrice; - } - // Minimum increment is configured percent or 1, whichever is greater - long minIncrement = Math.max(1, (long) (currentBid * ConfigMarket.MinBidIncrementPercent)); - return currentBid + minIncrement; - } - - /** - * Place a bid - * @return true if bid was successful - */ - public boolean placeBid(UUID bidder, String bidderName, long amount) { - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - if (isExpired()) { - return false; - } - if (amount < getMinimumBid()) { - return false; - } - if (bidder.equals(sellerUUID)) { - return false; // Can't bid on own item - } - - // Store previous bidder for refund notification - if (highBidderUUID != null) { - previousBidderUUID = highBidderUUID; - previousBidderName = highBidderName; - previousBidAmount = currentBid; - } - - // Update high bidder - highBidderUUID = bidder; - highBidderName = bidderName; - currentBid = amount; - bidCount++; - - // Snipe protection - extend if bid placed in final minutes - if (ConfigMarket.SnipeProtectionMinutes > 0) { - long timeRemaining = getTimeRemaining(); - long thresholdMs = ConfigMarket.SnipeProtectionThreshold * 60L * 1000L; - - if (timeRemaining < thresholdMs) { - // Extend TO snipe protection time, not by it - long newExpiration = System.currentTimeMillis() + - (ConfigMarket.SnipeProtectionMinutes * 60L * 1000L); - // Only extend if it would actually extend the auction - if (newExpiration > expirationTime) { - expirationTime = newExpiration; - } - } - } - - return true; - } - - /** - * Execute buyout (instant purchase at buyout price) - * @return true if buyout was successful - */ - public boolean buyout(UUID buyer, String buyerName) { - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - if (isExpired()) { - return false; - } - if (buyoutPrice <= 0) { - return false; // No buyout available - } - if (buyer.equals(sellerUUID)) { - return false; // Can't buy own item - } - - // Store previous bidder for refund - if (highBidderUUID != null) { - previousBidderUUID = highBidderUUID; - previousBidderName = highBidderName; - previousBidAmount = currentBid; - } - - highBidderUUID = buyer; - highBidderName = buyerName; - currentBid = buyoutPrice; - status = EnumAuctionStatus.SOLD; - - return true; - } - - /** - * Cancel listing (seller only, before any bids) - * @return true if cancellation was successful - */ - public boolean cancel(UUID requester) { - if (!requester.equals(sellerUUID)) { - return false; // Only seller can cancel - } - if (hasBids()) { - return false; // Can't cancel with active bids - } - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - - status = EnumAuctionStatus.CANCELLED; - return true; - } - - /** - * Finalize auction when it expires - * Call this when auction timer runs out - */ - public void finalize() { - if (status != EnumAuctionStatus.ACTIVE) { - return; - } - - if (hasBids()) { - status = EnumAuctionStatus.SOLD; - } else { - status = EnumAuctionStatus.EXPIRED; - } - } - - // ==================== Validation ==================== - - /** - * Validate that the item can be listed - * @return null if valid, error message if invalid - */ - public static String validateItem(ItemStack item) { - if (item == null || item.getItem() == null) { - return "Invalid item"; - } - - // Check trade restrictions (Untradeable, ProfileSlot Bound, Soulbound) - if (!ItemTradeAttribute.canTrade(item)) { - String reason = ItemTradeAttribute.getTradeBlockReason(item); - return reason != null ? reason : "Item cannot be traded"; - } - - // Check blacklist - if (AuctionBlacklist.Instance != null && AuctionBlacklist.Instance.isBlacklisted(item)) { - return "Item is blacklisted from auction"; - } - - return null; // Valid - } - - // ==================== Financial Calculations ==================== - - /** - * Get listing fee for this auction - */ - public long getListingFee() { - long flatFee = duration.getListingFee(); - long percentFee = (long) (startingPrice * ConfigMarket.ListingFeePercent); - return flatFee + percentFee; - } - - /** - * Get seller's proceeds after sales tax - */ - public long getSellerProceeds() { - if (status != EnumAuctionStatus.SOLD) { - return 0; - } - long tax = (long) (currentBid * ConfigMarket.SalesTaxPercent); - return currentBid - tax; - } - - // ==================== NBT Serialization ==================== - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("Id", id); - compound.setString("SellerUUID", sellerUUID.toString()); - compound.setString("SellerName", sellerName); - compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); - compound.setLong("StartPrice", startingPrice); - compound.setLong("BuyoutPrice", buyoutPrice); - compound.setLong("CurrentBid", currentBid); - - if (highBidderUUID != null) { - compound.setString("BidderUUID", highBidderUUID.toString()); - compound.setString("BidderName", highBidderName); - } - - if (previousBidderUUID != null) { - compound.setString("PrevBidderUUID", previousBidderUUID.toString()); - compound.setString("PrevBidderName", previousBidderName); - compound.setLong("PrevBidAmount", previousBidAmount); - } - - compound.setLong("ListingTime", listingTime); - compound.setLong("ExpirationTime", expirationTime); - compound.setInteger("Duration", duration.ordinal()); - compound.setInteger("Status", status.ordinal()); - compound.setBoolean("SellerClaimed", sellerClaimed); - compound.setBoolean("BuyerClaimed", buyerClaimed); - compound.setInteger("BidCount", bidCount); - - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - id = compound.getInteger("Id"); - sellerUUID = UUID.fromString(compound.getString("SellerUUID")); - sellerName = compound.getString("SellerName"); - item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); - startingPrice = compound.getLong("StartPrice"); - buyoutPrice = compound.getLong("BuyoutPrice"); - currentBid = compound.getLong("CurrentBid"); - - if (compound.hasKey("BidderUUID")) { - highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); - highBidderName = compound.getString("BidderName"); - } - - if (compound.hasKey("PrevBidderUUID")) { - previousBidderUUID = UUID.fromString(compound.getString("PrevBidderUUID")); - previousBidderName = compound.getString("PrevBidderName"); - previousBidAmount = compound.getLong("PrevBidAmount"); - } - - listingTime = compound.getLong("ListingTime"); - expirationTime = compound.getLong("ExpirationTime"); - - int durationOrdinal = compound.getInteger("Duration"); - if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { - duration = EnumAuctionDuration.values()[durationOrdinal]; - } else { - duration = EnumAuctionDuration.LONG; - } - - int statusOrdinal = compound.getInteger("Status"); - if (statusOrdinal >= 0 && statusOrdinal < EnumAuctionStatus.values().length) { - status = EnumAuctionStatus.values()[statusOrdinal]; - } else { - status = EnumAuctionStatus.ACTIVE; - } - - sellerClaimed = compound.getBoolean("SellerClaimed"); - buyerClaimed = compound.getBoolean("BuyerClaimed"); - bidCount = compound.getInteger("BidCount"); - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java index 511321dbb..1271594ba 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -1,24 +1,49 @@ package noppes.npcs.controllers.data; +import kamkeel.npcs.util.VaultUtil; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IPlayerCurrencyData; import noppes.npcs.config.ConfigMarket; /** * Stores player currency balance. * This data is NOT profile slot bound - all profile slots share the same currency pool. - * When Vault API is available, this serves as a cache/fallback. + * When Vault API is available and UseVault config is enabled, operations delegate to Vault. + * The built-in CNPC+ currency data is always preserved in NBT, but unused when Vault is active. */ -public class PlayerCurrencyData { +public class PlayerCurrencyData implements IPlayerCurrencyData { + private final PlayerData playerData; + + // Built-in CNPC+ currency (always saved, used as fallback) private long balance; private long lifetimeEarned; private long lifetimeSpent; - public PlayerCurrencyData() { + public PlayerCurrencyData(PlayerData playerData) { + this.playerData = playerData; this.balance = ConfigMarket.StartingBalance; this.lifetimeEarned = 0; this.lifetimeSpent = 0; } + /** + * Check if Vault should be used for currency operations. + * Requires both config enabled AND Vault actually available. + */ + private boolean shouldUseVault() { + return ConfigMarket.UseVault && VaultUtil.isEnabled(); + } + + /** + * Get player name for Vault operations + */ + private String getPlayerName() { + if (playerData != null && playerData.player != null) { + return playerData.player.getCommandSenderName(); + } + return playerData != null ? playerData.playername : null; + } + public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("CurrencyBalance")) { this.balance = compound.getLong("CurrencyBalance"); @@ -30,6 +55,7 @@ public class PlayerCurrencyData { } public void writeToNBT(NBTTagCompound compound) { + // Always save built-in currency data (preserved even when using Vault) compound.setLong("CurrencyBalance", this.balance); compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); @@ -38,13 +64,21 @@ public class PlayerCurrencyData { /** * Get the current balance */ + @Override public long getBalance() { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return (long) VaultUtil.getBalance(playerName); + } + } return balance; } /** * Set the balance directly (use with caution) */ + @Override public void setBalance(long balance) { if (balance < 0) { balance = 0; @@ -52,6 +86,14 @@ public class PlayerCurrencyData { if (balance > ConfigMarket.MaxBalance) { balance = ConfigMarket.MaxBalance; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + VaultUtil.setBalance(playerName, balance); + return; + } + } this.balance = balance; } @@ -60,10 +102,24 @@ public class PlayerCurrencyData { * @param amount Amount to add * @return true if successful, false if would exceed max balance */ + @Override public boolean deposit(long amount) { if (amount <= 0) { return false; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.addMoney(playerName, amount); + if (success) { + this.lifetimeEarned += amount; + } + return success; + } + } + + // Built-in currency long newBalance = this.balance + amount; if (newBalance > ConfigMarket.MaxBalance) { return false; @@ -82,10 +138,24 @@ public class PlayerCurrencyData { * @param amount Amount to remove * @return true if successful, false if insufficient funds */ + @Override public boolean withdraw(long amount) { if (amount <= 0) { return false; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.withdrawMoney(playerName, amount); + if (success) { + this.lifetimeSpent += amount; + } + return success; + } + } + + // Built-in currency if (this.balance < amount) { return false; } @@ -97,13 +167,21 @@ public class PlayerCurrencyData { /** * Check if player can afford an amount */ + @Override public boolean canAfford(long amount) { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return VaultUtil.has(playerName, amount); + } + } return this.balance >= amount; } /** * Get lifetime earned currency */ + @Override public long getLifetimeEarned() { return lifetimeEarned; } @@ -111,6 +189,7 @@ public class PlayerCurrencyData { /** * Get lifetime spent currency */ + @Override public long getLifetimeSpent() { return lifetimeSpent; } @@ -118,7 +197,11 @@ public class PlayerCurrencyData { /** * Format the balance for display */ + @Override public String formatBalance() { + if (shouldUseVault()) { + return VaultUtil.format(getBalance()); + } return formatAmount(this.balance); } @@ -128,4 +211,20 @@ public class PlayerCurrencyData { public static String formatAmount(long amount) { return String.format("%,d %s", amount, ConfigMarket.CurrencyName); } + + /** + * Check if Vault is being used for currency operations. + * Returns true only if config is enabled AND Vault is actually available. + */ + @Override + public boolean isUsingVault() { + return shouldUseVault(); + } + + /** + * Check if Vault is configured (regardless of availability) + */ + public boolean isVaultConfigured() { + return ConfigMarket.UseVault; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 08fdebdd0..d2a949074 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -17,6 +17,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; +import noppes.npcs.api.handler.IPlayerCurrencyData; import noppes.npcs.api.handler.IPlayerData; import noppes.npcs.api.handler.IPlayerDialogData; import noppes.npcs.api.handler.IPlayerFactionData; @@ -60,7 +61,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public MagicData magicData = new MagicData(); // Currency data - NOT slot-bound, shared across all profile slots - public PlayerCurrencyData currencyData = new PlayerCurrencyData(); + public PlayerCurrencyData currencyData = new PlayerCurrencyData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -400,6 +401,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return mailData; } + public IPlayerCurrencyData getCurrencyData() { + return currencyData; + } + public synchronized void save() { // Don't Save this is a Modification of a Profile's PlayerData Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid)); @@ -409,7 +414,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); - // Add currency data to main save (NOT slot-bound, preserved across profile changes) + // Currency data is saved at player level, NOT per profile slot currencyData.writeToNBT(compound); final String filename; @@ -449,11 +454,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { data = getNBT(); } - // Load currency BEFORE setNBT so we can preserve it - // Currency is stored in main file but NOT in slot data - if (data.hasKey("CurrencyBalance")) { - currencyData.readFromNBT(data); - } + // Load currency BEFORE setNBT - currency is player-level, NOT per profile slot + currencyData.readFromNBT(data); setNBT(data); } diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index f3c5f4db9..68bc96c53 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -45,7 +45,7 @@ public class TraderStock { * @param playerName The player name (for per-player mode) * @return Available stock count, or Integer.MAX_VALUE if unlimited */ - public int getAvailableStock(int slot, String playerName) { + public synchronized int getAvailableStock(int slot, String playerName) { if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { return Integer.MAX_VALUE; // Unlimited } @@ -75,7 +75,7 @@ public class TraderStock { * Consume stock when purchase is made * @return true if successful, false if insufficient stock */ - public boolean consumeStock(int slot, String playerName, int amount) { + public synchronized boolean consumeStock(int slot, String playerName, int amount) { if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { return true; // Unlimited, always succeeds } @@ -115,7 +115,7 @@ public class TraderStock { * Reset all stock to max values * @param currentTime Current time to record as reset time */ - public void resetStock(long currentTime) { + public synchronized void resetStock(long currentTime) { lastResetTime = currentTime; // Reset per-server stock @@ -123,7 +123,7 @@ public class TraderStock { currentStock[i] = maxStock[i]; } - // Clear per-player stock + // Clear per-player stock (cleanup per-player data on reset) playerStock.clear(); } @@ -217,6 +217,44 @@ public class TraderStock { } } + /** + * Validate and clamp all current stock values to not exceed max stock. + * Should be called when trader GUI opens to ensure consistency. + * @return true if any stock was clamped + */ + public boolean validateStock() { + boolean changed = false; + for (int i = 0; i < 18; i++) { + if (maxStock[i] >= 0) { + // Has a max limit + if (currentStock[i] < 0) { + // Not initialized, set to max + currentStock[i] = maxStock[i]; + changed = true; + } else if (currentStock[i] > maxStock[i]) { + // Exceeds max, clamp down + currentStock[i] = maxStock[i]; + changed = true; + } + } + } + + // Also validate per-player stock + for (PlayerTraderStock pStock : playerStock.values()) { + for (int i = 0; i < 18; i++) { + if (maxStock[i] >= 0) { + int available = pStock.getStock(i, maxStock[i]); + if (available < 0) { + // Over-purchased somehow, reset + pStock.purchasedAmounts[i] = maxStock[i]; + changed = true; + } + } + } + } + return changed; + } + // ==================== NBT Serialization ==================== public NBTTagCompound writeToNBT(NBTTagCompound compound) { @@ -280,7 +318,6 @@ public class TraderStock { public static class PlayerTraderStock { public int[] purchasedAmounts = new int[18]; - public long lastPurchaseTime = 0; public PlayerTraderStock() { for (int i = 0; i < 18; i++) { @@ -297,7 +334,6 @@ public class TraderStock { if (slot < 0 || slot >= 18) return false; if (getStock(slot, maxStock) >= amount) { purchasedAmounts[slot] += amount; - lastPurchaseTime = System.currentTimeMillis(); return true; } return false; @@ -305,7 +341,6 @@ public class TraderStock { public void writeToNBT(NBTTagCompound compound) { compound.setIntArray("Purchased", purchasedAmounts); - compound.setLong("LastPurchase", lastPurchaseTime); } public void readFromNBT(NBTTagCompound compound) { @@ -313,7 +348,6 @@ public class TraderStock { if (loaded != null && loaded.length == 18) { purchasedAmounts = loaded; } - lastPurchaseTime = compound.getLong("LastPurchase"); } } } diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java deleted file mode 100644 index 4d8febaf1..000000000 --- a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java +++ /dev/null @@ -1,135 +0,0 @@ -package noppes.npcs.roles; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NoppesUtilServer; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumGuiType; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.data.Line; -import noppes.npcs.entity.EntityNPCInterface; - -/** - * NPC role for Auctioneer - provides access to the global auction house. - */ -public class RoleAuctioneer extends RoleInterface { - - // Configuration - public boolean showActiveListings = true; // Show active listings tab - public boolean showMyListings = true; // Show player's listings tab - public boolean showMyBids = true; // Show player's bids tab - public boolean showClaims = true; // Show claimable items tab - public boolean allowCreatingListings = true; // Allow creating new listings - public boolean allowBidding = true; // Allow bidding on items - public boolean allowBuyout = true; // Allow buyout purchases - - // Custom greeting messages - public String welcomeMessage = ""; - public String noAuctionMessage = ""; - - public RoleAuctioneer(EntityNPCInterface npc) { - super(npc); - } - - @Override - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setBoolean("ShowActiveListings", showActiveListings); - compound.setBoolean("ShowMyListings", showMyListings); - compound.setBoolean("ShowMyBids", showMyBids); - compound.setBoolean("ShowClaims", showClaims); - compound.setBoolean("AllowCreatingListings", allowCreatingListings); - compound.setBoolean("AllowBidding", allowBidding); - compound.setBoolean("AllowBuyout", allowBuyout); - compound.setString("WelcomeMessage", welcomeMessage); - compound.setString("NoAuctionMessage", noAuctionMessage); - return compound; - } - - @Override - public void readFromNBT(NBTTagCompound compound) { - showActiveListings = compound.hasKey("ShowActiveListings") ? - compound.getBoolean("ShowActiveListings") : true; - showMyListings = compound.hasKey("ShowMyListings") ? - compound.getBoolean("ShowMyListings") : true; - showMyBids = compound.hasKey("ShowMyBids") ? - compound.getBoolean("ShowMyBids") : true; - showClaims = compound.hasKey("ShowClaims") ? - compound.getBoolean("ShowClaims") : true; - allowCreatingListings = compound.hasKey("AllowCreatingListings") ? - compound.getBoolean("AllowCreatingListings") : true; - allowBidding = compound.hasKey("AllowBidding") ? - compound.getBoolean("AllowBidding") : true; - allowBuyout = compound.hasKey("AllowBuyout") ? - compound.getBoolean("AllowBuyout") : true; - welcomeMessage = compound.getString("WelcomeMessage"); - noAuctionMessage = compound.getString("NoAuctionMessage"); - } - - @Override - public void interact(EntityPlayer player) { - // Check if auction system is enabled - if (!ConfigMarket.AuctionEnabled) { - if (!noAuctionMessage.isEmpty()) { - npc.say(player, new Line(noAuctionMessage)); - } else { - npc.say(player, new Line("The auction house is currently closed.")); - } - return; - } - - // Process expired auctions before opening GUI - if (AuctionController.Instance != null) { - AuctionController.Instance.processExpiredAuctions(); - } - - // Say welcome message - if (!welcomeMessage.isEmpty()) { - npc.say(player, new Line(welcomeMessage)); - } else { - npc.say(player, npc.advanced.getInteractLine()); - } - - // Open auction GUI - NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); - } - - /** - * Check if player has claims waiting - */ - public boolean hasClaimsWaiting(EntityPlayer player) { - if (AuctionController.Instance == null) { - return false; - } - return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); - } - - /** - * Get number of active listings in the auction house - */ - public int getActiveListingCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingCount(); - } - - /** - * Get player's current listing count - */ - public int getPlayerListingCount(EntityPlayer player) { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); - } - - /** - * Get player's maximum allowed listings - */ - public int getPlayerMaxListings(EntityPlayer player) { - if (AuctionController.Instance == null) { - return ConfigMarket.DefaultMaxListings; - } - return AuctionController.Instance.getMaxListings(player); - } -} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 6215eef2f..107076a44 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -1,7 +1,9 @@ package noppes.npcs.roles; import foxz.utils.Market; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -10,10 +12,14 @@ import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.NoppesUtilServer; import noppes.npcs.NpcMiscInventory; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.MarketRegistry; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.TraderStock; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -35,9 +41,11 @@ public class RoleTrader extends RoleInterface { // Stock system public TraderStock stock = new TraderStock(); - // Currency system - public boolean useCurrency = false; // Use virtual currency instead of items - public long[] slotPrices = new long[18]; // Currency prices per slot + // Currency cost per trade slot (ADDITIVE to item costs, 0 = no currency required) + public long[] currencyCost = new long[18]; + + // Local viewer tracking for non-linked traders (per-NPC sync) + private final Set localViewers = new HashSet<>(); public RoleTrader(EntityNPCInterface npc) { super(npc); @@ -72,24 +80,27 @@ public class RoleTrader extends RoleInterface { // Stock system nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); - // Currency system - nbttagcompound.setBoolean("UseCurrency", useCurrency); - NBTTagList priceList = new NBTTagList(); - for (long price : slotPrices) { - NBTTagCompound priceTag = new NBTTagCompound(); - priceTag.setLong("Price", price); - priceList.appendTag(priceTag); + // Currency cost per slot (additive to item costs) + NBTTagList currencyList = new NBTTagList(); + for (long cost : currencyCost) { + NBTTagCompound costTag = new NBTTagCompound(); + costTag.setLong("Cost", cost); + currencyList.appendTag(costTag); } - nbttagcompound.setTag("SlotPrices", priceList); + nbttagcompound.setTag("CurrencyCost", currencyList); return nbttagcompound; } @Override public void readFromNBT(NBTTagCompound nbttagcompound) { + String oldMarket = marketName; marketName = nbttagcompound.getString("TraderMarket"); readNBT(nbttagcompound); + // Register with MarketRegistry for linked market sync + MarketRegistry.updateTraderMarket(oldMarket, marketName, this); + try { Market.getMarket(this, marketName); } catch (Exception ex) { @@ -123,39 +134,50 @@ public class RoleTrader extends RoleInterface { stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); } - // Currency system - useCurrency = nbttagcompound.getBoolean("UseCurrency"); - if (nbttagcompound.hasKey("SlotPrices")) { - NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); - for (int i = 0; i < priceList.tagCount() && i < 18; i++) { - slotPrices[i] = priceList.getCompoundTagAt(i).getLong("Price"); + // Currency cost per slot (additive to item costs) + if (nbttagcompound.hasKey("CurrencyCost")) { + NBTTagList currencyList = nbttagcompound.getTagList("CurrencyCost", 10); + for (int i = 0; i < currencyList.tagCount() && i < 18; i++) { + currencyCost[i] = currencyList.getCompoundTagAt(i).getLong("Cost"); } } } @Override public void interact(EntityPlayer player) { - // Check for stock reset before opening GUI - if (stock.enableStock) { - long currentTime = stock.resetType.isRealTime() - ? System.currentTimeMillis() - : player.worldObj.getTotalWorldTime(); - - if (stock.shouldReset(currentTime)) { - stock.resetStock(currentTime); - // Save to market if using shared market - if (!marketName.isEmpty()) { - Market.save(this, marketName); - } - } - } - npc.say(player, npc.advanced.getInteractLine()); + + // Load from market FIRST to get latest shared data try { Market.getMarket(this, marketName); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); } + + // THEN check for stock reset (after loading latest market data) + if (stock.enableStock) { + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + boolean needsSave = false; + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + needsSave = true; + } + + // Validate stock levels (clamp current to max if exceeded) + if (stock.validateStock()) { + needsSave = true; + } + + // Save to market if using shared market and changes were made + if (needsSave && !marketName.isEmpty()) { + Market.save(this, marketName); + } + } + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); } @@ -213,15 +235,35 @@ public class RoleTrader extends RoleInterface { } /** - * Consume stock after purchase + * Consume stock after purchase. + * For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise). + * + * @param slot The slot purchased from + * @param playerName The player making the purchase + * @param amount Amount to consume + * @return true if stock was consumed successfully */ public boolean consumeStock(int slot, String playerName, int amount) { if (!stock.enableStock) { return true; // Stock system disabled } boolean consumed = stock.consumeStock(slot, playerName, amount); - if (consumed && !marketName.isEmpty()) { - Market.save(this, marketName); + if (consumed) { + // Save to market if linked + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + + // Sync viewers for shared stock (not per-player) + if (!stock.perPlayer) { + if (!marketName.isEmpty()) { + // Linked market: sync all traders and viewers market-wide + MarketRegistry.syncMarket(marketName); + } else { + // No linked market: sync only this NPC's viewers + syncLocalViewers(); + } + } } return consumed; } @@ -239,29 +281,175 @@ public class RoleTrader extends RoleInterface { return stock.getTimeUntilResetFormatted(currentTime); } - // ==================== Currency System Methods ==================== - /** - * Get the currency price for a slot + * Get time until stock reset in real-time milliseconds. + * Converts MC ticks to approximate real-time for client countdown display. + * @return Remaining time in milliseconds, or -1 if no reset scheduled */ - public long getSlotPrice(int slot) { - if (slot < 0 || slot >= 18) return 0; - return slotPrices[slot]; + public long getResetTimeRemainingMillis() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return -1; + } + + long currentTime; + long remaining; + + if (stock.resetType.isRealTime()) { + // Real time - already in milliseconds + currentTime = System.currentTimeMillis(); + remaining = stock.getTimeUntilReset(currentTime); + } else { + // MC time - convert ticks to real milliseconds (1 tick = 50ms) + currentTime = (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + long remainingTicks = stock.getTimeUntilReset(currentTime); + remaining = remainingTicks * 50L; + } + + return remaining; } /** - * Set the currency price for a slot + * Reset the stock cooldown timer (for admin use) */ - public void setSlotPrice(int slot, long price) { + public void resetCooldown() { + stock.lastResetTime = 0; + } + + // ==================== Currency Cost Methods ==================== + + /** + * Get the currency cost for a slot (additive to item costs) + */ + public long getCurrencyCost(int slot) { + if (slot < 0 || slot >= 18) return 0; + return currencyCost[slot]; + } + + /** + * Set the currency cost for a slot (additive to item costs) + */ + public void setCurrencyCost(int slot, long cost) { if (slot >= 0 && slot < 18) { - slotPrices[slot] = Math.max(0, price); + currencyCost[slot] = Math.max(0, cost); } } /** - * Check if a slot has a currency price set + * Check if a slot has a currency cost (greater than 0) */ - public boolean hasSlotPrice(int slot) { - return useCurrency && slot >= 0 && slot < 18 && slotPrices[slot] > 0; + public boolean hasCurrencyCost(int slot) { + return slot >= 0 && slot < 18 && currencyCost[slot] > 0; + } + + // ==================== Viewer Tracking Methods ==================== + + /** + * Register a player as viewing this trader's GUI. + * - For linked markets: uses MarketRegistry (market-wide sync) + * - For normal traders: uses local viewer set (per-NPC sync) + */ + public void registerViewer(EntityPlayerMP player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + // Linked market: use MarketRegistry for market-wide tracking + MarketRegistry.registerViewer(marketName, player); + } else { + // No linked market: use local viewer set + localViewers.add(player); + } + } + + /** + * Unregister a player from viewing this trader's GUI. + * - For linked markets: uses MarketRegistry + * - For normal traders: uses local viewer set + */ + public void unregisterViewer(EntityPlayerMP player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + MarketRegistry.unregisterViewer(marketName, player); + } else { + localViewers.remove(player); + } + } + + /** + * Sync trader data to all players viewing this trader. + * - For linked markets: syncs ALL viewers across ALL NPCs sharing the market + * - For normal traders: syncs only viewers of THIS specific NPC + */ + public void syncAllViewers() { + if (!marketName.isEmpty()) { + // Linked market: sync all viewers across all NPCs with this market + MarketRegistry.syncMarketViewers(marketName); + } else { + // No linked market: sync only local viewers + syncLocalViewers(); + } + } + + /** + * Sync to all players viewing this specific NPC (for non-linked traders). + */ + private void syncLocalViewers() { + // Remove disconnected players + localViewers.removeIf(player -> player.playerNetServerHandler == null); + + for (EntityPlayerMP viewer : localViewers) { + syncToPlayer(viewer); + } + } + + /** + * Called when this trader is unloaded (NPC death, chunk unload, etc.) + * Cleans up MarketRegistry registration. + */ + public void onUnload() { + if (!marketName.isEmpty()) { + MarketRegistry.unregisterTrader(marketName, this); + } + localViewers.clear(); + } + + /** + * Send trader data to a specific player + */ + public void syncToPlayer(EntityPlayerMP player) { + if (player == null || player.playerNetServerHandler == null) { + return; + } + + NBTTagCompound compound = new NBTTagCompound(); + + // Player balance + PlayerData data = PlayerData.get(player); + compound.setLong("Balance", data.currencyData.getBalance()); + + // Stock info + compound.setBoolean("StockEnabled", stock.enableStock); + compound.setString("ResetTime", getTimeUntilResetFormatted()); + + // Available stock per slot (player-specific if perPlayer mode) + String playerName = player.getCommandSenderName(); + int[] stockArr = new int[18]; + for (int i = 0; i < 18; i++) { + stockArr[i] = getAvailableStock(i, playerName); + } + compound.setIntArray("Stock", stockArr); + + // Currency cost per slot + for (int i = 0; i < 18; i++) { + compound.setLong("Cost" + i, getCurrencyCost(i)); + } + + GuiDataPacket.sendGuiData(player, compound); + } + + @Override + public void delete() { + onUnload(); + super.delete(); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 9fc547260..85a15041e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,14 +49,11 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.PlayerQuestController; import noppes.npcs.controllers.QuestController; -import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.data.Dialog; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -899,185 +896,42 @@ public class ScriptPlayer extends ScriptLivingBase return (IPlayer[]) list.toArray(new IPlayer[list.size()]); } - // ==================== CURRENCY METHODS ==================== + // ========================================= + // Currency Methods + // ========================================= - /** - * Get the player's current currency balance. - * Uses Vault if available, otherwise uses internal currency system. - * @return The player's currency balance - * @since 1.11 - */ + @Override public long getCurrencyBalance() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.getBalance(player); + return getData().currencyData.getBalance(); } - /** - * Add currency to the player's balance. - * @param amount The amount to add (must be positive) - * @return true if successful, false if failed (e.g., would exceed max balance) - * @since 1.11 - */ - public boolean addCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.deposit(player, amount); + @Override + public void setCurrencyBalance(long amount) { + getData().currencyData.setBalance(amount); } - /** - * Remove currency from the player's balance. - * @param amount The amount to remove (must be positive) - * @return true if successful, false if insufficient funds - * @since 1.11 - */ - public boolean removeCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.withdraw(player, amount); + @Override + public boolean depositCurrency(long amount) { + return getData().currencyData.deposit(amount); } - /** - * Check if the player can afford a specific amount. - * @param amount The amount to check - * @return true if player has sufficient balance - * @since 1.11 - */ + @Override + public boolean withdrawCurrency(long amount) { + return getData().currencyData.withdraw(amount); + } + + @Override public boolean canAffordCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.canAfford(player, amount); + return getData().currencyData.canAfford(amount); } - /** - * Get the configured currency name (e.g., "Gold"). - * @return The currency name - * @since 1.11 - */ - public String getCurrencyName() { - if (VaultHelper.Instance == null) { - return "Currency"; - } - return VaultHelper.Instance.getCurrencyName(); + @Override + public boolean isUsingVaultCurrency() { + return getData().currencyData.isUsingVault(); } - /** - * Format a currency amount for display (e.g., "1,000 Gold"). - * @param amount The amount to format - * @return Formatted string - * @since 1.11 - */ - public String formatCurrency(long amount) { - if (CurrencyController.Instance == null) { - return String.valueOf(amount); - } - return CurrencyController.Instance.formatAmount(amount); - } - - /** - * Check if the player has pending currency claims. - * Claims are created by auction refunds, NPC payments, etc. - * @return true if there are pending claims - * @since 1.11 - */ - public boolean hasPendingClaims() { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.hasPendingClaims(player); - } - - /** - * Get the total amount of pending currency claims. - * @return Total pending claim amount - * @since 1.11 - */ - public long getPendingClaimsTotal() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.getTotalPendingClaims(player); - } - - /** - * Claim all pending currency claims and add them to balance. - * @return The total amount claimed - * @since 1.11 - */ - public long claimAllCurrency() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.claimAll(player); - } - - // ==================== AUCTION METHODS ==================== - - /** - * Get the number of active auction listings the player has. - * @return Number of active listings - * @since 1.11 - */ - public int getAuctionListingCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); - } - - /** - * Get the maximum number of auction listings the player is allowed. - * @return Maximum listing count - * @since 1.11 - */ - public int getMaxAuctionListings() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getMaxListings(player); - } - - /** - * Check if the player has auction items waiting to be claimed. - * This includes won items, expired listings, and sale proceeds. - * @return true if there are claimable items - * @since 1.11 - */ - public boolean hasAuctionClaims() { - if (AuctionController.Instance == null) { - return false; - } - return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); - } - - /** - * Get the number of auction items waiting to be claimed. - * @return Number of claimable items/listings - * @since 1.11 - */ - public int getAuctionClaimCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getClaimableListings(player.getUniqueID()).size(); - } - - /** - * Get the number of auctions where the player is the current high bidder. - * @return Number of active bids - * @since 1.11 - */ - public int getActiveAuctionBidCount() { - if (AuctionController.Instance == null) { - return 0; - } - return (int) AuctionController.Instance.getListingsAsBidder(player.getUniqueID()) - .stream() - .filter(l -> l.isActive()) - .count(); + @Override + public String getFormattedCurrencyBalance() { + return getData().currencyData.formatBalance(); } } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java index a2588059f..6736369b9 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java @@ -1,7 +1,6 @@ package noppes.npcs.scripted.item; import com.google.common.collect.Multimap; -import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; import kamkeel.npcs.util.AttributeItemUtil; @@ -593,117 +592,4 @@ public class ScriptItemStack implements IItemStack { Set keys = reqTag.func_150296_c(); return keys.toArray(new String[keys.size()]); } - - // ==================== Trade Restriction Methods ==================== - - /** - * Check if this item can be traded or auctioned. - * An item cannot be traded if it is: - * - Marked as untradeable - * - Bound to a profile slot - * - Soulbound to a player - * @return true if the item can be traded - * @since 1.11 - */ - public boolean canTrade() { - return ItemTradeAttribute.canTrade(item); - } - - /** - * Get the reason why this item cannot be traded. - * @return Localization key for the block reason, or null if tradeable - * @since 1.11 - */ - public String getTradeBlockReason() { - return ItemTradeAttribute.getTradeBlockReason(item); - } - - /** - * Check if this item is marked as untradeable - * @return true if untradeable - * @since 1.11 - */ - public boolean isUntradeable() { - return ItemTradeAttribute.isUntradeable(item); - } - - /** - * Set whether this item is untradeable - * @param untradeable true to mark as untradeable - * @since 1.11 - */ - public void setUntradeable(boolean untradeable) { - ItemTradeAttribute.setUntradeable(item, untradeable); - } - - /** - * Check if this item is bound to a profile slot - * @return true if profile slot bound - * @since 1.11 - */ - public boolean isProfileSlotBound() { - return ItemTradeAttribute.isProfileSlotBound(item); - } - - /** - * Get the profile slot this item is bound to - * @return Profile slot ID, or -1 if not bound - * @since 1.11 - */ - public int getProfileSlot() { - return ItemTradeAttribute.getProfileSlot(item); - } - - /** - * Bind this item to a specific profile slot - * @param slot The profile slot ID, or -1 to unbind - * @since 1.11 - */ - public void setProfileSlotBound(int slot) { - ItemTradeAttribute.setProfileSlotBound(item, slot); - } - - /** - * Check if this item is soulbound to any player - * @return true if soulbound - * @since 1.11 - */ - public boolean isSoulbound() { - return ItemTradeAttribute.isSoulbound(item); - } - - /** - * Get the UUID of the player this item is soulbound to - * @return UUID string, or null if not soulbound - * @since 1.11 - */ - public String getSoulbindOwner() { - return ItemTradeAttribute.getSoulbindOwner(item); - } - - /** - * Remove the soulbind from this item - * @since 1.11 - */ - public void removeSoulbind() { - ItemTradeAttribute.removeSoulbind(item); - } - - /** - * Clear all trade restrictions from this item - * (untradeable, profile slot bound, soulbound) - * @since 1.11 - */ - public void clearTradeRestrictions() { - ItemTradeAttribute.clearAllRestrictions(item); - } - - /** - * Get a summary of all trade restrictions on this item - * @return Human-readable string of restrictions, or empty if none - * @since 1.11 - */ - public String getRestrictionSummary() { - return ItemTradeAttribute.getRestrictionSummary(item); - } } diff --git a/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java b/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java index 825c2e46b..3574e529e 100644 --- a/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java +++ b/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java @@ -42,7 +42,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader public IItemStack getSellOption(int slot) { if (slot >= 18 || slot < 0) return null; if (role.inventorySold.items.get(slot) == null) return null; - return NpcAPI.Instance().getIItemStack(role.inventoryCurrency.items.get(slot)); + return NpcAPI.Instance().getIItemStack(role.inventorySold.items.get(slot)); } public IItemStack[] getCurrency(int slot) { @@ -100,7 +100,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader public boolean isSlotEnabled(int slot) { if (slot >= 18 || slot < 0) return false; - return role.disableSlot[slot] > 0; + return role.disableSlot[slot] <= 0; // enabled when NOT disabled } public boolean isSlotEnabled(int slot, IPlayer player) { @@ -128,6 +128,132 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader role.getArrayByName(player.getDisplayName(), role.playerDisableSlot)[slot] = 0; } + // ==================== Stock System ==================== + + public boolean isStockEnabled() { + return role.stock.enableStock; + } + + public void setStockEnabled(boolean enabled) { + role.stock.enableStock = enabled; + } + + public boolean isPerPlayerStock() { + return role.stock.perPlayer; + } + + public void setPerPlayerStock(boolean perPlayer) { + role.stock.perPlayer = perPlayer; + } + + public int getStockResetType() { + return role.stock.resetType.ordinal(); + } + + public void setStockResetType(int type) { + noppes.npcs.constants.EnumStockReset[] values = noppes.npcs.constants.EnumStockReset.values(); + if (type >= 0 && type < values.length) { + role.stock.resetType = values[type]; + } + } + + public long getCustomResetTime() { + return role.stock.customResetTime; + } + + public void setCustomResetTime(long time) { + role.stock.customResetTime = Math.max(0, time); + } + + public int getMaxStock(int slot) { + if (slot < 0 || slot >= 18) return -1; + return role.stock.maxStock[slot]; + } + + public void setMaxStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + role.stock.setMaxStock(slot, amount); + } + } + + public int getAvailableStock(int slot) { + if (slot < 0 || slot >= 18) return 0; + return role.stock.getAvailableStock(slot, ""); + } + + public int getAvailableStock(int slot, IPlayer player) { + if (slot < 0 || slot >= 18) return 0; + return role.stock.getAvailableStock(slot, player.getDisplayName()); + } + + public void resetStock() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + role.stock.resetStock(currentTime); + } + + public void resetCooldown() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + role.stock.lastResetTime = currentTime; + } + + public int getCurrentStock(int slot) { + if (slot < 0 || slot >= 18) return -1; + return role.stock.currentStock[slot]; + } + + public void setCurrentStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + role.stock.currentStock[slot] = amount; + } + } + + public int getPlayerPurchased(int slot, IPlayer player) { + if (slot < 0 || slot >= 18) return 0; + noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock = + role.stock.playerStock.get(player.getDisplayName()); + if (pStock == null) return 0; + return pStock.purchasedAmounts[slot]; + } + + public void setPlayerPurchased(int slot, IPlayer player, int amount) { + if (slot < 0 || slot >= 18) return; + noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock = + role.stock.playerStock.computeIfAbsent( + player.getDisplayName(), + k -> new noppes.npcs.controllers.data.TraderStock.PlayerTraderStock() + ); + pStock.purchasedAmounts[slot] = Math.max(0, amount); + } + + public long getLastResetTime() { + return role.stock.lastResetTime; + } + + public long getTimeUntilReset() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + return role.stock.getTimeUntilReset(currentTime); + } + + // ==================== Currency Cost System ==================== + + public long getCurrencyCost(int slot) { + return role.getCurrencyCost(slot); + } + + public void setCurrencyCost(int slot, long cost) { + role.setCurrencyCost(slot, cost); + } + + public boolean hasCurrencyCost(int slot) { + return role.hasCurrencyCost(slot); + } + @Override public int getType() { return RoleType.TRADER; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 7f7314d1c..715ca4884 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -443,9 +443,54 @@ role.auctioneer.messages=Messages role.auctioneer.welcome=Welcome Message role.auctioneer.noAuction=Auction Disabled Message -trader.insufficient=Insufficient Currency -trader.sufficient=Sufficient Currency +trader.unavailable=Unavailable +trader.insufficient.currency=Insufficient Balance +trader.available=Available trader.slotdisabled=Slot Disabled +trader.currencycost=Cost +trader.outofstock=Out of Stock +trader.instock=in stock +trader.restock=Restock +trader.balance=Balance +trader.reopentrader=Reopen Trader + +stock.options=Stock Options +stock.enable=Enable Stock +stock.perplayer=Per-Player +stock.resettype=Reset Type +stock.customtime=Custom Time +stock.defaultmax=Default Max +stock.perslot=Per-Slot Stock +stock.reset=Reset Stock +stock.resetcooldown=Reset Cooldown +stock.perslot.title=Per-Slot Stock Limits +stock.reset.confirm=Reset all stock? +stock.reset.warning=This cannot be undone. +stock.cooldown.confirm=Reset cooldown timer? +stock.cooldown.warning=Stock will reset on next open. + +stock.reset.never=Never +stock.reset.mcdaily=MC Daily +stock.reset.mcweekly=MC Weekly +stock.reset.mccustom=MC Custom +stock.reset.rldaily=Real Daily +stock.reset.rlweekly=Real Weekly +stock.reset.rlcustom=Real Custom + +stock.options.hover=Configure stock limits and reset timers +stock.enable.hover=Enable limited stock for trade slots +stock.perplayer.hover=Track stock separately for each player +stock.resettype.hover=When stock automatically replenishes +stock.customtime.hover=Set a custom reset interval +stock.defaultmax.hover=Default max stock for all slots (-1 = unlimited) +stock.perslot.hover=Set max stock for individual slots +stock.reset.hover=Reset all stock to max values +stock.resetcooldown.hover=Force stock to reset on next trader open + +gui.settings.hover=Configure trading options +gui.ignoreDamage.hover=Ignore item damage when comparing currency +gui.ignoreNBT.hover=Ignore NBT data when comparing currency +gui.recordHistory.hover=Record purchase history for analytics job.name=Job job.none=No Job @@ -1660,72 +1705,3 @@ condition.hit_count=Hit Count condition.percent=Threshold condition.hits=Required Hits condition.within=Within (ticks) - -auction.title=Auction House -auction.browse=Browse -auction.myListings=My Listings -auction.myBids=My Bids -auction.claims=Claims -auction.sell=Sell -auction.search=Search -auction.refresh=Refresh -auction.viewDetails=View Details -auction.placeBid=Place Bid -auction.buyout=Buyout -auction.cancel=Cancel -auction.prevPage=< Prev -auction.nextPage=Next > -auction.noResults=No results -auction.page=Page %d of %d -auction.balance=Balance: %s -auction.listings=%d listings -auction.browseTitle=Browse Auctions -auction.myListingsTitle=My Listings -auction.myBidsTitle=My Active Bids -auction.claimsTitle=Claim Items -auction.sellTitle=Create Listing -auction.currentPrice=Current Price: -auction.minimumBid=Minimum Bid: -auction.yourBalance=Your Balance: -auction.timeLeft=Time Left: -auction.yourBid=Your Bid: -auction.minBid=Min Bid -auction.bidPlus10=+10% -auction.bidPlus25=+25% -auction.insufficientFunds=Insufficient funds! -auction.auctionEnded=Auction has ended! -auction.ended=Ended -auction.item=Item: -auction.seller=Seller: -auction.startingPrice=Starting Price: -auction.buyoutPrice=Buyout Price: -auction.noBuyout=None -auction.bidCount=Bid Count: -auction.highBidder=High Bidder: -auction.noBids=No bids -auction.claim=Claim -auction.createListing=Create Listing -auction.duration=Duration: -auction.listingFee=Listing Fee: -auction.sort.newest=Newest First -auction.sort.oldest=Oldest First -auction.sort.priceHigh=Most Expensive -auction.sort.priceLow=Least Expensive -auction.sort.endingSoon=Ending Soon -auction.sort.mostTime=Most Time Left -auction.sort.nameAZ=Name: A to Z -auction.sort.nameZA=Name: Z to A -auction.sort.mostBids=Most Bids -auction.sort.leastBids=Least Bids -auction.duration.short=Short (12h) -auction.duration.medium=Medium (24h) -auction.duration.long=Long (48h) -auction.duration.veryLong=Very Long (72h) -auction.placeItemBelow=Place item in slot below: -auction.noItemSelected=(No item selected) -auction.noBuyoutHint=(0 = no buyout) -auction.quantity=Quantity: -auction.totalBids=Total Bids: -auction.noBidsYet=No bids yet -auction.auctionDetails=Auction Details -auction.currentBid=Current Bid: