Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Vidal committed 2026-02-04 23:30:37 -03:00
commit 921ec259ee
99 files changed
+4581 -1651

No files matched your search

-729
View File
@@ -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 <item\|mod\|nbt> <value>` | Add to blacklist (runtime only) |
| `/cnpc auction blacklist remove <item\|mod\|nbt> <value>` | 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<AuctionClaim> │ │
│ │ - 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<UUID, Set<String>>` | Active listings by seller |
| `playerBidIds` | `Map<UUID, Set<String>>` | 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 <player>` | 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 <item\|mod\|nbt> <value>` | OP | Add to blacklist |
| `remove` | `/cnpc auction blacklist remove <item\|mod\|nbt> <value>` | 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 <player>` - 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
+144
View File
@@ -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.
+199
View File
@@ -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 <name|uuid>` - View ability details
- `/kam ability delete <uuid>` - Delete a custom ability
- `/kam ability reload` - Reload abilities from disk
---
-301
View File
@@ -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()`
+57
View File
@@ -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.
+17 -23
View File
@@ -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 <item|mod|nbt> <value>` - Add entry
- `/kam auction blacklist remove <item|mod|nbt> <value>` - 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 <player>` - 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).
---
+1 -1
Submodule src/api updated: 29ad8637d6...f383fc3037.
@@ -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 = "<npc> <tab> [clonedname]",
usage = "<npc> <tab|folder> [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 = "<tab>",
usage = "<tab|folder>",
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<String> 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 = "<name> <tab>",
usage = "<name> <tab|folder>",
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 = "<name> <tab> [[world:]x,y,z]] [newname]",
usage = "<name> <tab|folder> [[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 = "<name> <tab> [[world:]x,y,z]] [newname]",
desc = "Spawn cloned NPC in a grid",
usage = "<name> <tab|folder> <width> <height> [[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 = "<name>",
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 = "<oldname> <newname>",
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 = "<name>",
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 = "<clonename> <from_tab|folder> <to_tab|folder>",
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) {
@@ -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;
}
@@ -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<IAbilityFieldProvider> 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<IAbilityFieldProvider> getFieldProviders() {
return fieldProviders;
}
}
@@ -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);
}
@@ -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.
* <p>
* 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<FieldDef> defs);
}
@@ -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;
}
}
@@ -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
@@ -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
@@ -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;
@@ -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
@@ -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
@@ -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;
@@ -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;
@@ -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
@@ -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
@@ -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;
@@ -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
@@ -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;
@@ -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
@@ -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;
@@ -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);
@@ -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);
@@ -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
@@ -163,6 +163,9 @@ public enum EnumRequestPacket {
CloneAllTags,
CloneAllTagsShort,
ClonePreSave,
CloneFolderList,
CloneFolderCrud,
CloneMove,
EventScript,
PlayerScript,
@@ -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(),
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
@@ -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);
}
}
@@ -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);
@@ -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);
@@ -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);
}
}
}
@@ -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);
@@ -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;
@@ -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;
@@ -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");
}
@@ -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;
}
}
+52 -9
View File
@@ -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;
}
@@ -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() {
@@ -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) {
@@ -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);
}
}
@@ -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)
@@ -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();
}
/**
@@ -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));
@@ -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;
@@ -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()) {
@@ -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) {
@@ -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);
}
}
}
@@ -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)
@@ -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;
@@ -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; }
}
@@ -41,6 +41,26 @@ public class ClientCloneController extends ServerCloneController {
return true;
}
@Override
public boolean addToTagMap(NBTTagCompound nbttagcompound, String name, String folderName) {
HashSet<UUID> 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<UUID> getTagUUIDsFromTab(String cloneName, int tab) {
TagMap tagMap = ClientTagMapController.Instance.getTagMap(tab);
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
}
@Override
protected HashSet<UUID> getTagUUIDsFromFolder(String cloneName, String folderName) {
TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName);
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
}
@Override
protected void setTagUUIDsForTab(String cloneName, int tab, HashSet<UUID> 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<UUID> tagUUIDs) {
if (tagUUIDs == null || tagUUIDs.isEmpty()) return;
TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName);
tagMap.putClone(cloneName, tagUUIDs);
ClientTagMapController.Instance.saveTagMap(tagMap);
}
}
@@ -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<UUID, Tag> tags = new HashMap<>();
public static HashMap<String, UUID> tagNames = new HashMap<>();
public static HashSet<String> 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<String> list;
private List<String> rawList;
private List<String> 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<UUID, Tag> 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<String> list = new ArrayList<String>();
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<String>(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<String> list = new ArrayList<String>();
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<String>(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<String> getSearchList() {
if (this.list == null) {
this.list = new ArrayList<String>();
@@ -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<String>(this.list);
// Yes Search
else {
List<String> list = new ArrayList<String>();
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<String> list = new ArrayList<String>();
for (String name : this.list) {
@@ -300,7 +376,6 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
return list;
}
// Yes Search
List<String> list = new ArrayList<String>();
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<String>();
}
// No Search // No Filters - or - Entities
if (tagSearch.isEmpty())
return new ArrayList<String>(tagNames.keySet());
// Yes Search // No Filters - or - Entities
List<String> list = new ArrayList<String>();
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<String>(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);
}
}
}
@@ -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<String> clientFolderNames = new ArrayList<>();
// Server-side folder names (populated via CloneFolderListPacket)
private static ArrayList<String> serverFolderNames = new ArrayList<>();
// Selected Tags to Add
public static NBTTagList tagsCompound;
public static HashSet<String> 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<String> 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<String> 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();
}
File diff suppressed because it is too large. Load diff
@@ -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() {
}
}
@@ -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<String> folderList;
private GuiCustomScroll folderScroll;
public SubGuiFolderSelect(List<String> 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() {
}
}
@@ -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() {
}
}
@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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");
@@ -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) */
@@ -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;
@@ -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() {
}
@@ -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;
}
@@ -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);
@@ -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<Integer> iconTypes = new ArrayList<>();
public GuiCustomScrollIcons(GuiScreen parent, int id) {
super(parent, id);
}
public void setListWithIcons(List<String> names, List<Integer> 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();
}
}
@@ -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);
}
@@ -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
@@ -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<UUID, Tag> getTags();
TagMap getTagMap();
}
@@ -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);
@@ -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;
@@ -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;
}
}
@@ -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());
@@ -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:");
@@ -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<String, CloneFolder> 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<File> 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<CloneFolder> getFolderList() {
return new ArrayList<>(folders.values());
}
public List<String> 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<String> 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<String> getClones(String folderName) {
List<String> 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<String> getClonesDate(String folderName) {
List<String> 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<File>() {
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<UUID> 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<UUID> 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<UUID> 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<UUID> 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<UUID> tagUUIDs = new HashSet<UUID>();
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<UUID> 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<UUID> getTagUUIDsFromTab(String cloneName, int tab) {
TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab);
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
}
protected HashSet<UUID> getTagUUIDsFromFolder(String cloneName, String folderName) {
TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName);
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
}
protected void setTagUUIDsForTab(String cloneName, int tab, HashSet<UUID> 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<UUID> 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<IEntity> 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);
}
}
@@ -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");
@@ -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;
}
}
@@ -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
@@ -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
@@ -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
@@ -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 {
@@ -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
@@ -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
@@ -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 {
@@ -13,10 +13,18 @@ import java.util.UUID;
public class TagMap {
public int cloneTab;
public String cloneFolder;
public HashMap<String, HashSet<UUID>> tagMap;
public TagMap(int tab) {
this.cloneTab = tab;
this.cloneFolder = null;
this.tagMap = new HashMap<String, HashSet<UUID>>();
}
public TagMap(String folder) {
this.cloneTab = -1;
this.cloneFolder = folder;
this.tagMap = new HashMap<String, HashSet<UUID>>();
}
@@ -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;
@@ -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<T> 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();
}
@@ -56,7 +56,6 @@ public class CustomNPCsMixinPlugin implements IMixinConfigPlugin {
}
mixins.add("MixinItemStack");
mixins.add("MixinItem");
}
return mixins;
}
@@ -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);
}
}
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B