diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md deleted file mode 100644 index 4236e0e63..000000000 --- a/1.11_Market_Update.md +++ /dev/null @@ -1,729 +0,0 @@ -# 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: COMPLETE ✅ - -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 -- **Unified Usage** - Both Trader and Auction systems use `PlayerCurrencyData` which auto-delegates to Vault - -### Vault Integration Flow -``` -┌─────────────────────────────────────────────────────────────────┐ -│ PlayerCurrencyData │ -│ │ -│ shouldUseVault() = ConfigMarket.UseVault && VaultUtil.isEnabled()│ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ All methods check shouldUseVault() first: │ │ -│ │ - getBalance() → VaultUtil.getBalance() or local balance │ │ -│ │ - withdraw() → VaultUtil.withdrawMoney() or local │ │ -│ │ - deposit() → VaultUtil.addMoney() or local │ │ -│ │ - canAfford() → VaultUtil.has() or local check │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ -│ Used by: ContainerNPCTrader, AuctionController │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Key Files -| File | Purpose | -|------|---------| -| `PlayerTradeData.java` | Per-player trade data (currency + auction claims), Vault delegation | -| `IPlayerTradeData.java` | API interface for trade operations (currency + claims) | -| `ConfigMarket.java` | Currency configuration (UseVault, CurrencyName, StartingBalance, MaxBalance) | -| `VaultUtil.java` | Vault API integration via reflection | - -### 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 - -// Via IPlayerData -player.getData().getTradeData() // Get IPlayerTradeData interface -``` - ---- - -## 2. Trader Enhancements - -### Status: COMPLETE ✅ - ---- - -### 2.1 Stock System - COMPLETE ✅ - -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 - COMPLETE ✅ -| 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 | - ---- - -### 2.2 Trader Currency Support - COMPLETE ✅ - -Trades can require BOTH items AND currency (additive costs). Uses `PlayerCurrencyData` which auto-delegates to Vault when enabled. - -#### 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 - -#### Implementation Details -- Currency cost stored as `long[] currencyCost[18]` in RoleTrader -- Uses `data.tradeData.getBalance()` and `data.tradeData.withdraw()` -- Automatically uses Vault when `ConfigMarket.UseVault` is enabled -- NBT serialization for persistence ✅ - ---- - -### 2.3 Player Trader GUI - COMPLETE ✅ - -#### 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 | - ---- - -### 2.4 Multi-Player Sync - COMPLETE ✅ - -MarketRegistry provides linked market synchronization across all NPCs sharing the same market. - ---- - -### 2.5 Scripting API - COMPLETE ✅ - -Full API available via `IRoleTrader` interface. - ---- - -## 3. Auction House System - -### Status: COMPLETE ✅ - -### 3.1 Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ AuctionController │ -│ - Manages auction listings with player indices │ -│ - Handles listing/bidding/buyout logic │ -│ - Uses ConcurrentHashMap for thread-safety (no locks) │ -│ - Async saves on CNPC+ thread every 30 seconds │ -│ - 30-second update cycle for processing ended auctions │ -└─────────────────────────────────────────────────────────────────┘ - │ - Uses PlayerCurrencyData (Vault-aware) - │ -┌─────────────────────────────────────────────────────────────────┐ -│ PlayerTradeData │ -│ - Combined currency + auction claims (shared across profiles) │ -│ - Currency: Vault integration with local fallback │ -│ - Claims: Thread-safe CopyOnWriteArrayList for async additions │ -│ - Expired claims processed on login │ -│ - Notifications sent immediately to online players │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 Vault Integration - -The Auction system uses `PlayerCurrencyData` for all currency operations: -- `AuctionController.createListing()` → `currencyData.withdraw(fee)` -- `AuctionController.placeBid()` → `currencyData.withdraw(bidAmount)` -- `AuctionController.buyout()` → `currencyData.withdraw(buyoutPrice)` -- `AuctionController.claimCurrency()` → `currencyData.deposit(amount)` - -**When Vault is enabled** (`ConfigMarket.UseVault = true`): -- All operations automatically use Vault economy -- No code changes required - `PlayerCurrencyData` handles delegation - -### 3.3 GUI Structure - -``` -GuiAuctionInterface (Base GUI - opened via Auctioneer NPC) - │ - ├── GuiAuctionListing (Browse auctions - PAGE_LISTINGS) - │ - 9x5 grid of auction items - │ - Filter button with tooltip showing current settings - │ - Click item → Opens GuiAuctionBidding - │ - Pagination with up/down arrows - │ - ├── GuiAuctionSell (Create new listing - PAGE_SELL) - │ - Sell slot for item to auction (cloned from inventory) - │ - Click inventory: left=add stack, right=add 1 - │ - Click sell slot: left=clear, right=remove 1 - │ - Starting price + buyout price text fields - │ - Allow Buyout toggle button - │ - Listing fee display - │ - Confirm button with coin icon - │ - ├── GuiAuctionTrades (My Trades - PAGE_CLAIMS) - │ - 9x5 grid showing: active listings, active bids, claims - │ - Blue tint: active listings/bids - │ - Green tint: claimable items - │ - Red tint: refunds - │ - Confirmation system for cancel/claim operations - │ - ├── GuiAuctionBidding (View listing details) - │ - Item display slot - │ - Auction info (seller, price, time, bids) - │ - Bid button → SubGuiAuctionBid - │ - Buy Now button → SubGuiAuctionBuyNow - │ - Own listings: buttons hidden, view-only - │ - ├── SubGuiAuctionBid (Bid confirmation popup) - │ - Shows minimum bid, player balance - │ - Bid amount input field - │ - Place Bid / Cancel buttons - │ - ├── SubGuiAuctionBuyNow (Buyout confirmation popup) - │ - Shows buyout price, player balance - │ - Yes / Cancel buttons - │ - └── SubGuiAuctionSearch (Filter popup) - - Search text input - - Apply/Cancel buttons -``` - -### 3.4 Client Config Sync - -The client uses `AuctionClientConfig` to cache server settings: -- Synced via `LoginPacket` on player join -- Client never uses `ConfigMarket` directly for display -- Ensures server authority over fees, currency name, etc. - -```java -// Server sends config in LoginPacket -AuctionClientConfig.writeToNBT(compound); // Server side - -// Client receives and caches -AuctionClientConfig.readFromNBT(compound); // Client side - -// Client GUI uses cached values -AuctionClientConfig.getListingFee(); -AuctionClientConfig.getCurrencyName(); -AuctionClientConfig.getMinBidIncrement(); -``` - -### 3.5 Data Structures - -#### AuctionListing -```java -public class AuctionListing { - String id; // Unique listing ID (UUID-based) - 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 { - String id; // Unique claim ID - UUID playerUUID; // Player who can claim - String 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 -} -``` - -### 3.6 Security Features - -All actions are validated server-side: - -| Check | Location | Description | -|-------|----------|-------------| -| Own item bid prevention | `placeBid()` | `listing.isSeller(playerUUID)` check | -| Own item buyout prevention | `buyout()` | `listing.isSeller(playerUUID)` check | -| Auction expiry check | `placeBid()/buyout()` | Double-check inside write lock | -| Currency validation | All actions | Verify balance before operations | -| Claim ownership | `claimItem()/claimCurrency()` | `claim.isForPlayer(playerUUID)` check | -| Blacklist check | `createListing()` | `AuctionBlacklist.isBlacklistedForPlayer()` check | -| Bound item check | `createListing()` | Auto-blocks Soulbound/Profile-Slotbound items | - -### 3.7 Snipe Protection - -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.8 Item Blacklist System - -Prevents specific items from being listed on the auction house. - -#### Blacklist Types - -| Type | Description | Example | -|------|-------------|---------| -| **Items** | Block specific items by registry name | `minecraft:diamond_sword` | -| **Wildcards** | Block items matching pattern | `minecraft:diamond*` | -| **Mods** | Block all items from a mod | `customnpcs` | -| **NBT Tags** | Block items with specific NBT | `ench` (enchanted items) | - -#### Auto-Blocked Items (Always Blocked) - -These items are **always** blocked regardless of blacklist settings: -- **Soulbound Items** - Items with `cnpc_soulbind` requirement -- **Profile-Slotbound Items** - Items with `cnpc_profile_slot` requirement - -The system checks for these under `RPGCore/Requirements` in the item's NBT. - -#### Bypass Permission - -Players with OP status or the `customnpcs.auction.blacklist.bypass` permission can list blacklisted items. - -#### Configuration - -``` -Market.Auction.Blacklist: - Enabled: true - BlacklistedItems: ["minecraft:barrier", "minecraft:command_block*"] - BlacklistedMods: [] - BlacklistedNBTTags: [] -``` - -#### Commands - -| Command | Description | -|---------|-------------| -| `/cnpc auction blacklist add ` | Add to blacklist (runtime only) | -| `/cnpc auction blacklist remove ` | Remove from blacklist (runtime only) | -| `/cnpc auction blacklist list [item\|mod\|nbt]` | List blacklisted entries | -| `/cnpc auction blacklist reload` | Reload blacklist from config | -| `/cnpc auction blacklist check` | Check if held item is blacklisted | - ---- - -## 4. AuctionController Performance & Thread Safety - -### 4.1 Threading Model - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ AuctionController │ -│ │ -│ ┌─────────────────────┐ ┌─────────────────┐ ┌───────────────┐ │ -│ │ ConcurrentHashMap │ │ AtomicBoolean │ │ CNPC+ Thread │ │ -│ │ (listings, indices) │ │ dirty, saving │ │ (async saves) │ │ -│ └─────────────────────┘ └─────────────────┘ └───────────────┘ │ -│ │ -│ All operations use ConcurrentHashMap - no explicit locking │ -│ Minecraft is single-threaded for gameplay - inherent safety │ -│ Async saves snapshot data before writing to disk │ -└─────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────────┐ -│ PlayerTradeData │ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ Currency: long balance (or Vault delegation) │ │ -│ │ Claims: CopyOnWriteArrayList │ │ -│ │ - Thread-safe for concurrent read/write │ │ -│ │ - Claims can be added from CNPC+ thread for offline players │ │ -│ │ - Stored in PlayerData (persisted with player, not auctions) │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 4.2 Player Indices for O(1) Lookups - -| Index | Type | Purpose | -|-------|------|---------| -| `playerListingIds` | `Map>` | Active listings by seller | -| `playerBidIds` | `Map>` | Listings where player has active bid | - -**Claims are now stored in PlayerData:** -- `PlayerTradeData.claims` - CopyOnWriteArrayList per player -- Accessed via `PlayerData.get(player).tradeData` -- Shared across all profile slots (combined with currency) - -**Performance Impact:** -- `getPlayerListingCount()`: O(n) → O(1) -- `getPlayerActiveListings()`: O(n) → O(k) where k = player's listings -- `getPlayerActiveBids()`: O(n) → O(k) -- `getPlayerClaims()`: Direct access to player's claims list - -### 4.3 Update Intervals - -| Operation | Interval | Rationale | -|-----------|----------|-----------| -| Process ended auctions | 30 seconds (600 ticks) | Reduces server load | -| Save auction data | 30 seconds (600 ticks) | Batches writes, reduces I/O | -| Process expired claims | On player login | Per-player, not global tick | - -### 4.4 Data Safety - -1. **Currency Deduction** - Withdraw before updating listing -2. **Previous Bidder Refund** - Store info before modification -3. **Concurrent Saves** - `AtomicBoolean` prevents duplicates -4. **Offline Player Claims** - Loaded async on CNPC+ thread, saved to PlayerData -5. **Claim Migration** - Old claims in auction.dat migrated to PlayerData on load - -### 4.5 Server Shutdown Handling - -On `FMLServerStoppedEvent`: -1. `AuctionController.save()` called synchronously -2. Uses CNPC+ thread (single-threaded executor) -3. All pending saves complete before shutdown - ---- - -## 5. File Structure - -### New Files Created - -``` -constants/ - EnumAuctionStatus.java ✅ - EnumAuctionSort.java ✅ - EnumClaimType.java ✅ - EnumNotificationType.java ✅ - -controllers/ - AuctionController.java ✅ (with threading, indices, Vault support) - AuctionConfigSync.java ✅ (server-side config sync utility) - -controllers/data/ - AuctionListing.java ✅ - AuctionClaim.java ✅ - AuctionFilter.java ✅ - AuctionBlacklist.java ✅ (item blacklist checking + bypass) - PlayerTradeData.java ✅ (currency + claims storage per player) - -roles/ - RoleAuctioneer.java ✅ - -containers/ - ContainerAuction.java ✅ - ContainerAuctionListing.java ✅ - ContainerAuctionBidding.java ✅ - ContainerAuctionTrades.java ✅ - ContainerAuctionSell.java ✅ - InventoryAuctionDisplay.java ✅ - SlotAuctionDisplay.java ✅ - -client/ - AuctionClientConfig.java ✅ (client-side config cache) - -client/gui/player/ - GuiAuctionInterface.java ✅ - GuiAuctionListing.java ✅ - GuiAuctionSell.java ✅ - GuiAuctionTrades.java ✅ - GuiAuctionBidding.java ✅ - SubGuiAuctionSearch.java ✅ - SubGuiAuctionBid.java ✅ - SubGuiAuctionBuyNow.java ✅ - AuctionTooltipHandler.java ✅ - -client/gui/util/ - GuiAuctionNavButton.java ✅ - -network/packets/player/ - AuctionActionPacket.java ✅ -``` - -### New Commands - -``` -command/ - AuctionCommand.java ✅ /cnpc auction with list, view, open, blacklist - command/auction/ - BlacklistSubCommand.java ✅ /cnpc auction blacklist subcommands -``` - -### Files Modified - -``` -CustomNpcs.java ✅ Initialize AuctionController, save on shutdown -EnumRoleType.java ✅ Added Auctioneer -EnumGuiType.java ✅ Added PlayerAuction, PlayerAuctionSell, PlayerAuctionTrades, PlayerAuctionBidding -EnumPlayerPacket.java ✅ Added AuctionData, AuctionAction -CommonProxy.java ✅ Handle auction container types -ClientProxy.java ✅ Handle auction GUI types -DataAdvanced.java ✅ Handle Auctioneer role -ConfigMarket.java ✅ Added Auction section -LoginPacket.java ✅ Sync auction config to client -PlayerData.java ✅ Added tradeData field (currency + claims, shared across profiles) -en_US.lang ✅ Added auction lang keys -``` - ---- - -## 6. Configuration - -### ConfigMarket.java Structure - -```java -// ========================================= -// Currency (Market.Currency) -// ========================================= -public static boolean UseVault = false; // Use Vault economy -public static String CurrencyName = "Coins"; // Display name -public static long StartingBalance = 0; // New player balance -public static long MaxBalance = Long.MAX_VALUE; // Balance cap - -// ========================================= -// Auction (Market.Auction) -// ========================================= -public static final int MAX_TRADE_SLOTS = 45; // 9x5 grid maximum -public static final int MIN_TRADE_SLOTS = 1; // Minimum allowed - -public static boolean AuctionEnabled = true; -public static int AuctionDurationHours = 24; // Auction length (hours) -public static long ListingFee = 10; // Fee to create listing -public static long MinimumListingPrice = 1; // Minimum starting price -public static double SalesTaxPercent = 0.05; // 5% tax on sales -public static int DefaultMaxTrades = 8; // Per player (listings + bids + claims) -public static int SnipeProtectionMinutes = 2; // Anti-snipe timer -public static int ClaimExpirationDays = 20; // Days before unclaimed deleted -public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase - -// ========================================= -// Auction Logging -// ========================================= -public static boolean AuctionLoggingEnabled = false; -public static boolean LogAuctionCreated = true; -public static boolean LogAuctionBid = true; -public static boolean LogAuctionBuyout = true; -public static boolean LogAuctionSold = true; -public static boolean LogAuctionExpired = true; -public static boolean LogAuctionCancelled = true; -public static boolean LogAuctionClaimed = true; - -// ========================================= -// Auction Blacklist (Market.Auction.Blacklist) -// ========================================= -public static boolean BlacklistEnabled = true; -public static String[] BlacklistedItems = {}; // Registry names (supports wildcards) -public static String[] BlacklistedMods = {}; // Mod IDs to block entirely -public static String[] BlacklistedNBTTags = {}; // NBT tag keys to block -``` - -### Auction Permissions - -| Permission | Description | -|------------|-------------| -| `customnpcs.auction.trades.*` | Unlimited trade slots (up to 45) | -| `customnpcs.auction.trades.X` | Exactly X trade slots (where X is 1-45) | -| `customnpcs.auction.blacklist.bypass` | Bypass item blacklist restrictions | - -**Trade Slot Permissions:** -- Default is 8 trade slots -- Permissions only **increase** the limit (permissions below 8 are ignored) -- Maximum is capped at 45 (9x5 grid) -- Trade slots = Active Listings + Active Bids + Pending Claims - -**Blacklist Bypass:** -- OP players or those with `customnpcs.auction.blacklist.bypass` can list blacklisted items -- Soulbound and Profile-Slotbound items are **always** blocked (no bypass) - ---- - -## 7. Commands - -### `/cnpc auction` Command - -Allows interaction with the Auction House without needing an Auctioneer NPC. - -| Subcommand | Usage | Permission | Description | -|------------|-------|------------|-------------| -| `list` | `/cnpc auction list [page]` | OP | View active auctions in chat | -| `view` | `/cnpc auction view ` | OP | View a player's auction activity | -| `open` | `/cnpc auction open` | Any | Opens the Auction House GUI | - -**Examples:** -``` -/cnpc auction list - Show first page of active auctions -/cnpc auction list 2 - Show page 2 of auctions -/cnpc auction view Steve - Show Steve's listings, bids, and claims -/cnpc auction open - Open the full Auction House GUI -``` - -### `/cnpc auction blacklist` Command - -Manage the auction item blacklist (runtime changes, not persisted to config). - -| Subcommand | Usage | Permission | Description | -|------------|-------|------------|-------------| -| `add` | `/cnpc auction blacklist add ` | OP | Add to blacklist | -| `remove` | `/cnpc auction blacklist remove ` | OP | Remove from blacklist | -| `list` | `/cnpc auction blacklist list [item\|mod\|nbt]` | OP | List blacklisted entries | -| `reload` | `/cnpc auction blacklist reload` | OP | Reload from config | -| `check` | `/cnpc auction blacklist check` | Any | Check if held item is blacklisted | - -**Examples:** -``` -/cnpc auction blacklist add item minecraft:diamond_sword -/cnpc auction blacklist add item minecraft:diamond* (wildcard) -/cnpc auction blacklist add mod industrialcraft2 -/cnpc auction blacklist add nbt ench -/cnpc auction blacklist remove item minecraft:diamond_sword -/cnpc auction blacklist list item -/cnpc auction blacklist check (checks held item) -``` - ---- - -## 8. Implementation Status - -### Phase 1: Currency System - COMPLETE ✅ -### Phase 2: Trader Enhancements - COMPLETE ✅ -### Phase 3: Auction Core - COMPLETE ✅ -### Phase 4: Auction GUI - COMPLETE ✅ -### Phase 5: Auction Network - COMPLETE ✅ -### Phase 6: Performance & Safety - COMPLETE ✅ - -1. ~~Threading model with ReentrantReadWriteLock~~ -2. ~~Player indices for O(1) lookups~~ -3. ~~20-second update cycle~~ -4. ~~Async saves on CNPC+ thread~~ -5. ~~Race condition prevention~~ -6. ~~Server shutdown handling~~ -7. ~~Own listing view (buttons hidden)~~ -8. ~~Server-side security checks~~ - -### Remaining Work - -- [ ] **GuiManageAuction (Admin GUI)** - Admin interface to view all auctions, cancel listings, manage claims -- [ ] Additional testing - -### Implemented Since Initial Plan - -- [x] **`/auction` Command** - Console/player command with subcommands: - - `/auction list [page]` - View active auctions from command line - - `/auction view ` - View a player's auction activity (listings, bids, claims) - - `/auction open` - Open Auction House GUI without needing an Auctioneer NPC -- [x] **Permission system integration** - Per-player trade slot limits via `customnpcs.auction.trades.X` permissions - - Default: 8 trade slots - - Maximum: 45 (9x5 grid) - - Synced to client when viewing My Trades - - Cached on login for performance -- [x] **Minimum Listing Price** - Configurable minimum starting price (default: 1) -- [x] **Buyout enabled by default** - Buyout toggle is ON by default when creating listings -- [x] **Search by seller** - Filter can now search by item name OR seller name -- [x] **Rebid prevention** - Players cannot rebid on auctions where they're already the highest bidder -- [x] **Buyout price difference** - When buying out an auction you're bidding on, only pay the difference -- [x] **Immediate claim cleanup** - Claims are removed from storage immediately when claimed -- [x] **Listing cleanup** - Ended listings are removed when all related claims are processed -- [x] **Live notifications** - Notifications sent immediately to online players instead of queued -- [x] **Login claim summary** - Shows detailed claim summary on login (X items and Y currency to claim) -- [x] **PlayerTradeData** - Merged currency + claims into single class (shared across profiles) -- [x] **No locking system** - Pure ConcurrentHashMap for better performance -- [x] **30-second intervals** - Combined process/save cycle for simplicity -- [x] **Async offline player claims** - Claims added to offline players via CNPC+ thread -- [x] **Claim migration** - Old claims in auction.dat automatically migrated to PlayerData -- [x] **Item Blacklist System** - Block items by registry name, mod, NBT tag, or wildcard pattern - - Auto-blocks Soulbound and Profile-Slotbound items (checks `RPGCore/Requirements`) - - Per-player bypass via `customnpcs.auction.blacklist.bypass` permission - - Runtime add/remove commands (not persisted to config) - - Check command to test held item - ---- - -## 9. Design Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Claims Storage | In PlayerData, not auction.dat | Claims persist with player, reduces auction.dat size, allows offline claim addition | -| Thread Safety | ConcurrentHashMap (no locks) | Minecraft is single-threaded for gameplay; locks add overhead without benefit | -| Vault Integration | Via PlayerCurrencyData | Single point of delegation, reused by Trader & Auction | -| Update Interval | 30 seconds | Performance vs responsiveness tradeoff | -| Player Indices | ConcurrentHashMap + Sets | O(1) lookups, thread-safe | -| Async Saves | CNPC+ Thread | Doesn't block server tick | -| Offline Claims | Load PlayerData async, add claim, save | Claims can be added to offline players without blocking | -| Config Sync | LoginPacket → AuctionClientConfig | Client never accesses server config directly | -| Item Blacklist | Static utility class | Simple, fast checks without instantiation; auto-blocks bound items | -| Bound Item Detection | Check `RPGCore/Requirements` NBT path | Uses AttributeItemUtil constants for consistency | - ---- - -## 10. Future Considerations (Not in 1.11) - -- Category-based filtering (by item type, mod, etc.) -- Auction history/statistics tracking -- Auction house tax sink (currency removed from economy) -- Reserve prices (hidden minimum price) -- Private auctions (invite-only) -- Watchlist/favorites system -- Price history graphs diff --git a/Ability_Beta_Update.md b/Ability_Beta_Update.md new file mode 100644 index 000000000..8733ebc8b --- /dev/null +++ b/Ability_Beta_Update.md @@ -0,0 +1,144 @@ +# CNPC+ 1.11 Ability System Beta + +🟡 **In Testing** + + + +## Features: ⚒️ + +- **Phase-Based Execution:** + +> All abilities follow a Wind-up → Active → Completion flow. Wind-up lets players react, Active deals the effect. + + + +- **Melee Abilities:** + +> - **Slam** - Leap and AOE on impact. +> - **Heavy Hit** - Single powerful strike with bonus knockback. +> - **Cutter** - Slashing cone arc attack. +> - **Shockwave** - AOE burst radiating from the caster. + + + +- **Energy Orbs:** + +> - **Orb** - Homing energy sphere. +> - **Dual Orb** - Two simultaneous homing orbs. +> - **Orb Barrage** - Rapid volley of orbs in sequence. + + + +- **Energy Discs:** + +> - **Disc** - Spinning flat projectile with optional boomerang. +> - **Dual Disc** - Two simultaneous discs. + + + +- **Energy Beams:** + +> - **Beam** - Sustained homing beam from caster to target. +> - **Dual Beam** - Two simultaneous beams. + + + +- **Laser Shot:** + +> Instant-fire projectile with no homing. Pure aim. + + + +- **Projectile:** + +> Standard projectile using classic Minecraft physics. + + + +- **Movement Abilities:** + +> - **Charge** - Rush forward in a line, damaging everything in path. +> - **Dash** - Quick directional movement without damage. +> - **Teleport** - Instant relocation to the target. + + + +- **AOE Abilities:** + +> - **Sweeper** - Wide rotational sweep around the caster. +> - **Vortex** - Pulls entities inward while dealing damage. + + + +- **Telegraph System:** + +> Visual ground indicators during wind-up. Supports Circle, Ring, Line, Cone, and Point shapes with color customization and warning phase transitions. + + + +- **Energy Projectile Customization:** + +> Modular data system for all energy projectiles: Anchor Point, Display Colors, Homing, Combat Stats, and Lifespan. + + + +- **Ability Effects:** + +> Abilities can apply potion effects on hit: Slowness, Weakness, Poison, Wither, Blindness, Nausea, Hunger, Mining Fatigue. + + + +- **Ability Conditions:** + +> Condition system controlling when abilities can execute based on target state and custom predicates. + + + +- **Interruptible Wind-ups:** + +> Abilities can be interrupted by damage during wind-up, forcing a configurable Dazed recovery state. + + + +- **Movement & Rotation Locking:** + +> Lock caster movement, rotation, or both during Wind-up, Active, or both phases. + + + +- **Animation & Sound Integration:** + +> Wind-up and Active animations with optional duration sync. Custom sound events per phase. + + + +- **Player Abilities:** + +> Players can unlock abilities, select one at a time, and share a universal cooldown. + + + +- **Ability Preview:** + +> Live preview in the ability editor GUI. Test telegraphs, animations, and projectiles on a dummy NPC. + + + +- **Scripting API:** + +> Full event support: Start, Execute, Interrupt, Complete, Hit (modifiable damage/knockback), and Tick events. Start, Execute, and Hit are cancelable. + + + +- **Ability Commands:** + +> `/kam ability list`, `types`, `info`, `delete`, `reload` for managing custom abilities. + + + +**Extras** + +- Global ability presets that update everywhere when edited. +- Weighted ability selection for NPC random usage. +- Configurable min/max range per ability. +- Reusable telegraph presets shared across abilities. diff --git a/Ability_Update.md b/Ability_Update.md new file mode 100644 index 000000000..cf160bc5f --- /dev/null +++ b/Ability_Update.md @@ -0,0 +1,199 @@ +# ⚔️ CustomNPC+ 1.11 - Abilities ⚔️ + +--- + +The **Ability Update** introduces a full combat ability system to CNPC+! Give your NPCs powerful attacks with **visual telegraphs**, launch **energy projectiles** that home in on targets, and let players unlock abilities of their own! + +--- + +## How Abilities Work 🎯 + +Every ability follows a **phase-based execution flow** that gives combat a readable, strategic feel. + +**Phases:** +- **Wind-up** - The ability charges. Telegraphs appear, animations play, and observant players can react +- **Active** - The ability fires. Damage is dealt, projectiles launch, effects apply +- **Dazed** - Optional recovery state if the ability is interrupted during wind-up + +**Core Configuration:** +- **Cooldown** - Time before the ability can be used again +- **Targeting Mode** - Aggro Target, Self, AOE on Self, or AOE on Target +- **Range** - Configurable min and max range for activation +- **Lock Movement** - Lock the caster's movement, rotation, or both during any phase +- **Interruptible** - Allow damage to interrupt wind-up and force a dazed state +- **Weight** - Priority weighting for NPC random ability selection + +> Abilities support both **inline configuration** on individual NPCs and **global presets** that update everywhere when edited. + +--- + +## Melee Abilities 🗡️ + +Close-range attacks that hit hard and punish positioning. + +- **Slam** - Leap toward the target in an arc, dealing AOE damage on impact. Configurable radius, knockback, and leap height +- **Heavy Hit** - A single powerful melee strike with bonus knockback +- **Cutter** - A slashing arc attack in a cone. Configurable angle and reach +- **Shockwave** - An AOE burst radiating outward from the caster + +> All melee abilities support configurable **damage**, **knockback**, and **potion effects** on hit. + +--- + +## Energy Projectiles ✨ + +A brand new projectile system built from the ground up with full visual customization. + +### Orbs + +Homing energy spheres that track their target through the air. + +- **Orb** - A single homing projectile +- **Dual Orb** - Fires two orbs simultaneously +- **Orb Barrage** - Fires a volley of orbs in rapid sequence + +### Discs + +Flat spinning projectiles with optional **boomerang** behavior. + +- **Disc** - A single spinning disc with configurable radius and thickness +- **Dual Disc** - Fires two discs simultaneously + +### Beams + +Sustained energy beams that connect the caster to their target. + +- **Beam** - A homing beam with configurable width and head size. Locks the caster in place during channeling +- **Dual Beam** - Fires two beams simultaneously + +### Laser Shot + +An **instant-fire** projectile with configurable width and speed. No homing — pure aim. + +### Projectile + +A standard projectile using classic Minecraft physics for a more traditional ranged attack. + +### Energy Configuration + +Every energy projectile shares a **modular data system** for deep customization: + +- **Anchor Point** - Choose where the projectile spawns: Right Hand, Left Hand, Front, Center, Chest, or Above Head +- **Display** - Set core color, edge color, glow effect, transparency, and size +- **Homing** - Toggle homing, adjust turn rate and max tracking range +- **Combat** - Damage, knockback, vertical knockback, hit cooldown, and optional AOE splash +- **Lifespan** - Projectile speed, max age, and max travel distance + +--- + +## Movement Abilities 💨 + +Abilities that reposition the caster in combat. + +- **Charge** - Rush forward in a line, damaging all entities in the path. Configurable speed, hit width, and knockback +- **Dash** - A quick directional movement without dealing damage +- **Teleport** - Instantly relocate to the target's position + +> **Charge** locks direction during wind-up, so the telegraph accurately shows the attack path. + +--- + +## AOE Abilities 🌀 + +Area control abilities that dominate space. + +- **Sweeper** - A wide rotational sweep attack around the caster with configurable radius and rotation speed +- **Vortex** - Creates a pulling force that drags nearby entities toward the center while dealing damage + +--- + +## Telegraph System 📍 + +**Telegraphs** are visual ground indicators that warn players before an ability lands. They appear during the wind-up phase and disappear when the ability fires. + +**Shapes:** +- **Circle** - Filled circle with configurable radius +- **Ring** - Donut shape with inner and outer radius +- **Line** - Rectangle along the attack direction +- **Cone** - Fan/wedge shape with configurable angle +- **Point** - Single point indicator + +**Features:** +- **Color Customization** - Set ARGB colors for both the telegraph and its warning phase +- **Warning Phase** - Color shifts in the final ticks before the ability fires +- **Entity Tracking** - Telegraphs can follow a moving entity or stay at a fixed position +- **Smooth Interpolation** - Position updates are interpolated for clean visuals + +> Telegraphs are saved as **reusable presets** and can be shared across multiple abilities. + +--- + +## Animations & Sound 🎬 + +Abilities integrate directly with the animation and sound systems. + +- **Wind-up Animation** - Plays during the charging phase +- **Active Animation** - Plays when the ability fires +- **Animation Sync** - Optionally sync wind-up duration to match animation length exactly +- **Sound Events** - Assign custom sounds to both wind-up and active phases +- **Phase Colors** - Visual color feedback on the caster during wind-up and active + +> Many ability types include **built-in animation names** that work automatically when matching animations exist. + +--- + +## Effects & Conditions ⚡ + +### Potion Effects + +Abilities can apply **potion effects** to any entity they hit. Each effect has configurable type, duration, and amplifier. + +**Available Effects:** +Slowness, Weakness, Poison, Wither, Blindness, Nausea, Hunger, Mining Fatigue + +### Conditions + +Abilities support a **condition system** that controls when they can be used. Conditions are evaluated before execution and can require specific target states, ranges, or custom predicates. + +--- + +## Player Abilities 🧑 + +Players can unlock and use abilities too! + +- **Unlock System** - Players receive abilities via scripting or commands +- **Ability Selection** - Choose one ability at a time from your unlocked list +- **Universal Cooldown** - Shared cooldown across all player abilities + +--- + +## Scripting API 📜 + +Full scripting support for ability lifecycle events: + +- **StartEvent** *(Cancelable)* - Fired when wind-up begins +- **ExecuteEvent** *(Cancelable)* - Fired when the ability activates +- **InterruptEvent** - Fired when wind-up is interrupted by damage +- **CompleteEvent** - Fired when the ability finishes +- **HitEvent** *(Cancelable)* - Fired on each entity hit. Damage and knockback values can be modified +- **TickEvent** - Fired every tick with current phase and tick count + +> Scripts can cancel, modify damage, and react to every phase of ability execution. + +--- + +## Ability Preview 🔎 + +The ability editor includes a **live preview system**. Test your telegraph shapes, animations, and projectile visuals directly in the GUI on a dummy NPC — no world effects applied. + +--- + +## Ability Commands 💬 + +- `/kam ability list` - List all custom abilities +- `/kam ability types` - List all registered ability types +- `/kam ability info ` - View ability details +- `/kam ability delete ` - Delete a custom ability +- `/kam ability reload` - Reload abilities from disk + +--- diff --git a/Ability_Update_Review.md b/Ability_Update_Review.md deleted file mode 100644 index 2ceb38797..000000000 --- a/Ability_Update_Review.md +++ /dev/null @@ -1,301 +0,0 @@ -# Ability System Review - CustomNPC+ - -A comprehensive technical review of the Ability System identifying bugs, logic issues, incomplete features, and inconsistencies. - ---- - -## Critical Issues - -### 1. Cooldown Uses Mixed Time Systems -**Status:** FIXED -**Files:** -- `Ability.java` - Removed cooldown tracking (cooldownEndTime, startCooldown(), isOnCooldown(), canUse()) -- `DataAbilities.java` - Now sole owner of cooldown management using world time - -**Solution:** Removed all cooldown tracking from `Ability.java`. Cooldowns are now managed exclusively by `DataAbilities` using `world.getTotalWorldTime()` for consistency. - ---- - -### 2. AbilityHeal - Heal Over Time Completely Broken -**Status:** FIXED -**File:** `AbilityHeal.java` - -**Solution:** Refactored to separate ally-finding from healing. The `findAlliesInRadius()` method is now called regardless of `instantHeal` flag, populating the list for HoT to use. Added heal particles during HoT ticks. - ---- - -### 3. AbilityGuard - Counter Integration Missing -**Status:** FIXED -**Files:** -- `DataAbilities.java:onDamage()` - Added integration to call `AbilityGuard.onDamageTaken()` -- `DataAbilities.java:getGuardDamageReduction()` - Added helper method for damage reduction - -**Solution:** `DataAbilities.onDamage()` now checks if current ability is an `AbilityGuard` and calls `onDamageTaken()` when the NPC is hit while guarding. - ---- - -## High Priority Issues - -### 4. knockbackUp Stored But Never Applied -**Status:** FIXED -**File:** `Ability.java` - -**Solution:** Updated `applyNpcKnockback()` to accept a `knockbackUp` parameter and use it instead of hardcoded `0.1D`. Both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` now pass the knockbackUp value from the HitEvent. - ---- - -### 5. Targeting Modes Not Implemented -**Status:** FIXED (Removed) -**File:** `TargetingMode.java` - -**Solution:** Removed the unused targeting modes (`RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT`) from the enum. Only the implemented modes remain: `AGGRO_TARGET`, `SELF`, `AOE_SELF`, `AOE_TARGET`. - ---- - -### 6. Damage Source Inconsistency -**Status:** FIXED -**File:** `Ability.java` - -**Solution:** Standardized both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` to use `NpcDamageSource("mob", npc)` for consistent damage source handling. - ---- - -## Incomplete Features - -### 7. AbilityProjectile - No Actual Projectile -**Status:** CONFIRMED - INCOMPLETE -**File:** `AbilityProjectile.java:88` - -```java -// TODO: Use custom EntityAbilityProjectile for actual tracking -applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz); -``` - -**Current behavior:** -- `speed`, `homing`, `homingStrength` fields exist but are unused -- No projectile entity is spawned -- Damage is instant, just with particle cosmetics - ---- - -### 8. AbilityOrb - No Visual Orb Rendering -**Status:** CONFIRMED - INCOMPLETE -**File:** `AbilityOrb.java:117-179` - -**Problem:** The orb tracks position (`orbX`, `orbY`, `orbZ`) with runtime getters but nothing renders it client-side. Players only see particle effects on hit, not the flying orb itself. - ---- - -### 9. AbilityBeam - No Visual Beam Rendering -**Status:** CONFIRMED - INCOMPLETE -**File:** `AbilityBeam.java:213-220` - -**Problem:** Only spawns flame particles along the path. No actual beam entity or proper beam visual. The `lockOnTarget` telegraph during active phase isn't visually communicated. - ---- - -## Medium Priority Issues - -### 10. Hazard/Trap Telegraph Offset Mismatch -**Status:** CONFIRMED -**Files:** -- `AbilityHazard.java:119-147, 201-248` -- `AbilityTrap.java:109-138, 166-205` - -**Problem:** Telegraph is created at target position, but actual effect applies random offset: - -```java -// createTelegraph() - No offset applied, shows at target position - -// onExecute() - Random offset applied AFTER telegraph locks -double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); -zoneX = pos[0]; // Actual effect position differs from telegraph -``` - -**Impact:** Players see telegraph at one location, damage happens elsewhere. Misleading visual feedback. - -**Fix:** Either apply offset during telegraph creation, or update telegraph position in `onExecute()`. - ---- - -### 11. AbilityCharge Telegraph Direction Timing -**Status:** CONFIRMED - MINOR -**File:** `AbilityCharge.java:91, 254-286` - -**Problem:** Telegraph created at tick 0, direction locked at tick 1: - -```java -// Telegraph created during setup (tick 0) with current target direction -// Line 91: Direction locked at tick 1 -if (tick == 1) { - lockChargeDirection(npc, target); -} -``` - -**Impact:** If target moves between tick 0-1, telegraph direction won't match actual charge. - ---- - -### 12. AbilitySlam - activeTicks May End Before Landing -**Status:** CONFIRMED - MINOR -**File:** `AbilitySlam.java:52-53, 230-250` - -**Problem:** `activeTicks = 60` is fixed, but if NPC takes longer to land, ability transitions to RECOVERY without `onLanding()` being called. - -**Secondary issue:** If NPC lands early (tick 15), active phase continues until tick 60 doing nothing - wasted ticks. - ---- - -### 13. FOLLOW Mode Telegraph Not Updated -**Status:** CONFIRMED -**Files:** `AbilityHazard.java:139-146`, `AbilityVortex.java` - -**Problem:** For `FOLLOW_TARGET` mode, telegraph locks position on execute, but the actual hazard zone follows the target. Visual mismatch between warning and danger. - ---- - -## Redundancies - -### 14. Duplicate findGroundLevel() Methods -**Status:** FIXED -**Files:** -- `Ability.java` - Made `findGroundLevel()` public static -- `TelegraphInstance.java` - Now uses `Ability.findGroundLevel()` - ---- - -### 15. Duplicate calculateOffsetPosition() Methods -**Status:** FIXED -**Files:** -- `Ability.java` - Added shared static `calculateOffsetPosition()` method -- `AbilityTrap.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()` -- `AbilityHazard.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()` - ---- - -### 16. Unused applyKnockback in AbilityOrb -**Status:** FIXED -**File:** `AbilityOrb.java` - -Removed the unused `applyKnockback()` method. - ---- - -### 17. Duplicate Reset/Interrupt Cleanup -**Status:** NOT FIXED (Low Priority) -**Multiple ability files** - -Most abilities override both `reset()` and `onInterrupt()` with duplicate cleanup code. Pattern should have `onInterrupt()` call `reset()` or use shared cleanup method. - ---- - -## Minor Issues & Inconsistencies - -### 18. Field Naming Confusion -- `AbilityHazard`: `damagePerSecond` with `damageInterval` - name suggests per-second but applied every N ticks -- `AbilityCharge`: `hitWidth` stored as `hitRadius` in older NBT (backward compat exists) - -### 19. AbilityVortex Entity Lookup Performance -**File:** `AbilityVortex.java:201-212` - -Iterates through ALL loaded entities every tick to find pulled entities by UUID. Should cache entity references or use entity IDs. - -### 20. Telegraph Yaw Rotation -**File:** `TelegraphRenderer.java:95` - -Uses `GL11.glRotatef(-instance.getYaw(), 0, 1, 0)` - negative yaw. Should verify this matches Minecraft's yaw conventions. - -### 21. Missing Null Checks -Various locations where `target` could be null but is used without defensive checks: -- `AbilityProjectile.java:73` -- `AbilityOrb.java:138-171` - ---- - -## Missing Features (Enhancement Requests) - -These are not bugs but notable missing functionality: - -1. **No Ability Chaining/Combo System** - No way to trigger abilities from other abilities -2. **No Cooldown Reduction on Hit** - Can't reduce cooldowns based on successful hits -3. **No Multi-Target Hit Limit** - AOE abilities hit unlimited targets (unlike Vortex's `maxTargets`) -4. **No Damage Type Configuration** - All abilities use mob/melee damage, no magic/fire/etc options -5. **No Target Validation During Execution** - If target dies/despawns mid-ability, behavior varies - ---- - -## Priority Summary - -| Priority | Issue | Status | -|----------|-------|--------| -| **CRITICAL** | Cooldown time system mismatch | FIXED | -| **CRITICAL** | AbilityHeal HoT broken | FIXED | -| **CRITICAL** | AbilityGuard counter not integrated | FIXED | -| **HIGH** | knockbackUp ignored | FIXED | -| **HIGH** | Targeting modes unimplemented | FIXED (Removed) | -| **HIGH** | Damage source inconsistency | FIXED | -| **MEDIUM** | Projectile/Orb/Beam no visuals | Not Fixed | -| **MEDIUM** | Hazard/Trap telegraph offset | Not Fixed | -| **LOW** | Charge telegraph timing | Not Fixed | -| **LOW** | Slam activeTicks timing | Not Fixed | -| **LOW** | Code redundancies | FIXED | - ---- - -## Remaining Issues - -The following issues were not addressed in this fix round: - -1. **Projectile/Orb/Beam Visual Rendering** - These abilities still only use particles, no actual entity rendering -2. **Hazard/Trap Telegraph Offset Mismatch** - Telegraph shows at target, effect spawns with offset -3. **Charge Telegraph Direction Timing** - Minor visual mismatch between tick 0-1 -4. **Slam activeTicks Timing** - Fixed at 60 ticks regardless of landing time - ---- - -## Changes Made - -### Ability.java -- Removed `cooldownEndTime` transient field -- Removed `startCooldown()`, `isOnCooldown()`, `canUse()` methods (cooldown managed by DataAbilities) -- Updated `tick()` to not call startCooldown (DataAbilities handles it) -- Updated `reset()` to not reset cooldownEndTime -- Made `findGroundLevel()` public static for shared use -- Added static `calculateOffsetPosition()` utility method -- Fixed `applyNpcKnockback()` to accept and use knockbackUp parameter -- Standardized damage source to `NpcDamageSource("mob", npc)` in both damage methods -- Removed deprecated getters/setters (telegraphColor, castSound, animationId) - -### IAbility.java (API) -- Removed `isOnCooldown()` method from interface - -### DataAbilities.java -- Added AbilityGuard import -- Added guard counter integration in `onDamage()` -- Added `getGuardDamageReduction()` helper method -- Removed deprecated `isMovementBlocked()` method -- Updated knockbackUp javadoc (no longer deprecated) - -### CombatHandler.java -- Renamed `isMovementBlocked()` to `isAbilityControllingMovement()` - -### TargetingMode.java -- Removed unused modes: `RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT` - -### AbilityHeal.java -- Fixed HoT by separating `findAlliesInRadius()` from healing logic -- Allies are now found in `onExecute()` regardless of instantHeal flag -- Added heal particles during HoT ticks - -### AbilityHazard.java -- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()` -- Removed deprecated `getDamagePerTick()`/`setDamagePerTick()` methods - -### AbilityTrap.java -- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()` - -### AbilityOrb.java -- Removed unused `applyKnockback()` method - -### TelegraphInstance.java -- Removed duplicate `findGroundLevel()`, uses `Ability.findGroundLevel()` diff --git a/Auction_Beta_Update.md b/Auction_Beta_Update.md new file mode 100644 index 000000000..8b382b4af --- /dev/null +++ b/Auction_Beta_Update.md @@ -0,0 +1,57 @@ +# CNPC+ 1.11 Beta - Market Update + +🟡 **In Testing** + +## Features: ⚒️ + +- **Built-in Currency System:** +> Fully configurable economy currency used across all Market features. Profile persistent across character slots with Vault support for Bukkit-compatible plugins. + +- **Auction House:** +> Global player marketplace for listing items, bidding, and instant buyouts. Auctions last 24 hours by default. + +- **Auction Bidding & Snipe Protection:** +> Competitive bidding with a 5% minimum bid increment. Snipe Protection extends auctions on last-second bids. Outbid players are automatically refunded. + +- **Buyout System:** +> Instant purchase at the seller's Buyout Price. Highest bidder only pays the difference between their bid and the buyout. + +- **My Trades:** +> Single screen for managing Active Listings, Active Bids, and Pending Claims. Players can cancel listings with a 10% penalty if bids exist. + +- **Auction Notifications:** +> Real-time notifications for sales, outbids, and wins. Offline players receive a claim summary on login. + +- **Trade Slots:** +> Limited slots (default 8, max 45) shared across listings, bids, and claims. Configurable via permissions. + +- **Claim Expiration:** +> Uncollected claims expire after 20 days. + +- **Item Blacklist:** +> Block items by registry name, wildcard pattern, mod, or NBT tag. Soulbound and Profile-Slotbound items are always blocked automatically. + +- **Listing Fee & Sales Tax:** +> Configurable listing fee charged on creation and sales tax taken from successful sales. + +- **Search & Sorting:** +> Search by item name or seller. Sort by newest, ending soon, price, or most bids. + +- **Trader Stock System:** +> Limited stock with Global or Per-Player modes. Restock timers for MC Daily/Weekly, RL Daily/Weekly, or Custom intervals. + +- **Trader Currency Trades:** +> Trades can require currency on top of item costs. + +- **Auctioneer NPC Role:** +> New role that opens the Auction House on interaction. No configuration needed. + +- **Auction Commands:** +> `/kam auction open` for players. Admin commands for listing view and player activity inspection. + +**Extras** + +- Auction logging for server admins (tracks creates, bids, buyouts, sales, expirations, cancellations, claims). +- Lifetime currency tracking per player (total earned and spent). +- Configurable starting balance and max balance cap. +- Blacklist bypass permission for admins. diff --git a/Auction_Update.md b/Auction_Update.md index ae839832f..a04cc12e9 100644 --- a/Auction_Update.md +++ b/Auction_Update.md @@ -10,9 +10,10 @@ The **Market Update** brings a complete economy system to CNPC+! Create bustling CNPC+ now has a **built-in currency** used across all Market features! -- **Fully Configurable** - Name it Coins, Gold, Credits, whatever fits your world +- **Fully Configurable** - Name it Coins, Gold, Credits, whatever fits your world. Set a starting balance and max balance cap to shape your economy - **Profile Persistent** - Currency carries across all your character slots -- **Vault Support** - CNPC+ integrates seamlessly with Bukkit-compatible economy plugins if Vault is installed +- **Vault Support** - Integrates seamlessly with Bukkit-compatible economy plugins if Vault is installed. Falls back to built-in currency otherwise +- **Lifetime Tracking** - Tracks total earned and spent per player > Full Scripting API support for currency operations on players. @@ -20,17 +21,17 @@ CNPC+ now has a **built-in currency** used across all Market features! ## Auction House 🏛️ -A **global player marketplace** where players can list items, bid against each other, and purchase instantly! +A **global player marketplace** where players can list items, bid against each other, and purchase instantly! Search for items by name or seller, and sort by newest, ending soon, price, or most bids. ### Selling -List any item with a **Starting Price** for competitive bidding. Set an optional **Buyout Price** for players who want to skip the wait and buy immediately. +List any item with a **Starting Price** for competitive bidding. Set an optional **Buyout Price** for players who want to skip the wait and buy immediately. Auctions last **24 hours** by default. > A configurable **Listing Fee** is charged when creating auctions, and a **Sales Tax** is taken from successful sales. ### Bidding -Compete for items by placing bids! **Snipe Protection** extends auctions when last-second bids come in, giving everyone a fair shot. If you get outbid, your currency is **automatically refunded**. +Compete for items by placing bids! Each bid must be at least **5%** higher than the current bid. **Snipe Protection** extends auctions when last-second bids come in, giving everyone a fair shot. If you get outbid, your currency is **automatically refunded**. ### Buyout @@ -44,7 +45,9 @@ One screen to manage all your auction activity: - **Active Bids** - Auctions you're competing in - **Ready to Claim** - Won items and currency waiting for pickup -> Players can **cancel** their own listings. Items are returned as a claim. +Players can **cancel** their own listings -- items are returned as a claim. If there are active bids on the listing, a **10% cancellation penalty** is charged. + +> Claims expire after **20 days**. Make sure to collect your items and currency before they're gone! ### Notifications @@ -52,7 +55,7 @@ Get notified immediately when your item sells, when you're outbid, or when you w ### Trade Slots -Each player has a limited number of **Trade Slots** (default 8). Trade Slots are shared between Active Listings, Active Bids, and Pending Claims. +Every player gets a limited number of **Trade Slots** (default 8). Slots are shared across Active Listings, Active Bids, **and** Pending Claims -- so uncollected claims take up space. Pick up your claims to free slots for new listings and bids! > Server owners can grant players more slots via permissions up to a maximum of 45. @@ -68,14 +71,9 @@ Server owners have full control over what items can be listed on the Auction Hou - **Mod** - Block all items from a specific mod - **NBT Tag** - Block items containing certain NBT data -> **Soulbound** and **Profile-Slotbound** items are always blocked automatically. Bound items cannot be traded! +> **Soulbound** and **Profile-Slotbound** items are always blocked automatically, regardless of blacklist settings. Bound items cannot be traded! -**Blacklist Commands:** -- `/kam auction blacklist add ` - Add entry -- `/kam auction blacklist remove ` - Remove entry -- `/kam auction blacklist list` - View all blacklisted entries -- `/kam auction blacklist check` - Check if your held item is blacklisted -- `/kam auction blacklist reload` - Reload from config +Manage the blacklist in-game with `/kam auction blacklist` commands, or configure it directly in the config file. --- @@ -94,14 +92,10 @@ Traders now support **limited stock** with automatic restocking! - **RL Daily / Weekly** - Restock based on real-life time - **Custom** - Set your own interval in MC ticks or real-time -**Visual Feedback:** -- Stock count displayed next to each trade -- **Out of Stock** overlay when unavailable -- Live **countdown timer** until next restock +Stock counts, out-of-stock status, and restock timers are all visible directly in the trade interface. > Admins can manually reset stock or cooldowns via the Trader GUI. - ### Currency Trades Trades can now require **currency on top of items**. Combine material costs with coin costs for balanced economy progression! @@ -110,10 +104,9 @@ Trades can now require **currency on top of items**. Combine material costs with ## Auctioneer Role 🧑‍💼 -New NPC role for the Auction House! Set the role on any NPC and players can access the full Auction House by interacting with them. -> No configuration needed +New NPC role for the Auction House! Set the role on any NPC and players can access the full Auction House by interacting with them -- no extra configuration needed. The role respects the global Auction Enabled setting. -### Auction Command +### Auction Commands Players can also access the Auction House via command: @@ -123,5 +116,6 @@ Players can also access the Auction House via command: - `/kam auction list [page]` - View active auctions in chat - `/kam auction view ` - View a player's auction activity ---- +> Server owners can enable **auction logging** to track all auction activity (creates, bids, buyouts, sales, expirations, cancellations, and claims). +--- diff --git a/gradle-plugins b/gradle-plugins index 45dbf494c..f1c82c10c 160000 --- a/gradle-plugins +++ b/gradle-plugins @@ -1 +1 @@ -Subproject commit 45dbf494ca06f58efa8d6f2f9d1a138a3ee573f8 +Subproject commit f1c82c10c8c92eaaf55044cf931da5e1c7a2668f diff --git a/src/api b/src/api index 29ad8637d..f383fc303 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 29ad8637d68f12b3138c5b0c266088147432733b +Subproject commit f383fc3037221791e2f22a272580df9be013810f diff --git a/src/main/java/kamkeel/npcs/command/CloneCommand.java b/src/main/java/kamkeel/npcs/command/CloneCommand.java index 027ff228e..312293ca5 100644 --- a/src/main/java/kamkeel/npcs/command/CloneCommand.java +++ b/src/main/java/kamkeel/npcs/command/CloneCommand.java @@ -13,6 +13,7 @@ import net.minecraft.util.MathHelper; import net.minecraft.world.World; import net.minecraft.world.WorldServer; import noppes.npcs.controllers.ServerCloneController; +import noppes.npcs.controllers.data.CloneFolder; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -35,20 +36,24 @@ public class CloneCommand extends CommandKamkeelBase { @SubCommand( desc = "Add NPC(s) to clone storage", - usage = " [clonedname]", + usage = " [clonedname]", permission = 4 ) public void add(ICommandSender sender, String[] args) { - int tab = 0; + int tab = -1; + String folder = null; try { tab = Integer.parseInt(args[1]); - if (tab < 0 || tab > 15) { + if (tab < 1 || tab > 15) { sendError(sender, "Tab must be within 1-15"); return; } } catch (NumberFormatException ex) { - sendError(sender, String.format("Tab is not a number: %s", args[1])); - return; + folder = args[1]; + if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) { + sendError(sender, String.format("Unknown folder: %s", folder)); + return; + } } int x = sender.getPlayerCoordinates().posX; @@ -64,32 +69,47 @@ public class CloneCommand extends CommandKamkeelBase { NBTTagCompound compound = new NBTTagCompound(); if (!npc.writeToNBTOptional(compound)) return; - ServerCloneController.Instance.addClone(compound, name, tab); - sendResult(sender, String.format("Added NPC \u00A7e%s\u00A77 to Tab \u00A7b%d\u00A77", name, tab)); + if (folder != null) { + ServerCloneController.Instance.addClone(compound, name, folder); + sendResult(sender, String.format("Added NPC \u00A7e%s\u00A77 to Folder \u00A7b%s\u00A77", name, folder)); + } else { + ServerCloneController.Instance.addClone(compound, name, tab); + sendResult(sender, String.format("Added NPC \u00A7e%s\u00A77 to Tab \u00A7b%d\u00A77", name, tab)); + } } } } @SubCommand( desc = "List NPC from clone storage", - usage = "", + usage = "", permission = 2 ) public void list(ICommandSender sender, String[] args) { sendMessage(sender, "--- Stored NPCs --- (server side)"); - int tab = 0; + int tab = -1; + String folder = null; try { tab = Integer.parseInt(args[1]); - if (tab < 0 || tab > 15) { + if (tab < 1 || tab > 15) { sendError(sender, "Tab must be within 1-15"); return; } } catch (NumberFormatException ex) { - sendError(sender, String.format("Tab is not a number: %s", args[1])); - return; + folder = args[1]; + if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) { + sendError(sender, String.format("Unknown folder: %s", folder)); + return; + } } - for (String name : ServerCloneController.Instance.getClones(tab)) { + List clones; + if (folder != null) { + clones = ServerCloneController.Instance.getClones(folder); + } else { + clones = ServerCloneController.Instance.getClones(tab); + } + for (String name : clones) { sendMessage(sender, name); } sendMessage(sender, "------------------------------------"); @@ -97,59 +117,75 @@ public class CloneCommand extends CommandKamkeelBase { @SubCommand( desc = "Remove NPC from clone storage", - usage = " ", + usage = " ", permission = 4 ) public void del(ICommandSender sender, String[] args) throws CommandException { String nametodel = args[0]; - int tab = 0; + int tab = -1; + String folder = null; try { tab = Integer.parseInt(args[1]); - if (tab < 0 || tab > 15) { - sendError(sender, String.format("Tab must be within 1-15")); + if (tab < 1 || tab > 15) { + sendError(sender, "Tab must be within 1-15"); return; } } catch (NumberFormatException ex) { - sendError(sender, String.format("Tab is not a number: %s", args[1])); - return; - } - - boolean deleted = false; - for (String name : ServerCloneController.Instance.getClones(tab)) { - if (nametodel.equalsIgnoreCase(name)) { - ServerCloneController.Instance.removeClone(name, tab); - deleted = true; - break; + folder = args[1]; + if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) { + sendError(sender, String.format("Unknown folder: %s", folder)); + return; } } - if (!ServerCloneController.Instance.removeClone(nametodel, tab)) { + + boolean success; + if (folder != null) { + success = ServerCloneController.Instance.removeClone(nametodel, folder); + } else { + success = ServerCloneController.Instance.removeClone(nametodel, tab); + } + + if (!success) { sendError(sender, String.format("NPC '%s' was not found", nametodel)); } else { - sendResult(sender, String.format("Removed NPC \u00A7e%s\u00A77 from Tab \u00A7b%d\u00A77", nametodel, tab)); + if (folder != null) { + sendResult(sender, String.format("Removed NPC \u00A7e%s\u00A77 from Folder \u00A7b%s\u00A77", nametodel, folder)); + } else { + sendResult(sender, String.format("Removed NPC \u00A7e%s\u00A77 from Tab \u00A7b%d\u00A77", nametodel, tab)); + } } } @SubCommand( desc = "Spawn cloned NPC", - usage = " [[world:]x,y,z]] [newname]", + usage = " [[world:]x,y,z]] [newname]", permission = 2 ) public void spawn(ICommandSender sender, String[] args) throws CommandException { String name = args[0].replaceAll("%", " "); // if name of npc separed by space, user must use % in place of space - int tab = 0; + int tab = -1; + String folder = null; try { tab = Integer.parseInt(args[1]); - if (tab < 0 || tab > 15) { - sendError(sender, String.format("Tab must be within 1-15")); + if (tab < 1 || tab > 15) { + sendError(sender, "Tab must be within 1-15"); return; } } catch (NumberFormatException ex) { - sendError(sender, String.format("Tab is not a number: %s", args[1])); - return; + folder = args[1]; + if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) { + sendError(sender, String.format("Unknown folder: %s", folder)); + return; + } } String newname = null; - NBTTagCompound compound = ServerCloneController.Instance.getCloneData(sender, name, tab); + NBTTagCompound compound; + if (folder != null) { + compound = ServerCloneController.Instance.getCloneData(sender, name, folder); + } else { + compound = ServerCloneController.Instance.getCloneData(sender, name, tab); + } if (compound == null) { sendError(sender, "Unknown npc"); return; @@ -211,22 +247,26 @@ public class CloneCommand extends CommandKamkeelBase { } @SubCommand( - desc = "Spawn cloned NPC", - usage = " [[world:]x,y,z]] [newname]", + desc = "Spawn cloned NPC in a grid", + usage = " [[world:]x,y,z]] [newname]", permission = 2 ) public void grid(ICommandSender sender, String[] args) throws CommandException { String name = args[0].replaceAll("%", " "); // if name of npc separed by space, user must use % in place of space - int tab = 0; + int tab = -1; + String folder = null; try { tab = Integer.parseInt(args[1]); - if (tab < 0 || tab > 15) { - sendError(sender, String.format("Tab must be within 1-15")); + if (tab < 1 || tab > 15) { + sendError(sender, "Tab must be within 1-15"); return; } } catch (NumberFormatException ex) { - sendError(sender, String.format("Tab is not a number: %s", args[1])); - return; + folder = args[1]; + if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) { + sendError(sender, String.format("Unknown folder: %s", folder)); + return; + } } int width, height; @@ -238,9 +278,13 @@ public class CloneCommand extends CommandKamkeelBase { return; } - String newname = null; - NBTTagCompound compound = ServerCloneController.Instance.getCloneData(sender, name, tab); + NBTTagCompound compound; + if (folder != null) { + compound = ServerCloneController.Instance.getCloneData(sender, name, folder); + } else { + compound = ServerCloneController.Instance.getCloneData(sender, name, tab); + } if (compound == null) { sendError(sender, "Unknown npc"); return; @@ -317,6 +361,163 @@ public class CloneCommand extends CommandKamkeelBase { return; } + @SubCommand( + desc = "List all custom clone folders", + usage = "", + permission = 2 + ) + public void listfolders(ICommandSender sender, String[] args) { + if (ServerCloneController.Instance == null) { + sendError(sender, "Folder system not initialized"); + return; + } + sendMessage(sender, "--- Clone Folders ---"); + for (CloneFolder f : ServerCloneController.Instance.getFolderList()) { + sendMessage(sender, f.name); + } + sendMessage(sender, "------------------------------------"); + } + + @SubCommand( + desc = "Create a custom clone folder", + usage = "", + permission = 4 + ) + public void createfolder(ICommandSender sender, String[] args) { + if (ServerCloneController.Instance == null) { + sendError(sender, "Folder system not initialized"); + return; + } + String name = args[0]; + if (!CloneFolder.isValidName(name)) { + sendError(sender, String.format("Invalid folder name: %s", name)); + return; + } + if (ServerCloneController.Instance.hasFolder(name)) { + sendError(sender, String.format("Folder '%s' already exists", name)); + return; + } + CloneFolder created = ServerCloneController.Instance.createFolder(name); + if (created != null) { + sendResult(sender, String.format("Created folder \u00A7b%s", name)); + } else { + sendError(sender, String.format("Failed to create folder '%s'", name)); + } + } + + @SubCommand( + desc = "Rename a custom clone folder", + usage = " ", + permission = 4 + ) + public void renamefolder(ICommandSender sender, String[] args) { + if (ServerCloneController.Instance == null) { + sendError(sender, "Folder system not initialized"); + return; + } + String oldName = args[0]; + String newName = args[1]; + if (!ServerCloneController.Instance.hasFolder(oldName)) { + sendError(sender, String.format("Unknown folder: %s", oldName)); + return; + } + if (!CloneFolder.isValidName(newName)) { + sendError(sender, String.format("Invalid folder name: %s", newName)); + return; + } + if (ServerCloneController.Instance.renameFolder(oldName, newName)) { + sendResult(sender, String.format("Renamed folder \u00A7b%s\u00A77 to \u00A7b%s", oldName, newName)); + } else { + sendError(sender, String.format("Failed to rename folder '%s'", oldName)); + } + } + + @SubCommand( + desc = "Delete an empty custom clone folder", + usage = "", + permission = 4 + ) + public void deletefolder(ICommandSender sender, String[] args) { + if (ServerCloneController.Instance == null) { + sendError(sender, "Folder system not initialized"); + return; + } + String name = args[0]; + if (!ServerCloneController.Instance.hasFolder(name)) { + sendError(sender, String.format("Unknown folder: %s", name)); + return; + } + if (ServerCloneController.Instance.deleteFolder(name)) { + sendResult(sender, String.format("Deleted folder \u00A7b%s", name)); + } else { + sendError(sender, String.format("Failed to delete folder '%s' (folder must be empty)", name)); + } + } + + @SubCommand( + desc = "Move a clone between tabs/folders", + usage = " ", + permission = 4 + ) + public void move(ICommandSender sender, String[] args) { + if (ServerCloneController.Instance == null) { + sendError(sender, "Folder system not initialized"); + return; + } + String cloneName = args[0]; + + int fromTab = -1; + String fromFolder = null; + try { + fromTab = Integer.parseInt(args[1]); + if (fromTab < 1 || fromTab > 15) { + sendError(sender, "Source tab must be within 1-15"); + return; + } + } catch (NumberFormatException ex) { + fromFolder = args[1]; + if (!ServerCloneController.Instance.hasFolder(fromFolder)) { + sendError(sender, String.format("Unknown source folder: %s", fromFolder)); + return; + } + } + + int toTab = -1; + String toFolder = null; + try { + toTab = Integer.parseInt(args[2]); + if (toTab < 1 || toTab > 15) { + sendError(sender, "Destination tab must be within 1-15"); + return; + } + } catch (NumberFormatException ex) { + toFolder = args[2]; + if (!ServerCloneController.Instance.hasFolder(toFolder)) { + sendError(sender, String.format("Unknown destination folder: %s", toFolder)); + return; + } + } + + boolean success; + if (fromFolder != null && toFolder != null) { + success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toFolder); + } else if (fromFolder != null) { + success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toTab); + } else if (toFolder != null) { + success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toFolder); + } else { + success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toTab); + } + + if (success) { + String from = fromFolder != null ? fromFolder : String.valueOf(fromTab); + String to = toFolder != null ? toFolder : String.valueOf(toTab); + sendResult(sender, String.format("Moved \u00A7e%s\u00A77 from \u00A7b%s\u00A77 to \u00A7b%s", cloneName, from, to)); + } else { + sendError(sender, String.format("Failed to move '%s'", cloneName)); + } + } + public World getWorld(String t) { WorldServer[] ws = MinecraftServer.getServer().worldServers; for (WorldServer w : ws) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 3d49c689e..03faf42d5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -67,6 +67,7 @@ public abstract class Ability implements IAbility { // Feedback protected LockMovementType lockMovement = LockMovementType.WINDUP; + protected LockType lockType = LockType.BOTH; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -137,6 +138,16 @@ public abstract class Ability implements IAbility { // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ + /** + * Returns true if this ability type deals damage. + * Override in non-damaging subclasses to return false. + * Used by external mods to determine whether to show + * damage-related configuration (e.g., DBC stats tab). + */ + public boolean hasDamage() { + return true; + } + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { } @@ -246,12 +257,22 @@ public abstract class Ability implements IAbility { // Apply damage if (damage > 0) { - if (caster instanceof EntityNPCInterface) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage); - } else if (caster instanceof EntityPlayer) { - hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage); - } else { - hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage); + // Check for external damage handler first (e.g., DBC Addon) + IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler(); + boolean handled = false; + if (handler != null) { + handled = handler.handleDamage(this, caster, hitEntity, damage, knockback, knockbackUp, + knockbackDirX, knockbackDirZ); + } + if (!handled) { + // Default damage path + if (caster instanceof EntityNPCInterface) { + hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage); + } else if (caster instanceof EntityPlayer) { + hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage); + } else { + hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage); + } } } @@ -390,15 +411,27 @@ public abstract class Ability implements IAbility { defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks) .tab("General").range(0, 10000)); defs.add(FieldDef.section("ability.section.movement").tab("General")); - defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(), - () -> this.getLockMovement().getDisplayKey(), - v -> { - String[] keys = LockMovementType.getDisplayKeys(); - for (int i = 0; i < keys.length; i++) { - if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; } - } - }) - .tab("General").hover("ability.hover.lockMove")); + defs.add(FieldDef.row( + FieldDef.stringEnumField("ability.lockPosition", LockMovementType.getDisplayKeys(), + () -> this.getLockMovement().getDisplayKey(), + v -> { + String[] keys = LockMovementType.getDisplayKeys(); + for (int i = 0; i < keys.length; i++) { + if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; } + } + }) + .hover("ability.hover.lockPosition"), + FieldDef.stringEnumField("ability.lockType", LockType.getDisplayKeys(), + () -> this.getLockType().getDisplayKey(), + v -> { + String[] keys = LockType.getDisplayKeys(); + for (int i = 0; i < keys.length; i++) { + if (keys[i].equals(v)) { this.setLockType(LockType.fromOrdinal(i)); break; } + } + }) + .hover("ability.hover.lockType") + .visibleWhen(() -> this.lockMovement != LockMovementType.NO) + ).tab("General")); defs.add(FieldDef.row( FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) .hover("ability.hover.interruptible"), @@ -470,6 +503,11 @@ public abstract class Ability implements IAbility { } } + // External field providers (e.g., DBC Addon injecting a "DBC" tab) + for (IAbilityFieldProvider provider : AbilityController.Instance.getFieldProviders()) { + provider.addFieldDefinitions(this, defs); + } + return defs; } @@ -940,6 +978,7 @@ public abstract class Ability implements IAbility { nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); nbt.setInteger("lockMovement", lockMovement.ordinal()); + nbt.setInteger("lockType", lockType.ordinal()); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); @@ -990,6 +1029,7 @@ public abstract class Ability implements IAbility { dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); + lockType = nbt.hasKey("lockType") ? LockType.fromOrdinal(nbt.getInteger("lockType")) : LockType.BOTH; windUpColor = nbt.getInteger("windUpColor"); activeColor = nbt.getInteger("activeColor"); windUpSound = nbt.getString("windUpSound"); @@ -1154,6 +1194,14 @@ public abstract class Ability implements IAbility { this.lockMovement = lockMovement; } + public LockType getLockType() { + return lockType; + } + + public void setLockType(LockType lockType) { + this.lockType = lockType; + } + /** * API method: Get lock movement type as integer. * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE @@ -1173,19 +1221,53 @@ public abstract class Ability implements IAbility { } /** - * Check if movement should be locked during WINDUP phase. + * API method: Get lock type as integer. + * @return 0=MOVEMENT, 1=ROTATION, 2=BOTH */ @Override - public boolean isMovementLockedDuringWindup() { - return lockMovement.locksWindup(); + public int getLockTypeOrdinal() { + return lockType.ordinal(); } /** - * Check if movement should be locked during ACTIVE phase. + * API method: Set lock type from integer. + * @param type 0=MOVEMENT, 1=ROTATION, 2=BOTH + */ + @Override + public void setLockTypeOrdinal(int type) { + this.lockType = LockType.fromOrdinal(type); + } + + /** + * Check if movement (pathfinding/motion) should be locked during WINDUP phase. + */ + @Override + public boolean isMovementLockedDuringWindup() { + return lockMovement.locksWindup() && lockType.locksMovement(); + } + + /** + * Check if movement (pathfinding/motion) should be locked during ACTIVE phase. */ @Override public boolean isMovementLockedDuringActive() { - return lockMovement.locksActive(); + return lockMovement.locksActive() && lockType.locksMovement(); + } + + /** + * Check if rotation (yaw/pitch) should be locked during WINDUP phase. + */ + @Override + public boolean isRotationLockedDuringWindup() { + return lockMovement.locksWindup() && lockType.locksRotation(); + } + + /** + * Check if rotation (yaw/pitch) should be locked during ACTIVE phase. + */ + @Override + public boolean isRotationLockedDuringActive() { + return lockMovement.locksActive() && lockType.locksRotation(); } /** @@ -1194,9 +1276,23 @@ public abstract class Ability implements IAbility { public boolean isMovementLockedForCurrentPhase() { switch (phase) { case WINDUP: - return lockMovement.locksWindup(); + return isMovementLockedDuringWindup(); case ACTIVE: - return lockMovement.locksActive(); + return isMovementLockedDuringActive(); + default: + return false; + } + } + + /** + * Check if rotation should be locked during the current phase. + */ + public boolean isRotationLockedForCurrentPhase() { + switch (phase) { + case WINDUP: + return isRotationLockedDuringWindup(); + case ACTIVE: + return isRotationLockedDuringActive(); default: return false; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index f12ebd421..213c1a214 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -39,6 +39,12 @@ public class AbilityController implements IAbilityHandler { * Used by AbilitySlot for cache invalidation. */ private int version = 0; + /** External damage handler (e.g., DBC Addon). */ + private IAbilityDamageHandler damageHandler = null; + + /** External field providers for ability GUI (e.g., DBC Addon). Client-side only. */ + private final List fieldProviders = new ArrayList<>(); + public AbilityController() { registerBuiltinTypes(); } @@ -386,4 +392,39 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:hazard", AbilityHazard::new); registerType("cnpc:trap", AbilityTrap::new); } + + // ═══════════════════════════════════════════════════════════════════════════ + // EXTERNAL DAMAGE HANDLER + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Register an external damage handler for abilities. + * When set, the handler is called before default damage application. + * If the handler returns true, default damage is skipped. + */ + public void registerDamageHandler(IAbilityDamageHandler handler) { + this.damageHandler = handler; + } + + /** Get the registered damage handler, or null if none. */ + public IAbilityDamageHandler getDamageHandler() { + return damageHandler; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // EXTERNAL FIELD PROVIDERS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Register an external field provider for ability GUIs. + * Providers are called during getAllDefinitions() to inject custom tabs/fields. + */ + public void registerFieldProvider(IAbilityFieldProvider provider) { + fieldProviders.add(provider); + } + + /** Get all registered field providers. */ + public List getFieldProviders() { + return fieldProviders; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java new file mode 100644 index 000000000..9a7f353c1 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java @@ -0,0 +1,27 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.EntityLivingBase; + +/** + * External damage handler for abilities. + * When registered, called AFTER script events but BEFORE default attackEntityFrom. + * If handleDamage returns true, default damage is skipped. + */ +public interface IAbilityDamageHandler { + /** + * Handle damage from an ability. + * + * @param ability The ability dealing damage + * @param caster The entity executing the ability + * @param target The entity being hit + * @param damage The damage amount (after script event modifications) + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction + * @return true if damage was handled (skip default), false to use default damage + */ + boolean handleDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityFieldProvider.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityFieldProvider.java new file mode 100644 index 000000000..f196b307b --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityFieldProvider.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability; + +import noppes.npcs.client.gui.builder.FieldDef; + +import java.util.List; + +/** + * External field definition provider for abilities. + * Called during getAllDefinitions() to allow mods to inject custom tabs/fields + * into the ability configuration GUI. + *

+ * Note: Although this interface references FieldDef (client-only), it is safe to + * register on both sides because the generic type is erased at bytecode level. + * The addFieldDefinitions method is only ever called from client-side GUI code. + */ +public interface IAbilityFieldProvider { + /** + * Add field definitions to the ability's GUI. + * Fields added here should use .tab() to assign them to a custom tab. + * + * @param ability The ability being configured + * @param defs The mutable list of field definitions to add to + */ + void addFieldDefinitions(Ability ability, List defs); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/LockType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/LockType.java new file mode 100644 index 000000000..904a6c876 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/LockType.java @@ -0,0 +1,47 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines what an ability locks when Lock Position is enabled. + */ +public enum LockType { + MOVEMENT, + ROTATION, + BOTH; + + public boolean locksMovement() { + return this == MOVEMENT || this == BOTH; + } + + public boolean locksRotation() { + return this == ROTATION || this == BOTH; + } + + public String getDisplayKey() { + switch (this) { + case MOVEMENT: + return "ability.lockType.movement"; + case ROTATION: + return "ability.lockType.rotation"; + case BOTH: + return "ability.lockType.both"; + default: + return "ability.lockType.both"; + } + } + + public static String[] getDisplayKeys() { + return new String[]{ + "ability.lockType.movement", + "ability.lockType.rotation", + "ability.lockType.both" + }; + } + + public static LockType fromOrdinal(int ordinal) { + LockType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return BOTH; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 495d0bdef..5157eea01 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -109,6 +109,7 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); beamEntity.setEffects(this.effects); + beamEntity.setSourceAbility(this); world.spawnEntityInWorld(beamEntity); } } @@ -126,6 +127,11 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } + @Override + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + cleanup(); + } + @Override public void cleanup() { // Despawn beam entity if still alive diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java index f7046d322..762145fc4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -137,6 +137,8 @@ public class AbilityBeamDual extends Ability { beamEntity.setEffects(this.effects); if (dualFire) beamEntity2.setEffects(this.effects); + beamEntity.setSourceAbility(this); + beamEntity2.setSourceAbility(this); world.spawnEntityInWorld(beamEntity); world.spawnEntityInWorld(beamEntity2); } @@ -167,6 +169,11 @@ public class AbilityBeamDual extends Ability { public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } + @Override + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + cleanup(); + } + @Override public void cleanup() { // Despawn beam entity if still alive diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 10cfd2978..1a1e76aa3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -111,6 +111,11 @@ public class AbilityDash extends Ability implements IAbilityDash { this.activeSound = "mob.endermen.portal"; } + @Override + public boolean hasDamage() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index f70e44ccb..0f91869fa 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -110,6 +110,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { discEntity.setupCharging(anchorData, windUpTicks); discEntity.setEffects(this.effects); + discEntity.setSourceAbility(this); world.spawnEntityInWorld(discEntity); } } @@ -127,6 +128,11 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } + @Override + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + cleanup(); + } + @Override public void cleanup() { // Despawn disc entity if still alive diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java index 98e924b8a..421861fcd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -146,6 +146,8 @@ public class AbilityDiscDual extends Ability { discEntity2.setEffects(this.effects); } + discEntity.setSourceAbility(this); + discEntity2.setSourceAbility(this); world.spawnEntityInWorld(discEntity); world.spawnEntityInWorld(discEntity2); } @@ -176,6 +178,11 @@ public class AbilityDiscDual extends Ability { public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } + @Override + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + cleanup(); + } + @Override public void cleanup() { // Despawn disc entity if still alive diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index eab84d138..ec9f157f9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -65,6 +65,11 @@ public class AbilityGuard extends Ability implements IAbilityGuard { this.counterAnimationName = "Ability_Guard_Counter"; } + @Override + public boolean hasDamage() { + return false; + } + public enum CounterType { FLAT, PERCENT; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 107db40c6..c1faf000b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -58,6 +58,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal { this.showTelegraph = false; } + @Override + public boolean hasDamage() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 3fb4335be..4be2315d5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -99,6 +99,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { laserEntity.setupCharging(anchorData, windUpTicks); laserEntity.setEffects(this.effects); + laserEntity.setSourceAbility(this); world.spawnEntityInWorld(laserEntity); } } @@ -136,6 +137,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + cleanup(); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index e3ae6c228..96881cf4e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -104,6 +104,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { orbEntity.setupCharging(anchorData, windUpTicks); orbEntity.setEffects(this.effects); + orbEntity.setSourceAbility(this); world.spawnEntityInWorld(orbEntity); } } @@ -122,6 +123,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb { // Nothing to clean up - entity manages itself } + @Override + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + cleanup(); + } + @Override public void cleanup() { // Despawn orb entity if still alive diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java index e158062d2..71be07a34 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java @@ -129,6 +129,7 @@ public class AbilityOrbBarrage extends Ability { ); orbEntity.setupCharging(anchorData[i], windUpTicks); + orbEntity.setSourceAbility(this); chargingEntities[i] = orbEntity; world.spawnEntityInWorld(orbEntity); @@ -179,6 +180,7 @@ public class AbilityOrbBarrage extends Ability { orbSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData ); + orb.setSourceAbility(this); world.spawnEntityInWorld(orb); orbEntities.add(orb); @@ -211,6 +213,11 @@ public class AbilityOrbBarrage extends Ability { return true; } + @Override + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + cleanup(); + } + @Override public void cleanup() { currentOrb = 0; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java index 390610a2e..bd2870438 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -133,6 +133,8 @@ public class AbilityOrbDual extends Ability { orbEntity2.setEffects(this.effects); } + orbEntity.setSourceAbility(this); + orbEntity2.setSourceAbility(this); world.spawnEntityInWorld(orbEntity); world.spawnEntityInWorld(orbEntity2); } @@ -161,7 +163,11 @@ public class AbilityOrbDual extends Ability { @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - // Nothing to clean up - entity manages itself + } + + @Override + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + cleanup(); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 30b57bf6d..1be741d35 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -217,9 +217,6 @@ public class AbilitySlam extends Ability implements IAbilitySlam { double dragPowN = Math.pow(drag, flightTicks); double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN); - // Speed scales horizontal velocity to cover ground faster - vHorizontal *= speedFactor; - // Adjust vertical velocity for height difference between start and target if (dy > 0) { vy += dy / flightTicks * 1.5; // Going up - need more velocity @@ -438,7 +435,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40)); double drag = 0.91; double dragPowN = Math.pow(drag, flightTicks); - double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN) * speedFactor; + double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN); double dirX = dx / horizontalDist; double dirZ = dz / horizontalDist; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 1aa6fa139..2fcc62956 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -100,6 +100,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { } } + @Override + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + cleanup(); + } + @Override public void cleanup() { // Clean up entity if still alive diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 887ef25fc..cb06b7b6b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -84,6 +84,11 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { this.activeSound = "mob.endermen.portal"; } + @Override + public boolean hasDamage() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index f8053509b..1a036573e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -205,10 +205,19 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { startY = posY; startZ = posZ; - // Calculate velocity toward target + // Always derive direction from the owner's current yaw/pitch. + // This ensures the laser fires in the direction the NPC is visually facing, + // which matches the telegraph line direction regardless of Lock Movement mode. Entity owner = getOwnerEntity(); - if (target != null) { + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.dirX = -Math.sin(yaw) * Math.cos(pitch); + this.dirY = -Math.sin(pitch); + this.dirZ = Math.cos(yaw) * Math.cos(pitch); + } else if (target != null) { + // Fallback: aim at target if owner is somehow unavailable double dx = target.posX - startX; double dy = (target.posY + target.getEyeHeight() - 0.4) - startY; double dz = target.posZ - startZ; @@ -218,13 +227,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { this.dirY = dy / len; this.dirZ = dz / len; } - } else if (owner != null) { - // Fire in NPC's facing direction - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = (float) Math.toRadians(owner.rotationPitch); - this.dirX = -Math.sin(yaw) * Math.cos(pitch); - this.dirY = -Math.sin(pitch); - this.dirZ = Math.cos(yaw) * Math.cos(pitch);; } setPosition(endX, endY, endZ); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index fce06741b..7080ce2ef 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -4,8 +4,11 @@ import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.IAbilityDamageHandler; import kamkeel.npcs.controllers.data.ability.data.*; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -77,6 +80,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP protected boolean previewMode = false; // Client-side preview mode (no damage/effects) protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup) + /** The ability that spawned this projectile. Transient, not saved to NBT. */ + protected transient Ability sourceAbility = null; + // ==================== ROTATION INTERPOLATION ==================== public float prevRotationValX, prevRotationValY, prevRotationValZ; public float rotationValX, rotationValY, rotationValZ; @@ -322,14 +328,30 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP if (previewMode) return; // Skip damage in preview mode Entity owner = getOwnerEntity(); - if (owner instanceof EntityNPCInterface) { - target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); - } else if (owner instanceof EntityPlayer) { - target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); - } else if (owner instanceof EntityLivingBase) { - target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); - } else { - target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + // Check for external damage handler (e.g., DBC Addon) + boolean handled = false; + if (sourceAbility != null && owner instanceof EntityLivingBase) { + IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler(); + if (handler != null) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; + handled = handler.handleDamage(sourceAbility, (EntityLivingBase) owner, target, + dmg, kb, kbUp, dx, dz); + } + } + + if (!handled) { + // Default damage path + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + } } if (kb > 0) { @@ -361,6 +383,21 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP this.effects = effects != null ? effects : new ArrayList<>(); } + /** + * Set the source ability that spawned this projectile. + * Used by external damage handlers to access ability-specific data. + */ + public void setSourceAbility(Ability ability) { + this.sourceAbility = ability; + } + + /** + * Get the source ability that spawned this projectile. + */ + public Ability getSourceAbility() { + return sourceAbility; + } + protected void doExplosion() { if (previewMode) return; // Skip explosion in preview mode worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f); diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 63ab9b192..bcfd18abd 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -108,6 +108,9 @@ import kamkeel.npcs.network.packets.request.bank.BankSavePacket; import kamkeel.npcs.network.packets.request.bank.BanksGetPacket; import kamkeel.npcs.network.packets.request.clone.CloneAllTagsPacket; import kamkeel.npcs.network.packets.request.clone.CloneAllTagsShortPacket; +import kamkeel.npcs.network.packets.request.clone.CloneFolderCrudPacket; +import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket; +import kamkeel.npcs.network.packets.request.clone.CloneMovePacket; import kamkeel.npcs.network.packets.request.clone.CloneListPacket; import kamkeel.npcs.network.packets.request.clone.ClonePreSavePacket; import kamkeel.npcs.network.packets.request.clone.CloneRemovePacket; @@ -325,6 +328,9 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new CloneTagListPacket()); REQUEST_PACKET.registerPacket(new CloneAllTagsPacket()); REQUEST_PACKET.registerPacket(new CloneAllTagsShortPacket()); + REQUEST_PACKET.registerPacket(new CloneFolderListPacket()); + REQUEST_PACKET.registerPacket(new CloneFolderCrudPacket()); + REQUEST_PACKET.registerPacket(new CloneMovePacket()); // Linked Packets diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 8183941d0..2f3e6ea94 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -163,6 +163,9 @@ public enum EnumRequestPacket { CloneAllTags, CloneAllTagsShort, ClonePreSave, + CloneFolderList, + CloneFolderCrud, + CloneMove, EventScript, PlayerScript, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java index 2717d6277..75e0c40ae 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java @@ -16,7 +16,9 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.config.ConfigMain; +import noppes.npcs.client.ScriptClientConfig; import noppes.npcs.controllers.AuctionConfigSync; +import noppes.npcs.controllers.ScriptConfigSync; import java.io.IOException; import java.util.EnumMap; @@ -65,6 +67,11 @@ public final class LoginPacket extends AbstractPacket { NBTTagCompound auctionConfig = new NBTTagCompound(); AuctionConfigSync.writeToNBT(auctionConfig); ByteBufUtils.writeNBT(out, auctionConfig); + + // Send script config (server-side sync) + NBTTagCompound scriptConfig = new NBTTagCompound(); + ScriptConfigSync.writeToNBT(scriptConfig); + ByteBufUtils.writeNBT(out, scriptConfig); } @SideOnly(Side.CLIENT) @@ -88,6 +95,10 @@ public final class LoginPacket extends AbstractPacket { NBTTagCompound auctionConfig = ByteBufUtils.readNBT(in); AuctionClientConfig.readFromNBT(auctionConfig); + // Read script config + NBTTagCompound scriptConfig = ByteBufUtils.readNBT(in); + ScriptClientConfig.readFromNBT(scriptConfig); + PacketClient.sendClient(new SyncRevisionInfoPacket( serverKey, ClientCacheHandler.getLastServerKey(), diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneFolderCrudPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneFolderCrudPacket.java new file mode 100644 index 000000000..061700e35 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneFolderCrudPacket.java @@ -0,0 +1,111 @@ +package kamkeel.npcs.network.packets.request.clone; + +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.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.controllers.ServerCloneController; +import noppes.npcs.controllers.data.CloneFolder; + +import java.io.IOException; + +public final class CloneFolderCrudPacket extends AbstractPacket { + public static String packetName = "Request|CloneFolderCrud"; + + public static final byte ACTION_CREATE = 0; + public static final byte ACTION_RENAME = 1; + public static final byte ACTION_DELETE = 2; + + private byte action; + private String name; + private String newName; + + public CloneFolderCrudPacket() { + } + + public CloneFolderCrudPacket(byte action, String name) { + this.action = action; + this.name = name; + this.newName = ""; + } + + public CloneFolderCrudPacket(byte action, String name, String newName) { + this.action = action; + this.name = name; + this.newName = newName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CloneFolderCrud; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.NPC_CLONE; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeByte(action); + ByteBufUtils.writeString(out, name); + if (action == ACTION_RENAME) { + ByteBufUtils.writeString(out, newName); + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER)) + return; + + byte action = in.readByte(); + String name = ByteBufUtils.readString(in); + + boolean success = false; + switch (action) { + case ACTION_CREATE: + if (CloneFolder.isValidName(name)) + success = ServerCloneController.Instance.createFolder(name) != null; + break; + case ACTION_RENAME: + String newName = ByteBufUtils.readString(in); + if (CloneFolder.isValidName(newName)) + success = ServerCloneController.Instance.renameFolder(name, newName); + break; + case ACTION_DELETE: + success = ServerCloneController.Instance.deleteFolder(name); + break; + } + + NBTTagCompound compound = new NBTTagCompound(); + compound.setBoolean("Success", success); + NBTTagList folderList = new NBTTagList(); + for (CloneFolder folder : ServerCloneController.Instance.getFolderList()) { + folderList.appendTag(folder.writeNBT(new NBTTagCompound())); + } + compound.setTag("CloneFolders", folderList); + + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneFolderListPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneFolderListPacket.java new file mode 100644 index 000000000..b7e7fe9f8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneFolderListPacket.java @@ -0,0 +1,59 @@ +package kamkeel.npcs.network.packets.request.clone; + +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.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +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 net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.ServerCloneController; +import noppes.npcs.controllers.data.CloneFolder; + +import java.io.IOException; + +public final class CloneFolderListPacket extends AbstractPacket { + public static String packetName = "Request|CloneFolderList"; + + public CloneFolderListPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CloneFolderList; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER)) + return; + + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList folderList = new NBTTagList(); + for (CloneFolder folder : ServerCloneController.Instance.getFolderList()) { + folderList.appendTag(folder.writeNBT(new NBTTagCompound())); + } + compound.setTag("CloneFolders", folderList); + + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneListPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneListPacket.java index 87bac4a29..e6ce35f40 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneListPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneListPacket.java @@ -23,9 +23,16 @@ public final class CloneListPacket extends AbstractPacket { public static String packetName = "Request|CloneList"; private int tab; + private String folderName; public CloneListPacket(int tab) { this.tab = tab; + this.folderName = null; + } + + public CloneListPacket(String folderName) { + this.tab = -1; + this.folderName = folderName; } public CloneListPacket() { @@ -45,6 +52,9 @@ public final class CloneListPacket extends AbstractPacket { @Override public void sendData(ByteBuf out) throws IOException { out.writeInt(tab); + if (tab == -1) { + kamkeel.npcs.util.ByteBufUtils.writeString(out, folderName); + } } @Override @@ -54,14 +64,26 @@ public final class CloneListPacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.MOUNTER, EnumItemPacketType.CLONER)) return; - NBTTagList list = new NBTTagList(); int tab = in.readInt(); - for (String name : ServerCloneController.Instance.getClones(tab)) - list.appendTag(new NBTTagString(name)); + String folder = null; + if (tab == -1) { + folder = kamkeel.npcs.util.ByteBufUtils.readString(in); + } + NBTTagList list = new NBTTagList(); NBTTagList listDate = new NBTTagList(); - for (String name : ServerCloneController.Instance.getClonesDate(tab)) - listDate.appendTag(new NBTTagString(name)); + + if (folder != null) { + for (String name : ServerCloneController.Instance.getClones(folder)) + list.appendTag(new NBTTagString(name)); + for (String name : ServerCloneController.Instance.getClonesDate(folder)) + listDate.appendTag(new NBTTagString(name)); + } else { + for (String name : ServerCloneController.Instance.getClones(tab)) + list.appendTag(new NBTTagString(name)); + for (String name : ServerCloneController.Instance.getClonesDate(tab)) + listDate.appendTag(new NBTTagString(name)); + } NBTTagCompound compound = new NBTTagCompound(); compound.setTag("List", list); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneMovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneMovePacket.java new file mode 100644 index 000000000..124bd5204 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneMovePacket.java @@ -0,0 +1,97 @@ +package kamkeel.npcs.network.packets.request.clone; + +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.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.controllers.ServerCloneController; + +import java.io.IOException; + +public final class CloneMovePacket extends AbstractPacket { + public static String packetName = "Request|CloneMove"; + + private String cloneName; + private int fromTab; + private String fromFolder; + private int toTab; + private String toFolder; + + public CloneMovePacket() { + } + + public CloneMovePacket(String cloneName, int fromTab, String fromFolder, int toTab, String toFolder) { + this.cloneName = cloneName; + this.fromTab = fromTab; + this.fromFolder = fromFolder; + this.toTab = toTab; + this.toFolder = toFolder; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CloneMove; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.NPC_CLONE; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, cloneName); + out.writeInt(fromTab); + ByteBufUtils.writeString(out, fromFolder != null ? fromFolder : ""); + out.writeInt(toTab); + ByteBufUtils.writeString(out, toFolder != null ? toFolder : ""); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER)) + return; + + String cloneName = ByteBufUtils.readString(in); + int fromTab = in.readInt(); + String fromFolder = ByteBufUtils.readString(in); + if (fromFolder.isEmpty()) fromFolder = null; + int toTab = in.readInt(); + String toFolder = ByteBufUtils.readString(in); + if (toFolder.isEmpty()) toFolder = null; + + boolean success = false; + if (fromFolder != null && toFolder != null) { + success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toFolder); + } else if (fromFolder != null) { + success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toTab); + } else if (toFolder != null) { + success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toFolder); + } else { + success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toTab); + } + + NBTTagCompound compound = new NBTTagCompound(); + compound.setBoolean("MoveSuccess", success); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/ClonePreSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/ClonePreSavePacket.java index 378145ddb..4d36eb531 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/clone/ClonePreSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/ClonePreSavePacket.java @@ -24,6 +24,7 @@ public final class ClonePreSavePacket extends AbstractPacket { private String name; private int tab; + private String folderName; public ClonePreSavePacket() { } @@ -31,6 +32,13 @@ public final class ClonePreSavePacket extends AbstractPacket { public ClonePreSavePacket(String name, int tab) { this.name = name; this.tab = tab; + this.folderName = null; + } + + public ClonePreSavePacket(String name, String folderName) { + this.name = name; + this.tab = -1; + this.folderName = folderName; } @Override @@ -53,6 +61,9 @@ public final class ClonePreSavePacket extends AbstractPacket { public void sendData(ByteBuf out) throws IOException { ByteBufUtils.writeString(out, this.name); out.writeInt(tab); + if (tab == -1) { + ByteBufUtils.writeString(out, this.folderName); + } } @Override @@ -62,7 +73,15 @@ public final class ClonePreSavePacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER)) return; - boolean bo = ServerCloneController.Instance.getCloneData(null, ByteBufUtils.readString(in), in.readInt()) != null; + String name = ByteBufUtils.readString(in); + int tab = in.readInt(); + boolean bo; + if (tab == -1) { + String folder = ByteBufUtils.readString(in); + bo = ServerCloneController.Instance.getCloneData(null, name, folder) != null; + } else { + bo = ServerCloneController.Instance.getCloneData(null, name, tab) != null; + } NBTTagCompound compound = new NBTTagCompound(); compound.setBoolean("NameExists", bo); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneRemovePacket.java index ce1fcb367..96069f414 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneRemovePacket.java @@ -25,6 +25,7 @@ public final class CloneRemovePacket extends AbstractPacket { public static String packetName = "Request|CloneRemove"; private int tab; + private String folderName; private String name; public CloneRemovePacket() { @@ -32,6 +33,13 @@ public final class CloneRemovePacket extends AbstractPacket { public CloneRemovePacket(int tab, String name) { this.tab = tab; + this.folderName = null; + this.name = name; + } + + public CloneRemovePacket(String folderName, String name) { + this.tab = -1; + this.folderName = folderName; this.name = name; } @@ -54,6 +62,9 @@ public final class CloneRemovePacket extends AbstractPacket { @Override public void sendData(ByteBuf out) throws IOException { out.writeInt(tab); + if (tab == -1) { + ByteBufUtils.writeString(out, this.folderName); + } ByteBufUtils.writeString(out, this.name); } @@ -65,16 +76,28 @@ public final class CloneRemovePacket extends AbstractPacket { return; int tab = in.readInt(); - ServerCloneController.Instance.removeClone(ByteBufUtils.readString(in), tab); + String folder = null; + if (tab == -1) { + folder = ByteBufUtils.readString(in); + } + String cloneName = ByteBufUtils.readString(in); NBTTagList list = new NBTTagList(); - - for (String name : ServerCloneController.Instance.getClones(tab)) - list.appendTag(new NBTTagString(name)); - NBTTagList listDate = new NBTTagList(); - for (String name : ServerCloneController.Instance.getClonesDate(tab)) - listDate.appendTag(new NBTTagString(name)); + + if (folder != null) { + ServerCloneController.Instance.removeClone(cloneName, folder); + for (String name : ServerCloneController.Instance.getClones(folder)) + list.appendTag(new NBTTagString(name)); + for (String name : ServerCloneController.Instance.getClonesDate(folder)) + listDate.appendTag(new NBTTagString(name)); + } else { + ServerCloneController.Instance.removeClone(cloneName, tab); + for (String name : ServerCloneController.Instance.getClones(tab)) + list.appendTag(new NBTTagString(name)); + for (String name : ServerCloneController.Instance.getClonesDate(tab)) + listDate.appendTag(new NBTTagString(name)); + } NBTTagCompound compound = new NBTTagCompound(); compound.setTag("List", list); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneSavePacket.java index 3a8abd9c0..2c66572e3 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneSavePacket.java @@ -25,6 +25,7 @@ public final class CloneSavePacket extends AbstractPacket { private String name; private int tab; + private String folderName; private NBTTagCompound tagExtra; private NBTTagCompound tagCompound; @@ -34,6 +35,15 @@ public final class CloneSavePacket extends AbstractPacket { public CloneSavePacket(String name, int tab, NBTTagCompound tagExtra, NBTTagCompound tagCompound) { this.name = name; this.tab = tab; + this.folderName = null; + this.tagExtra = tagExtra; + this.tagCompound = tagCompound; + } + + public CloneSavePacket(String name, String folderName, NBTTagCompound tagExtra, NBTTagCompound tagCompound) { + this.name = name; + this.tab = -1; + this.folderName = folderName; this.tagExtra = tagExtra; this.tagCompound = tagCompound; } @@ -58,6 +68,9 @@ public final class CloneSavePacket extends AbstractPacket { public void sendData(ByteBuf out) throws IOException { ByteBufUtils.writeString(out, this.name); out.writeInt(tab); + if (tab == -1) { + ByteBufUtils.writeString(out, this.folderName); + } ByteBufUtils.writeNBT(out, this.tagExtra); ByteBufUtils.writeNBT(out, this.tagCompound); } @@ -75,11 +88,20 @@ public final class CloneSavePacket extends AbstractPacket { String name = ByteBufUtils.readString(in); int tab = in.readInt(); + String folder = null; + if (tab == -1) { + folder = ByteBufUtils.readString(in); + } NBTTagCompound tagExtra = ByteBufUtils.readNBT(in); NBTTagCompound tagCompound = ByteBufUtils.readNBT(in); NBTTagList tagList = tagCompound.getTagList("TagUUIDs", 8); data.cloned.setTag("TagUUIDs", tagList); - ServerCloneController.Instance.addClone(data.cloned, name, tab, tagExtra); + + if (folder != null) { + ServerCloneController.Instance.addClone(data.cloned, name, folder, tagExtra); + } else { + ServerCloneController.Instance.addClone(data.cloned, name, tab, tagExtra); + } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java index 4d7b21aac..926c5519c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java @@ -13,6 +13,7 @@ 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 kamkeel.npcs.util.ByteBufUtils; import noppes.npcs.controllers.ServerTagMapController; import noppes.npcs.controllers.data.TagMap; @@ -22,9 +23,16 @@ public final class CloneTagListPacket extends AbstractPacket { public static String packetName = "Request|CloneTagList"; private int tab; + private String folderName; public CloneTagListPacket(int tab) { this.tab = tab; + this.folderName = null; + } + + public CloneTagListPacket(String folderName) { + this.tab = -1; + this.folderName = folderName; } public CloneTagListPacket() { @@ -44,6 +52,9 @@ public final class CloneTagListPacket extends AbstractPacket { @Override public void sendData(ByteBuf out) throws IOException { out.writeInt(tab); + if (tab == -1) { + ByteBufUtils.writeString(out, folderName); + } } @Override @@ -54,7 +65,13 @@ public final class CloneTagListPacket extends AbstractPacket { return; int tab = in.readInt(); - TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); + TagMap tagMap; + if (tab == -1) { + String folder = ByteBufUtils.readString(in); + tagMap = ServerTagMapController.Instance.getTagMap(folder); + } else { + tagMap = ServerTagMapController.Instance.getTagMap(tab); + } NBTTagCompound compound = new NBTTagCompound(); compound.setTag("CloneTags", tagMap.writeNBT()); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/MobSpawnerPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/MobSpawnerPacket.java index cc4dfb3ed..0ad817b06 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/clone/MobSpawnerPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/MobSpawnerPacket.java @@ -31,6 +31,7 @@ public class MobSpawnerPacket extends AbstractPacket { private String selectedName; private int tab; + private String folderName; private NBTTagCompound compound; @@ -44,6 +45,17 @@ public class MobSpawnerPacket extends AbstractPacket { this.posz = posz; this.selectedName = selectedName; this.tab = tab; + this.folderName = null; + } + + public MobSpawnerPacket(Action type, int posX, int posY, int posz, String selectedName, String folderName) { + this.type = type; + this.posX = posX; + this.posY = posY; + this.posz = posz; + this.selectedName = selectedName; + this.tab = -1; + this.folderName = folderName; } public MobSpawnerPacket(Action type, int posX, int posY, int posz, NBTTagCompound compound) { @@ -58,6 +70,10 @@ public class MobSpawnerPacket extends AbstractPacket { PacketClient.sendClient(new MobSpawnerPacket(Action.Server, x, y, z, name, tab)); } + public static void ServerFolder(int x, int y, int z, String name, String folderName) { + PacketClient.sendClient(new MobSpawnerPacket(Action.Server, x, y, z, name, folderName)); + } + public static void Client(int x, int y, int z, NBTTagCompound compound) { PacketClient.sendClient(new MobSpawnerPacket(Action.Client, x, y, z, compound)); } @@ -88,6 +104,9 @@ public class MobSpawnerPacket extends AbstractPacket { if (type == Action.Server) { ByteBufUtils.writeString(out, this.selectedName); out.writeInt(this.tab); + if (this.tab == -1) { + ByteBufUtils.writeString(out, this.folderName); + } } else { ByteBufUtils.writeNBT(out, this.compound); } @@ -107,10 +126,18 @@ public class MobSpawnerPacket extends AbstractPacket { int z = in.readInt(); NBTTagCompound compound; - if (requestedAction == Action.Server) - compound = ServerCloneController.Instance.getCloneData(player, ByteBufUtils.readString(in), in.readInt()); - else + if (requestedAction == Action.Server) { + String name = ByteBufUtils.readString(in); + int tab = in.readInt(); + if (tab == -1) { + String folder = ByteBufUtils.readString(in); + compound = ServerCloneController.Instance.getCloneData(player, name, folder); + } else { + compound = ServerCloneController.Instance.getCloneData(player, name, tab); + } + } else { compound = ByteBufUtils.readNBT(in); + } if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) { return; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/SpawnMobPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/SpawnMobPacket.java index b83fb9286..8afde02cf 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/clone/SpawnMobPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/SpawnMobPacket.java @@ -37,6 +37,7 @@ public class SpawnMobPacket extends AbstractPacket { private String selectedName; private int tab; + private String folderName; private NBTTagCompound compound; @@ -50,6 +51,17 @@ public class SpawnMobPacket extends AbstractPacket { this.posz = posz; this.selectedName = selectedName; this.tab = tab; + this.folderName = null; + } + + public SpawnMobPacket(Action type, int posX, int posY, int posz, String selectedName, String folderName) { + this.type = type; + this.posX = posX; + this.posY = posY; + this.posz = posz; + this.selectedName = selectedName; + this.tab = -1; + this.folderName = folderName; } public SpawnMobPacket(Action type, int posX, int posY, int posz, NBTTagCompound compound) { @@ -64,6 +76,10 @@ public class SpawnMobPacket extends AbstractPacket { PacketClient.sendClient(new SpawnMobPacket(Action.Server, x, y, z, name, tab)); } + public static void ServerFolder(int x, int y, int z, String name, String folderName) { + PacketClient.sendClient(new SpawnMobPacket(Action.Server, x, y, z, name, folderName)); + } + public static void Client(int x, int y, int z, NBTTagCompound compound) { PacketClient.sendClient(new SpawnMobPacket(Action.Client, x, y, z, compound)); } @@ -94,6 +110,9 @@ public class SpawnMobPacket extends AbstractPacket { if (type == Action.Server) { ByteBufUtils.writeString(out, this.selectedName); out.writeInt(this.tab); + if (this.tab == -1) { + ByteBufUtils.writeString(out, this.folderName); + } } else { ByteBufUtils.writeNBT(out, this.compound); } @@ -113,10 +132,18 @@ public class SpawnMobPacket extends AbstractPacket { int z = in.readInt(); NBTTagCompound compound; - if (requestedAction == Action.Server) - compound = ServerCloneController.Instance.getCloneData(player, ByteBufUtils.readString(in), in.readInt()); - else + if (requestedAction == Action.Server) { + String name = ByteBufUtils.readString(in); + int tab = in.readInt(); + if (tab == -1) { + String folder = ByteBufUtils.readString(in); + compound = ServerCloneController.Instance.getCloneData(player, name, folder); + } else { + compound = ServerCloneController.Instance.getCloneData(player, name, tab); + } + } else { compound = ByteBufUtils.readNBT(in); + } if (compound == null) return; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 700513d78..d025fcb26 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -13,6 +13,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NBTTags; +import noppes.npcs.client.ScriptClientConfig; import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; @@ -69,8 +70,8 @@ public final class ScriptFilesPacket extends AbstractPacket { } //Set client side configs - ConfigScript.ScriptingEnabled = compound.getBoolean("ScriptingEnabled"); - ConfigScript.RunLoadedScriptsFirst = compound.getBoolean("LoadedFirst"); + ScriptClientConfig.setScriptingEnabled(compound.getBoolean("ScriptingEnabled")); + ScriptClientConfig.setRunLoadedScriptsFirst(compound.getBoolean("LoadedFirst")); ScriptController.Instance.globalRevision = compound.getInteger("GlobalRevision"); } diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index 6fefe01c1..ee65ad6a5 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -43,6 +43,7 @@ import noppes.npcs.containers.ContainerNpcQuestReward; import noppes.npcs.containers.ContainerNpcQuestTypeItem; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.config.ConfigScript; import noppes.npcs.entity.EntityNPCInterface; public class CommonProxy implements IGuiHandler { @@ -220,4 +221,24 @@ public class CommonProxy implements IGuiHandler { public void buildPackageIndex() { } + + public boolean isScriptingEnabled() { + return ConfigScript.ScriptingEnabled; + } + + public boolean isRunLoadedScriptsFirst() { + return ConfigScript.RunLoadedScriptsFirst; + } + + public boolean isGlobalPlayerScripts() { + return ConfigScript.GlobalPlayerScripts; + } + + public boolean isGlobalForgeScripts() { + return ConfigScript.GlobalForgeScripts; + } + + public boolean isGlobalNPCScripts() { + return ConfigScript.GlobalNPCScripts; + } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 1d91dad97..3f8cb5357 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -18,6 +18,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; + import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -147,10 +148,15 @@ public class DataAbilities { // Remove telegraph - it has served its purpose removeTelegraph(currentAbility); - // Lock rotation if movement is locked during ACTIVE phase - // Capture current rotation values to force them every tick - if (currentAbility.isMovementLockedDuringActive()) { - captureLockedRotation(); + // Handle rotation lock transition from WINDUP to ACTIVE + if (currentAbility.isRotationLockedDuringActive()) { + // ACTIVE or WINDUP_AND_ACTIVE: capture (or keep) rotation lock + if (!rotationLocked) { + captureLockedRotation(); + } + } else if (rotationLocked) { + // WINDUP only: release the lock now that windup is over + releaseLockedRotation(); } // Play active sound and animation @@ -390,6 +396,9 @@ public class DataAbilities { executeImmediate(ability, target); } else { // Normal windup flow + if (ability.isRotationLockedDuringWindup()) { + captureLockedRotation(); + } spawnTelegraph(ability, target); playAbilitySound(ability.getWindUpSound()); playAbilityAnimation(ability.getWindUpAnimation()); @@ -403,8 +412,8 @@ public class DataAbilities { * Called when windUpTicks is 0. */ private void executeImmediate(Ability ability, EntityLivingBase target) { - // Lock rotation if movement is locked during ACTIVE phase - if (ability.isMovementLockedDuringActive()) { + // Lock rotation if rotation is locked during ACTIVE phase + if (ability.isRotationLockedDuringActive()) { captureLockedRotation(); } @@ -934,9 +943,12 @@ public class DataAbilities { // ROTATION LOCKING // ═══════════════════════════════════════════════════════════════════ + /** Bit flag for rotation lock in the existing data watcher slot 15 bitfield */ + private static final int ROTATION_LOCKED_FLAG = 16; + /** * Capture current rotation values to lock NPC's look direction. - * Called when entering ACTIVE phase with movement locked during active. + * Called when entering a locked phase (WINDUP or ACTIVE depending on LockMovementType). */ private void captureLockedRotation() { lockedYaw = npc.rotationYaw; @@ -944,22 +956,53 @@ public class DataAbilities { lockedRenderYawOffset = npc.renderYawOffset; lockedPitch = npc.rotationPitch; rotationLocked = true; + npc.setBoolFlag(true, ROTATION_LOCKED_FLAG); } /** * Release the rotation lock. - * Called when leaving ACTIVE phase or ability completes. + * Called when leaving the locked phase or ability completes. */ private void releaseLockedRotation() { rotationLocked = false; + npc.setBoolFlag(false, ROTATION_LOCKED_FLAG); } /** * Apply locked rotation values to the NPC. * Called AFTER super.onLivingUpdate() in EntityNPCInterface to override - * any rotation changes made by the look helper or AI tasks. + * any rotation changes made by the look helper, AI tasks, or body smoothing. + * Runs on both client and server. */ public void applyLockedRotation() { + if (npc.worldObj.isRemote) { + // Client-side: check bit 16 in the existing flags data watcher (slot 15) + boolean flagActive = npc.getBoolFlag(ROTATION_LOCKED_FLAG); + if (!flagActive) { + rotationLocked = false; + return; + } + // First tick the flag is active: capture current rotation values + if (!rotationLocked) { + lockedYaw = npc.rotationYaw; + lockedYawHead = npc.rotationYawHead; + lockedRenderYawOffset = npc.renderYawOffset; + lockedPitch = npc.rotationPitch; + rotationLocked = true; + } + // Force all rotation values back to locked state + npc.rotationYaw = lockedYaw; + npc.rotationYawHead = lockedYawHead; + npc.renderYawOffset = lockedRenderYawOffset; + npc.rotationPitch = lockedPitch; + npc.prevRotationYaw = lockedYaw; + npc.prevRotationYawHead = lockedYawHead; + npc.prevRenderYawOffset = lockedRenderYawOffset; + npc.prevRotationPitch = lockedPitch; + return; + } + + // Server-side: use stored locked values if (!rotationLocked) { return; } diff --git a/src/main/java/noppes/npcs/ai/EntityAIAmbushTarget.java b/src/main/java/noppes/npcs/ai/EntityAIAmbushTarget.java index 4ee229b0e..3ba29ecd1 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIAmbushTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIAmbushTarget.java @@ -93,7 +93,9 @@ public class EntityAIAmbushTarget extends EntityAIBase { * Updates the task */ public void updateTask() { - this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + if (!this.theEntity.abilities.isRotationLocked()) { + this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + } } private Vec3 findHidingSpot() { diff --git a/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java b/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java index 5fe559fc2..473c0bfb8 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java @@ -118,7 +118,9 @@ public class EntityAIAttackTarget extends EntityAIBase { * Updates the task */ public void updateTask() { - this.npc.getLookHelper().setLookPositionWithEntity(this.entityTarget, 30.0F, 30.0F); + if (!this.npc.abilities.isRotationLocked()) { + this.npc.getLookHelper().setLookPositionWithEntity(this.entityTarget, 30.0F, 30.0F); + } // Skip pathfinding updates if ability is controlling movement if (!navOverride && !this.npc.abilities.isAbilityControllingMovement() && --this.field_75445_i <= 0) { diff --git a/src/main/java/noppes/npcs/ai/EntityAIDodgeShoot.java b/src/main/java/noppes/npcs/ai/EntityAIDodgeShoot.java index ef7b3f5ac..b07b911f4 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIDodgeShoot.java +++ b/src/main/java/noppes/npcs/ai/EntityAIDodgeShoot.java @@ -63,7 +63,7 @@ public class EntityAIDodgeShoot extends EntityAIBase { * Updates the task */ public void updateTask() { - if (this.entity.getAttackTarget() != null) + if (this.entity.getAttackTarget() != null && !this.entity.abilities.isRotationLocked()) this.entity.getLookHelper().setLookPositionWithEntity(this.entity.getAttackTarget(), 30.0F, 30.0F); } } diff --git a/src/main/java/noppes/npcs/ai/EntityAIFollow.java b/src/main/java/noppes/npcs/ai/EntityAIFollow.java index 61cabd38f..f236c0727 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIFollow.java +++ b/src/main/java/noppes/npcs/ai/EntityAIFollow.java @@ -55,7 +55,9 @@ public class EntityAIFollow extends EntityAIBase { if (updateTick < 10) return; updateTick = 0; - this.npc.getLookHelper().setLookPositionWithEntity(owner, 10.0F, (float) this.npc.getVerticalFaceSpeed()); + if (!this.npc.abilities.isRotationLocked()) { + this.npc.getLookHelper().setLookPositionWithEntity(owner, 10.0F, (float) this.npc.getVerticalFaceSpeed()); + } double speed = 1 + distance / 150; if (speed > 3) diff --git a/src/main/java/noppes/npcs/ai/EntityAILook.java b/src/main/java/noppes/npcs/ai/EntityAILook.java index 3448fc4ed..b9474a60c 100644 --- a/src/main/java/noppes/npcs/ai/EntityAILook.java +++ b/src/main/java/noppes/npcs/ai/EntityAILook.java @@ -26,7 +26,7 @@ public class EntityAILook extends EntityAIBase { */ @Override public boolean shouldExecute() { - return !npc.isAttacking() && npc.getNavigator().noPath() && !npc.isPlayerSleeping() && npc.isEntityAlive(); + return !npc.abilities.isRotationLocked() && !npc.isAttacking() && npc.getNavigator().noPath() && !npc.isPlayerSleeping() && npc.isEntityAlive(); } /** diff --git a/src/main/java/noppes/npcs/ai/EntityAIOrbitTarget.java b/src/main/java/noppes/npcs/ai/EntityAIOrbitTarget.java index 4c72ec278..0a9532261 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIOrbitTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIOrbitTarget.java @@ -91,7 +91,9 @@ public class EntityAIOrbitTarget extends EntityAIBase { * Updates the task */ public void updateTask() { - this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + if (!this.theEntity.abilities.isRotationLocked()) { + this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + } if (this.theEntity.getNavigator().noPath() && this.tick >= 0) { if (this.theEntity.onGround && !this.theEntity.isInWater()) { double d0 = this.targetDistance * (double) (MathHelper.cos(angle / 180.0F * (float) Math.PI)); diff --git a/src/main/java/noppes/npcs/ai/EntityAIRangedAttack.java b/src/main/java/noppes/npcs/ai/EntityAIRangedAttack.java index af88ac905..5e8fa6ec0 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIRangedAttack.java +++ b/src/main/java/noppes/npcs/ai/EntityAIRangedAttack.java @@ -100,7 +100,9 @@ public class EntityAIRangedAttack extends EntityAIBase { * Updates the task */ public void updateTask() { - this.entityHost.getLookHelper().setLookPositionWithEntity(this.attackTarget, 30.0F, 30.0F); + if (!this.entityHost.abilities.isRotationLocked()) { + this.entityHost.getLookHelper().setLookPositionWithEntity(this.attackTarget, 30.0F, 30.0F); + } double var1 = this.entityHost.getDistanceSq(this.attackTarget.posX, this.attackTarget.boundingBox.minY, this.attackTarget.posZ); float field_82642_h = this.entityHost.stats.rangedRange * this.entityHost.stats.rangedRange; diff --git a/src/main/java/noppes/npcs/ai/EntityAIStalkTarget.java b/src/main/java/noppes/npcs/ai/EntityAIStalkTarget.java index 1b64a12b7..8adad1ee2 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIStalkTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIStalkTarget.java @@ -68,7 +68,9 @@ public class EntityAIStalkTarget extends EntityAIBase { * Updates the task */ public void updateTask() { - this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + if (!this.theEntity.abilities.isRotationLocked()) { + this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + } if (this.theEntity.getNavigator().noPath() || this.overRide) { if (this.isLookingAway()) { diff --git a/src/main/java/noppes/npcs/ai/EntityAIWatchClosest.java b/src/main/java/noppes/npcs/ai/EntityAIWatchClosest.java index d30ae3183..159c38fc9 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIWatchClosest.java +++ b/src/main/java/noppes/npcs/ai/EntityAIWatchClosest.java @@ -31,7 +31,7 @@ public class EntityAIWatchClosest extends EntityAIBase { */ @Override public boolean shouldExecute() { - if (this.theWatcher.getRNG().nextFloat() >= this.field_75331_e || theWatcher.isInteracting()) { + if (this.theWatcher.abilities.isRotationLocked() || this.theWatcher.getRNG().nextFloat() >= this.field_75331_e || theWatcher.isInteracting()) { return false; } else { if (this.theWatcher.getAttackTarget() != null) { diff --git a/src/main/java/noppes/npcs/ai/EntityAIZigZagTarget.java b/src/main/java/noppes/npcs/ai/EntityAIZigZagTarget.java index 3e91cc84b..e99d31ca2 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIZigZagTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIZigZagTarget.java @@ -8,6 +8,7 @@ import net.minecraft.pathfinding.PathPoint; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import noppes.npcs.constants.AiMutex; +import noppes.npcs.entity.EntityNPCInterface; public class EntityAIZigZagTarget extends EntityAIBase { private EntityCreature theEntity; @@ -82,6 +83,8 @@ public class EntityAIZigZagTarget extends EntityAIBase { * Updates the task */ public void updateTask() { - this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + if (!(this.theEntity instanceof EntityNPCInterface) || !((EntityNPCInterface) this.theEntity).abilities.isRotationLocked()) { + this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F); + } } } diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java index 7a839fbb5..a7a0f2b39 100644 --- a/src/main/java/noppes/npcs/client/ClientEventHandler.java +++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java @@ -19,6 +19,8 @@ import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.client.event.RenderHandEvent; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.client.event.RenderPlayerEvent; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import noppes.npcs.client.gui.player.AuctionTooltipHandler; import noppes.npcs.client.gui.customoverlay.OverlayCustom; import noppes.npcs.client.gui.hud.ClientHudManager; import noppes.npcs.client.gui.hud.CompassHudComponent; @@ -75,6 +77,11 @@ public class ClientEventHandler { } } + @SubscribeEvent + public void onItemTooltip(ItemTooltipEvent event) { + AuctionTooltipHandler.onItemTooltip(event); + } + @SubscribeEvent public void onMouse(MouseEvent event) { if (Minecraft.getMinecraft().currentScreen != null) diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index bb90e0659..3255452c9 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -76,6 +76,7 @@ import noppes.npcs.client.gui.GuiBorderBlock; import noppes.npcs.client.gui.GuiMerchantAdd; import noppes.npcs.client.gui.GuiNpcDimension; import noppes.npcs.client.gui.GuiNpcMobSpawner; +import noppes.npcs.client.gui.GuiNpcMobSpawnerFullscreen; import noppes.npcs.client.gui.GuiNpcMobSpawnerMounter; import noppes.npcs.client.gui.GuiNpcPather; import noppes.npcs.client.gui.GuiNpcRedstoneBlock; @@ -191,6 +192,7 @@ import noppes.npcs.client.renderer.items.ItemShortLampRenderer; import noppes.npcs.client.renderer.items.ItemTallLampRenderer; import noppes.npcs.client.renderer.items.ItemToolRenderer; import noppes.npcs.client.renderer.items.ScriptedBlockItemRenderer; +import noppes.npcs.client.ScriptClientConfig; import noppes.npcs.config.ConfigClient; import noppes.npcs.config.ConfigItem; import noppes.npcs.config.ConfigMain; @@ -334,7 +336,6 @@ public class ClientProxy extends CommonProxy { ClientCloneController.Instance = new ClientCloneController(); ClientTagMapController.Instance = new ClientTagMapController(); - MinecraftForge.EVENT_BUS.register(new ClientEventHandler()); // Telegraph rendering system @@ -603,7 +604,9 @@ public class ClientProxy extends CommonProxy { return new GuiNpcRedstoneBlock(x, y, z); else if (gui == EnumGuiType.Cloner) - return new GuiNpcMobSpawner(x, y, z); + return GuiNpcMobSpawner.isFullscreen + ? new GuiNpcMobSpawnerFullscreen(x, y, z) + : new GuiNpcMobSpawner(x, y, z); else if (gui == EnumGuiType.MobSpawnerMounter) return new GuiNpcMobSpawnerMounter(x, y, z); @@ -822,6 +825,31 @@ public class ClientProxy extends CommonProxy { } } + @Override + public boolean isScriptingEnabled() { + return ConfigClient.AllowClientScripts && ScriptClientConfig.isScriptingEnabled(); + } + + @Override + public boolean isRunLoadedScriptsFirst() { + return ScriptClientConfig.isRunLoadedScriptsFirst(); + } + + @Override + public boolean isGlobalPlayerScripts() { + return ConfigClient.AllowClientScripts && ScriptClientConfig.isGlobalPlayerScripts(); + } + + @Override + public boolean isGlobalForgeScripts() { + return ConfigClient.AllowClientScripts && ScriptClientConfig.isGlobalForgeScripts(); + } + + @Override + public boolean isGlobalNPCScripts() { + return ConfigClient.AllowClientScripts && ScriptClientConfig.isGlobalNPCScripts(); + } + public static class FontContainer { public StringCache textFont = null; public boolean useCustomFont = true; diff --git a/src/main/java/noppes/npcs/client/ScriptClientConfig.java b/src/main/java/noppes/npcs/client/ScriptClientConfig.java new file mode 100644 index 000000000..863f991ba --- /dev/null +++ b/src/main/java/noppes/npcs/client/ScriptClientConfig.java @@ -0,0 +1,61 @@ +package noppes.npcs.client; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.nbt.NBTTagCompound; + +/** + * Client-side cache for script configuration synced from server. + * Prevents client from using local ConfigScript which may differ from server settings. + * + * IMPORTANT: This class is CLIENT-ONLY. Server-side NBT writing is done in + * ScriptConfigSync to avoid @SideOnly crashes on dedicated servers. + */ +@SideOnly(Side.CLIENT) +public class ScriptClientConfig { + // Cached values from server + private static boolean scriptingEnabled = true; + private static boolean runLoadedScriptsFirst = true; + private static boolean globalPlayerScripts = true; + private static boolean globalForgeScripts = true; + private static boolean globalNPCScripts = false; + + /** + * Update cached config from server NBT data. + * Called when receiving login packet or script sync packets. + */ + public static void readFromNBT(NBTTagCompound compound) { + if (compound == null) return; + + scriptingEnabled = compound.getBoolean("ScriptingEnabled"); + runLoadedScriptsFirst = compound.getBoolean("RunLoadedScriptsFirst"); + globalPlayerScripts = compound.getBoolean("GlobalPlayerScripts"); + globalForgeScripts = compound.getBoolean("GlobalForgeScripts"); + globalNPCScripts = compound.getBoolean("GlobalNPCScripts"); + } + + /** + * Reset to defaults (called on disconnect). + */ + public static void reset() { + scriptingEnabled = true; + runLoadedScriptsFirst = true; + globalPlayerScripts = true; + globalForgeScripts = true; + globalNPCScripts = false; + } + + // Setters for individual packet updates + public static void setScriptingEnabled(boolean value) { scriptingEnabled = value; } + public static void setRunLoadedScriptsFirst(boolean value) { runLoadedScriptsFirst = value; } + public static void setGlobalPlayerScripts(boolean value) { globalPlayerScripts = value; } + public static void setGlobalForgeScripts(boolean value) { globalForgeScripts = value; } + public static void setGlobalNPCScripts(boolean value) { globalNPCScripts = value; } + + // Getters + public static boolean isScriptingEnabled() { return scriptingEnabled; } + public static boolean isRunLoadedScriptsFirst() { return runLoadedScriptsFirst; } + public static boolean isGlobalPlayerScripts() { return globalPlayerScripts; } + public static boolean isGlobalForgeScripts() { return globalForgeScripts; } + public static boolean isGlobalNPCScripts() { return globalNPCScripts; } +} diff --git a/src/main/java/noppes/npcs/client/controllers/ClientCloneController.java b/src/main/java/noppes/npcs/client/controllers/ClientCloneController.java index 81f174170..9e62177f0 100644 --- a/src/main/java/noppes/npcs/client/controllers/ClientCloneController.java +++ b/src/main/java/noppes/npcs/client/controllers/ClientCloneController.java @@ -41,6 +41,26 @@ public class ClientCloneController extends ServerCloneController { return true; } + @Override + public boolean addToTagMap(NBTTagCompound nbttagcompound, String name, String folderName) { + HashSet tagUUIDs = new HashSet<>(); + if (nbttagcompound.hasKey("TagUUIDs")) { + NBTTagList nbtTagList = nbttagcompound.getTagList("TagUUIDs", 8); + for (int i = 0; i < nbtTagList.tagCount(); i++) { + tagUUIDs.add(UUID.fromString(nbtTagList.getStringTagAt(i))); + } + } + + TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName); + if (!tagUUIDs.isEmpty()) { + tagMap.putClone(name, tagUUIDs); + } else { + tagMap.removeClone(name); + } + ClientTagMapController.Instance.saveTagMap(tagMap); + return true; + } + @Override public boolean removeFromTagMap(String name, int tab) { TagMap tagMap = ClientTagMapController.Instance.getTagMap(tab); @@ -51,4 +71,43 @@ public class ClientCloneController extends ServerCloneController { return false; } + @Override + public boolean removeFromTagMap(String name, String folderName) { + TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName); + if (tagMap.removeClone(name)) { + ClientTagMapController.Instance.saveTagMap(tagMap); + return true; + } + return false; + } + + @Override + protected HashSet getTagUUIDsFromTab(String cloneName, int tab) { + TagMap tagMap = ClientTagMapController.Instance.getTagMap(tab); + HashSet uuids = tagMap.getUUIDs(cloneName); + return uuids != null ? new HashSet<>(uuids) : new HashSet<>(); + } + + @Override + protected HashSet getTagUUIDsFromFolder(String cloneName, String folderName) { + TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName); + HashSet uuids = tagMap.getUUIDs(cloneName); + return uuids != null ? new HashSet<>(uuids) : new HashSet<>(); + } + + @Override + protected void setTagUUIDsForTab(String cloneName, int tab, HashSet tagUUIDs) { + if (tagUUIDs == null || tagUUIDs.isEmpty()) return; + TagMap tagMap = ClientTagMapController.Instance.getTagMap(tab); + tagMap.putClone(cloneName, tagUUIDs); + ClientTagMapController.Instance.saveTagMap(tagMap); + } + + @Override + protected void setTagUUIDsForFolder(String cloneName, String folderName, HashSet tagUUIDs) { + if (tagUUIDs == null || tagUUIDs.isEmpty()) return; + TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName); + tagMap.putClone(cloneName, tagUUIDs); + ClientTagMapController.Instance.saveTagMap(tagMap); + } } diff --git a/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawner.java b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawner.java index b3670d9e8..7fd5c174e 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawner.java +++ b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawner.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.clone.CloneAllTagsPacket; +import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket; import kamkeel.npcs.network.packets.request.clone.CloneListPacket; import kamkeel.npcs.network.packets.request.clone.CloneRemovePacket; import kamkeel.npcs.network.packets.request.clone.CloneTagListPacket; @@ -26,6 +27,8 @@ import noppes.npcs.client.gui.util.GuiNPCInterface; 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.IClonerGui; +import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.controllers.data.Tag; import noppes.npcs.controllers.data.TagMap; @@ -38,52 +41,84 @@ import java.util.List; import java.util.Map; import java.util.UUID; -public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { +public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData, ICustomScrollListener, IClonerGui { + + // ==================== STATIC STATE (persists across GUI opens, shared with fullscreen) ==================== + public static boolean isFullscreen = false; + public static int activeTab = 1; + public static String activeFolder = null; + public static int showingClones = 0; // 0=Clones, 1=Entities, 2=Server, 3=Filters + public static String search = ""; + public static String tagSearch = ""; + public static HashMap tags = new HashMap<>(); public static HashMap tagNames = new HashMap<>(); public static HashSet tagFilters = new HashSet<>(); public static byte displayTags = 0; - // 0 - Any, 1 - All, 2 - Not Any, 3 - Not All public static byte filterCondition = 0; - public static byte ascending = 0; - // 0 - By Name, 1 - By Date public static byte sortType = 0; - public TagMap tagMap; + // Scroll position preservation (shared with fullscreen) + public static int savedContentScrollY = 0; + public static int savedNavScrollY = 0; + // ==================== INSTANCE STATE ==================== + public TagMap tagMap; private final GuiCustomScrollCloner scroll = new GuiCustomScrollCloner(this, 0); private final GuiCustomScroll filterScroll = new GuiCustomScroll(this, 1); - private int posX, posY, posZ; - + public int posX, posY, posZ; private List list; private List rawList; private List tagList; - private static int showingClones = 0; - - private static String search = ""; - private static String tagSearch = ""; - - private int activeTab = 1; - + // ==================== CONSTRUCTOR ==================== public GuiNpcMobSpawner(int i, int j, int k) { super(); - xSize = 354; posX = i; posY = j; posZ = k; - - this.closeOnEsc = true; - + closeOnEsc = true; + xSize = 354; setBackground("menubg.png"); PacketClient.sendClient(new CloneAllTagsPacket()); + if (showingClones == 2) { + PacketClient.sendClient(new CloneFolderListPacket()); + } + + // Minimized view doesn't support folders — ensure we're on a tab + if (activeFolder != null) { + if (activeTab < 1 || activeTab > 15) { + activeTab = 1; + } + activeFolder = null; + } } + // ==================== IClonerGui ==================== + @Override + public int getShowingClones() { + return showingClones; + } + + @Override + public HashMap getTags() { + return tags; + } + + @Override + public TagMap getTagMap() { + return tagMap; + } + + // ==================== INIT GUI ==================== public void initGui() { + // Save scroll position before clear + savedContentScrollY = scroll.scrollY; + super.initGui(); guiTop += 10; guiLeft += 30; @@ -92,6 +127,7 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { scroll.setSize(293, 188); scroll.guiLeft = guiLeft + 4; scroll.guiTop = guiTop + 26; + filterScroll.clear(); filterScroll.setSize(140, 166); filterScroll.guiLeft = guiLeft + 4; @@ -99,112 +135,133 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { filterScroll.multipleSelection = true; filterScroll.setSelectedList(tagFilters); - GuiMenuTopButton button; - addTopButton(button = new GuiMenuTopButton(3, guiLeft + 4, guiTop - 17, "spawner.clones")); - button.active = showingClones == 0; - addTopButton(button = new GuiMenuTopButton(5, button, "gui.server")); - button.active = showingClones == 2; - addTopButton(button = new GuiMenuTopButton(4, button, "spawner.entities")); - button.active = showingClones == 1; - addTopButton(button = new GuiMenuTopButton(16, guiLeft + (xSize - 67), guiTop - 17, "gui.filters")); - button.active = showingClones == 3; - addTopButton(button = new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); + // Top buttons — build right-to-left to avoid overlap + GuiMenuTopButton closeBtn = new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X"); + addTopButton(closeBtn); + + // Position "+" dynamically to the left of "X" + int plusX = closeBtn.xPosition - (fontRendererObj.getStringWidth("+") + 12) - 2; + GuiMenuTopButton fullscreenBtn = new GuiMenuTopButton(61, plusX, guiTop - 17, "+"); + addTopButton(fullscreenBtn); + + // Position "Filters" dynamically to the left of "+" + int filterBtnW = fontRendererObj.getStringWidth(StatCollector.translateToLocal("gui.filters")) + 12; + int filterX = fullscreenBtn.xPosition - filterBtnW - 2; + GuiMenuTopButton filterBtn = new GuiMenuTopButton(16, filterX, guiTop - 17, "gui.filters"); + filterBtn.active = showingClones == 3; + addTopButton(filterBtn); + + // Mode buttons from left + GuiMenuTopButton btn; + addTopButton(btn = new GuiMenuTopButton(3, guiLeft + 4, guiTop - 17, "spawner.clones")); + btn.active = showingClones == 0; + addTopButton(btn = new GuiMenuTopButton(5, btn, "gui.server")); + btn.active = showingClones == 2; + addTopButton(btn = new GuiMenuTopButton(4, btn, "spawner.entities")); + btn.active = showingClones == 1; if (showingClones < 3) { addScroll(scroll); addTextField(new GuiNpcTextField(1, this, fontRendererObj, guiLeft + 4, guiTop + 4, 293, 20, search)); + addButton(new GuiNpcButton(1, guiLeft + 298, guiTop + 6, 52, 20, "item.monsterPlacer.name")); addButton(new GuiNpcButton(2, guiLeft + 298, guiTop + 140, 52, 20, "spawner.mobspawner")); if (showingClones == 0 || showingClones == 2) { - - addSideButton(new GuiMenuSideButton(21, guiLeft - 70, this.guiTop + 2, 70, 22, "1")); - addSideButton(new GuiMenuSideButton(22, guiLeft - 70, this.guiTop + 23, 70, 22, "2")); - addSideButton(new GuiMenuSideButton(23, guiLeft - 70, this.guiTop + 44, 70, 22, "3")); - addSideButton(new GuiMenuSideButton(24, guiLeft - 70, this.guiTop + 65, 70, 22, "4")); - addSideButton(new GuiMenuSideButton(25, guiLeft - 70, this.guiTop + 86, 70, 22, "5")); - addSideButton(new GuiMenuSideButton(26, guiLeft - 70, this.guiTop + 107, 35, 22, "6")); - addSideButton(new GuiMenuSideButton(27, guiLeft - 35, this.guiTop + 107, 35, 22, "7")); - addSideButton(new GuiMenuSideButton(28, guiLeft - 70, this.guiTop + 128, 35, 22, "8")); - addSideButton(new GuiMenuSideButton(29, guiLeft - 35, this.guiTop + 128, 35, 22, "9")); - addSideButton(new GuiMenuSideButton(30, guiLeft - 70, this.guiTop + 149, 35, 22, "10")); - addSideButton(new GuiMenuSideButton(31, guiLeft - 35, this.guiTop + 149, 35, 22, "11")); - addSideButton(new GuiMenuSideButton(32, guiLeft - 70, this.guiTop + 170, 35, 22, "12")); - addSideButton(new GuiMenuSideButton(33, guiLeft - 35, this.guiTop + 170, 35, 22, "13")); - addSideButton(new GuiMenuSideButton(34, guiLeft - 70, this.guiTop + 191, 35, 22, "14")); - addSideButton(new GuiMenuSideButton(35, guiLeft - 35, this.guiTop + 191, 35, 22, "15")); - addButton(new GuiNpcButton(6, guiLeft + 298, guiTop + 190, 52, 20, "gui.remove")); - getSideButton(20 + activeTab).active = true; + initTabSideButtons(); showClones(); + + // Restore scroll position + scroll.scrollY = Math.min(savedContentScrollY, Math.max(0, scroll.maxScrollY)); } else { showEntities(); + scroll.scrollY = Math.min(savedContentScrollY, Math.max(0, scroll.maxScrollY)); } } else { - // Show Filters - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("cloner.tagFilters"), guiLeft + 7, guiTop + 7)); - addScroll(filterScroll); - addTextField(new GuiNpcTextField(2, this, fontRendererObj, guiLeft + 4, guiTop + 190, 140, 20, tagSearch)); - - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("cloner.tagVisibility"), guiLeft + 150, guiTop + 27)); - addButton(new GuiNpcButton(12, guiLeft + 215, guiTop + 20, new String[]{"display.show", "display.all", "display.hide"}, displayTags)); - - addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("filter.contains"), guiLeft + 150, guiTop + 50)); - addButton(new GuiNpcButton(13, guiLeft + 215, guiTop + 43, new String[]{"filter.any", "filter.all", "filter.notany", "filter.notall"}, filterCondition)); - - addButton(new GuiNpcButton(11, guiLeft + 150, guiTop + 66, 130, 20, "gui.deselectAll")); - - - addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("cloner.order"), guiLeft + 150, guiTop + 112)); - addButton(new GuiNpcButton(14, guiLeft + 215, guiTop + 105, new String[]{"cloner.ascending", "cloner.descending"}, ascending)); - - addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("cloner.type"), guiLeft + 150, guiTop + 135)); - addButton(new GuiNpcButton(15, guiLeft + 215, guiTop + 128, new String[]{"cloner.name", "cloner.date"}, sortType)); - - - getButton(12).width = 65; - getButton(12).height = 20; - - getButton(13).width = 65; - getButton(13).height = 20; - - getButton(14).width = 65; - getButton(14).height = 20; - - getButton(15).width = 65; - getButton(15).height = 20; - - showFilters(); + showFiltersPage(); } } - public int getShowingClones() { - return showingClones; + private void initTabSideButtons() { + // Tabs 1-5: full width (70px) + addSideButton(new GuiMenuSideButton(21, guiLeft - 70, guiTop + 2, 70, 22, "1")); + addSideButton(new GuiMenuSideButton(22, guiLeft - 70, guiTop + 23, 70, 22, "2")); + addSideButton(new GuiMenuSideButton(23, guiLeft - 70, guiTop + 44, 70, 22, "3")); + addSideButton(new GuiMenuSideButton(24, guiLeft - 70, guiTop + 65, 70, 22, "4")); + addSideButton(new GuiMenuSideButton(25, guiLeft - 70, guiTop + 86, 70, 22, "5")); + // Tabs 6-15: half width (35px), 2-column grid + addSideButton(new GuiMenuSideButton(26, guiLeft - 70, guiTop + 107, 35, 22, "6")); + addSideButton(new GuiMenuSideButton(27, guiLeft - 35, guiTop + 107, 35, 22, "7")); + addSideButton(new GuiMenuSideButton(28, guiLeft - 70, guiTop + 128, 35, 22, "8")); + addSideButton(new GuiMenuSideButton(29, guiLeft - 35, guiTop + 128, 35, 22, "9")); + addSideButton(new GuiMenuSideButton(30, guiLeft - 70, guiTop + 149, 35, 22, "10")); + addSideButton(new GuiMenuSideButton(31, guiLeft - 35, guiTop + 149, 35, 22, "11")); + addSideButton(new GuiMenuSideButton(32, guiLeft - 70, guiTop + 170, 35, 22, "12")); + addSideButton(new GuiMenuSideButton(33, guiLeft - 35, guiTop + 170, 35, 22, "13")); + addSideButton(new GuiMenuSideButton(34, guiLeft - 70, guiTop + 191, 35, 22, "14")); + addSideButton(new GuiMenuSideButton(35, guiLeft - 35, guiTop + 191, 35, 22, "15")); + + if (activeTab >= 1 && activeTab <= 15) { + GuiMenuSideButton active = getSideButton(20 + activeTab); + if (active != null) active.active = true; + } } - public void showFilters() { + // ==================== FILTERS PAGE ==================== + private void showFiltersPage() { + int baseY = guiTop; + + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("cloner.tagFilters"), + guiLeft + 7, baseY + 7)); + + filterScroll.clear(); + filterScroll.setSize(140, 166); + filterScroll.guiLeft = guiLeft + 4; + filterScroll.guiTop = baseY + 19; + filterScroll.multipleSelection = true; + filterScroll.setSelectedList(tagFilters); + addScroll(filterScroll); + + addTextField(new GuiNpcTextField(2, this, fontRendererObj, + guiLeft + 4, baseY + 190, 140, 20, tagSearch)); + + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("cloner.tagVisibility"), + guiLeft + 150, baseY + 27)); + addButton(new GuiNpcButton(12, guiLeft + 215, baseY + 20, + new String[]{"display.show", "display.all", "display.hide"}, displayTags)); + + addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("filter.contains"), + guiLeft + 150, baseY + 50)); + addButton(new GuiNpcButton(13, guiLeft + 215, baseY + 43, + new String[]{"filter.any", "filter.all", "filter.notany", "filter.notall"}, filterCondition)); + + addButton(new GuiNpcButton(11, guiLeft + 150, baseY + 66, 130, 20, "gui.deselectAll")); + + addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("cloner.order"), + guiLeft + 150, baseY + 112)); + addButton(new GuiNpcButton(14, guiLeft + 215, baseY + 105, + new String[]{"cloner.ascending", "cloner.descending"}, ascending)); + + addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("cloner.type"), + guiLeft + 150, baseY + 135)); + addButton(new GuiNpcButton(15, guiLeft + 215, baseY + 128, + new String[]{"cloner.name", "cloner.date"}, sortType)); + + getButton(12).width = 65; + getButton(12).height = 20; + getButton(13).width = 65; + getButton(13).height = 20; + getButton(14).width = 65; + getButton(14).height = 20; + getButton(15).width = 65; + getButton(15).height = 20; + filterScroll.setList(getTagList()); } - private void showEntities() { - Map data = EntityList.stringToClassMapping; - ArrayList list = new ArrayList(); - for (Object name : data.keySet()) { - Class c = (Class) data.get(name); - try { - if (EntityLiving.class.isAssignableFrom(c) && c.getConstructor(new Class[]{World.class}) != null && !Modifier.isAbstract(c.getModifiers())) - list.add(name.toString()); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (NoSuchMethodException e) { - } - } - this.list = list; - this.rawList = new ArrayList(this.list); - scroll.setList(getSearchList(), ascending == 0, sortType == 0); - } - + // ==================== CLONE / ENTITY DISPLAY ==================== private void showClones() { if (showingClones == 2) { PacketClient.sendClient(new CloneTagListPacket(activeTab)); @@ -219,16 +276,41 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { } this.tagMap = ClientTagMapController.Instance.getTagMap(activeTab); + populateRawListWithTags(); + scroll.setList(getSearchList(), ascending == 0, sortType == 0); + } + + private void showEntities() { + Map data = EntityList.stringToClassMapping; + ArrayList list = new ArrayList(); + for (Object name : data.keySet()) { + Class c = (Class) data.get(name); + try { + if (EntityLiving.class.isAssignableFrom(c) + && c.getConstructor(new Class[]{World.class}) != null + && !Modifier.isAbstract(c.getModifiers())) + list.add(name.toString()); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + } + } + this.list = list; this.rawList = new ArrayList(this.list); + scroll.setList(getSearchList(), ascending == 0, sortType == 0); + } + + private void populateRawListWithTags() { + this.rawList = new ArrayList<>(this.list); if (this.tagMap != null) { - if (GuiNpcMobSpawner.displayTags == 0 || GuiNpcMobSpawner.displayTags == 1) { + if (displayTags == 0 || displayTags == 1) { for (int i = 0; i < this.list.size(); i++) { StringBuilder npcName = new StringBuilder(list.get(i)); if (this.tagMap.hasClone(list.get(i))) { for (UUID tagUUID : this.tagMap.getUUIDsList(list.get(i))) { Tag tag = tags.get(tagUUID); if (tag != null) { - if (GuiNpcMobSpawner.displayTags == 1 || !tag.getIsHidden()) { + if (displayTags == 1 || !tag.getIsHidden()) { npcName.append(" [" + tag.name + "]"); } } @@ -238,27 +320,26 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { } } } - - scroll.setList(getSearchList(), ascending == 0, sortType == 0); } - public void keyTyped(char c, int i) { - super.keyTyped(c, i); - if (getTextField(1) != null) { - if (search.equals(getTextField(1).getText())) - return; - search = getTextField(1).getText().toLowerCase(); - scroll.setList(getSearchList(), ascending == 0, sortType == 0); - } - if (getTextField(2) != null) { - if (tagSearch.equals(getTextField(2).getText())) - return; - tagSearch = getTextField(2).getText().toLowerCase(); - filterScroll.setList(getTagList()); - } + private NBTTagCompound getCompound() { + String sel = scroll.getSelected(); + if (sel == null) + return null; + if (showingClones == 0) { + return ClientCloneController.Instance.getCloneData(player, sel, activeTab); + } else { + Entity entity = EntityList.createEntityByName(sel, Minecraft.getMinecraft().theWorld); + if (entity == null) + return null; + NBTTagCompound compound = new NBTTagCompound(); + entity.writeToNBTOptional(compound); + return compound; + } } + // ==================== SEARCH / FILTER ==================== private List getSearchList() { if (this.list == null) { this.list = new ArrayList(); @@ -269,10 +350,8 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { // No Filters - or - Entities if (tagFilters.size() == 0 || showingClones == 1) { - // No Search if (search.isEmpty()) return new ArrayList(this.list); - // Yes Search else { List list = new ArrayList(); for (int i = 0; i < this.list.size(); i++) { @@ -284,10 +363,7 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { } } - //////////////////////// - //// WITH FILTERS - - // No Search + // With Filters if (search.isEmpty()) { List list = new ArrayList(); for (String name : this.list) { @@ -300,7 +376,6 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { return list; } - // Yes Search List list = new ArrayList(); for (int i = 0; i < this.list.size(); i++) { if (this.rawList.get(i).toLowerCase().contains(search)) { @@ -311,7 +386,6 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { } } } - } return list; } @@ -346,7 +420,6 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { } } } - return conditionMet; } @@ -354,10 +427,8 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { if (this.tagList == null) { this.tagList = new ArrayList(); } - // No Search // No Filters - or - Entities if (tagSearch.isEmpty()) return new ArrayList(tagNames.keySet()); - // Yes Search // No Filters - or - Entities List list = new ArrayList(); for (String name : this.tagList) { @@ -367,125 +438,162 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { return list; } - private NBTTagCompound getCompound() { - String sel = scroll.getSelected(); - if (sel == null) - return null; - - if (showingClones == 0) { - return ClientCloneController.Instance.getCloneData(player, sel, activeTab); - } else { - Entity entity = EntityList.createEntityByName(sel, Minecraft.getMinecraft().theWorld); - if (entity == null) - return null; - NBTTagCompound compound = new NBTTagCompound(); - entity.writeToNBTOptional(compound); - return compound; - } + // ==================== MOUSE / KEYBOARD EVENTS ==================== + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { } + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) { + } + + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + + GuiNpcTextField searchField = getTextField(1); + if (searchField != null) { + String newText = searchField.getText().toLowerCase(); + if (!search.equals(newText)) { + search = newText; + scroll.setList(getSearchList(), ascending == 0, sortType == 0); + } + } + + GuiNpcTextField tagSearchField = getTextField(2); + if (tagSearchField != null) { + String newText = tagSearchField.getText().toLowerCase(); + if (!tagSearch.equals(newText)) { + tagSearch = newText; + filterScroll.setList(getTagList()); + } + } + } + + // ==================== BUTTON ACTIONS ==================== @Override protected void actionPerformed(GuiButton guibutton) { int id = guibutton.id; - if (id == 0) { - close(); - } - if (id == 1) { - if (showingClones == 2) { - String sel = scroll.getSelected(); - if (sel == null) - return; - SpawnMobPacket.Server(posX, posY, posZ, sel, activeTab); - close(); - } else { - NBTTagCompound compound = getCompound(); - if (compound == null) - return; - SpawnMobPacket.Client(posX, posY, posZ, compound); - close(); - } - } - if (id == 2) { - if (showingClones == 2) { - String sel = scroll.getSelected(); - if (sel == null) - return; - MobSpawnerPacket.Server(posX, posY, posZ, sel, activeTab); - close(); - } else { - NBTTagCompound compound = getCompound(); - if (compound == null) - return; - MobSpawnerPacket.Client(posX, posY, posZ, compound); - close(); - } + // Tab side buttons (IDs 21-35) + if (id >= 21 && id <= 35) { + activeTab = id - 20; + activeFolder = null; + initGui(); + return; } + + // Mode buttons if (id == 3) { + savedContentScrollY = 0; showingClones = 0; initGui(); } if (id == 4) { + savedContentScrollY = 0; showingClones = 1; initGui(); } if (id == 5) { + savedContentScrollY = 0; showingClones = 2; + PacketClient.sendClient(new CloneFolderListPacket()); initGui(); } if (id == 16) { + savedContentScrollY = 0; showingClones = 3; initGui(); } if (id == 17) { close(); } + + // Open fullscreen GUI + if (id == 61) { + isFullscreen = true; + Minecraft.getMinecraft().displayGuiScreen( + new GuiNpcMobSpawnerFullscreen(posX, posY, posZ)); + } + + // Spawn + if (id == 1) { + if (showingClones == 2) { + String sel = scroll.getSelected(); + if (sel == null) return; + SpawnMobPacket.Server(posX, posY, posZ, sel, activeTab); + close(); + } else { + NBTTagCompound compound = getCompound(); + if (compound == null) return; + SpawnMobPacket.Client(posX, posY, posZ, compound); + close(); + } + } + + // Mob Spawner + if (id == 2) { + if (showingClones == 2) { + String sel = scroll.getSelected(); + if (sel == null) return; + MobSpawnerPacket.Server(posX, posY, posZ, sel, activeTab); + close(); + } else { + NBTTagCompound compound = getCompound(); + if (compound == null) return; + MobSpawnerPacket.Client(posX, posY, posZ, compound); + close(); + } + } + + // Remove if (id == 6) { if (scroll.getSelected() != null) { + int prevSelected = scroll.selected; if (showingClones == 2) { PacketClient.sendClient(new CloneRemovePacket(activeTab, scroll.getSelected())); return; } ClientCloneController.Instance.removeClone(scroll.getSelected(), activeTab); - scroll.selected = -1; initGui(); + if (scroll.list != null && !scroll.list.isEmpty()) { + scroll.selected = Math.min(prevSelected, scroll.list.size() - 1); + } else { + scroll.selected = -1; + } } } + + // Filter controls if (id == 11) { tagFilters = new HashSet<>(); filterScroll.setSelectedList(tagFilters); initGui(); } if (id == 12) { - GuiNpcButton button = (GuiNpcButton) guibutton; - displayTags = (byte) button.getValue(); + displayTags = (byte) ((GuiNpcButton) guibutton).getValue(); } if (id == 13) { - GuiNpcButton button = (GuiNpcButton) guibutton; - filterCondition = (byte) button.getValue(); + filterCondition = (byte) ((GuiNpcButton) guibutton).getValue(); } if (id == 14) { - GuiNpcButton button = (GuiNpcButton) guibutton; - ascending = (byte) button.getValue(); + ascending = (byte) ((GuiNpcButton) guibutton).getValue(); } if (id == 15) { - GuiNpcButton button = (GuiNpcButton) guibutton; - sortType = (byte) button.getValue(); - } - if (id > 20) { - activeTab = id - 20; - initGui(); + sortType = (byte) ((GuiNpcButton) guibutton).getValue(); } } + // ==================== DATA FROM SERVER ==================== @Override public void save() { - } @Override public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("CloneTags")) { + if (compound.hasKey("CloneFolders")) { + // Minimized doesn't use folders, but still handle the packet + initGui(); + } else if (compound.hasKey("CloneTags")) { this.tagMap = new TagMap(activeTab); NBTTagCompound cloneTags = compound.getCompoundTag("CloneTags"); this.tagMap.readNBT(cloneTags); @@ -521,29 +629,8 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData { } this.list = list; - - this.rawList = new ArrayList(this.list); - if (this.tagMap != null) { - if (GuiNpcMobSpawner.displayTags == 0 || GuiNpcMobSpawner.displayTags == 1) { - for (int i = 0; i < this.list.size(); i++) { - StringBuilder npcName = new StringBuilder(list.get(i)); - if (this.tagMap.hasClone(list.get(i))) { - for (UUID tagUUID : this.tagMap.getUUIDsList(list.get(i))) { - Tag tag = tags.get(tagUUID); - if (tag != null) { - if (GuiNpcMobSpawner.displayTags == 1 || !tag.getIsHidden()) { - npcName.append(" [" + tag.name + "]"); - } - } - } - } - this.rawList.set(i, npcName.toString()); - } - } - } - + populateRawListWithTags(); scroll.setList(getSearchList(), ascending == 0, sortType == 0); } } - } diff --git a/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerAdd.java b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerAdd.java index d2895fc44..5cea7fe28 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerAdd.java +++ b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerAdd.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.clone.CloneAllTagsShortPacket; +import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket; import kamkeel.npcs.network.packets.request.clone.ClonePreSavePacket; import kamkeel.npcs.network.packets.request.clone.CloneSavePacket; import net.minecraft.client.Minecraft; @@ -22,12 +23,14 @@ import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.ISubGuiListener; import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.data.CloneFolder; import noppes.npcs.controllers.data.Tag; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.UUID; public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ISubGuiListener { @@ -35,9 +38,16 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall private Entity toClone; private NBTTagCompound compound; private static boolean serverSide = false; + private static int saveMode = 0; // 0=Tab, 1=Folder private static int tab = 1; + private static String folder = null; public boolean isNPC = false; + // Client-side folder names + private ArrayList clientFolderNames = new ArrayList<>(); + // Server-side folder names (populated via CloneFolderListPacket) + private static ArrayList serverFolderNames = new ArrayList<>(); + // Selected Tags to Add public static NBTTagList tagsCompound; public static HashSet addTags; @@ -68,6 +78,37 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall if (isNPC) { PacketClient.sendClient(new CloneAllTagsShortPacket()); } + + buildClientFolderNames(); + + // Request server folders if in server mode + if (serverSide) { + PacketClient.sendClient(new CloneFolderListPacket()); + } + + // Validate saved folder selection + validateFolder(); + } + + private void buildClientFolderNames() { + clientFolderNames.clear(); + if (ClientCloneController.Instance != null) { + for (CloneFolder f : ClientCloneController.Instance.getFolderList()) { + clientFolderNames.add(f.name); + } + } + } + + private List getActiveFolderList() { + return serverSide ? serverFolderNames : clientFolderNames; + } + + private void validateFolder() { + if (saveMode == 1 && folder != null) { + if (!getActiveFolderList().contains(folder)) { + folder = null; + } + } } @Override @@ -77,61 +118,138 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall addLabel(new GuiNpcLabel(0, "Save as", guiLeft + 4, guiTop + 6)); addTextField(new GuiNpcTextField(0, this, fontRendererObj, guiLeft + 4, guiTop + 18, 200, 20, name)); + // Tab/Folder toggle + addButton(new GuiNpcButton(6, guiLeft + 4, guiTop + 45, 50, 20, + new String[]{"Tab", "Folder"}, saveMode)); - addLabel(new GuiNpcLabel(1, "Tab", guiLeft + 10, guiTop + 50)); - addButton(new GuiButtonBiDirectional(2, guiLeft + 40, guiTop + 45, 60, 20, new String[]{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"}, tab - 1)); + // Destination selector based on saveMode + if (saveMode == 0) { + // Tab selector: 1-15 + String[] tabLabels = new String[15]; + for (int i = 0; i < 15; i++) tabLabels[i] = String.valueOf(i + 1); + int selectedTab = (tab >= 1 && tab <= 15) ? tab - 1 : 0; + addButton(new GuiButtonBiDirectional(2, guiLeft + 56, guiTop + 45, 90, 20, tabLabels, selectedTab)); + } else { + // Folder selector button — opens SubGuiFolderSelect + String btnText = folder != null ? folder : "Select a Folder"; + addButton(new GuiNpcButton(2, guiLeft + 56, guiTop + 45, 148, 20, btnText)); + } - addButton(new GuiNpcButton(3, guiLeft + 4, guiTop + 95, new String[]{"Client side", "Server side"}, serverSide ? 1 : 0)); - addLabel(new GuiNpcLabel(1, "Tab", guiLeft + 10, guiTop + 50)); - - addButton(new GuiNpcButton(0, guiLeft + 4, guiTop + 70, 80, 20, "gui.save")); + // Save / Cancel + GuiNpcButton saveBtn = new GuiNpcButton(0, guiLeft + 4, guiTop + 70, 80, 20, "gui.save"); + if (saveMode == 1 && folder == null) { + saveBtn.enabled = false; + } + addButton(saveBtn); addButton(new GuiNpcButton(1, guiLeft + 86, guiTop + 70, 80, 20, "gui.cancel")); + // Client / Server toggle + addButton(new GuiNpcButton(3, guiLeft + 4, guiTop + 95, new String[]{"Client side", "Server side"}, serverSide ? 1 : 0)); + if (isNPC) { addButton(new GuiNpcButton(4, guiLeft + 4, guiTop + 120, 99, 20, "cloner.wandTags")); addButton(new GuiNpcButton(5, guiLeft + 106, guiTop + 120, 99, 20, "cloner.npcTags")); if (addTags.size() > 0) { - addLabel(new GuiNpcLabel(6, "cloner.wandtagsapplied", guiLeft + 10, guiTop + 160)); + addLabel(new GuiNpcLabel(8, "cloner.wandtagsapplied", guiLeft + 10, guiTop + 160)); } } } + private void updateDestinationFromSelector() { + if (saveMode == 0) { + GuiNpcButton selector = getButton(2); + if (selector == null) return; + tab = selector.getValue() + 1; + folder = null; + } + // In folder mode, folder is already set via SubGuiFolderSelect + } + public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; + + // Save if (id == 0) { + updateDestinationFromSelector(); String name = getTextField(0).getText(); if (name.isEmpty()) return; - int tab = ((GuiNpcButton) guibutton).getValue() + 1; + + if (saveMode == 1 && folder == null) + return; + if (!serverSide) { - if (ClientCloneController.Instance.getCloneData(null, name, tab) != null) + boolean exists; + if (saveMode == 1 && folder != null && ClientCloneController.Instance != null) { + exists = ClientCloneController.Instance.getCloneData(null, name, folder) != null; + } else { + exists = ClientCloneController.Instance.getCloneData(null, name, tab) != null; + } + if (exists) displayGuiScreen(new GuiYesNo(this, "Warning", "You are about to overwrite a clone", 1)); else confirmClicked(true, 0); - } else - PacketClient.sendClient(new ClonePreSavePacket(name, tab)); + } else { + if (saveMode == 1 && folder != null) { + PacketClient.sendClient(new ClonePreSavePacket(name, folder)); + } else { + PacketClient.sendClient(new ClonePreSavePacket(name, tab)); + } + } } + + // Cancel if (id == 1) { close(); } + + // Destination selector if (id == 2) { - tab = ((GuiNpcButton) guibutton).getValue() + 1; + if (saveMode == 0) { + // Tab mode — update from BiDirectional + updateDestinationFromSelector(); + } else { + // Folder mode — open folder selector Sub GUI + List folders = getActiveFolderList(); + setSubGui(new SubGuiFolderSelect(folders, folder)); + } } + + // Client / Server toggle if (id == 3) { + boolean wasServerSide = serverSide; serverSide = ((GuiNpcButton) guibutton).getValue() == 1; + + if (serverSide && !wasServerSide) { + // Switching to server mode — request fresh folder list + PacketClient.sendClient(new CloneFolderListPacket()); + } + + // Validate folder exists on the new side + validateFolder(); + initGui(); } + + // Wand Tags if (id == 4) { if (isNPC) { this.setSubGui(new SubGuiClonerQuickTags(this)); } } + + // NPC Tags if (id == 5) { if (isNPC) { this.setSubGui(new SubGuiClonerNPCTags((EntityNPCInterface) toClone, this)); } } - } + // Tab/Folder mode toggle + if (id == 6) { + saveMode = ((GuiNpcButton) guibutton).getValue(); + initGui(); + } + } @Override public void confirmClicked(boolean confirm, int id) { @@ -145,13 +263,21 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall if (isNPC) { compound.setTag("TagUUIDs", tagsCompound); } - ClientCloneController.Instance.addClone(compound, name, tab, extraTags); + if (saveMode == 1 && folder != null && ClientCloneController.Instance != null) { + ClientCloneController.Instance.addClone(compound, name, folder, extraTags); + } else { + ClientCloneController.Instance.addClone(compound, name, tab, extraTags); + } } else { NBTTagCompound compounder = new NBTTagCompound(); if (isNPC) { compounder.setTag("TagUUIDs", tagsCompound); } - PacketClient.sendClient(new CloneSavePacket(name, tab, extraTags, compounder)); + if (saveMode == 1 && folder != null) { + PacketClient.sendClient(new CloneSavePacket(name, folder, extraTags, compounder)); + } else { + PacketClient.sendClient(new CloneSavePacket(name, tab, extraTags, compounder)); + } } close(); @@ -159,7 +285,6 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall displayGuiScreen(this); } - @Override public void save() { } @@ -171,6 +296,17 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall displayGuiScreen(new GuiYesNo(this, "Warning", "You are about to overwrite a clone", 1)); else confirmClicked(true, 0); + } else if (compound.hasKey("CloneFolders")) { + // Server folder list arrived + NBTTagList folderList = compound.getTagList("CloneFolders", 10); + serverFolderNames.clear(); + for (int i = 0; i < folderList.tagCount(); i++) { + CloneFolder f = new CloneFolder(); + f.readNBT(folderList.getCompoundTagAt(i)); + serverFolderNames.add(f.name); + } + validateFolder(); + initGui(); } else if (compound.hasKey("ShortTags")) { NBTTagList validTags = compound.getTagList("ShortTags", 10); tagMap.clear(); @@ -193,6 +329,12 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall @Override public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiFolderSelect) { + SubGuiFolderSelect sel = (SubGuiFolderSelect) subgui; + if (sel.selectedFolder != null) { + folder = sel.selectedFolder; + } + } initGui(); } diff --git a/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerFullscreen.java b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerFullscreen.java new file mode 100644 index 000000000..4919ccbae --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerFullscreen.java @@ -0,0 +1,1045 @@ +package noppes.npcs.client.gui; + +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.clone.CloneAllTagsPacket; +import kamkeel.npcs.network.packets.request.clone.CloneFolderCrudPacket; +import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket; +import kamkeel.npcs.network.packets.request.clone.CloneListPacket; +import kamkeel.npcs.network.packets.request.clone.CloneMovePacket; +import kamkeel.npcs.network.packets.request.clone.CloneRemovePacket; +import kamkeel.npcs.network.packets.request.clone.CloneTagListPacket; +import kamkeel.npcs.network.packets.request.clone.MobSpawnerPacket; +import kamkeel.npcs.network.packets.request.clone.SpawnMobPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityList; +import net.minecraft.entity.EntityLiving; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.StatCollector; +import net.minecraft.world.World; +import noppes.npcs.client.controllers.ClientCloneController; +import noppes.npcs.client.controllers.ClientTagMapController; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiCustomScrollCloner; +import noppes.npcs.client.gui.util.GuiCustomScrollIcons; +import noppes.npcs.client.gui.util.GuiDirectory; +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.IClonerGui; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.data.CloneFolder; +import noppes.npcs.controllers.data.Tag; +import noppes.npcs.controllers.data.TagMap; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class GuiNpcMobSpawnerFullscreen extends GuiDirectory implements IGuiData, IClonerGui { + + // ==================== INSTANCE STATE ==================== + public TagMap tagMap; + private final GuiCustomScrollCloner scroll = new GuiCustomScrollCloner(this, 0); + private final GuiCustomScroll filterScroll = new GuiCustomScroll(this, 1); + private final GuiCustomScrollIcons navScroll = new GuiCustomScrollIcons(this, 2); + + private int posX, posY, posZ; + private List list; + private List rawList; + private List tagList; + private List folderNames = new ArrayList<>(); + + // Left nav list data + private List navNames = new ArrayList<>(); + private List navIcons = new ArrayList<>(); + private String navSearch = ""; + + // Move workflow state + private int movePhase = 0; // 0=off, 1=selecting clones, 2=selecting destination + private HashSet moveSelection = new HashSet<>(); + + // ==================== CONSTRUCTOR ==================== + public GuiNpcMobSpawnerFullscreen(int posX, int posY, int posZ) { + super(); + this.posX = posX; + this.posY = posY; + this.posZ = posZ; + + // Configure GuiDirectory layout percentages + leftPanelPercent = 0.15f; + rightPanelPercent = 0.08f; + minLeftPanelW = 120; + minRightPanelW = 62; + + PacketClient.sendClient(new CloneAllTagsPacket()); + if (GuiNpcMobSpawner.showingClones == 2) { + PacketClient.sendClient(new CloneFolderListPacket()); + } + loadFolderNames(); + } + + private void loadFolderNames() { + folderNames.clear(); + if (ClientCloneController.Instance != null) { + for (CloneFolder folder : ClientCloneController.Instance.getFolderList()) { + folderNames.add(folder.name); + } + } + } + + // ==================== IClonerGui ==================== + @Override + public int getShowingClones() { + return GuiNpcMobSpawner.showingClones; + } + + @Override + public HashMap getTags() { + return GuiNpcMobSpawner.tags; + } + + @Override + public TagMap getTagMap() { + return tagMap; + } + + // ==================== GuiDirectory HOOKS ==================== + @Override + public void initGui() { + // Save scroll positions before super.initGui() clears everything + GuiNpcMobSpawner.savedContentScrollY = scroll.scrollY; + GuiNpcMobSpawner.savedNavScrollY = navScroll.scrollY; + + super.initGui(); + } + + @Override + protected void drawPanels() { + if (GuiNpcMobSpawner.showingClones < 3) { + super.drawPanels(); + } else { + // Filters page: only draw top bar, no side panels + } + } + + @Override + protected void initTopBar(int topBtnY) { + int topBtnW = 55; + int x = originX + 2; + + GuiNpcButton clonesBtn = new GuiNpcButton(3, x, topBtnY, topBtnW, btnH, "spawner.clones"); + clonesBtn.enabled = GuiNpcMobSpawner.showingClones != 0; + addButton(clonesBtn); + x += topBtnW + 2; + + GuiNpcButton serverBtn = new GuiNpcButton(5, x, topBtnY, topBtnW, btnH, "gui.server"); + serverBtn.enabled = GuiNpcMobSpawner.showingClones != 2; + addButton(serverBtn); + x += topBtnW + 2; + + GuiNpcButton entitiesBtn = new GuiNpcButton(4, x, topBtnY, topBtnW, btnH, "spawner.entities"); + entitiesBtn.enabled = GuiNpcMobSpawner.showingClones != 1; + addButton(entitiesBtn); + + // Top bar right side + int closeX = originX + usableW - btnH - 2; + addButton(new GuiNpcButton(17, closeX, topBtnY, btnH, btnH, "X")); + int minimizeX = closeX - btnH - 2; + addButton(new GuiNpcButton(61, minimizeX, topBtnY, btnH, btnH, "-")); + int filterBtnW = 50; + int filterX = minimizeX - filterBtnW - 2; + GuiNpcButton filterBtn = new GuiNpcButton(16, filterX, topBtnY, filterBtnW, btnH, "gui.filters"); + filterBtn.enabled = GuiNpcMobSpawner.showingClones != 3; + addButton(filterBtn); + + // Clone search field in top bar (only when not on filters page) + if (GuiNpcMobSpawner.showingClones < 3) { + int searchX = x + topBtnW + 6; + int searchW = filterX - searchX - 4; + if (searchW > 20) { + addTextField(new GuiNpcTextField(1, this, fontRendererObj, + searchX, topBtnY, searchW, btnH, GuiNpcMobSpawner.search)); + } + } + + // Disable mode buttons during move + if (movePhase > 0) { + clonesBtn.enabled = false; + serverBtn.enabled = false; + entitiesBtn.enabled = false; + filterBtn.enabled = false; + } + } + + @Override + protected void initLeftPanel() { + if (GuiNpcMobSpawner.showingClones >= 3) return; + + boolean folderSelected = GuiNpcMobSpawner.activeFolder != null; + int bottomRows = folderSelected ? 3 : 2; + int bottomH = bottomRows * (btnH + gap); + int navH = contentH - bottomH; + buildNavList(); + + int savedNavScroll = GuiNpcMobSpawner.savedNavScrollY; + navScroll.clear(); + navScroll.colors.clear(); + navScroll.setSize(leftPanelW, navH); + navScroll.guiLeft = originX; + navScroll.guiTop = contentY; + navScroll.setListWithIcons(navNames, navIcons); + navScroll.setSelected(getNavSelection()); + navScroll.scrollY = Math.min(savedNavScroll, Math.max(0, navScroll.maxScrollY)); + + // Phase 2: color nav items orange (selectable) or red (current location, not selectable) + if (movePhase == 2) { + String currentNav = getNavSelection(); + for (String name : navNames) { + navScroll.colors.put(name, name.equals(currentNav) ? 0xFF5555 : 0xFFAA00); + } + } + + addScroll(navScroll); + + // Nav search field below the scroll + int navSearchY = contentY + navH + gap; + addTextField(new GuiNpcTextField(3, this, fontRendererObj, + originX, navSearchY, leftPanelW, btnH, navSearch)); + + // Add folder button + int addFolderY = navSearchY + btnH + gap; + GuiNpcButton addFolderBtn = new GuiNpcButton(40, originX, addFolderY, leftPanelW, btnH, "+ Add Folder"); + if (movePhase > 0) addFolderBtn.enabled = false; + addButton(addFolderBtn); + + // Rename/Delete folder buttons (only when a folder is selected) + if (folderSelected) { + int folderCrudY = addFolderY + btnH + gap; + int halfW = (leftPanelW - gap) / 2; + GuiNpcButton renameBtn = new GuiNpcButton(41, originX, folderCrudY, halfW, btnH, "Rename"); + if (movePhase > 0) renameBtn.enabled = false; + addButton(renameBtn); + GuiNpcButton delBtn = new GuiNpcButton(42, originX + halfW + gap, folderCrudY, halfW, btnH, "gui.remove"); + if (ClientCloneController.Instance != null) { + List clones = ClientCloneController.Instance.getClones(GuiNpcMobSpawner.activeFolder); + delBtn.enabled = clones == null || clones.isEmpty(); + } + if (movePhase > 0) delBtn.enabled = false; + addButton(delBtn); + } + } + + @Override + protected void initCenterPanel() { + if (GuiNpcMobSpawner.showingClones >= 3) { + showFiltersPage(); + return; + } + + int savedScroll = GuiNpcMobSpawner.savedContentScrollY; + scroll.clear(); + scroll.setSize(contentW, contentH); + scroll.guiLeft = contentX; + scroll.guiTop = contentY; + addScroll(scroll); + + if (GuiNpcMobSpawner.showingClones == 0 || GuiNpcMobSpawner.showingClones == 2) { + // Always clear colors — only phase 2 re-populates them + scroll.colors.clear(); + + // Multi-select mode during move phase 1 + if (movePhase == 1) { + scroll.multipleSelection = true; + scroll.setSelectable(true); + scroll.setSelectedList(moveSelection); + } else if (movePhase == 2) { + // Phase 2: show selection as yellow, freeze scroll + scroll.multipleSelection = true; + scroll.setSelectedList(moveSelection); + scroll.setSelectable(false); + } else { + scroll.multipleSelection = false; + scroll.setSelectable(true); + } + + showClones(); + + // Phase 2: color selected clones yellow + if (movePhase == 2) { + for (String name : moveSelection) { + scroll.colors.put(name, 0xFFFF55); + } + } + + // Restore scroll position after list is populated + scroll.scrollY = Math.min(savedScroll, Math.max(0, scroll.maxScrollY)); + } else { + showEntities(); + scroll.scrollY = Math.min(savedScroll, Math.max(0, scroll.maxScrollY)); + } + } + + @Override + protected void initRightPanel(int startY) { + if (GuiNpcMobSpawner.showingClones >= 3) return; + + int btnGap = 4; + int panelH = contentH - 4; + int midY = startY + (panelH / 2); + int bottomY = contentY + contentH - btnH; + + // Spawn — top + GuiNpcButton spawnBtn = new GuiNpcButton(1, rightX, startY, rightPanelW, btnH, "item.monsterPlacer.name"); + if (movePhase > 0) spawnBtn.enabled = false; + addButton(spawnBtn); + + // Spawner — below midpoint + GuiNpcButton spawnerBtn = new GuiNpcButton(2, rightX, midY + btnGap / 2, rightPanelW, btnH, "spawner.mobspawner"); + if (movePhase > 0) spawnerBtn.enabled = false; + addButton(spawnerBtn); + + if (GuiNpcMobSpawner.showingClones == 0 || GuiNpcMobSpawner.showingClones == 2) { + // Move — above midpoint + String moveLabel = movePhase > 0 ? "Moving" : "Move"; + GuiNpcButton moveBtn = new GuiNpcButton(50, rightX, midY - btnH - btnGap / 2, rightPanelW, btnH, moveLabel); + addButton(moveBtn); + + // Confirm — below Move (only visible during phase 1) + if (movePhase == 1) { + GuiNpcButton confirmBtn = new GuiNpcButton(51, rightX, midY - btnH - btnGap / 2 + btnH + btnGap, rightPanelW, btnH, "Confirm"); + confirmBtn.enabled = !moveSelection.isEmpty(); + addButton(confirmBtn); + } + + // Remove — bottom + GuiNpcButton removeBtn = new GuiNpcButton(6, rightX, bottomY, rightPanelW, btnH, "gui.remove"); + if (movePhase > 0) removeBtn.enabled = false; + addButton(removeBtn); + } + } + + // ==================== OVERLAY DRAWING ==================== + @Override + protected void drawOverlay(int mouseX, int mouseY, float partialTicks) { + if (movePhase == 1) { + String text = "Select Clones to Move"; + int textW = fontRendererObj.getStringWidth(text); + fontRendererObj.drawStringWithShadow(text, width / 2 - textW / 2, height - pad - 10, 0x55FF55); + } else if (movePhase == 2) { + String text = "Select a Tab or Folder"; + int textW = fontRendererObj.getStringWidth(text); + fontRendererObj.drawStringWithShadow(text, width / 2 - textW / 2, height - pad - 10, 0xFF5555); + } + } + + // ==================== LEFT NAVIGATION ==================== + private void buildNavList() { + navNames.clear(); + navIcons.clear(); + + String searchLower = navSearch.toLowerCase(); + + for (int i = 1; i <= 15; i++) { + String name = "Tab " + i; + if (searchLower.isEmpty() || name.toLowerCase().contains(searchLower)) { + navNames.add(name); + navIcons.add(GuiCustomScrollIcons.ICON_TAB); + } + } + for (String folder : folderNames) { + if (searchLower.isEmpty() || folder.toLowerCase().contains(searchLower)) { + navNames.add(folder); + navIcons.add(GuiCustomScrollIcons.ICON_FOLDER); + } + } + } + + private String getNavSelection() { + if (GuiNpcMobSpawner.activeFolder != null) return GuiNpcMobSpawner.activeFolder; + if (GuiNpcMobSpawner.activeTab >= 1 && GuiNpcMobSpawner.activeTab <= 15) { + return "Tab " + GuiNpcMobSpawner.activeTab; + } + return "Tab 1"; + } + + private void handleNavClick(String selected) { + if (selected == null) return; + + if (selected.startsWith("Tab ")) { + try { + int tabNum = Integer.parseInt(selected.substring(4)); + if (tabNum >= 1 && tabNum <= 15) { + GuiNpcMobSpawner.activeTab = tabNum; + GuiNpcMobSpawner.activeFolder = null; + initGui(); + return; + } + } catch (NumberFormatException ignored) { + } + } + + GuiNpcMobSpawner.activeFolder = selected; + GuiNpcMobSpawner.activeTab = -1; + initGui(); + } + + // ==================== FILTERS PAGE ==================== + private void showFiltersPage() { + int scrollW = Math.min(usableW / 2 - gap * 2, 240); + int scrollX = originX + gap; + int scrollTopY = contentY + 16; + int scrollH = contentH - 16 - btnH - gap * 2; + + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("cloner.tagFilters"), + scrollX + 2, contentY + 4, 0xFFFFFF)); + + filterScroll.clear(); + filterScroll.setSize(scrollW, scrollH); + filterScroll.guiLeft = scrollX; + filterScroll.guiTop = scrollTopY; + filterScroll.multipleSelection = true; + filterScroll.setSelectedList(GuiNpcMobSpawner.tagFilters); + addScroll(filterScroll); + + addTextField(new GuiNpcTextField(2, this, fontRendererObj, + scrollX, scrollTopY + scrollH + gap, scrollW, btnH, GuiNpcMobSpawner.tagSearch)); + + // Settings column + int col2X = scrollX + scrollW + gap * 4; + int controlW = 90; + int controlX = col2X + 80; + int rowH = 28; + int rowY = scrollTopY; + + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("cloner.tagVisibility"), + col2X, rowY + 5, 0xFFFFFF)); + addButton(new GuiNpcButton(12, controlX, rowY, controlW, btnH, + new String[]{"display.show", "display.all", "display.hide"}, GuiNpcMobSpawner.displayTags)); + + rowY += rowH; + addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("filter.contains"), + col2X, rowY + 5, 0xFFFFFF)); + addButton(new GuiNpcButton(13, controlX, rowY, controlW, btnH, + new String[]{"filter.any", "filter.all", "filter.notany", "filter.notall"}, GuiNpcMobSpawner.filterCondition)); + + rowY += rowH + 4; + addButton(new GuiNpcButton(11, col2X, rowY, controlW + 80, btnH, "gui.deselectAll")); + + rowY += rowH + 12; + addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("cloner.order"), + col2X, rowY + 5, 0xFFFFFF)); + addButton(new GuiNpcButton(14, controlX, rowY, controlW, btnH, + new String[]{"cloner.ascending", "cloner.descending"}, GuiNpcMobSpawner.ascending)); + + rowY += rowH; + addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("cloner.type"), + col2X, rowY + 5, 0xFFFFFF)); + addButton(new GuiNpcButton(15, controlX, rowY, controlW, btnH, + new String[]{"cloner.name", "cloner.date"}, GuiNpcMobSpawner.sortType)); + + filterScroll.setList(getTagList()); + } + + // ==================== CLONE / ENTITY DISPLAY ==================== + private void showClones() { + if (GuiNpcMobSpawner.activeFolder != null) { + showFolderClones(); + return; + } + + if (GuiNpcMobSpawner.showingClones == 2) { + PacketClient.sendClient(new CloneTagListPacket(GuiNpcMobSpawner.activeTab)); + PacketClient.sendClient(new CloneListPacket(GuiNpcMobSpawner.activeTab)); + return; + } + + if (GuiNpcMobSpawner.sortType == 0) { + this.list = ClientCloneController.Instance.getClones(GuiNpcMobSpawner.activeTab); + } else { + this.list = ClientCloneController.Instance.getClonesDate(GuiNpcMobSpawner.activeTab); + } + + this.tagMap = ClientTagMapController.Instance.getTagMap(GuiNpcMobSpawner.activeTab); + populateRawListWithTags(); + scroll.setList(getSearchList(), GuiNpcMobSpawner.ascending == 0, GuiNpcMobSpawner.sortType == 0); + } + + private void showFolderClones() { + if (GuiNpcMobSpawner.showingClones == 2) { + PacketClient.sendClient(new CloneTagListPacket(GuiNpcMobSpawner.activeFolder)); + PacketClient.sendClient(new CloneListPacket(GuiNpcMobSpawner.activeFolder)); + return; + } + + if (ClientCloneController.Instance != null) { + if (GuiNpcMobSpawner.sortType == 0) { + this.list = ClientCloneController.Instance.getClones(GuiNpcMobSpawner.activeFolder); + } else { + this.list = ClientCloneController.Instance.getClonesDate(GuiNpcMobSpawner.activeFolder); + } + this.tagMap = ClientTagMapController.Instance.getTagMap(GuiNpcMobSpawner.activeFolder); + } else { + this.list = new ArrayList<>(); + this.tagMap = null; + } + + populateRawListWithTags(); + scroll.setList(getSearchList(), GuiNpcMobSpawner.ascending == 0, GuiNpcMobSpawner.sortType == 0); + } + + private void showEntities() { + Map data = EntityList.stringToClassMapping; + ArrayList list = new ArrayList(); + for (Object name : data.keySet()) { + Class c = (Class) data.get(name); + try { + if (EntityLiving.class.isAssignableFrom(c) + && c.getConstructor(new Class[]{World.class}) != null + && !Modifier.isAbstract(c.getModifiers())) + list.add(name.toString()); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + } + } + this.list = list; + this.rawList = new ArrayList(this.list); + scroll.setList(getSearchList(), GuiNpcMobSpawner.ascending == 0, GuiNpcMobSpawner.sortType == 0); + } + + private void populateRawListWithTags() { + this.rawList = new ArrayList<>(this.list); + if (this.tagMap != null) { + if (GuiNpcMobSpawner.displayTags == 0 || GuiNpcMobSpawner.displayTags == 1) { + for (int i = 0; i < this.list.size(); i++) { + StringBuilder npcName = new StringBuilder(list.get(i)); + if (this.tagMap.hasClone(list.get(i))) { + for (UUID tagUUID : this.tagMap.getUUIDsList(list.get(i))) { + Tag tag = GuiNpcMobSpawner.tags.get(tagUUID); + if (tag != null) { + if (GuiNpcMobSpawner.displayTags == 1 || !tag.getIsHidden()) { + npcName.append(" [" + tag.name + "]"); + } + } + } + } + this.rawList.set(i, npcName.toString()); + } + } + } + } + + private NBTTagCompound getCompound() { + String sel = scroll.getSelected(); + if (sel == null) + return null; + + if (GuiNpcMobSpawner.showingClones == 0) { + if (GuiNpcMobSpawner.activeFolder != null && ClientCloneController.Instance != null) { + return ClientCloneController.Instance.getCloneData(player, sel, GuiNpcMobSpawner.activeFolder); + } + return ClientCloneController.Instance.getCloneData(player, sel, GuiNpcMobSpawner.activeTab); + } else { + Entity entity = EntityList.createEntityByName(sel, Minecraft.getMinecraft().theWorld); + if (entity == null) + return null; + NBTTagCompound compound = new NBTTagCompound(); + entity.writeToNBTOptional(compound); + return compound; + } + } + + // ==================== SEARCH / FILTER ==================== + private List getSearchList() { + if (this.list == null) { + this.list = new ArrayList(); + } + if (this.rawList == null) { + this.rawList = new ArrayList(); + } + + if (GuiNpcMobSpawner.tagFilters.size() == 0 || GuiNpcMobSpawner.showingClones == 1) { + if (GuiNpcMobSpawner.search.isEmpty()) + return new ArrayList(this.list); + else { + List list = new ArrayList(); + for (int i = 0; i < this.list.size(); i++) { + if (this.rawList.get(i).toLowerCase().contains(GuiNpcMobSpawner.search)) { + list.add(this.list.get(i)); + } + } + return list; + } + } + + if (GuiNpcMobSpawner.search.isEmpty()) { + List list = new ArrayList(); + for (String name : this.list) { + if (tagMap.hasClone(name)) { + if (meetsCondition(name)) { + list.add(name); + } + } + } + return list; + } + + List list = new ArrayList(); + for (int i = 0; i < this.list.size(); i++) { + if (this.rawList.get(i).toLowerCase().contains(GuiNpcMobSpawner.search)) { + String npcName = this.list.get(i); + if (tagMap.hasClone(npcName)) { + if (meetsCondition(npcName)) { + list.add(npcName); + } + } + } + } + return list; + } + + private boolean meetsCondition(String name) { + if (tagMap == null) { + return true; + } + boolean conditionMet = true; + boolean allRequirement = GuiNpcMobSpawner.filterCondition == 1 || GuiNpcMobSpawner.filterCondition == 3; + + for (String tagName : GuiNpcMobSpawner.tagFilters) { + UUID tagUUID = GuiNpcMobSpawner.tagNames.get(tagName); + if (tagUUID != null) { + boolean hasTag = tagMap.hasTag(name, tagUUID); + if (!allRequirement) { + if (!hasTag && GuiNpcMobSpawner.filterCondition == 0) { + conditionMet = false; + break; + } else if (hasTag && GuiNpcMobSpawner.filterCondition == 2) { + conditionMet = false; + break; + } + } else { + if (!hasTag && GuiNpcMobSpawner.filterCondition == 1) { + conditionMet = false; + break; + } else if (hasTag && GuiNpcMobSpawner.filterCondition == 3) { + conditionMet = false; + break; + } + } + } + } + return conditionMet; + } + + private List getTagList() { + if (this.tagList == null) { + this.tagList = new ArrayList(); + } + if (GuiNpcMobSpawner.tagSearch.isEmpty()) + return new ArrayList(GuiNpcMobSpawner.tagNames.keySet()); + + List list = new ArrayList(); + for (String name : this.tagList) { + if (name.toLowerCase().contains(GuiNpcMobSpawner.tagSearch)) + list.add(name); + } + return list; + } + + // ==================== SCROLL EVENTS ==================== + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { + // Phase 2: ignore clicks on the center clone scroll + if (guiCustomScroll.id == 0 && movePhase == 2) { + return; + } + + if (guiCustomScroll.id == 0 && movePhase == 1) { + moveSelection = scroll.getSelectedList(); + GuiNpcButton confirmBtn = getButton(51); + if (confirmBtn != null) { + confirmBtn.enabled = !moveSelection.isEmpty(); + } + return; + } + + if (guiCustomScroll.id == 2) { + String selected = navScroll.getSelected(); + if (selected == null) return; + + if (movePhase == 2) { + // Cannot move to the same location + if (selected.equals(getNavSelection())) { + return; + } + // Selecting destination from nav + if (selected.startsWith("Tab ")) { + try { + int tabNum = Integer.parseInt(selected.substring(4)); + setSubGui(new SubGuiMoveConfirm(moveSelection.size(), tabNum, null)); + } catch (NumberFormatException ignored) { + } + } else { + setSubGui(new SubGuiMoveConfirm(moveSelection.size(), -1, selected)); + } + return; + } + + if (movePhase == 1) { + movePhase = 0; + moveSelection.clear(); + } + + handleNavClick(selected); + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) { + } + + // ==================== KEYBOARD ==================== + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + + GuiNpcTextField searchField = getTextField(1); + if (searchField != null) { + String newText = searchField.getText().toLowerCase(); + if (!GuiNpcMobSpawner.search.equals(newText)) { + GuiNpcMobSpawner.search = newText; + scroll.setList(getSearchList(), GuiNpcMobSpawner.ascending == 0, GuiNpcMobSpawner.sortType == 0); + } + } + + GuiNpcTextField tagSearchField = getTextField(2); + if (tagSearchField != null) { + String newText = tagSearchField.getText().toLowerCase(); + if (!GuiNpcMobSpawner.tagSearch.equals(newText)) { + GuiNpcMobSpawner.tagSearch = newText; + filterScroll.setList(getTagList()); + } + } + + GuiNpcTextField navSearchField = getTextField(3); + if (navSearchField != null) { + String newNavSearch = navSearchField.getText().toLowerCase(); + if (!navSearch.equals(newNavSearch)) { + navSearch = newNavSearch; + buildNavList(); + navScroll.setListWithIcons(navNames, navIcons); + navScroll.setSelected(getNavSelection()); + } + } + } + + // ==================== BUTTON ACTIONS ==================== + @Override + protected void actionPerformed(GuiButton guibutton) { + int id = guibutton.id; + + if (id == 3) { + movePhase = 0; + moveSelection.clear(); + GuiNpcMobSpawner.savedContentScrollY = 0; + GuiNpcMobSpawner.savedNavScrollY = 0; + GuiNpcMobSpawner.showingClones = 0; + loadFolderNames(); + initGui(); + } + if (id == 4) { + movePhase = 0; + moveSelection.clear(); + GuiNpcMobSpawner.savedContentScrollY = 0; + GuiNpcMobSpawner.savedNavScrollY = 0; + GuiNpcMobSpawner.showingClones = 1; + initGui(); + } + if (id == 5) { + movePhase = 0; + moveSelection.clear(); + GuiNpcMobSpawner.savedContentScrollY = 0; + GuiNpcMobSpawner.savedNavScrollY = 0; + GuiNpcMobSpawner.showingClones = 2; + PacketClient.sendClient(new CloneFolderListPacket()); + initGui(); + } + if (id == 16) { + movePhase = 0; + moveSelection.clear(); + GuiNpcMobSpawner.savedContentScrollY = 0; + GuiNpcMobSpawner.savedNavScrollY = 0; + GuiNpcMobSpawner.showingClones = 3; + initGui(); + } + if (id == 17) { + close(); + } + + // Minimize — return to normal GUI + if (id == 61) { + GuiNpcMobSpawner.isFullscreen = false; + Minecraft.getMinecraft().displayGuiScreen( + new GuiNpcMobSpawner(posX, posY, posZ)); + } + + // Spawn + if (id == 1) { + if (GuiNpcMobSpawner.showingClones == 2) { + String sel = scroll.getSelected(); + if (sel == null) return; + if (GuiNpcMobSpawner.activeFolder != null) + SpawnMobPacket.ServerFolder(posX, posY, posZ, sel, GuiNpcMobSpawner.activeFolder); + else + SpawnMobPacket.Server(posX, posY, posZ, sel, GuiNpcMobSpawner.activeTab); + close(); + } else { + NBTTagCompound compound = getCompound(); + if (compound == null) return; + SpawnMobPacket.Client(posX, posY, posZ, compound); + close(); + } + } + + // Mob Spawner + if (id == 2) { + if (GuiNpcMobSpawner.showingClones == 2) { + String sel = scroll.getSelected(); + if (sel == null) return; + if (GuiNpcMobSpawner.activeFolder != null) + MobSpawnerPacket.ServerFolder(posX, posY, posZ, sel, GuiNpcMobSpawner.activeFolder); + else + MobSpawnerPacket.Server(posX, posY, posZ, sel, GuiNpcMobSpawner.activeTab); + close(); + } else { + NBTTagCompound compound = getCompound(); + if (compound == null) return; + MobSpawnerPacket.Client(posX, posY, posZ, compound); + close(); + } + } + + // Remove + if (id == 6) { + if (scroll.getSelected() != null) { + int prevSelected = scroll.selected; + if (GuiNpcMobSpawner.showingClones == 2) { + if (GuiNpcMobSpawner.activeFolder != null) + PacketClient.sendClient(new CloneRemovePacket(GuiNpcMobSpawner.activeFolder, scroll.getSelected())); + else + PacketClient.sendClient(new CloneRemovePacket(GuiNpcMobSpawner.activeTab, scroll.getSelected())); + return; + } + if (GuiNpcMobSpawner.activeFolder != null && ClientCloneController.Instance != null) + ClientCloneController.Instance.removeClone(scroll.getSelected(), GuiNpcMobSpawner.activeFolder); + else + ClientCloneController.Instance.removeClone(scroll.getSelected(), GuiNpcMobSpawner.activeTab); + initGui(); + if (scroll.list != null && !scroll.list.isEmpty()) { + scroll.selected = Math.min(prevSelected, scroll.list.size() - 1); + } else { + scroll.selected = -1; + } + } + } + + // Move toggle + if (id == 50) { + if (movePhase == 0) { + movePhase = 1; + moveSelection.clear(); + } else { + movePhase = 0; + moveSelection.clear(); + } + initGui(); + } + // Confirm move selection + if (id == 51) { + if (movePhase == 1 && !moveSelection.isEmpty()) { + movePhase = 2; + initGui(); + } + } + + // Filter controls + if (id == 11) { + GuiNpcMobSpawner.tagFilters = new HashSet<>(); + filterScroll.setSelectedList(GuiNpcMobSpawner.tagFilters); + initGui(); + } + if (id == 12) { + GuiNpcMobSpawner.displayTags = (byte) ((GuiNpcButton) guibutton).getValue(); + } + if (id == 13) { + GuiNpcMobSpawner.filterCondition = (byte) ((GuiNpcButton) guibutton).getValue(); + } + if (id == 14) { + GuiNpcMobSpawner.ascending = (byte) ((GuiNpcButton) guibutton).getValue(); + } + if (id == 15) { + GuiNpcMobSpawner.sortType = (byte) ((GuiNpcButton) guibutton).getValue(); + } + + // Create folder + if (id == 40) { + setSubGui(new SubGuiCloneFolderName("")); + } + // Rename folder + if (id == 41) { + if (GuiNpcMobSpawner.activeFolder != null) + setSubGui(new SubGuiCloneFolderName(GuiNpcMobSpawner.activeFolder)); + } + // Delete folder + if (id == 42) { + if (GuiNpcMobSpawner.activeFolder != null) { + if (GuiNpcMobSpawner.showingClones == 2) { + PacketClient.sendClient(new CloneFolderCrudPacket( + CloneFolderCrudPacket.ACTION_DELETE, GuiNpcMobSpawner.activeFolder)); + } else if (ClientCloneController.Instance != null) { + ClientCloneController.Instance.deleteFolder(GuiNpcMobSpawner.activeFolder); + loadFolderNames(); + } + GuiNpcMobSpawner.activeFolder = null; + GuiNpcMobSpawner.activeTab = 1; + initGui(); + } + } + } + + // ==================== SUB GUI ==================== + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiCloneFolderName) { + SubGuiCloneFolderName folderGui = (SubGuiCloneFolderName) subgui; + if (folderGui.cancelled) return; + + String newName = folderGui.getFolderName(); + if (newName == null || newName.isEmpty()) return; + + if (folderGui.isRename()) { + if (GuiNpcMobSpawner.showingClones == 2) { + PacketClient.sendClient(new CloneFolderCrudPacket( + CloneFolderCrudPacket.ACTION_RENAME, folderGui.getOriginalName(), newName)); + } else if (ClientCloneController.Instance != null) { + ClientCloneController.Instance.renameFolder(folderGui.getOriginalName(), newName); + loadFolderNames(); + } + GuiNpcMobSpawner.activeFolder = newName; + GuiNpcMobSpawner.activeTab = -1; + } else { + if (GuiNpcMobSpawner.showingClones == 2) { + PacketClient.sendClient(new CloneFolderCrudPacket( + CloneFolderCrudPacket.ACTION_CREATE, newName)); + } else if (ClientCloneController.Instance != null) { + ClientCloneController.Instance.createFolder(newName); + loadFolderNames(); + } + GuiNpcMobSpawner.activeFolder = newName; + GuiNpcMobSpawner.activeTab = -1; + } + initGui(); + } + + if (subgui instanceof SubGuiMoveConfirm) { + SubGuiMoveConfirm confirm = (SubGuiMoveConfirm) subgui; + if (confirm.confirmed && !moveSelection.isEmpty()) { + int fromTab = GuiNpcMobSpawner.activeFolder != null ? -1 : GuiNpcMobSpawner.activeTab; + String fromFolder = GuiNpcMobSpawner.activeFolder; + int toTab = confirm.destTab; + String toFolder = confirm.destFolder; + + for (String cloneName : moveSelection) { + if (GuiNpcMobSpawner.showingClones == 2) { + PacketClient.sendClient(new CloneMovePacket( + cloneName, fromTab, fromFolder, toTab, toFolder)); + } else if (ClientCloneController.Instance != null) { + if (fromFolder != null && toFolder != null) { + ClientCloneController.Instance.moveClone(cloneName, fromFolder, toFolder); + } else if (fromFolder != null) { + ClientCloneController.Instance.moveClone(cloneName, fromFolder, toTab); + } else if (toFolder != null) { + ClientCloneController.Instance.moveClone(cloneName, fromTab, toFolder); + } else { + ClientCloneController.Instance.moveClone(cloneName, fromTab, toTab); + } + } + } + } + movePhase = 0; + moveSelection.clear(); + initGui(); + } + } + + // ==================== DATA FROM SERVER ==================== + @Override + public void save() { + } + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("CloneFolders")) { + NBTTagList folderList = compound.getTagList("CloneFolders", 10); + folderNames.clear(); + for (int i = 0; i < folderList.tagCount(); i++) { + CloneFolder folder = new CloneFolder(); + folder.readNBT(folderList.getCompoundTagAt(i)); + folderNames.add(folder.name); + } + initGui(); + } else if (compound.hasKey("MoveSuccess")) { + initGui(); + } else if (compound.hasKey("CloneTags")) { + if (GuiNpcMobSpawner.activeFolder != null) { + this.tagMap = new TagMap(GuiNpcMobSpawner.activeFolder); + } else { + this.tagMap = new TagMap(GuiNpcMobSpawner.activeTab); + } + NBTTagCompound cloneTags = compound.getCompoundTag("CloneTags"); + this.tagMap.readNBT(cloneTags); + } else if (compound.hasKey("AllTags")) { + NBTTagList validTags = compound.getTagList("AllTags", 10); + if (validTags != null) { + HashMap tagsUpdate = new HashMap<>(); + HashMap tagNamesUpdate = new HashMap<>(); + for (int j = 0; j < validTags.tagCount(); j++) { + NBTTagCompound tagStructure = validTags.getCompoundTagAt(j); + Tag tag = new Tag(); + tag.readNBT(tagStructure); + tagsUpdate.put(tag.uuid, tag); + tagNamesUpdate.put(tag.name, tag.uuid); + } + GuiNpcMobSpawner.tags = tagsUpdate; + GuiNpcMobSpawner.tagNames = tagNamesUpdate; + this.tagList = new ArrayList(GuiNpcMobSpawner.tagNames.keySet()); + filterScroll.setList(getTagList()); + } + } else { + List list = new ArrayList(); + if (GuiNpcMobSpawner.sortType == 1) { + NBTTagList nbtlist = compound.getTagList("ListDate", 8); + for (int i = 0; i < nbtlist.tagCount(); i++) { + list.add(nbtlist.getStringTagAt(i)); + } + } else { + NBTTagList nbtlist = compound.getTagList("List", 8); + for (int i = 0; i < nbtlist.tagCount(); i++) { + list.add(nbtlist.getStringTagAt(i)); + } + } + + this.list = list; + populateRawListWithTags(); + scroll.setList(getSearchList(), GuiNpcMobSpawner.ascending == 0, GuiNpcMobSpawner.sortType == 0); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiCloneFolderName.java b/src/main/java/noppes/npcs/client/gui/SubGuiCloneFolderName.java new file mode 100644 index 000000000..508b00518 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/SubGuiCloneFolderName.java @@ -0,0 +1,67 @@ +package noppes.npcs.client.gui; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.data.CloneFolder; + +public class SubGuiCloneFolderName extends SubGuiInterface { + private String originalName; + public boolean cancelled = true; + private String folderName = ""; + + public SubGuiCloneFolderName(String existingName) { + this.originalName = existingName; + this.folderName = existingName; + xSize = 200; + ySize = 80; + this.closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + String title = isRename() ? "Rename Folder" : "New Folder"; + addLabel(new GuiNpcLabel(0, title, guiLeft + 5, guiTop + 5)); + addTextField(new GuiNpcTextField(0, this, fontRendererObj, guiLeft + 5, guiTop + 20, 190, 20, folderName)); + + addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 50, 90, 20, "gui.done")); + addButton(new GuiNpcButton(1, guiLeft + 100, guiTop + 50, 90, 20, "gui.cancel")); + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == 0) { + // Done + folderName = getTextField(0).getText().trim(); + if (CloneFolder.isValidName(folderName)) { + cancelled = false; + close(); + } + } + if (guibutton.id == 1) { + // Cancel + cancelled = true; + close(); + } + } + + public boolean isRename() { + return originalName != null && !originalName.isEmpty(); + } + + public String getFolderName() { + return folderName; + } + + public String getOriginalName() { + return originalName; + } + + @Override + public void save() { + } +} diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiFolderSelect.java b/src/main/java/noppes/npcs/client/gui/SubGuiFolderSelect.java new file mode 100644 index 000000000..3ec727543 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/SubGuiFolderSelect.java @@ -0,0 +1,74 @@ +package noppes.npcs.client.gui; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; + +public class SubGuiFolderSelect extends SubGuiInterface implements ICustomScrollListener { + + public String selectedFolder; + private final List folderList; + private GuiCustomScroll folderScroll; + + public SubGuiFolderSelect(List folders, String currentSelection) { + this.folderList = new ArrayList<>(folders); + this.selectedFolder = currentSelection; + setBackground("menubg.png"); + xSize = 200; + ySize = 220; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + folderScroll = new GuiCustomScroll(this, 0); + folderScroll.setSize(xSize - 8, ySize - 50); + folderScroll.guiLeft = guiLeft + 4; + folderScroll.guiTop = guiTop + 4; + folderScroll.setList(new ArrayList<>(folderList)); + if (selectedFolder != null) { + folderScroll.setSelected(selectedFolder); + } + addScroll(folderScroll); + + int btnW = (xSize - 12) / 2; + addButton(new GuiNpcButton(0, guiLeft + 4, guiTop + ySize - 44, btnW, 20, "gui.done")); + addButton(new GuiNpcButton(1, guiLeft + 4 + btnW + 4, guiTop + ySize - 44, btnW, 20, "gui.cancel")); + + getButton(0).enabled = folderScroll.hasSelected(); + } + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { + getButton(0).enabled = folderScroll.hasSelected(); + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) { + selectedFolder = selection; + close(); + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == 0) { + selectedFolder = folderScroll.getSelected(); + close(); + } + if (guibutton.id == 1) { + selectedFolder = null; + close(); + } + } + + @Override + public void save() { + } +} diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiMoveConfirm.java b/src/main/java/noppes/npcs/client/gui/SubGuiMoveConfirm.java new file mode 100644 index 000000000..ee70367f7 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/SubGuiMoveConfirm.java @@ -0,0 +1,57 @@ +package noppes.npcs.client.gui; + +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; + +public class SubGuiMoveConfirm extends SubGuiInterface { + public boolean confirmed = false; + + private final int count; + public final int destTab; + public final String destFolder; + + public SubGuiMoveConfirm(int count, int destTab, String destFolder) { + this.count = count; + this.destTab = destTab; + this.destFolder = destFolder; + xSize = 220; + ySize = 90; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + String line1 = "Move " + count + " clone" + (count != 1 ? "s" : "") + " to:"; + addLabel(new GuiNpcLabel(0, line1, guiLeft + 10, guiTop + 10, 0xFFFFFF)); + + String dest; + if (destFolder != null) { + dest = "Folder: \"" + destFolder + "\""; + } else { + dest = "Tab: " + destTab; + } + addLabel(new GuiNpcLabel(1, " " + dest, guiLeft + 10, guiTop + 25, 0xFFFFFF)); + + addButton(new GuiNpcButton(0, guiLeft + 10, guiTop + 55, 95, 20, "gui.yes")); + addButton(new GuiNpcButton(1, guiLeft + 115, guiTop + 55, 95, 20, "gui.cancel")); + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == 0) { + confirmed = true; + close(); + } + if (guibutton.id == 1) { + close(); + } + } + + @Override + public void save() { + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index a50622529..11ffa198d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -4,14 +4,18 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.StatCollector; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.containers.ContainerAuctionListing; import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.containers.SlotAuctionDisplay; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -19,45 +23,60 @@ import java.util.List; /** * Handles adding auction information to item tooltips when viewing auction GUIs. - * Called from MixinItem when Item.addInformation is invoked. + * Called from ItemTooltipEvent in ClientEventHandler. + * + * Uses slot-based lookup (slot index) instead of ItemStack comparison to correctly + * handle duplicate items with different auction data (e.g., two porkchops at different prices). */ @SideOnly(Side.CLIENT) public class AuctionTooltipHandler { /** - * Add auction information to an item's tooltip if we're in an auction GUI. + * Handle ItemTooltipEvent - append auction info to the end of the tooltip. */ - public static void addAuctionInfo(ItemStack stack, List tooltip) { - if (stack == null || tooltip == null) return; + public static void onItemTooltip(ItemTooltipEvent event) { + if (event.itemStack == null) return; Minecraft mc = Minecraft.getMinecraft(); if (mc == null || mc.currentScreen == null) return; GuiScreen screen = mc.currentScreen; - // Check if we're in an auction listing GUI if (screen instanceof GuiAuctionListing) { - addListingTooltip((GuiAuctionListing) screen, stack, tooltip); - } - // Check if we're in the trades GUI - else if (screen instanceof GuiAuctionTrades) { - addTradesTooltip((GuiAuctionTrades) screen, stack, tooltip); - } - // Check if we're in the sell GUI - else if (screen instanceof GuiAuctionSell) { - addSellTooltip((GuiAuctionSell) screen, stack, tooltip); + addListingTooltip((GuiAuctionListing) screen, event.itemStack, event.toolTip); + } else if (screen instanceof GuiAuctionTrades) { + addTradesTooltip((GuiAuctionTrades) screen, event.itemStack, event.toolTip); + } else if (screen instanceof GuiAuctionSell) { + addSellTooltip((GuiAuctionSell) screen, event.itemStack, event.toolTip); } } /** - * Add auction info when viewing the main listings page. + * Find the slot in the container whose getStack() returns the exact same ItemStack reference. + * This avoids the duplicate item bug where ItemStack.areItemStacksEqual matches the wrong listing. */ + private static Slot findHoveredSlot(GuiContainer gui, ItemStack stack) { + if (stack == null) return null; + for (Object obj : gui.inventorySlots.inventorySlots) { + Slot slot = (Slot) obj; + if (slot.getHasStack() && slot.getStack() == stack) { + return slot; + } + } + return null; + } + + // ========== Listing Page ========== + private static void addListingTooltip(GuiAuctionListing gui, ItemStack stack, List tooltip) { - ContainerAuctionListing container = (ContainerAuctionListing) gui.inventorySlots; + Slot slot = findHoveredSlot(gui, stack); + if (slot == null || !(slot instanceof SlotAuctionDisplay)) return; + + ContainerAuctionListing container = gui.getListingContainer(); if (container == null) return; - // Find the listing that matches this ItemStack - AuctionListing listing = container.getListingForItem(stack); + int displayIndex = slot.getSlotIndex(); + AuctionListing listing = container.getListingAt(displayIndex); if (listing == null) return; tooltip.add(""); @@ -90,149 +109,153 @@ public class AuctionTooltipHandler { tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); } - /** - * Add auction info when viewing the trades page. - */ + // ========== Trades Page ========== + private static void addTradesTooltip(GuiAuctionTrades gui, ItemStack stack, List tooltip) { - ContainerAuctionTrades container = (ContainerAuctionTrades) gui.inventorySlots; + Slot slot = findHoveredSlot(gui, stack); + if (slot == null || !(slot instanceof SlotAuctionDisplay)) return; + + ContainerAuctionTrades container = gui.getTradesContainer(); if (container == null) return; - // Check if it's an item I'm selling - AuctionListing sellingListing = container.getSellingListingForItem(stack); - if (sellingListing != null) { - tooltip.add(""); - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.selling")); + int displayIndex = slot.getSlotIndex(); - if (sellingListing.hasBids()) { - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(sellingListing.currentBid))); - tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), sellingListing.bidCount)); - } else { - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); + // Check listings first (selling or bidding) + AuctionListing listing = container.getListingAt(displayIndex); + if (listing != null) { + if (container.isSellingAt(displayIndex)) { + addSellingTooltip(listing, tooltip); + } else if (container.isBiddingAt(displayIndex)) { + addBiddingTooltip(listing, tooltip); } - - long timeRemaining = sellingListing.getTimeRemaining(); - String timeText = StatCollector.translateToLocal("auction.timeLeft") - .replace("%s", formatTimeRemaining(timeRemaining)); - tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); - - tooltip.add(""); - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel")); return; } - // Check if it's an item I'm bidding on - AuctionListing biddingListing = container.getBiddingListingForItem(stack); - if (biddingListing != null) { - tooltip.add(""); - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding")); - - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(biddingListing.currentBid))); - tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), biddingListing.bidCount)); - - long timeRemaining = biddingListing.getTimeRemaining(); - String timeText = StatCollector.translateToLocal("auction.timeLeft") - .replace("%s", formatTimeRemaining(timeRemaining)); - tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); - - tooltip.add(""); - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid")); - return; - } - - // Check if it's a claim - AuctionClaim claim = container.getClaimForItem(stack); + // Check claims + AuctionClaim claim = container.getClaimAt(displayIndex); if (claim != null) { - tooltip.add(""); - - // Title based on claim type - switch (claim.type) { - case ITEM: - if (claim.isReturned) { - // Expired - returned to seller - tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expired")); - } else { - // Won item - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.won")); - } - break; - case CURRENCY: - // Sold - money to claim - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.sold")); - if (claim.itemName != null && !claim.itemName.isEmpty()) { - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem") - .replace("%s", EnumChatFormatting.WHITE + claim.itemName)); - } - if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer") - .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); - } - break; - case REFUND: - // Outbid - refund available - tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid")); - if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy") - .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); - } - break; - } - - // Amount for currency claims (including REFUND) - if (claim.type.isCurrency()) { - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency))); - } - - // Expiration - int expirationDays = AuctionClientConfig.getClaimExpirationDays(); - long daysLeft = claim.getDaysUntilExpiration(expirationDays); - if (daysLeft <= 1) { - tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon")); - } else { - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires") - .replace("%s", "" + daysLeft)); - } - - tooltip.add(""); - - // Show rebid option for REFUND claims with item (active auction) - if (claim.type == EnumClaimType.REFUND && claim.item != null) { - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid")); - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund")); - } else { - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); - } + addClaimTooltip(claim, tooltip); } } - /** - * Add sell page tooltip info. - */ + private static void addSellingTooltip(AuctionListing listing, List tooltip) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.selling")); + + if (listing.hasBids()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); + } else { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); + } + + long timeRemaining = listing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", formatTimeRemaining(timeRemaining)); + tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel")); + } + + private static void addBiddingTooltip(AuctionListing listing, List tooltip) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding")); + + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); + + long timeRemaining = listing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", formatTimeRemaining(timeRemaining)); + tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid")); + } + + private static void addClaimTooltip(AuctionClaim claim, List tooltip) { + tooltip.add(""); + + switch (claim.type) { + case ITEM: + if (claim.isReturned) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expired")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.won")); + } + break; + case CURRENCY: + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.sold")); + if (claim.itemName != null && !claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem") + .replace("%s", EnumChatFormatting.WHITE + claim.itemName)); + } + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } + break; + case REFUND: + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid")); + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } + break; + } + + // Amount for currency claims + if (claim.type.isCurrency()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency))); + } + + // Expiration + int expirationDays = AuctionClientConfig.getClaimExpirationDays(); + long daysLeft = claim.getDaysUntilExpiration(expirationDays); + if (daysLeft <= 1) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon")); + } else { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires") + .replace("%s", "" + daysLeft)); + } + + tooltip.add(""); + + // Rebid option for REFUND claims with item + if (claim.type == EnumClaimType.REFUND && claim.item != null) { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + } + } + + // ========== Sell Page ========== + private static void addSellTooltip(GuiAuctionSell gui, ItemStack stack, List tooltip) { + Slot slot = findHoveredSlot(gui, stack); + if (slot == null) return; + ContainerAuctionSell container = (ContainerAuctionSell) gui.inventorySlots; if (container == null) return; - // Check if this is the sell slot item - ItemStack sellItem = container.getItemToSell(); - if (sellItem != null && ItemStack.areItemStacksEqual(stack, sellItem)) { - // This is the item in the sell slot + if (container.isSellSlot(slot.slotNumber)) { tooltip.add(""); tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); - return; + } else if (container.isPlayerInventorySlot(slot.slotNumber)) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); } - - // This is an inventory item - show add controls - tooltip.add(""); - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); } - /** - * Format currency with commas. - */ + // ========== Formatting Utilities ========== + private static String formatCurrency(long amount) { if (amount < 1000) { return amount + " " + AuctionClientConfig.getCurrencyName(); @@ -250,9 +273,6 @@ public class AuctionTooltipHandler { return sb.toString() + " " + AuctionClientConfig.getCurrencyName(); } - /** - * Format time remaining. - */ private static String formatTimeRemaining(long milliseconds) { if (milliseconds <= 0) { return StatCollector.translateToLocal("auction.ended"); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java index 7efdbe9c9..c34223550 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java @@ -112,16 +112,16 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { mc.renderEngine.bindTexture(AUCTION_BACKGROUND); drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + + drawAuctionContent(partialTicks, mouseX, mouseY); + RenderHelper.enableGUIStandardItemLighting(); GL11.glEnable(GL12.GL_RESCALE_NORMAL); GL11.glEnable(GL11.GL_COLOR_MATERIAL); - GL11.glEnable(GL11.GL_LIGHTING); - - drawAuctionContent(partialTicks, mouseX, mouseY); - GL11.glEnable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_DEPTH_TEST); - RenderHelper.enableStandardItemLighting(); super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); } @@ -167,12 +167,12 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { Gui.drawRect(x + 1, y + 1, x + 17, y + 17, color); GL11.glDisable(GL11.GL_BLEND); GL11.glEnable(GL11.GL_DEPTH_TEST); - GL11.glEnable(GL11.GL_LIGHTING); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); } /** Draw dark overlay for unavailable slots */ protected void drawDarkenedOverlay(int x, int y) { - drawColoredOverlay(x, y, 0x80000000); + drawColoredOverlay(x, y, 0xC0000000); } /** Draw icon texture on slot (16x16) */ diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java index 21b0f6f01..f52d5fa63 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java @@ -38,10 +38,10 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { private static final int TOTAL_SLOTS = COLS * ROWS; // Slot tint colors (ARGB) - private static final int TINT_BLUE = 0x303060FF; // Active listing/bid (selling) - private static final int TINT_GREEN = 0x3030FF30; // Sold (currency claim) or Won (item claim) - private static final int TINT_YELLOW = 0x30FFFF30; // Outbid (refund claim) - private static final int TINT_RED = 0x30FF3030; // Expired/Returned (item returned to seller) + private static final int TINT_BLUE = 0x605080FF; // Active listing/bid (selling) + private static final int TINT_GREEN = 0x6050FF50; // Sold (currency claim) or Won (item claim) + private static final int TINT_YELLOW = 0x60FFFF50; // Outbid (refund claim) + private static final int TINT_RED = 0x60FF5050; // Expired/Returned (item returned to seller) private final ContainerAuctionTrades tradesContainer; private int maxTradeSlots; 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 b0a1c6c32..55f88ac8c 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -3,6 +3,7 @@ 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.inventory.Slot; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; @@ -332,6 +333,22 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { return sb.toString(); } + @Override + protected void handleMouseClick(Slot slot, int slotId, int mouseButton, int modifier) { + // Client-side balance prediction: immediately subtract currency cost on click + // so the displayed balance updates instantly without waiting for server response. + if (slotId >= 0 && slotId < 18 && mouseButton == 0) { + long cost = currencyCost[slotId]; + if (cost > 0 && playerBalance >= cost) { + ItemStack sold = role.inventorySold.items.get(slotId); + if (sold != null && container.canBuy(slotId, player)) { + playerBalance -= cost; + } + } + } + super.handleMouseClick(slot, slotId, mouseButton, modifier); + } + @Override public void save() { } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java index 3f3ca9223..e726239b1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java @@ -367,6 +367,13 @@ public class GuiCustomScroll extends GuiScreen { return this; } + public void setSelectable(boolean selectable) { + this.selectable = selectable; + if (!selectable) { + hover = -1; + } + } + public boolean isMouseOver(int x, int y) { return x >= this.guiLeft && x <= this.guiLeft + this.xSize && y >= this.guiTop && y <= this.guiTop + this.ySize; } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScrollCloner.java b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScrollCloner.java index a69d449e7..b147a3af9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScrollCloner.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScrollCloner.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.GuiNpcMobSpawner; import noppes.npcs.controllers.data.Tag; @@ -7,10 +8,10 @@ import noppes.npcs.controllers.data.Tag; import java.util.UUID; public class GuiCustomScrollCloner extends GuiCustomScroll { - private final GuiNpcMobSpawner parent; + private final IClonerGui parent; - public GuiCustomScrollCloner(GuiNpcMobSpawner parent, int id) { - super(parent, id); + public GuiCustomScrollCloner(IClonerGui parent, int id) { + super((GuiScreen) parent, id); this.parent = parent; } @@ -38,24 +39,25 @@ public class GuiCustomScrollCloner extends GuiCustomScroll { text += "..."; } else text = displayString; - if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) { + int itemColor = this.colors.getOrDefault(list.get(i), 0xffffff); + if ((multipleSelection && selectedList.contains(list.get(i))) || (!multipleSelection && selected == i)) { drawVerticalLine(j - 2, k - 4, k + 10, 0xffffffff); drawVerticalLine(j + xSize - 18 + xOffset, k - 4, k + 10, 0xffffffff); drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k - 3, 0xffffffff); drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k + 10, 0xffffffff); - fontRendererObj.drawString(text, j, k, this.colors.getOrDefault(text, 0xffffff)); + fontRendererObj.drawString(text, j, k, itemColor); } else if (i == hover) { fontRendererObj.drawString(text, j, k, 0x00ff00); } else { - fontRendererObj.drawString(text, j, k, this.colors.getOrDefault(text, 0xffffff)); + fontRendererObj.drawString(text, j, k, itemColor); } - if ((this.parent.getShowingClones() == 0 || this.parent.getShowingClones() == 2) && this.parent.tagMap != null) { + if ((parent.getShowingClones() == 0 || parent.getShowingClones() == 2) && parent.getTagMap() != null) { int tagStartX = j + fontRendererObj.getStringWidth(displayString) + 3; if (GuiNpcMobSpawner.displayTags == 0 || GuiNpcMobSpawner.displayTags == 1) { - if (this.parent.tagMap.hasClone(list.get(i))) { - for (UUID tagUUID : this.parent.tagMap.getUUIDsList(list.get(i))) { - Tag tag = this.parent.tags.get(tagUUID); + if (parent.getTagMap().hasClone(list.get(i))) { + for (UUID tagUUID : parent.getTagMap().getUUIDsList(list.get(i))) { + Tag tag = parent.getTags().get(tagUUID); if (tag != null) { if (GuiNpcMobSpawner.displayTags == 1 || !tag.getIsHidden()) { fontRendererObj.drawString("[" + tag.name + "]", tagStartX, k, tag.color); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScrollIcons.java b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScrollIcons.java new file mode 100644 index 000000000..78686dac0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScrollIcons.java @@ -0,0 +1,108 @@ +package noppes.npcs.client.gui.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +public class GuiCustomScrollIcons extends GuiCustomScroll { + public static final int ICON_NONE = 0; + public static final int ICON_TAB = 1; + public static final int ICON_FOLDER = 2; + + private static final ResourceLocation TAB_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/cloner/tab.png"); + private static final ResourceLocation FOLDER_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/cloner/folder.png"); + + private static final int ICON_RENDER_SIZE = 10; + private static final int ICON_PADDING = 14; + + private List iconTypes = new ArrayList<>(); + + public GuiCustomScrollIcons(GuiScreen parent, int id) { + super(parent, id); + } + + public void setListWithIcons(List names, List icons) { + this.setUnsortedList(names); + this.iconTypes = new ArrayList<>(icons); + } + + @Override + protected void drawItems() { + Minecraft mc = Minecraft.getMinecraft(); + for (int i = 0; i < list.size(); i++) { + int j = 4; + int k = (14 * i + 4) - scrollY; + if (k >= 4 && k + 12 < ySize) { + int xOffset = scrollHeight < ySize - 8 ? 0 : 10; + + int iconType = (i < iconTypes.size()) ? iconTypes.get(i) : ICON_NONE; + int textX = j; + if (iconType != ICON_NONE) { + textX = j + ICON_PADDING; + } + + // Draw icon texture + if (iconType == ICON_FOLDER || iconType == ICON_TAB) { + ResourceLocation tex = (iconType == ICON_FOLDER) ? FOLDER_TEXTURE : TAB_TEXTURE; + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.getTextureManager().bindTexture(tex); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + drawFullTexture(j, k - 1, ICON_RENDER_SIZE, ICON_RENDER_SIZE); + GL11.glDisable(GL11.GL_BLEND); + } + + // Draw text + String displayString = StatCollector.translateToLocal(list.get(i)); + String text = ""; + float maxWidth = (xSize + xOffset - 8 - (iconType != ICON_NONE ? ICON_PADDING : 0)) * 0.8f; + if (fontRendererObj.getStringWidth(displayString) > maxWidth) { + for (int h = 0; h < displayString.length(); h++) { + char c = displayString.charAt(h); + text += c; + if (fontRendererObj.getStringWidth(text) > maxWidth) + break; + } + if (displayString.length() > text.length()) + text += "..."; + } else { + text = displayString; + } + + int itemColor = this.colors.getOrDefault(list.get(i), 0xffffff); + + if ((multipleSelection && selectedList.contains(list.get(i))) || (!multipleSelection && selected == i)) { + drawVerticalLine(j - 2, k - 4, k + 10, 0xffffffff); + drawVerticalLine(j + xSize - 18 + xOffset, k - 4, k + 10, 0xffffffff); + drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k - 3, 0xffffffff); + drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k + 10, 0xffffffff); + fontRendererObj.drawString(text, textX, k, itemColor); + } else if (i == hover) { + fontRendererObj.drawString(text, textX, k, 0x00ff00); + } else { + fontRendererObj.drawString(text, textX, k, itemColor); + } + } + } + } + + /** + * Draw a full texture (UV 0,0 to 1,1) scaled to the given width/height. + * Works with any texture size (not just 256x256). + */ + private void drawFullTexture(int x, int y, int w, int h) { + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.addVertexWithUV(x, y + h, 0, 0, 1); + tessellator.addVertexWithUV(x + w, y + h, 0, 1, 1); + tessellator.addVertexWithUV(x + w, y, 0, 1, 0); + tessellator.addVertexWithUV(x, y, 0, 0, 0); + tessellator.draw(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiDirectory.java b/src/main/java/noppes/npcs/client/gui/util/GuiDirectory.java new file mode 100644 index 000000000..750c60668 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiDirectory.java @@ -0,0 +1,206 @@ +package noppes.npcs.client.gui.util; + +/** + * Abstract base class for fullscreen directory-browsing GUIs. + * Provides a configurable three-panel layout (left nav, center content, right actions) + * with percentage-based sizing, panel backgrounds/borders, and an optional resizable divider. + * + * Subclasses override abstract hooks to populate each panel region. + * Designed for reuse across Cloner, Quest Manager, Dialog Manager, etc. + */ +public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScrollListener, ISubGuiListener { + + // ===== LAYOUT CONFIG (subclasses may override in constructor) ===== + protected float leftPanelPercent = 0.20f; + protected float rightPanelPercent = 0.10f; + protected int minLeftPanelW = 80; + protected int minRightPanelW = 40; + protected int minCenterW = 100; + + // ===== LAYOUT CONSTANTS ===== + protected int pad = 10; + protected int topBarH = 24; + protected int gap = 4; + protected int btnH = 20; + + // ===== PANEL COLORS ===== + protected int panelBg = 0xC0101010; + protected int panelBorder = 0xFF333333; + protected int topBarBg = 0xC0181818; + + // ===== COMPUTED LAYOUT (populated by computeLayout) ===== + protected int originX, originY, usableW, usableH; + protected int leftPanelW, rightPanelW; + protected int contentX, contentY, contentW, contentH; + protected int rightX; + + // ===== OPTIONAL RESIZABLE DIVIDER (between left and center) ===== + protected boolean enableDivider = false; + protected int dividerWidth = 5; + protected int dividerLineHeight = 20; + protected int minDividerPanelW = 50; + private int dividerOffset; + private boolean isDragging = false; + private int dragStartX; + + // ===== CONSTRUCTOR ===== + public GuiDirectory() { + super(); + closeOnEsc = true; + drawDefaultBackground = true; + } + + // ===== LAYOUT COMPUTATION ===== + protected void computeLayout() { + originX = pad; + originY = pad; + usableW = width - 2 * pad; + usableH = height - 2 * pad; + + leftPanelW = Math.max(minLeftPanelW, (int)(usableW * leftPanelPercent)); + rightPanelW = Math.max(minRightPanelW, (int)(usableW * rightPanelPercent)); + + if (rightPanelPercent <= 0) { + rightPanelW = 0; + } + + int gapCount = rightPanelW > 0 ? 3 : 2; + contentW = usableW - leftPanelW - rightPanelW - gapCount * gap; + if (contentW < minCenterW) { + contentW = minCenterW; + } + + contentX = originX + leftPanelW + gap; + contentY = originY + topBarH + gap; + contentH = usableH - topBarH - gap; + rightX = contentX + contentW + gap; + + if (enableDivider && dividerOffset == 0) { + dividerOffset = leftPanelW; + } + } + + // ===== INIT GUI ===== + @Override + public void initGui() { + super.initGui(); + computeLayout(); + initTopBar(originY + 2); + initLeftPanel(); + initCenterPanel(); + if (rightPanelW > 0) { + initRightPanel(contentY + 4); + } + } + + // ===== DRAWING ===== + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + drawTopBar(); + drawPanels(); + if (enableDivider) { + drawDivider(mouseX, mouseY); + } + super.drawScreen(mouseX, mouseY, partialTicks); + drawOverlay(mouseX, mouseY, partialTicks); + } + + protected void drawTopBar() { + GuiUtil.drawRectD(originX, originY, originX + usableW, originY + topBarH, topBarBg); + } + + protected void drawPanels() { + // Left nav panel border + GuiUtil.drawRectD(originX - 1, contentY - 1, originX + leftPanelW + 1, originY + usableH + 1, panelBorder); + + // Right action panel border (if present) + if (rightPanelW > 0) { + GuiUtil.drawRectD(rightX - 1, contentY - 1, rightX + rightPanelW + 1, originY + usableH + 1, panelBorder); + } + } + + protected void drawDivider(int mouseX, int mouseY) { + if (!enableDivider) return; + int divX = originX + dividerOffset; + int regionTop = contentY; + int regionHeight = contentH; + int handleTop = regionTop + (regionHeight - dividerLineHeight) / 2; + drawRect(divX + 1, handleTop, divX + dividerWidth - 1, handleTop + dividerLineHeight, 0xFF707070); + } + + /** Override for extra drawing after super.drawScreen (e.g. status text) */ + protected void drawOverlay(int mouseX, int mouseY, float partialTicks) { + } + + // ===== DIVIDER MOUSE HANDLING ===== + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + if (enableDivider && !hasSubGui()) { + int divX = originX + dividerOffset; + int regionTop = contentY; + int handleTop = regionTop + (contentH - dividerLineHeight) / 2; + int handleBottom = handleTop + dividerLineHeight; + if (mouseX >= divX && mouseX <= divX + dividerWidth && + mouseY >= handleTop && mouseY <= handleBottom) { + isDragging = true; + resizingActive = true; + dragStartX = mouseX; + return; + } + } + super.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + public void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { + if (isDragging) { + int dx = mouseX - dragStartX; + dragStartX = mouseX; + dividerOffset += dx; + + int maxOffset = (usableW - (rightPanelW > 0 ? rightPanelW + gap : 0)) - dividerWidth - minDividerPanelW; + dividerOffset = Math.max(minDividerPanelW, Math.min(dividerOffset, maxOffset)); + + onDividerMoved(dividerOffset); + return; + } + super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); + } + + @Override + protected void mouseMovedOrUp(int mouseX, int mouseY, int state) { + if (isDragging) { + isDragging = false; + resizingActive = false; + return; + } + super.mouseMovedOrUp(mouseX, mouseY, state); + } + + /** Called when divider is dragged. Override to update scroll sizes. */ + protected void onDividerMoved(int newOffset) { + } + + // Accessors for divider offset + protected int getDividerOffset() { + return dividerOffset; + } + + protected void setDividerOffset(int offset) { + this.dividerOffset = offset; + } + + // ===== ABSTRACT HOOKS (subclass must implement) ===== + + /** Populate top bar buttons. topBtnY is the y-coordinate for the button row. */ + protected abstract void initTopBar(int topBtnY); + + /** Populate the left navigation panel (scrolls, search fields, buttons). */ + protected abstract void initLeftPanel(); + + /** Populate the center content panel (main scroll list). */ + protected abstract void initCenterPanel(); + + /** Populate the right action panel. startY is the first button y-coordinate. */ + protected abstract void initRightPanel(int startY); +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiMenuSideButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiMenuSideButton.java index 0aa6c7d3d..94398bb60 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiMenuSideButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiMenuSideButton.java @@ -15,6 +15,7 @@ public class GuiMenuSideButton extends GuiNpcButton { public boolean active; public boolean rightSided; + public int textColor = -1; public ItemStack renderStack; public ResourceLocation renderResource; public int renderIconPosX = 0; @@ -96,13 +97,15 @@ public class GuiMenuSideButton extends GuiNpcButton { text += "..."; } else text = displayString; - if (active) { - drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, 0xffffa0); - } else if (field_146123_n) { - drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, 0xffffa0); + int color; + if (textColor != -1) { + color = textColor; + } else if (active || field_146123_n) { + color = 0xffffa0; } else { - drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, 0xe0e0e0); + color = 0xe0e0e0; } + drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, color); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/IClonerGui.java b/src/main/java/noppes/npcs/client/gui/util/IClonerGui.java new file mode 100644 index 000000000..db4989b56 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/IClonerGui.java @@ -0,0 +1,13 @@ +package noppes.npcs.client.gui.util; + +import noppes.npcs.controllers.data.Tag; +import noppes.npcs.controllers.data.TagMap; + +import java.util.HashMap; +import java.util.UUID; + +public interface IClonerGui { + int getShowingClones(); + HashMap getTags(); + TagMap getTagMap(); +} diff --git a/src/main/java/noppes/npcs/config/ConfigClient.java b/src/main/java/noppes/npcs/config/ConfigClient.java index c24a2e301..d8c093382 100644 --- a/src/main/java/noppes/npcs/config/ConfigClient.java +++ b/src/main/java/noppes/npcs/config/ConfigClient.java @@ -22,6 +22,9 @@ public class ConfigClient { /** * General Properties **/ + public static Property AllowClientScriptsProperty; + public static boolean AllowClientScripts = true; + public static Property CacheLifeProperty; public static int CacheLife = 10; @@ -185,6 +188,9 @@ public class ConfigClient { // General + AllowClientScriptsProperty = config.get(GENERAL, "Allow Client Scripts", true, "Allow the server to run scripts on the client. If disabled, no server scripts will execute client-side."); + AllowClientScripts = AllowClientScriptsProperty.getBoolean(true); + CacheLifeProperty = config.get(GENERAL, "Cache Life", 10, "How long should downloaded imagery data be saved client side? (In minutes)"); CacheLife = CacheLifeProperty.getInt(10); diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index 807190816..03a690bcd 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -127,15 +127,11 @@ public class ContainerNPCTrader extends ContainerNpcInterface { 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) + // Always sync the purchasing player's data (balance + stock) after trade. + // For shared stocks, consumeStock() syncs stock to other viewers, + // but the purchasing player's balance still needs an explicit sync. 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() + role.syncToPlayer((EntityPlayerMP) entityplayer); } return soldItem; diff --git a/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java b/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java new file mode 100644 index 000000000..5a9d32ed3 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java @@ -0,0 +1,25 @@ +package noppes.npcs.controllers; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigScript; + +/** + * Server-side utility for syncing script configuration to clients. + * + * This class is SERVER-SIDE ONLY. Client reads via ScriptClientConfig. + */ +public class ScriptConfigSync { + + /** + * Write script config to NBT for sending to client. + * Called on server side when building login packet. + */ + public static NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("ScriptingEnabled", ConfigScript.ScriptingEnabled); + compound.setBoolean("RunLoadedScriptsFirst", ConfigScript.RunLoadedScriptsFirst); + compound.setBoolean("GlobalPlayerScripts", ConfigScript.GlobalPlayerScripts); + compound.setBoolean("GlobalForgeScripts", ConfigScript.GlobalForgeScripts); + compound.setBoolean("GlobalNPCScripts", ConfigScript.GlobalNPCScripts); + return compound; + } +} diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index 60799c767..efdbc5b2d 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -3,6 +3,7 @@ package noppes.npcs.controllers; import cpw.mods.fml.common.eventhandler.Event; import jdk.nashorn.api.scripting.ScriptObjectMirror; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; @@ -104,7 +105,7 @@ public class ScriptContainer implements IScriptUnit { if (!this.evaluated) { // build includes first depending on config setting StringBuilder sb = new StringBuilder(); - if (ConfigScript.RunLoadedScriptsFirst) { + if (CustomNpcs.proxy.isRunLoadedScriptsFirst()) { this.appendExternalScripts(sb); } @@ -114,7 +115,7 @@ public class ScriptContainer implements IScriptUnit { } // build includes last if not built first - if (!ConfigScript.RunLoadedScriptsFirst) { + if (!CustomNpcs.proxy.isRunLoadedScriptsFirst()) { this.appendExternalScripts(sb); } this.fullscript = sb.toString(); @@ -158,14 +159,14 @@ public class ScriptContainer implements IScriptUnit { } public void run(EnumScriptType type, Event event) { - if (!ConfigScript.ScriptingEnabled) + if (!CustomNpcs.proxy.isScriptingEnabled()) return; this.run((String) type.function, (Object) event); } public void run(String type, Object event) { - if (!ConfigScript.ScriptingEnabled || errored || !hasCode() || unknownFunctions.contains(type)) + if (!CustomNpcs.proxy.isScriptingEnabled() || errored || !hasCode() || unknownFunctions.contains(type)) return; this.setEngine(handler.getLanguage()); diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index af2153cff..546b3680d 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -73,7 +73,7 @@ public class ScriptController { public ScriptController() { Instance = this; manager = new ScriptEngineManager(); - if (!ConfigScript.ScriptingEnabled) + if (!CustomNpcs.proxy.isScriptingEnabled()) return; LogWriter.info("Script Engines Available:"); diff --git a/src/main/java/noppes/npcs/controllers/ServerCloneController.java b/src/main/java/noppes/npcs/controllers/ServerCloneController.java index ea61a0112..32bd0e15c 100644 --- a/src/main/java/noppes/npcs/controllers/ServerCloneController.java +++ b/src/main/java/noppes/npcs/controllers/ServerCloneController.java @@ -15,6 +15,7 @@ import noppes.npcs.api.IPos; import noppes.npcs.api.IWorld; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.handler.ICloneHandler; +import noppes.npcs.controllers.data.CloneFolder; import noppes.npcs.controllers.data.TagMap; import noppes.npcs.scripted.CustomNPCsException; import noppes.npcs.scripted.NpcAPI; @@ -27,6 +28,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -34,8 +36,11 @@ import java.util.UUID; public class ServerCloneController implements ICloneHandler { public static ServerCloneController Instance; + protected Map folders = new LinkedHashMap<>(); + public ServerCloneController() { loadClones(); + loadFolders(); } private void loadClones() { @@ -103,6 +108,8 @@ public class ServerCloneController implements ICloneHandler { return clones; } + // ==================== Tab-Based Clone Operations ==================== + public NBTTagCompound getCloneData(ICommandSender player, String name, int tab) { File file = new File(new File(getDir(), tab + ""), name + ".json"); if (!file.exists()) { @@ -194,6 +201,250 @@ public class ServerCloneController implements ICloneHandler { return name; } + // ==================== Folder Registry ==================== + + public void loadFolders() { + folders.clear(); + File dir = getDir(); + File[] files = dir.listFiles(); + if (files != null) { + List folderDirs = new ArrayList<>(); + for (File f : files) { + if (!f.isDirectory()) continue; + String name = f.getName(); + if (name.startsWith("___")) continue; + try { + Integer.parseInt(name); + continue; + } catch (NumberFormatException ignored) { + } + folderDirs.add(f); + } + folderDirs.sort(Comparator.comparing(File::getName)); + for (File f : folderDirs) { + CloneFolder folder = new CloneFolder(f.getName()); + folder.createdDate = f.lastModified(); + folders.put(f.getName(), folder); + } + } + } + + public List getFolderList() { + return new ArrayList<>(folders.values()); + } + + public List getFolderNames() { + return new ArrayList<>(folders.keySet()); + } + + public boolean hasFolder(String name) { + return folders.containsKey(name); + } + + // ==================== Folder CRUD ==================== + + public CloneFolder createFolder(String name) { + if (!CloneFolder.isValidName(name) || folders.containsKey(name)) { + return null; + } + CloneFolder folder = new CloneFolder(name); + File dir = getFolderDir(name); + if (!dir.exists()) { + dir.mkdir(); + } + folders.put(name, folder); + return folder; + } + + public boolean renameFolder(String oldName, String newName) { + if (!folders.containsKey(oldName) || !CloneFolder.isValidName(newName) || folders.containsKey(newName)) { + return false; + } + CloneFolder folder = folders.get(oldName); + File oldDir = getFolderDir(oldName); + File newDir = new File(getDir(), newName); + if (!oldDir.renameTo(newDir)) { + return false; + } + folders.remove(oldName); + folder.name = newName; + folders.put(newName, folder); + return true; + } + + public boolean deleteFolder(String name) { + if (!folders.containsKey(name)) { + return false; + } + List clones = getClones(name); + if (!clones.isEmpty()) { + return false; + } + File dir = getFolderDir(name); + if (dir.exists()) { + File tagMapFile = new File(dir, "___tagmap.dat"); + if (tagMapFile.exists()) tagMapFile.delete(); + File tagMapOld = new File(dir, "___tagmap.dat_old"); + if (tagMapOld.exists()) tagMapOld.delete(); + File tagMapNew = new File(dir, "___tagmap.dat_new"); + if (tagMapNew.exists()) tagMapNew.delete(); + dir.delete(); + } + folders.remove(name); + return true; + } + + // ==================== Folder-Based Clone Operations ==================== + + public File getFolderDir(String folderName) { + return new File(getDir(), folderName); + } + + public List getClones(String folderName) { + List list = new ArrayList<>(); + File dir = getFolderDir(folderName); + if (!dir.exists() || !dir.isDirectory()) + return list; + for (String file : dir.list()) { + if (file.endsWith(".json")) + list.add(file.substring(0, file.length() - 5)); + } + return list; + } + + public List getClonesDate(String folderName) { + List list = new ArrayList<>(); + File dir = getFolderDir(folderName); + if (!dir.exists() || !dir.isDirectory()) + return list; + File[] files = dir.listFiles(); + if (files == null) return list; + Arrays.sort(files, new Comparator() { + public int compare(File f1, File f2) { + return Long.compare(f1.lastModified(), f2.lastModified()); + } + }); + for (File file : files) { + String fileName = file.getName(); + if (fileName.endsWith(".json")) + list.add(fileName.substring(0, fileName.length() - 5)); + } + return list; + } + + public NBTTagCompound getCloneData(ICommandSender player, String name, String folderName) { + File file = new File(getFolderDir(folderName), name + ".json"); + if (!file.exists()) { + if (player != null) + player.addChatMessage(new ChatComponentText("Could not find clone file")); + return null; + } + try { + return NBTJsonUtil.LoadFile(file); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + if (player != null) + player.addChatMessage(new ChatComponentText(e.getMessage())); + } + return null; + } + + public void saveClone(String folderName, String name, NBTTagCompound compound) { + try { + File dir = getFolderDir(folderName); + if (!dir.exists()) dir.mkdir(); + String filename = name + ".json"; + File file = new File(dir, filename + "_new"); + File file2 = new File(dir, filename); + NBTJsonUtil.SaveFile(file, compound); + addToTagMap(compound, name, folderName); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + } catch (Exception e) { + LogWriter.except(e); + } + } + + public boolean removeClone(String name, String folderName) { + File file = new File(getFolderDir(folderName), name + ".json"); + if (!file.exists()) + return false; + file.delete(); + removeFromTagMap(name, folderName); + return true; + } + + public String addClone(NBTTagCompound nbttagcompound, String name, String folderName) { + cleanTags(nbttagcompound); + saveClone(folderName, name, nbttagcompound); + return name; + } + + public String addClone(NBTTagCompound nbttagcompound, String name, String folderName, NBTTagCompound tempTags) { + cleanTagList(nbttagcompound, tempTags); + cleanTags(nbttagcompound); + saveClone(folderName, name, nbttagcompound); + return name; + } + + // ==================== Move Operations ==================== + + public boolean moveClone(String cloneName, int fromTab, String toFolder) { + NBTTagCompound data = getCloneData(null, cloneName, fromTab); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromTab(cloneName, fromTab); + + saveClone(toFolder, cloneName, data); + setTagUUIDsForFolder(cloneName, toFolder, tagUUIDs); + + removeClone(cloneName, fromTab); + return true; + } + + public boolean moveClone(String cloneName, String fromFolder, int toTab) { + NBTTagCompound data = getCloneData(null, cloneName, fromFolder); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromFolder(cloneName, fromFolder); + + saveClone(toTab, cloneName, data); + setTagUUIDsForTab(cloneName, toTab, tagUUIDs); + + removeClone(cloneName, fromFolder); + return true; + } + + public boolean moveClone(String cloneName, String fromFolder, String toFolder) { + NBTTagCompound data = getCloneData(null, cloneName, fromFolder); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromFolder(cloneName, fromFolder); + + saveClone(toFolder, cloneName, data); + setTagUUIDsForFolder(cloneName, toFolder, tagUUIDs); + + removeClone(cloneName, fromFolder); + return true; + } + + public boolean moveClone(String cloneName, int fromTab, int toTab) { + NBTTagCompound data = getCloneData(null, cloneName, fromTab); + if (data == null) return false; + + HashSet tagUUIDs = getTagUUIDsFromTab(cloneName, fromTab); + + saveClone(toTab, cloneName, data); + setTagUUIDsForTab(cloneName, toTab, tagUUIDs); + + removeClone(cloneName, fromTab); + return true; + } + + // ==================== Tag Utilities ==================== + public NBTTagCompound cleanTagList(NBTTagCompound nbttagcompound, NBTTagCompound tempTags) { HashSet tagUUIDs = new HashSet(); if (nbttagcompound.hasKey("TagUUIDs")) { @@ -243,6 +494,25 @@ public class ServerCloneController implements ICloneHandler { return true; } + public boolean addToTagMap(NBTTagCompound nbttagcompound, String name, String folderName) { + HashSet tagUUIDs = new HashSet<>(); + if (nbttagcompound.hasKey("TagUUIDs")) { + NBTTagList nbtTagList = nbttagcompound.getTagList("TagUUIDs", 8); + for (int i = 0; i < nbtTagList.tagCount(); i++) { + tagUUIDs.add(UUID.fromString(nbtTagList.getStringTagAt(i))); + } + } + + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + if (!tagUUIDs.isEmpty()) { + tagMap.putClone(name, tagUUIDs); + } else { + tagMap.removeClone(name); + } + ServerTagMapController.Instance.saveTagMap(tagMap); + return true; + } + public boolean removeFromTagMap(String name, int tab) { TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); if (tagMap.removeClone(name)) { @@ -252,6 +522,41 @@ public class ServerCloneController implements ICloneHandler { return false; } + public boolean removeFromTagMap(String name, String folderName) { + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + if (tagMap.removeClone(name)) { + ServerTagMapController.Instance.saveTagMap(tagMap); + return true; + } + return false; + } + + protected HashSet getTagUUIDsFromTab(String cloneName, int tab) { + TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); + HashSet uuids = tagMap.getUUIDs(cloneName); + return uuids != null ? new HashSet<>(uuids) : new HashSet<>(); + } + + protected HashSet getTagUUIDsFromFolder(String cloneName, String folderName) { + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + HashSet uuids = tagMap.getUUIDs(cloneName); + return uuids != null ? new HashSet<>(uuids) : new HashSet<>(); + } + + protected void setTagUUIDsForTab(String cloneName, int tab, HashSet tagUUIDs) { + if (tagUUIDs == null || tagUUIDs.isEmpty()) return; + TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab); + tagMap.putClone(cloneName, tagUUIDs); + ServerTagMapController.Instance.saveTagMap(tagMap); + } + + protected void setTagUUIDsForFolder(String cloneName, String folderName, HashSet tagUUIDs) { + if (tagUUIDs == null || tagUUIDs.isEmpty()) return; + TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName); + tagMap.putClone(cloneName, tagUUIDs); + ServerTagMapController.Instance.saveTagMap(tagMap); + } + public void cleanTags(NBTTagCompound nbttagcompound) { if (nbttagcompound.hasKey("ItemGiverId")) nbttagcompound.setInteger("ItemGiverId", 0); @@ -288,6 +593,8 @@ public class ServerCloneController implements ICloneHandler { } } + // ==================== API Methods (ICloneHandler) ==================== + public IEntity spawn(double x, double y, double z, int tab, String name, IWorld world, boolean ignoreProtection) { NBTTagCompound compound = this.getCloneData((ICommandSender) null, name, tab); if (compound == null) { @@ -327,7 +634,7 @@ public class ServerCloneController implements ICloneHandler { for (File file : dir.listFiles()) { if (file.getName().endsWith(".json")) { NBTTagCompound compound = NBTJsonUtil.LoadFile(file); - Instance.cleanTags(compound); + cleanTags(compound); Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld()); arrayList.add(entity == null ? null : NpcAPI.Instance().getIEntity(entity)); } @@ -343,7 +650,7 @@ public class ServerCloneController implements ICloneHandler { if (compound == null) { throw new CustomNPCsException("Unknown clone tab:" + tab + " name:" + name, new Object[0]); } else { - Instance.cleanTags(compound); + cleanTags(compound); Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld()); return entity == null ? null : NpcAPI.Instance().getIEntity(entity); } @@ -354,7 +661,6 @@ public class ServerCloneController implements ICloneHandler { return compound != null; } - public void set(int tab, String name, IEntity entity) { NBTTagCompound compound = new NBTTagCompound(); if (!entity.getMCEntity().writeMountToNBT(compound)) @@ -367,4 +673,85 @@ public class ServerCloneController implements ICloneHandler { public void remove(int tab, String name) { this.removeClone(name, tab); } + + // --- Folder API Methods --- + + public String[] getFolders() { + return getFolderNames().toArray(new String[0]); + } + + public IEntity spawn(double x, double y, double z, String folderName, String name, IWorld world, boolean ignoreProtection) { + NBTTagCompound compound = getCloneData(null, name, folderName); + if (compound == null) { + throw new CustomNPCsException("Unknown clone folder:" + folderName + " name:" + name, new Object[0]); + } + Entity entity; + if (!ignoreProtection) { + entity = NoppesUtilServer.spawnCloneWithProtection(compound, (int) x, (int) y, (int) z, world.getMCWorld()); + } else { + entity = NoppesUtilServer.spawnClone(compound, (int) x, (int) y, (int) z, world.getMCWorld()); + } + return entity == null ? null : NpcAPI.Instance().getIEntity(entity); + } + + public IEntity spawn(IPos pos, String folderName, String name, IWorld world, boolean ignoreProtection) { + return this.spawn(pos.getX(), pos.getY(), pos.getZ(), folderName, name, world, ignoreProtection); + } + + public IEntity spawn(double x, double y, double z, String folderName, String name, IWorld world) { + return spawn(x, y, z, folderName, name, world, true); + } + + public IEntity spawn(IPos pos, String folderName, String name, IWorld world) { + return this.spawn(pos.getX(), pos.getY(), pos.getZ(), folderName, name, world); + } + + public IEntity[] getFolder(String folderName, IWorld world) { + File dir = getFolderDir(folderName); + if (!dir.exists() || !dir.isDirectory() || dir.listFiles() == null) { + return new IEntity[]{}; + } + + ArrayList arrayList = new ArrayList<>(); + try { + for (File file : dir.listFiles()) { + if (file.getName().endsWith(".json")) { + NBTTagCompound compound = NBTJsonUtil.LoadFile(file); + cleanTags(compound); + Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld()); + arrayList.add(entity == null ? null : NpcAPI.Instance().getIEntity(entity)); + } + } + } catch (Exception ignored) { + } + return arrayList.toArray(new IEntity[]{}); + } + + public IEntity get(String folderName, String name, IWorld world) { + NBTTagCompound compound = getCloneData(null, name, folderName); + if (compound == null) { + throw new CustomNPCsException("Unknown clone folder:" + folderName + " name:" + name, new Object[0]); + } + cleanTags(compound); + Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld()); + return entity == null ? null : NpcAPI.Instance().getIEntity(entity); + } + + public boolean has(String folderName, String name) { + NBTTagCompound compound = getCloneData(null, name, folderName); + return compound != null; + } + + public void set(String folderName, String name, IEntity entity) { + NBTTagCompound compound = new NBTTagCompound(); + if (!entity.getMCEntity().writeMountToNBT(compound)) + throw new CustomNPCsException("Cannot save dead entities", new Object[0]); + + this.cleanTags(compound); + this.saveClone(folderName, name, compound); + } + + public void remove(String folderName, String name) { + this.removeClone(name, folderName); + } } diff --git a/src/main/java/noppes/npcs/controllers/ServerTagMapController.java b/src/main/java/noppes/npcs/controllers/ServerTagMapController.java index 0221481e3..42ca6afeb 100644 --- a/src/main/java/noppes/npcs/controllers/ServerTagMapController.java +++ b/src/main/java/noppes/npcs/controllers/ServerTagMapController.java @@ -35,6 +35,13 @@ public class ServerTagMapController { return dir; } + public File getCloneFolderDir(String folderName) { + File dir = new File(getDir(), folderName); + if (!dir.exists()) + dir.mkdir(); + return dir; + } + public TagMap getTagMap(int tab) { this.tagMap = new TagMap(tab); try { @@ -65,9 +72,33 @@ public class ServerTagMapController { this.tagMap.readNBT(nbtCompound); } + public TagMap getTagMap(String folderName) { + this.tagMap = new TagMap(folderName); + try { + File file = new File(getCloneFolderDir(folderName), "___tagmap.dat"); + if (file.exists()) { + loadTagMapFile(file); + } + } catch (Exception e) { + try { + File file = new File(getCloneFolderDir(folderName), "___tagmap.dat_old"); + if (file.exists()) { + loadTagMapFile(file); + } + } catch (Exception ignored) { + } + } + return this.tagMap; + } + public void saveTagMap(TagMap tagMap) { try { - File saveDir = getCloneTabDir(tagMap.cloneTab); + File saveDir; + if (tagMap.cloneFolder != null) { + saveDir = getCloneFolderDir(tagMap.cloneFolder); + } else { + saveDir = getCloneTabDir(tagMap.cloneTab); + } File file = new File(saveDir, "___tagmap.dat_new"); File file1 = new File(saveDir, "___tagmap.dat_old"); File file2 = new File(saveDir, "___tagmap.dat"); diff --git a/src/main/java/noppes/npcs/controllers/data/CloneFolder.java b/src/main/java/noppes/npcs/controllers/data/CloneFolder.java new file mode 100644 index 000000000..d2d0c7bcf --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/CloneFolder.java @@ -0,0 +1,61 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; + +public class CloneFolder { + public String name; + public long createdDate; + + public CloneFolder() { + this.name = ""; + this.createdDate = System.currentTimeMillis(); + } + + public CloneFolder(String name) { + this.name = name; + this.createdDate = System.currentTimeMillis(); + } + + public void readNBT(NBTTagCompound compound) { + name = compound.getString("Name"); + createdDate = compound.getLong("Created"); + } + + public NBTTagCompound writeNBT(NBTTagCompound compound) { + compound.setString("Name", name); + compound.setLong("Created", createdDate); + return compound; + } + + public static boolean isValidName(String name) { + if (name == null || name.isEmpty()) { + return false; + } + if (name.length() > 32) { + return false; + } + if (!name.equals(name.trim())) { + return false; + } + if (name.equals(".") || name.equals("..")) { + return false; + } + if (name.startsWith("___")) { + return false; + } + try { + Integer.parseInt(name); + return false; + } catch (NumberFormatException ignored) { + } + for (char c : name.toCharArray()) { + if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|') { + return false; + } + if (c < 32) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 74806605f..b112bf470 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -9,13 +9,13 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.world.World; import net.minecraft.world.WorldServer; +import noppes.npcs.CustomNpcs; import noppes.npcs.EventHooks; import noppes.npcs.LogWriter; import noppes.npcs.NBTTags; import noppes.npcs.api.IWorld; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigDebug; -import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; @@ -74,7 +74,7 @@ public class DataScript implements IScriptHandlerPacket { public void readFromNBT(NBTTagCompound compound) { scripts = readScript(compound.getTagList("ScriptsContainers", 10)); this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { + if (scriptLanguage == null || scriptLanguage.isEmpty()) { if (!ScriptController.Instance.languages.isEmpty()) { this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; } else { @@ -209,7 +209,7 @@ public class DataScript implements IScriptHandlerPacket { } public boolean isEnabled() { - return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && ConfigScript.ScriptingEnabled; + return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && CustomNpcs.proxy.isScriptingEnabled(); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index 1c6f09da7..f8c961db7 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -12,8 +12,8 @@ import net.minecraftforge.event.world.ChunkDataEvent; import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.world.ChunkWatchEvent; import net.minecraftforge.event.world.WorldEvent; +import noppes.npcs.CustomNpcs; import noppes.npcs.EventHooks; -import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; @@ -36,7 +36,7 @@ public class ForgeDataScript extends MultiScriptHandler { } public boolean isEnabled() { - return this.enabled && ConfigScript.GlobalForgeScripts && ScriptController.HasStart && this.scripts.size() > 0; + return this.enabled && CustomNpcs.proxy.isGlobalForgeScripts() && ScriptController.HasStart && this.scripts.size() > 0; } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index 805ae7c8d..4654ff68f 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -3,8 +3,8 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; +import noppes.npcs.CustomNpcs; import noppes.npcs.api.entity.ICustomNpc; -import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; @@ -28,7 +28,7 @@ public class GlobalNPCDataScript extends MultiScriptHandler { } public boolean isEnabled() { - return ConfigScript.GlobalNPCScripts && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; + return CustomNpcs.proxy.isGlobalNPCScripts() && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java index d4b4c027b..699b0542d 100644 --- a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -55,7 +55,7 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip * Normalize the language to a valid one if the current language is not available. */ protected void normalizeLanguage() { - if (!ScriptController.Instance.languages.containsKey(this.scriptLanguage)) { + if (this.scriptLanguage == null || this.scriptLanguage.isEmpty()) { if (!ScriptController.Instance.languages.isEmpty()) { this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; } else { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 38d9e8447..95f8bcb8e 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -5,6 +5,7 @@ import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; +import noppes.npcs.CustomNpcs; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.api.entity.IPlayer; @@ -60,7 +61,7 @@ public class PlayerDataScript extends MultiScriptHandler { } public boolean isEnabled() { - return ConfigScript.GlobalPlayerScripts && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); + return CustomNpcs.proxy.isGlobalPlayerScripts() && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index 684c9a45b..096f8f381 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.config.ConfigScript; +import noppes.npcs.CustomNpcs; import noppes.npcs.controllers.ScriptController; import java.util.ArrayList; @@ -14,7 +14,7 @@ public abstract class ScriptHandler implements IScriptHandler { protected long lastInited = -1; protected boolean canRunScripts() { - return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && scripts != null && !scripts.isEmpty(); + return enabled && ScriptController.HasStart && CustomNpcs.proxy.isScriptingEnabled() && scripts != null && !scripts.isEmpty(); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java index 00c273bca..854e77941 100644 --- a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -5,7 +5,7 @@ import io.netty.buffer.ByteBuf; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; -import noppes.npcs.config.ConfigScript; +import noppes.npcs.CustomNpcs; import noppes.npcs.controllers.ScriptController; import java.io.IOException; @@ -32,7 +32,7 @@ public abstract class SingleScriptHandler extends ScriptHandler { } protected boolean canRunScripts() { - return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + return enabled && ScriptController.HasStart && CustomNpcs.proxy.isScriptingEnabled() && container != null; } @Override @@ -142,7 +142,7 @@ public abstract class SingleScriptHandler extends ScriptHandler { * Normalize the language to a valid one if the current language is not available. */ protected void normalizeLanguage() { - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { + if (this.getLanguage() == null || this.getLanguage().isEmpty()) { if (!ScriptController.Instance.languages.isEmpty()) { this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); } else { diff --git a/src/main/java/noppes/npcs/controllers/data/TagMap.java b/src/main/java/noppes/npcs/controllers/data/TagMap.java index e4c0ad57f..2205d96fd 100644 --- a/src/main/java/noppes/npcs/controllers/data/TagMap.java +++ b/src/main/java/noppes/npcs/controllers/data/TagMap.java @@ -13,10 +13,18 @@ import java.util.UUID; public class TagMap { public int cloneTab; + public String cloneFolder; public HashMap> tagMap; public TagMap(int tab) { this.cloneTab = tab; + this.cloneFolder = null; + this.tagMap = new HashMap>(); + } + + public TagMap(String folder) { + this.cloneTab = -1; + this.cloneFolder = folder; this.tagMap = new HashMap>(); } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 16389f74d..9e6366fdf 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -1693,7 +1693,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } /** - * 1: walking, 2:interacting, 4:attacking, 8:killed + * 1: walking, 2:interacting, 4:attacking, 8:killed, 16:rotationLocked */ public boolean getBoolFlag(int id) { return (dataWatcher.getWatchableObjectInt(15) & id) != 0; diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 1800cc895..2afe8052f 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -7,7 +7,6 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IHookDefinition; -import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; @@ -160,9 +159,9 @@ public abstract class JaninoScript implements IScriptUnit { private String getFullCode() { StringBuilder sb = new StringBuilder(); - if (ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); + if (CustomNpcs.proxy.isRunLoadedScriptsFirst()) appendExternalScripts(sb); if (script != null && !script.isEmpty()) sb.append(script).append("\n"); - if (!ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); + if (!CustomNpcs.proxy.isRunLoadedScriptsFirst()) appendExternalScripts(sb); return sb.toString(); } diff --git a/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java b/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java index c997729ca..9b09759c0 100644 --- a/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java +++ b/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java @@ -56,7 +56,6 @@ public class CustomNPCsMixinPlugin implements IMixinConfigPlugin { } mixins.add("MixinItemStack"); - mixins.add("MixinItem"); } return mixins; } diff --git a/src/main/java/noppes/npcs/mixin/MixinItem.java b/src/main/java/noppes/npcs/mixin/MixinItem.java deleted file mode 100644 index 638b515ae..000000000 --- a/src/main/java/noppes/npcs/mixin/MixinItem.java +++ /dev/null @@ -1,27 +0,0 @@ -package noppes.npcs.mixin; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import noppes.npcs.client.gui.player.AuctionTooltipHandler; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.List; - -/** - * Mixin to inject auction information into item tooltips when viewing auction GUIs. - */ -@Mixin(Item.class) -public abstract class MixinItem { - - @SideOnly(Side.CLIENT) - @Inject(method = "addInformation", at = @At("HEAD")) - public void onAddInformation(ItemStack stack, EntityPlayer player, List tooltip, boolean advanced, CallbackInfo ci) { - AuctionTooltipHandler.addAuctionInfo(stack, tooltip); - } -} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 38a38407c..2158e9ea4 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1631,7 +1631,11 @@ ability.cnpc.sweeper=Sweeper ability.weight=Weight ability.windUpTicks=Wind Up ability.cooldownTicks=Cooldown -ability.lockMovement=Lock Movement +ability.lockPosition=Lock Position +ability.lockType=Lock Type +ability.lockType.movement=Movement +ability.lockType.rotation=Rotation +ability.lockType.both=Both ability.interruptible=Interruptible ability.dazedTicks=Dazed Ticks ability.targetingMode=Targeting @@ -1684,6 +1688,7 @@ ability.section.healing=Healing ability.section.trigger=Trigger ability.section.aoe=AOE ability.section.pullDamage=Pull Damage +ability.section.hitZone=Hit Zone # Shared Sub-Section Labels (used under section headers) gui.radius=Radius @@ -1713,6 +1718,7 @@ ability.maxDistance=Max Distance ability.lifetime=Lifetime ability.stunTicks=Stun Ticks ability.hitWidth=Hit Width +ability.hitLength=Hit Length ability.effects=Effects ability.mode=Mode ability.duration=Duration @@ -1856,7 +1862,8 @@ ability.noTypeSettings=No type-specific settings # ═══════════════════════════════════════════════════════════════ ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available -ability.hover.lockMove=Prevents NPC from moving while ability is charging or active +ability.hover.lockPosition=Prevents NPC from moving or rotating while ability is charging or active +ability.hover.lockType=What to lock: Movement (pathfinding), Rotation (facing direction), or Both ability.hover.interruptible=If enabled, ability can be interrupted by taking damage ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second) ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second) diff --git a/src/main/resources/assets/customnpcs/textures/gui/cloner/folder.png b/src/main/resources/assets/customnpcs/textures/gui/cloner/folder.png new file mode 100644 index 000000000..d98408b92 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/cloner/folder.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/cloner/tab.png b/src/main/resources/assets/customnpcs/textures/gui/cloner/tab.png new file mode 100644 index 000000000..a041d03b0 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/cloner/tab.png differ