From a1461665d1625d3d8addebde0f312ab0f4cac3bb Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 02:30:39 -0500 Subject: [PATCH 01/64] Update api --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index d003d6d87..305a3778c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit d003d6d870db13d56fce223050a06faf15050eae +Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 From c647179c26a64ebe052b788f32fd07ab028d320b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 07:37:28 +0000 Subject: [PATCH 02/64] Update CNPC 1.11 Market Plan with design decisions - Add confirmed design decisions section based on discussion: - Outbid refunds go to claims (not direct balance) - Use existing CustomNPC+ permission system pattern - Currency is NOT profile slot bound (shared across slots) - Use existing gold coin (CustomItems.coinGold) for display - Stock reset timer shown as banner ABOVE trader GUI - Add Feature 4: Item Attributes & Restrictions - Untradeable, SlotBound, SoulBound item attributes - Auction house item blacklist system (by item, mod, NBT tag) - Add Feature 5: Admin Auction Management - Admin GUI for viewing/managing all listings - Force cancel, edit, end auctions early - Blacklist management GUI - Audit logging for admin actions - Update implementation phases (now 5 phases) - Update file summary with new files - Update testing strategy with new test cases - Update API/scripting extensions with item attribute methods --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 589 ++++++++++++++++++++++++++- 1 file changed, 567 insertions(+), 22 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index fb1c33158..62e744e27 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -10,15 +10,64 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f --- +## Design Decisions (Confirmed) + +### 1. Auction Refund System +**Decision:** When a player is outbid, the refund goes to **claims** (not direct to balance). +- Players can manage their bids from claims +- If auction ends while player is offline, they can claim their money later +- This keeps the flow consistent with item claims + +### 2. Permission System +**Decision:** Use the **existing CustomNPC+ permission system** used for Profiles. +- Pattern: `customnpcs.auction.slots.X` where X is the max slot count +- Implementation mirrors `ProfileController.allowSlotPermission()` approach +- Uses `CustomNpcsPermissions.hasCustomPermission(player, permissionString)` + +### 3. Virtual Currency Storage +**Decision:** Currency is **NOT profile slot bound** - all profile slots share the same currency pool per player. +- Stored in PlayerData outside of slot components +- Balance persists across profile slot switches +- Balance viewing system to be added later (for now, visible via GUI only) + +### 4. Currency Display Icon +**Decision:** Use the **existing gold coin** from the mod (`CustomItems.coinGold`). +- Already has textures and localization for all supported languages +- Available coin types: Wood, Stone, Bronze, Iron, Gold, Diamond, Emerald + +### 5. Trader Stock Reset Timer Display +**Decision:** Show "Stocks reset in X hours" as a **banner ABOVE the GUI**. +- Positioned at a higher Y level than the main GUI +- Not overlayed on the GUI content +- Displayed when opening the trader interface + +### 6. Item Trading Restrictions +**Decision:** Implement the following restrictions for auction listings: +- **Untradeable Attribute**: New item attribute that prevents auction listing +- **SlotBound Items**: Cannot be listed (needs to be implemented) +- **SoulBound Items**: Cannot be listed (needs to be implemented) +- **Auction House Blacklist**: Configurable list of items that cannot be auctioned + +### 7. Admin Auction Management +**Decision:** Create an **Auction Management system** with a Global Admin button. +- Admins can modify, remove, and edit auction listings in real-time +- Accessible via special admin GUI +- Permission-gated for server operators + +--- + ## Table of Contents -1. [Architecture Overview](#architecture-overview) -2. [Feature 1: Currency System](#feature-1-currency-system) -3. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) -4. [Feature 3: Auction House](#feature-3-auction-house) -5. [Implementation Order](#implementation-order) -6. [File Summary](#file-summary) -7. [Testing Strategy](#testing-strategy) +1. [Design Decisions](#design-decisions-confirmed) +2. [Architecture Overview](#architecture-overview) +3. [Feature 1: Currency System](#feature-1-currency-system) +4. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) +5. [Feature 3: Auction House](#feature-3-auction-house) +6. [Feature 4: Item Attributes & Restrictions](#feature-4-item-attributes--restrictions) +7. [Feature 5: Admin Auction Management](#feature-5-admin-auction-management) +8. [Implementation Order](#implementation-order) +9. [File Summary](#file-summary) +10. [Testing Strategy](#testing-strategy) --- @@ -1720,6 +1769,433 @@ public void onServerTick(TickEvent.ServerTickEvent event) { --- +## Feature 4: Item Attributes & Restrictions + +### 4.1 Overview + +Implement item attribute system to control which items can be traded/auctioned. Items with certain attributes will be blocked from the auction house. + +### 4.2 New Item Attributes + +#### 4.2.1 Untradeable Attribute + +A new NBT-based item attribute that marks items as untradeable. + +**File: `src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java`** + +```java +package kamkeel.npcs.controllers.data.attribute; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +public class ItemTradeAttribute { + public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; + public static final String TAG_SLOT_BOUND = "CNPC_SlotBound"; + public static final String TAG_SOUL_BOUND = "CNPC_SoulBound"; + + /** + * Check if item can be traded/auctioned + */ + public static boolean canTrade(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return true; + } + NBTTagCompound tag = item.getTagCompound(); + + // Check all trade-blocking attributes + if (tag.getBoolean(TAG_UNTRADEABLE)) return false; + if (tag.getBoolean(TAG_SLOT_BOUND)) return false; + if (tag.getBoolean(TAG_SOUL_BOUND)) return false; + + return true; + } + + /** + * Mark item as untradeable + */ + public static void setUntradeable(ItemStack item, boolean untradeable) { + if (item == null) return; + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + item.getTagCompound().setBoolean(TAG_UNTRADEABLE, untradeable); + } + + public static boolean isUntradeable(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); + } + + public static boolean isSlotBound(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + return item.getTagCompound().getBoolean(TAG_SLOT_BOUND); + } + + public static boolean isSoulBound(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + return item.getTagCompound().getBoolean(TAG_SOUL_BOUND); + } + + /** + * Get reason why item cannot be traded (for UI display) + */ + public static String getTradeBlockReason(ItemStack item) { + if (item == null || !item.hasTagCompound()) return null; + NBTTagCompound tag = item.getTagCompound(); + + if (tag.getBoolean(TAG_UNTRADEABLE)) return "gui.auction.untradeable"; + if (tag.getBoolean(TAG_SLOT_BOUND)) return "gui.auction.slotbound"; + if (tag.getBoolean(TAG_SOUL_BOUND)) return "gui.auction.soulbound"; + + return null; + } +} +``` + +### 4.3 Auction House Item Blacklist + +A configurable system to blacklist specific items from being auctioned. + +**File: `src/main/java/noppes/npcs/controllers/AuctionBlacklist.java`** + +```java +package noppes.npcs.controllers; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +public class AuctionBlacklist { + private static AuctionBlacklist instance; + + // Blacklist by item registry name (e.g., "minecraft:diamond_sword") + private Set blacklistedItems = new HashSet<>(); + + // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) + private Set blacklistedMods = new HashSet<>(); + + // Blacklist by NBT tag presence (e.g., items with specific custom data) + private Set blacklistedNBTTags = new HashSet<>(); + + public static AuctionBlacklist getInstance() { + if (instance == null) { + instance = new AuctionBlacklist(); + instance.load(); + } + return instance; + } + + /** + * Check if item is blacklisted + */ + public boolean isBlacklisted(ItemStack item) { + if (item == null) return false; + + // Check item registry name + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null && blacklistedItems.contains(itemName)) { + return true; + } + + // Check mod ID + if (itemName != null) { + String modId = itemName.split(":")[0]; + if (blacklistedMods.contains(modId)) { + return true; + } + } + + // Check NBT tags + if (item.hasTagCompound()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtTag : blacklistedNBTTags) { + if (tag.hasKey(nbtTag)) { + return true; + } + } + } + + return false; + } + + // Blacklist management methods + public void addItem(String itemName) { + blacklistedItems.add(itemName); + save(); + } + + public void removeItem(String itemName) { + blacklistedItems.remove(itemName); + save(); + } + + public void addMod(String modId) { + blacklistedMods.add(modId); + save(); + } + + public void removeMod(String modId) { + blacklistedMods.remove(modId); + save(); + } + + public void addNBTTag(String tag) { + blacklistedNBTTags.add(tag); + save(); + } + + public void removeNBTTag(String tag) { + blacklistedNBTTags.remove(tag); + save(); + } + + public Set getBlacklistedItems() { + return new HashSet<>(blacklistedItems); + } + + public Set getBlacklistedMods() { + return new HashSet<>(blacklistedMods); + } + + public Set getBlacklistedNBTTags() { + return new HashSet<>(blacklistedNBTTags); + } + + // Persistence + public void save() { + // Save to auction_blacklist.dat + } + + public void load() { + // Load from auction_blacklist.dat + } +} +``` + +### 4.4 Integration with Auction Listing + +Modify `AuctionController.createListing()` to check restrictions: + +```java +public AuctionListing createListing(EntityPlayer seller, ItemStack item, ...) { + // Check item attribute restrictions + if (!ItemTradeAttribute.canTrade(item)) { + String reason = ItemTradeAttribute.getTradeBlockReason(item); + // Send message to player with reason + return null; + } + + // Check blacklist + if (AuctionBlacklist.getInstance().isBlacklisted(item)) { + // Send "This item cannot be auctioned" message + return null; + } + + // ... rest of listing creation +} +``` + +--- + +## Feature 5: Admin Auction Management + +### 5.1 Overview + +A comprehensive admin system for managing the auction house in real-time. + +### 5.2 Admin Permissions + +**File: `src/main/java/noppes/npcs/CustomNpcsPermissions.java`** + +Add new permissions: + +```java +// Auction Administration +public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); +public static final Permission AUCTION_MANAGE = new Permission("customnpcs.auction.manage"); +public static final Permission AUCTION_BLACKLIST = new Permission("customnpcs.auction.blacklist"); +``` + +### 5.3 Admin GUI + +**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionAdmin.java`** + +Features: +- **View All Listings**: See all active, expired, and cancelled listings +- **Search/Filter**: Find listings by seller, item, status +- **Cancel Any Listing**: Force-cancel listings (returns item to seller) +- **Modify Listing**: Edit prices, duration (with warning) +- **End Auction Early**: Force-end with current highest bidder winning +- **Refund Bidders**: Manually refund specific bidders +- **View Blacklist**: Manage item blacklist +- **Audit Log**: View recent auction actions + +```java +public class GuiAuctionAdmin extends GuiNPCInterface { + private List allListings; + private int currentPage = 0; + private EnumAuctionStatus filterStatus = null; + private String searchQuery = ""; + + // Admin action buttons + private static final int BTN_CANCEL = 10; + private static final int BTN_END_EARLY = 11; + private static final int BTN_EDIT = 12; + private static final int BTN_REFUND = 13; + private static final int BTN_BLACKLIST = 14; + private static final int BTN_AUDIT = 15; + + @Override + public void initGui() { + // Tab buttons: All | Active | Sold | Expired | Cancelled + // Search bar + // Listing table with columns: ID, Seller, Item, Price, Bids, Status, Time + // Action buttons for selected listing + // Blacklist management button + } + + @Override + protected void actionPerformed(GuiButton button) { + switch (button.id) { + case BTN_CANCEL: + adminCancelListing(selectedListing); + break; + case BTN_END_EARLY: + adminEndAuction(selectedListing); + break; + case BTN_EDIT: + openEditDialog(selectedListing); + break; + case BTN_REFUND: + openRefundDialog(selectedListing); + break; + case BTN_BLACKLIST: + openBlacklistGui(); + break; + case BTN_AUDIT: + openAuditLog(); + break; + } + } +} +``` + +### 5.4 Admin Actions in AuctionController + +```java +// Admin-only methods +public boolean adminCancelListing(EntityPlayer admin, int listingId, String reason) { + if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { + return false; + } + + AuctionListing listing = listings.get(listingId); + if (listing == null) return false; + + // Refund current bidder if any + if (listing.highBidderUUID != null) { + addClaim(listing.highBidderUUID, listing.highBidderName, + null, listing.currentBid, "Admin cancelled auction: " + reason); + } + + // Return item to seller + addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, + "Listing cancelled by admin: " + reason); + + listing.status = EnumAuctionStatus.CANCELLED; + + // Log action + logAdminAction(admin, "CANCEL", listingId, reason); + + return true; +} + +public boolean adminEndAuction(EntityPlayer admin, int listingId) { + if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { + return false; + } + + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { + return false; + } + + // End as if time expired + listing.expirationTime = System.currentTimeMillis(); + processExpiredAuctions(); + + // Log action + logAdminAction(admin, "END_EARLY", listingId, ""); + + return true; +} + +public boolean adminEditListing(EntityPlayer admin, int listingId, + double newStartPrice, double newBuyout, + long newExpiration) { + if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { + return false; + } + + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { + return false; + } + + // Only allow if no bids placed + if (listing.highBidderUUID != null) { + return false; // Cannot edit listing with bids + } + + listing.startingPrice = newStartPrice; + listing.currentBid = newStartPrice; + listing.buyoutPrice = newBuyout; + listing.expirationTime = newExpiration; + + // Log action + logAdminAction(admin, "EDIT", listingId, + String.format("price=%.2f,buyout=%.2f", newStartPrice, newBuyout)); + + return true; +} + +// Audit logging +private void logAdminAction(EntityPlayer admin, String action, int listingId, String details) { + // Log to file: timestamp, admin name, action, listing id, details + LogWriter.info(String.format("[AUCTION ADMIN] %s performed %s on listing %d: %s", + admin.getCommandSenderName(), action, listingId, details)); +} +``` + +### 5.5 Blacklist Management GUI + +**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionBlacklist.java`** + +Features: +- Add item by holding it and clicking "Add" +- Add by typing item registry name +- Add entire mod by mod ID +- Add NBT tag pattern +- View current blacklist with remove buttons +- Test item against blacklist + +### 5.6 Access Points + +The Admin GUI can be accessed via: +1. **Global Menu**: New "Auction Management" button (requires permission) +2. **Auctioneer NPC**: Admin gear icon when editing NPC role (requires permission) +3. **Command**: `/cnpc auction admin` (requires OP or permission) + +--- + ## Implementation Order The features should be implemented in this order due to dependencies: @@ -1742,10 +2218,17 @@ The features should be implemented in this order due to dependencies: 6. Update network packets for stock synchronization 7. Test stock depletion and reset timers -### Phase 3: Auction House +### Phase 3: Item Attributes & Restrictions +1. Create `ItemTradeAttribute.java` with Untradeable, SlotBound, SoulBound tags +2. Create `AuctionBlacklist.java` controller +3. Add API methods to scripting interface for setting item attributes +4. Create blacklist persistence (load/save to file) +5. Test attribute blocking in listing creation + +### Phase 4: Auction House 1. Create auction enums (`EnumAuctionDuration`, `EnumAuctionStatus`, `EnumAuctionCategory`) 2. Create `AuctionListing.java` and `AuctionClaim` classes -3. Create `AuctionController.java` +3. Create `AuctionController.java` with item restriction checks 4. Update `EnumRoleType.java` to add Auctioneer 5. Create `RoleAuctioneer.java` 6. Update `DataAdvanced.java` for role instantiation @@ -1757,6 +2240,16 @@ The features should be implemented in this order due to dependencies: 12. Create auctioneer setup GUI 13. Comprehensive testing +### Phase 5: Admin Auction Management +1. Add auction admin permissions to `CustomNpcsPermissions.java` +2. Create `GuiAuctionAdmin.java` admin interface +3. Create `GuiAuctionBlacklist.java` blacklist management +4. Add admin methods to `AuctionController.java` +5. Add audit logging +6. Create admin network packets +7. Add command handler for `/cnpc auction admin` +8. Test admin operations + --- ## File Summary @@ -1767,9 +2260,11 @@ The features should be implemented in this order due to dependencies: |------|-------------| | `kamkeel/npcs/compat/VaultHelper.java` | VaultAPI integration | | `kamkeel/npcs/controllers/CurrencyController.java` | Currency management | -| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data | +| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data (NOT slot-bound) | | `noppes/npcs/controllers/data/TraderStock.java` | Trader stock configuration | | `noppes/npcs/constants/EnumStockReset.java` | Stock reset types | +| `kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java` | Untradeable/SlotBound/SoulBound item attributes | +| `noppes/npcs/controllers/AuctionBlacklist.java` | Auction item blacklist controller | | `noppes/npcs/controllers/AuctionController.java` | Auction house management | | `noppes/npcs/controllers/data/AuctionListing.java` | Auction listing data | | `noppes/npcs/constants/EnumAuctionDuration.java` | Auction duration options | @@ -1779,8 +2274,11 @@ The features should be implemented in this order due to dependencies: | `noppes/npcs/containers/ContainerAuction*.java` | Auction containers | | `noppes/npcs/client/gui/player/GuiAuction*.java` | Auction GUIs | | `noppes/npcs/client/gui/roles/GuiNpcAuctioneerSetup.java` | Auctioneer config | +| `noppes/npcs/client/gui/global/GuiAuctionAdmin.java` | Admin auction management | +| `noppes/npcs/client/gui/global/GuiAuctionBlacklist.java` | Blacklist management GUI | | `kamkeel/npcs/network/packets/player/AuctionActionPacket.java` | Auction actions | | `kamkeel/npcs/network/packets/data/AuctionSyncPacket.java` | Auction data sync | +| `kamkeel/npcs/network/packets/admin/AuctionAdminPacket.java` | Admin auction actions | ### Files to Modify @@ -1788,17 +2286,20 @@ The features should be implemented in this order due to dependencies: |------|---------| | `dependencies.gradle` | Add VaultAPI dependency | | `repositories.gradle` | Add Vault repository | -| `CustomNpcs.java` | Initialize VaultHelper, AuctionController | +| `CustomNpcs.java` | Initialize VaultHelper, AuctionController, AuctionBlacklist | +| `CustomNpcsPermissions.java` | Add auction admin permissions | | `EnumRoleType.java` | Add Auctioneer | -| `EnumGuiType.java` | Add auction GUI types | +| `EnumGuiType.java` | Add auction GUI types, admin GUIs | | `DataAdvanced.java` | Handle Auctioneer role | | `RoleTrader.java` | Add stock and currency support | | `ContainerNPCTrader.java` | Handle stock/currency transactions | +| `GuiNpcTrader.java` | Add stock reset timer banner above GUI | | `GuiNpcTraderSetup.java` | Add stock/currency UI | -| `PlayerData.java` | Add PlayerCurrencyData | +| `PlayerData.java` | Add PlayerCurrencyData (NOT in slot components - shared across slots) | | `CommonProxy.java` | Register auction containers | | `ServerTickHandler.java` | Add auction expiration check | -| `PacketHandler.java` | Register auction packets | +| `PacketHandler.java` | Register auction packets and admin packets | +| `CommandKamkeel.java` | Add `/cnpc auction admin` command | --- @@ -1806,29 +2307,41 @@ The features should be implemented in this order due to dependencies: ### Unit Testing - Currency operations (deposit, withdraw, balance) +- Currency shared across profile slots (not slot-bound) - Stock depletion and reset calculations - Auction bid validation - Auction expiration handling +- Item attribute checks (Untradeable, SlotBound, SoulBound) +- Blacklist matching (by item, mod, NBT tag) ### Integration Testing - VaultAPI integration on hybrid servers - Fallback to item currency when Vault unavailable - Stock persistence across server restarts - Auction persistence and recovery +- Profile slot switching with shared currency +- Outbid refunds going to claims correctly +- Admin actions affecting live auctions ### User Acceptance Testing - Complete trading flow with stock limits - Complete auction flow (list → bid → win → claim) -- Currency display formatting +- Currency display formatting with gold coin icon +- Stock reset timer banner display above trader GUI - GUI usability +- Admin management workflow ### Edge Cases - Negative currency amounts - Concurrent bids on same auction - Server crash during transaction -- Player offline when auction ends -- Maximum listings per player +- Player offline when auction ends (claim on next login) +- Maximum listings per player (permission-based) - Inventory full when claiming +- Trying to auction Untradeable/SlotBound/SoulBound items +- Trying to auction blacklisted items +- Admin cancelling auction with active bids +- Profile slot switch while having pending claims --- @@ -1843,29 +2356,61 @@ boolean withdraw(IPlayer player, double amount); boolean deposit(IPlayer player, double amount); String format(double amount); +// IItemStack extensions for trade attributes +boolean isUntradeable(); +void setUntradeable(boolean value); +boolean isSlotBound(); +void setSlotBound(boolean value); +boolean isSoulBound(); +void setSoulBound(boolean value); +boolean canTrade(); // Returns false if any blocking attribute is set +String getTradeBlockReason(); // Returns localization key for reason + // IAuction interface for scripts IAuctionListing createListing(IPlayer seller, IItemStack item, double startPrice, double buyout, int durationHours); boolean cancelListing(int listingId); List getPlayerListings(IPlayer player); +boolean isItemBlacklisted(IItemStack item); + +// IAuctionBlacklist interface for scripts (admin) +void addItemToBlacklist(String registryName); +void removeItemFromBlacklist(String registryName); +void addModToBlacklist(String modId); +void removeModFromBlacklist(String modId); +boolean isBlacklisted(IItemStack item); // Events onAuctionCreated(IAuctionListing listing); onAuctionBid(IAuctionListing listing, IPlayer bidder, double amount); +onAuctionOutbid(IAuctionListing listing, IPlayer previousBidder, double refundAmount); onAuctionSold(IAuctionListing listing, IPlayer buyer, double price); onAuctionExpired(IAuctionListing listing); +onAuctionCancelled(IAuctionListing listing, boolean byAdmin); onTraderPurchase(ITrader trader, IPlayer buyer, int slot, int stockRemaining); onTraderStockReset(ITrader trader); +onCurrencyTransaction(IPlayer player, double amount, boolean isDeposit); ``` --- ## Conclusion -This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The three features are designed to work together seamlessly: +This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The five features are designed to work together seamlessly: -1. **Currency System** provides the foundation for monetary transactions -2. **Trader Enhancements** modernize the existing trader with stock and currency support -3. **Auction House** creates a global marketplace leveraging the currency system +1. **Currency System** provides the foundation for monetary transactions (shared across profile slots) +2. **Trader Enhancements** modernize the existing trader with stock, currency support, and reset timer banners +3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, SlotBound, SoulBound attributes and blacklists +4. **Auction House** creates a global marketplace with claims-based refunds and permission-based slot limits +5. **Admin Management** provides comprehensive tools for server operators to manage the auction house in real-time -The implementation follows existing codebase patterns and can be developed incrementally, with each phase building on the previous one. +The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems) and can be developed incrementally, with each phase building on the previous one. + +### Key Design Decisions Summary +- Outbid refunds → Claims (not direct to balance) +- Currency → Shared across all profile slots per player +- Permissions → Uses existing CustomNPC+ permission pattern +- Display icon → Existing `CustomItems.coinGold` +- Stock reset timer → Banner displayed ABOVE the trader GUI +- Trade restrictions → Untradeable attribute + SlotBound/SoulBound + Blacklist system +- Admin tools → Full management GUI with audit logging From cdf30af20ca0587dfcfdadbe48ebea4009afa0cb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 07:49:44 +0000 Subject: [PATCH 03/64] Add auction configuration, timing rules, and finalize design decisions Design Decisions Added: - Modify Bid feature: auto-reclaim and re-bid from claims - Default 5 auction slots per player (configurable) - 30-day claim expiration (configurable) - Percentage-based minimum bid increment (configurable) - Snipe protection: extend TO 5 minutes when bid near end (configurable) - Auction durations: 2h, 12h, 24h, 48h (configurable) Item Restrictions Updated: - Reuse existing ProfileSlotRequirement (cnpc_profile_slot NBT tag) - Reuse existing SoulbindRequirement (cnpc_soulbind NBT tag) - Add new CNPC_Untradeable attribute Configuration Reference Added: - Full auction.cfg with all settings - Currency configuration - Trader stock configuration - Complete permission nodes table Updated conclusion with all finalized decisions. --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 202 +++++++++++++++++++++++---- 1 file changed, 173 insertions(+), 29 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index 62e744e27..cd9b80885 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -17,12 +17,14 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f - Players can manage their bids from claims - If auction ends while player is offline, they can claim their money later - This keeps the flow consistent with item claims +- **Modify Bid Feature**: Players can use a "Modify Bid" button that auto-reclaims their money and applies it to a higher bid amount ### 2. Permission System **Decision:** Use the **existing CustomNPC+ permission system** used for Profiles. - Pattern: `customnpcs.auction.slots.X` where X is the max slot count - Implementation mirrors `ProfileController.allowSlotPermission()` approach - Uses `CustomNpcsPermissions.hasCustomPermission(player, permissionString)` +- **Default**: 5 auction slots per player (configurable) ### 3. Virtual Currency Storage **Decision:** Currency is **NOT profile slot bound** - all profile slots share the same currency pool per player. @@ -43,9 +45,9 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f ### 6. Item Trading Restrictions **Decision:** Implement the following restrictions for auction listings: -- **Untradeable Attribute**: New item attribute that prevents auction listing -- **SlotBound Items**: Cannot be listed (needs to be implemented) -- **SoulBound Items**: Cannot be listed (needs to be implemented) +- **Untradeable Attribute**: New item attribute (`CNPC_Untradeable`) that prevents auction listing +- **ProfileSlot Bound Items**: Use existing `cnpc_profile_slot` NBT tag (from `ProfileSlotRequirement.java`) +- **Soulbound Items**: Use existing `cnpc_soulbind` NBT tag (from `SoulbindRequirement.java`) - **Auction House Blacklist**: Configurable list of items that cannot be auctioned ### 7. Admin Auction Management @@ -54,6 +56,15 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f - Accessible via special admin GUI - Permission-gated for server operators +### 8. Auction Timing & Bidding Rules +**Decision:** Configurable auction timing with snipe protection. +- **Claim Expiration**: 30 days by default (configurable) - claims auto-expire after this period +- **Bid Increment**: Percentage-based minimum increment (configurable, e.g., 5%) +- **Snipe Protection**: When a bid is placed near auction end, extend time TO 5 minutes remaining (never higher) + - Extension amount is configurable + - Example: If 2 minutes left and bid placed, extend to 5 minutes. If 10 minutes left, no extension. +- **Auction Durations**: Configurable default options (2h, 12h, 24h, 48h) + --- ## Table of Contents @@ -65,9 +76,10 @@ This document outlines the implementation plan for the CustomNPC+ 1.11 update, f 5. [Feature 3: Auction House](#feature-3-auction-house) 6. [Feature 4: Item Attributes & Restrictions](#feature-4-item-attributes--restrictions) 7. [Feature 5: Admin Auction Management](#feature-5-admin-auction-management) -8. [Implementation Order](#implementation-order) -9. [File Summary](#file-summary) -10. [Testing Strategy](#testing-strategy) +8. [Configuration Reference](#configuration-reference) +9. [Implementation Order](#implementation-order) +10. [File Summary](#file-summary) +11. [Testing Strategy](#testing-strategy) --- @@ -1775,9 +1787,16 @@ public void onServerTick(TickEvent.ServerTickEvent event) { Implement item attribute system to control which items can be traded/auctioned. Items with certain attributes will be blocked from the auction house. -### 4.2 New Item Attributes +### 4.2 Existing Item Requirements (Reuse) -#### 4.2.1 Untradeable Attribute +The codebase already has requirement types that we can leverage: + +| Requirement | NBT Key | File | Purpose | +|-------------|---------|------|---------| +| ProfileSlot Bound | `cnpc_profile_slot` | `ProfileSlotRequirement.java` | Item bound to specific profile slot | +| Soulbound | `cnpc_soulbind` | `SoulbindRequirement.java` | Item bound to specific player UUID | + +### 4.3 New Untradeable Attribute A new NBT-based item attribute that marks items as untradeable. @@ -1786,27 +1805,35 @@ A new NBT-based item attribute that marks items as untradeable. ```java package kamkeel.npcs.controllers.data.attribute; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; public class ItemTradeAttribute { + // New untradeable attribute public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; - public static final String TAG_SLOT_BOUND = "CNPC_SlotBound"; - public static final String TAG_SOUL_BOUND = "CNPC_SoulBound"; + + // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) + public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; + public static final String TAG_SOULBIND = "cnpc_soulbind"; /** * Check if item can be traded/auctioned */ - public static boolean canTrade(ItemStack item) { + public static boolean canTrade(ItemStack item, EntityPlayer player) { if (item == null || !item.hasTagCompound()) { return true; } NBTTagCompound tag = item.getTagCompound(); - // Check all trade-blocking attributes + // Check untradeable flag if (tag.getBoolean(TAG_UNTRADEABLE)) return false; - if (tag.getBoolean(TAG_SLOT_BOUND)) return false; - if (tag.getBoolean(TAG_SOUL_BOUND)) return false; + + // Check profile slot bound (item tied to specific profile slot) + if (tag.hasKey(TAG_PROFILE_SLOT)) return false; + + // Check soulbound (item tied to specific player) + if (tag.hasKey(TAG_SOULBIND)) return false; return true; } @@ -1819,7 +1846,11 @@ public class ItemTradeAttribute { if (!item.hasTagCompound()) { item.setTagCompound(new NBTTagCompound()); } - item.getTagCompound().setBoolean(TAG_UNTRADEABLE, untradeable); + if (untradeable) { + item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); + } else { + item.getTagCompound().removeTag(TAG_UNTRADEABLE); + } } public static boolean isUntradeable(ItemStack item) { @@ -1827,14 +1858,14 @@ public class ItemTradeAttribute { return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); } - public static boolean isSlotBound(ItemStack item) { + public static boolean isProfileSlotBound(ItemStack item) { if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().getBoolean(TAG_SLOT_BOUND); + return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); } - public static boolean isSoulBound(ItemStack item) { + public static boolean isSoulbound(ItemStack item) { if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().getBoolean(TAG_SOUL_BOUND); + return item.getTagCompound().hasKey(TAG_SOULBIND); } /** @@ -1845,15 +1876,15 @@ public class ItemTradeAttribute { NBTTagCompound tag = item.getTagCompound(); if (tag.getBoolean(TAG_UNTRADEABLE)) return "gui.auction.untradeable"; - if (tag.getBoolean(TAG_SLOT_BOUND)) return "gui.auction.slotbound"; - if (tag.getBoolean(TAG_SOUL_BOUND)) return "gui.auction.soulbound"; + if (tag.hasKey(TAG_PROFILE_SLOT)) return "gui.auction.profileslotbound"; + if (tag.hasKey(TAG_SOULBIND)) return "gui.auction.soulbound"; return null; } } ``` -### 4.3 Auction House Item Blacklist +### 4.4 Auction House Item Blacklist A configurable system to blacklist specific items from being auctioned. @@ -1980,7 +2011,7 @@ public class AuctionBlacklist { } ``` -### 4.4 Integration with Auction Listing +### 4.5 Integration with Auction Listing Modify `AuctionController.createListing()` to check restrictions: @@ -2196,6 +2227,115 @@ The Admin GUI can be accessed via: --- +## Configuration Reference + +All auction and market settings will be added to the configuration system. + +### Auction Configuration + +**File: `config/customnpcs/auction.cfg`** (or section in existing config) + +```properties +# =========================================== +# Auction House Configuration +# =========================================== + +# General Settings +# ---------------- +# Enable/disable the auction house system +B:AuctionEnabled=true + +# Default maximum auction listings per player (without permission override) +I:DefaultMaxListings=5 + +# Listing fee as percentage of starting price (0.0 = no fee) +D:ListingFeePercent=0.0 + +# Sales tax as percentage taken from final sale price (0.05 = 5%) +D:SalesTaxPercent=0.05 + +# Require an Auctioneer NPC to access auction house +B:RequireAuctioneerNPC=true + +# Timing Settings +# --------------- +# Available auction durations in hours (comma-separated) +S:AvailableDurations=2,12,24,48 + +# Default auction duration in hours +I:DefaultDurationHours=24 + +# Claim expiration in days (claims auto-expire after this period, 0 = never) +I:ClaimExpirationDays=30 + +# Bidding Rules +# ------------- +# Minimum bid increment as percentage (0.05 = 5% minimum increase) +D:MinBidIncrementPercent=0.05 + +# Snipe protection: extend auction TO this many minutes when bid placed near end +# Set to 0 to disable snipe protection +I:SnipeProtectionMinutes=5 + +# How many minutes before end triggers snipe protection +# (bids placed with less than this remaining will extend the auction) +I:SnipeProtectionThreshold=5 + +# Permissions +# ----------- +# Permission pattern for auction slot limits: customnpcs.auction.slots.X +# Players without any permission get DefaultMaxListings +# Example: Player with customnpcs.auction.slots.10 can have 10 listings +``` + +### Currency Configuration + +```properties +# =========================================== +# Currency System Configuration +# =========================================== + +# Use VaultAPI if available on Bukkit hybrid servers +B:UseVaultIfAvailable=true + +# Fallback currency item (registry name) when Vault unavailable +# Leave empty to disable fallback currency +S:FallbackCurrencyItem=customnpcs:npcCoinGold + +# Conversion ratio: 1 fallback item = X virtual currency +D:FallbackItemRatio=1.0 +``` + +### Trader Stock Configuration + +```properties +# =========================================== +# Trader Stock Configuration +# =========================================== + +# Enable stock system by default for new traders +B:EnableStockByDefault=false + +# Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) +S:DefaultResetType=NONE +``` + +### Permission Nodes + +| Permission | Description | Default | +|------------|-------------|---------| +| `customnpcs.auction.use` | Access auction house | true | +| `customnpcs.auction.sell` | Create auction listings | true | +| `customnpcs.auction.buy` | Bid and buyout auctions | true | +| `customnpcs.auction.slots.X` | Have X maximum listings | - | +| `customnpcs.auction.admin` | Access admin management | op | +| `customnpcs.auction.manage` | Modify/cancel any listing | op | +| `customnpcs.auction.blacklist` | Manage item blacklist | op | +| `customnpcs.auction.bypass.fee` | Bypass listing fees | op | +| `customnpcs.auction.bypass.tax` | Bypass sales tax | op | + +--- + ## Implementation Order The features should be implemented in this order due to dependencies: @@ -2400,17 +2540,21 @@ This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.1 1. **Currency System** provides the foundation for monetary transactions (shared across profile slots) 2. **Trader Enhancements** modernize the existing trader with stock, currency support, and reset timer banners -3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, SlotBound, SoulBound attributes and blacklists -4. **Auction House** creates a global marketplace with claims-based refunds and permission-based slot limits +3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, ProfileSlot Bound, and Soulbound attributes plus blacklists +4. **Auction House** creates a global marketplace with claims-based refunds, bid modification, and permission-based slot limits 5. **Admin Management** provides comprehensive tools for server operators to manage the auction house in real-time -The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems) and can be developed incrementally, with each phase building on the previous one. +The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems, existing requirement types) and can be developed incrementally, with each phase building on the previous one. ### Key Design Decisions Summary -- Outbid refunds → Claims (not direct to balance) +- Outbid refunds → Claims (not direct to balance) with **Modify Bid** feature - Currency → Shared across all profile slots per player -- Permissions → Uses existing CustomNPC+ permission pattern +- Permissions → Uses existing CustomNPC+ permission pattern (default: 5 slots) - Display icon → Existing `CustomItems.coinGold` - Stock reset timer → Banner displayed ABOVE the trader GUI -- Trade restrictions → Untradeable attribute + SlotBound/SoulBound + Blacklist system +- Trade restrictions → New Untradeable + existing ProfileSlotRequirement/SoulbindRequirement + Blacklist +- Claim expiration → 30 days default (configurable) +- Bid increment → Percentage-based minimum (configurable, e.g., 5%) +- Snipe protection → Extend TO 5 minutes when bid placed near end (configurable) +- Auction durations → 2h, 12h, 24h, 48h (configurable) - Admin tools → Full management GUI with audit logging From 4093bf9adf8c6a5ada5684b44d026c4b53f04cde Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 07:56:30 +0000 Subject: [PATCH 04/64] Move market configs to main.cfg with dedicated categories Configuration now uses the main config file with nested categories: - market (main category) - market.currency (currency system settings) - market.auction (auction house settings) - market.trader (trader stock settings) This keeps all market/economy settings in one place within the existing main configuration file. --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 133 +++++++++++++-------------- 1 file changed, 66 insertions(+), 67 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index cd9b80885..e9b4cd0ce 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -2229,95 +2229,94 @@ The Admin GUI can be accessed via: ## Configuration Reference -All auction and market settings will be added to the configuration system. +All auction and market settings will be added to the **main configuration file** under dedicated categories. -### Auction Configuration +### Location -**File: `config/customnpcs/auction.cfg`** (or section in existing config) +**File: `config/CustomNPCs/main.cfg`** (existing main config) + +New categories to add: +- `market` - General market/economy settings +- `market.currency` - Currency system settings +- `market.auction` - Auction house settings +- `market.trader` - Trader stock settings + +### Market Configuration (Main Category) ```properties # =========================================== -# Auction House Configuration +# Market & Economy Configuration # =========================================== +market { + # Enable the market/economy system + B:Enabled=true -# General Settings -# ---------------- -# Enable/disable the auction house system -B:AuctionEnabled=true + # =========================================== + # Currency Settings + # =========================================== + currency { + # Use VaultAPI if available on Bukkit hybrid servers + B:UseVaultIfAvailable=true -# Default maximum auction listings per player (without permission override) -I:DefaultMaxListings=5 + # Fallback currency item (registry name) when Vault unavailable + # Leave empty to disable fallback currency + S:FallbackCurrencyItem=customnpcs:npcCoinGold -# Listing fee as percentage of starting price (0.0 = no fee) -D:ListingFeePercent=0.0 + # Conversion ratio: 1 fallback item = X virtual currency + D:FallbackItemRatio=1.0 + } -# Sales tax as percentage taken from final sale price (0.05 = 5%) -D:SalesTaxPercent=0.05 + # =========================================== + # Auction House Settings + # =========================================== + auction { + # Enable/disable the auction house system + B:Enabled=true -# Require an Auctioneer NPC to access auction house -B:RequireAuctioneerNPC=true + # Default maximum auction listings per player (without permission override) + I:DefaultMaxListings=5 -# Timing Settings -# --------------- -# Available auction durations in hours (comma-separated) -S:AvailableDurations=2,12,24,48 + # Listing fee as percentage of starting price (0.0 = no fee) + D:ListingFeePercent=0.0 -# Default auction duration in hours -I:DefaultDurationHours=24 + # Sales tax as percentage taken from final sale price (0.05 = 5%) + D:SalesTaxPercent=0.05 -# Claim expiration in days (claims auto-expire after this period, 0 = never) -I:ClaimExpirationDays=30 + # Require an Auctioneer NPC to access auction house + B:RequireAuctioneerNPC=true -# Bidding Rules -# ------------- -# Minimum bid increment as percentage (0.05 = 5% minimum increase) -D:MinBidIncrementPercent=0.05 + # Available auction durations in hours (comma-separated) + S:AvailableDurations=2,12,24,48 -# Snipe protection: extend auction TO this many minutes when bid placed near end -# Set to 0 to disable snipe protection -I:SnipeProtectionMinutes=5 + # Default auction duration in hours + I:DefaultDurationHours=24 -# How many minutes before end triggers snipe protection -# (bids placed with less than this remaining will extend the auction) -I:SnipeProtectionThreshold=5 + # Claim expiration in days (claims auto-expire after this period, 0 = never) + I:ClaimExpirationDays=30 -# Permissions -# ----------- -# Permission pattern for auction slot limits: customnpcs.auction.slots.X -# Players without any permission get DefaultMaxListings -# Example: Player with customnpcs.auction.slots.10 can have 10 listings -``` + # Minimum bid increment as percentage (0.05 = 5% minimum increase) + D:MinBidIncrementPercent=0.05 -### Currency Configuration + # Snipe protection: extend auction TO this many minutes when bid placed near end + # Set to 0 to disable snipe protection + I:SnipeProtectionMinutes=5 -```properties -# =========================================== -# Currency System Configuration -# =========================================== + # How many minutes before end triggers snipe protection + # (bids placed with less than this remaining will extend the auction) + I:SnipeProtectionThreshold=5 + } -# Use VaultAPI if available on Bukkit hybrid servers -B:UseVaultIfAvailable=true + # =========================================== + # Trader Stock Settings + # =========================================== + trader { + # Enable stock system by default for new traders + B:EnableStockByDefault=false -# Fallback currency item (registry name) when Vault unavailable -# Leave empty to disable fallback currency -S:FallbackCurrencyItem=customnpcs:npcCoinGold - -# Conversion ratio: 1 fallback item = X virtual currency -D:FallbackItemRatio=1.0 -``` - -### Trader Stock Configuration - -```properties -# =========================================== -# Trader Stock Configuration -# =========================================== - -# Enable stock system by default for new traders -B:EnableStockByDefault=false - -# Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) -S:DefaultResetType=NONE + # Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) + S:DefaultResetType=NONE + } +} ``` ### Permission Nodes From 7e6b17c106afd719b0b0ee69a59e7c0c622e820c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:14:29 +0000 Subject: [PATCH 05/64] Implement Phase 1: Currency System foundation - Add ConfigMarket.java with separate market.cfg config file - Currency settings (name, starting balance, max balance, Vault integration) - Auction settings (durations, fees, snipe protection, claim expiration) - Trader settings (stock system defaults) - Add PlayerCurrencyData.java for balance storage - NOT profile slot bound - shared across all slots - Tracks balance, lifetime earned, and lifetime spent - Deposit/withdraw with overflow protection - Add VaultHelper.java for Bukkit hybrid server integration - Reflection-based VaultAPI integration - Falls back to internal currency when Vault unavailable - Supports getBalance, deposit, withdraw, transfer operations - Add CurrencyController.java for currency operations - Claims system for auction refunds and NPC payments - Claim expiration with configurable days - Persistence via currency_claims.dat - Update PlayerData.java to integrate currency - Separate loadCurrency/saveCurrency methods - Currency file stored separately from slot data - Add scripting API methods to ScriptPlayer.java - getCurrencyBalance(), addCurrency(), removeCurrency() - canAffordCurrency(), getCurrencyName(), formatCurrency() - hasPendingClaims(), getPendingClaimsTotal(), claimAllCurrency() --- src/main/java/noppes/npcs/CustomNpcs.java | 4 + .../java/noppes/npcs/config/ConfigMarket.java | 217 ++++++++++ .../noppes/npcs/config/LoadConfiguration.java | 3 + .../npcs/controllers/CurrencyController.java | 400 ++++++++++++++++++ .../noppes/npcs/controllers/VaultHelper.java | 272 ++++++++++++ .../controllers/data/PlayerCurrencyData.java | 131 ++++++ .../npcs/controllers/data/PlayerData.java | 66 +++ .../npcs/scripted/entity/ScriptPlayer.java | 118 ++++++ 8 files changed, 1211 insertions(+) create mode 100644 src/main/java/noppes/npcs/config/ConfigMarket.java create mode 100644 src/main/java/noppes/npcs/controllers/CurrencyController.java create mode 100644 src/main/java/noppes/npcs/controllers/VaultHelper.java create mode 100644 src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 4b5750c6a..422be3e61 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -58,6 +58,8 @@ import noppes.npcs.controllers.GlobalDataController; import noppes.npcs.controllers.LinkedItemController; import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.VaultHelper; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -304,6 +306,8 @@ public class CustomNpcs { //Loading items in the about to start event was corrupting items with a damage value @EventHandler public void started(FMLServerStartedEvent event) { + new VaultHelper(); + new CurrencyController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java new file mode 100644 index 000000000..9c5791929 --- /dev/null +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -0,0 +1,217 @@ +package noppes.npcs.config; + +import cpw.mods.fml.common.FMLLog; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.common.config.Property; +import org.apache.logging.log4j.Level; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; + +public class ConfigMarket { + public static Configuration config; + + public final static String MARKET = "Market"; + public final static String CURRENCY = "Market.Currency"; + public final static String AUCTION = "Market.Auction"; + public final static String TRADER = "Market.Trader"; + + // ========================================= + // Market General + // ========================================= + public static boolean MarketEnabled = true; + + // ========================================= + // Currency Settings + // ========================================= + public static boolean UseVaultIfAvailable = true; + public static String CurrencyName = "Gold"; + public static String FallbackCurrencyItem = "customnpcs:npcCoinGold"; + public static long FallbackItemRatio = 1; + public static long StartingBalance = 1000; + public static long MaxBalance = 999999999999L; + + // ========================================= + // Auction Settings + // ========================================= + public static boolean AuctionEnabled = true; + public static int DefaultMaxListings = 5; + public static double ListingFeePercent = 0.0; + public static double SalesTaxPercent = 0.05; + public static boolean RequireAuctioneerNPC = true; + public static String AvailableDurations = "2,12,24,48"; + public static int DefaultDurationHours = 24; + public static int ClaimExpirationDays = 30; + public static double MinBidIncrementPercent = 0.05; + public static int SnipeProtectionMinutes = 5; + public static int SnipeProtectionThreshold = 5; + + // ========================================= + // Trader Settings + // ========================================= + public static boolean EnableStockByDefault = false; + public static String DefaultResetType = "NONE"; + + public static void init(File configFile) { + config = new Configuration(configFile); + + try { + config.load(); + + // ========================================= + // Market General + // ========================================= + MarketEnabled = config.get(MARKET, "Enable Market System", true, + "Enable the market/economy system").getBoolean(true); + + // ========================================= + // Currency Settings + // ========================================= + config.setCategoryComment(CURRENCY, "Currency system settings for the virtual economy"); + + UseVaultIfAvailable = config.get(CURRENCY, "Use Vault If Available", true, + "Use VaultAPI if available on Bukkit hybrid servers").getBoolean(true); + + CurrencyName = config.get(CURRENCY, "Currency Name", "Gold", + "Display name for the currency").getString(); + + FallbackCurrencyItem = config.get(CURRENCY, "Fallback Currency Item", "customnpcs:npcCoinGold", + "Item registry name to use as currency when Vault is unavailable. Leave empty to disable.").getString(); + + FallbackItemRatio = config.get(CURRENCY, "Fallback Item Ratio", 1, + "Conversion ratio: 1 fallback item = X virtual currency").getInt(1); + + StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, + "Starting balance for new players (only used when Vault is not available)").getInt(1000); + + MaxBalance = config.get(CURRENCY, "Maximum Balance", 999999999999L, + "Maximum balance a player can have (only used when Vault is not available)").getLong(999999999999L); + + // ========================================= + // Auction Settings + // ========================================= + config.setCategoryComment(AUCTION, "Auction house settings"); + + AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, + "Enable/disable the auction house system").getBoolean(true); + + DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, + "Default maximum auction listings per player (without permission override)").getInt(5); + + ListingFeePercent = config.get(AUCTION, "Listing Fee Percent", 0.0, + "Listing fee as percentage of starting price (0.0 = no fee)").getDouble(0.0); + + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, + "Sales tax as percentage taken from final sale price (0.05 = 5%)").getDouble(0.05); + + RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, + "Require an Auctioneer NPC to access auction house").getBoolean(true); + + AvailableDurations = config.get(AUCTION, "Available Durations", "2,12,24,48", + "Available auction durations in hours (comma-separated)").getString(); + + DefaultDurationHours = config.get(AUCTION, "Default Duration Hours", 24, + "Default auction duration in hours").getInt(24); + + ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, + "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); + + MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, + "Minimum bid increment as percentage (0.05 = 5% minimum increase)").getDouble(0.05); + + SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 5, + "Extend auction TO this many minutes when bid placed near end. Set to 0 to disable.").getInt(5); + + SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, + "How many minutes before end triggers snipe protection").getInt(5); + + // ========================================= + // Trader Settings + // ========================================= + config.setCategoryComment(TRADER, "Trader stock system settings"); + + EnableStockByDefault = config.get(TRADER, "Enable Stock By Default", false, + "Enable stock system by default for new traders").getBoolean(false); + + DefaultResetType = config.get(TRADER, "Default Reset Type", "NONE", + "Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY)").getString(); + + // Set category order + config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList( + "Enable Market System" + ))); + + config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( + "Use Vault If Available", + "Currency Name", + "Fallback Currency Item", + "Fallback Item Ratio", + "Starting Balance", + "Maximum Balance" + ))); + + config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( + "Enable Auction House", + "Default Max Listings", + "Listing Fee Percent", + "Sales Tax Percent", + "Require Auctioneer NPC", + "Available Durations", + "Default Duration Hours", + "Claim Expiration Days", + "Min Bid Increment Percent", + "Snipe Protection Minutes", + "Snipe Protection Threshold" + ))); + + config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( + "Enable Stock By Default", + "Default Reset Type" + ))); + + // Validation + if (DefaultMaxListings < 1) { + DefaultMaxListings = 1; + } + if (ClaimExpirationDays < 0) { + ClaimExpirationDays = 0; + } + if (SnipeProtectionMinutes < 0) { + SnipeProtectionMinutes = 0; + } + if (SnipeProtectionThreshold < 0) { + SnipeProtectionThreshold = 0; + } + if (StartingBalance < 0) { + StartingBalance = 0; + } + if (MaxBalance < 1) { + MaxBalance = 1; + } + + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } + + /** + * Parse available durations from config string + */ + public static int[] getAvailableDurationsArray() { + String[] parts = AvailableDurations.split(","); + int[] durations = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + durations[i] = Integer.parseInt(parts[i].trim()); + } catch (NumberFormatException e) { + durations[i] = 24; // Default to 24 hours if parsing fails + } + } + return durations; + } +} diff --git a/src/main/java/noppes/npcs/config/LoadConfiguration.java b/src/main/java/noppes/npcs/config/LoadConfiguration.java index 19fad65c2..dfac43d90 100644 --- a/src/main/java/noppes/npcs/config/LoadConfiguration.java +++ b/src/main/java/noppes/npcs/config/LoadConfiguration.java @@ -9,6 +9,7 @@ public class LoadConfiguration { public static File debugConfigFile; public static File clientConfigFile; public static File experimentalConfigFile; + public static File marketConfigFile; public static void init(String configpath) { mainConfigFile = new File(configpath + "main.cfg"); @@ -17,6 +18,7 @@ public class LoadConfiguration { scriptConfigFile = new File(configpath + "script.cfg"); debugConfigFile = new File(configpath + "debug.cfg"); experimentalConfigFile = new File(configpath + "experimental.cfg"); + marketConfigFile = new File(configpath + "market.cfg"); ConfigMain.init(mainConfigFile); ConfigItem.init(itemConfigFile); @@ -24,5 +26,6 @@ public class LoadConfiguration { ConfigScript.init(scriptConfigFile); ConfigDebug.init(debugConfigFile); ConfigExperimental.init(experimentalConfigFile); + ConfigMarket.init(marketConfigFile); } } diff --git a/src/main/java/noppes/npcs/controllers/CurrencyController.java b/src/main/java/noppes/npcs/controllers/CurrencyController.java new file mode 100644 index 000000000..16d6c99f4 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/CurrencyController.java @@ -0,0 +1,400 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.PlayerData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.*; + +/** + * Controller for managing currency operations including: + * - Claims system (for auction refunds, NPC payments, etc.) + * - Transaction history + * - Currency administration + * + * Delegates actual balance operations to VaultHelper. + */ +public class CurrencyController { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static CurrencyController Instance; + + // Pending claims per player UUID - claims that can be collected + private Map> pendingClaims = new HashMap<>(); + + // File path for persistence + private String filePath = ""; + + public CurrencyController() { + Instance = this; + load(); + } + + /** + * Get player's current balance + */ + public long getBalance(EntityPlayer player) { + return VaultHelper.Instance.getBalance(player); + } + + /** + * Deposit currency directly to player's balance + */ + public boolean deposit(EntityPlayer player, long amount) { + return VaultHelper.Instance.deposit(player, amount); + } + + /** + * Withdraw currency from player's balance + */ + public boolean withdraw(EntityPlayer player, long amount) { + return VaultHelper.Instance.withdraw(player, amount); + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(EntityPlayer player, long amount) { + return VaultHelper.Instance.canAfford(player, amount); + } + + /** + * Transfer currency between players + */ + public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { + return VaultHelper.Instance.transfer(from, to, amount); + } + + /** + * Format amount for display + */ + public String formatAmount(long amount) { + return VaultHelper.Instance.formatAmount(amount); + } + + // ==================== CLAIMS SYSTEM ==================== + + /** + * Add a claim for a player (e.g., auction refund, NPC payment) + * Claims can be collected later by the player. + * + * @param playerUUID The player's UUID + * @param amount The amount to claim + * @param source Description of where this claim came from + * @param sourceId Optional ID of the source (e.g., auction ID) + */ + public void addClaim(UUID playerUUID, long amount, String source, String sourceId) { + if (amount <= 0) { + return; + } + + CurrencyClaim claim = new CurrencyClaim(); + claim.amount = amount; + claim.source = source; + claim.sourceId = sourceId; + claim.timestamp = System.currentTimeMillis(); + claim.expiresAt = claim.timestamp + (ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L); + + List claims = pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()); + claims.add(claim); + + save(); + logger.info("Added claim of {} for player {} from {}", amount, playerUUID, source); + } + + /** + * Get all pending claims for a player + */ + public List getPendingClaims(EntityPlayer player) { + return getPendingClaims(player.getUniqueID()); + } + + /** + * Get all pending claims for a player by UUID + */ + public List getPendingClaims(UUID playerUUID) { + List claims = pendingClaims.get(playerUUID); + if (claims == null) { + return new ArrayList<>(); + } + + // Filter out expired claims + long now = System.currentTimeMillis(); + claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); + + return new ArrayList<>(claims); + } + + /** + * Get total pending claim amount for a player + */ + public long getTotalPendingClaims(EntityPlayer player) { + return getTotalPendingClaims(player.getUniqueID()); + } + + /** + * Get total pending claim amount for a player by UUID + */ + public long getTotalPendingClaims(UUID playerUUID) { + List claims = getPendingClaims(playerUUID); + long total = 0; + for (CurrencyClaim claim : claims) { + total += claim.amount; + } + return total; + } + + /** + * Claim all pending claims for a player + * @return Amount successfully claimed + */ + public long claimAll(EntityPlayer player) { + List claims = getPendingClaims(player); + if (claims.isEmpty()) { + return 0; + } + + long totalClaimed = 0; + + for (CurrencyClaim claim : claims) { + if (deposit(player, claim.amount)) { + totalClaimed += claim.amount; + } + } + + // Clear claims after successful deposit + pendingClaims.remove(player.getUniqueID()); + save(); + + return totalClaimed; + } + + /** + * Claim a specific claim by index + * @return true if successful + */ + public boolean claimSingle(EntityPlayer player, int claimIndex) { + List claims = pendingClaims.get(player.getUniqueID()); + if (claims == null || claimIndex < 0 || claimIndex >= claims.size()) { + return false; + } + + CurrencyClaim claim = claims.get(claimIndex); + if (deposit(player, claim.amount)) { + claims.remove(claimIndex); + if (claims.isEmpty()) { + pendingClaims.remove(player.getUniqueID()); + } + save(); + return true; + } + + return false; + } + + /** + * Check if player has any pending claims + */ + public boolean hasPendingClaims(EntityPlayer player) { + return !getPendingClaims(player).isEmpty(); + } + + // ==================== PERSISTENCE ==================== + + public void load() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + filePath = saveDir.getAbsolutePath(); + pendingClaims.clear(); + + try { + File file = new File(saveDir, "currency_claims.dat"); + if (file.exists()) { + loadFromFile(file); + } + } catch (Exception e) { + logger.error("Error loading currency claims", e); + try { + File file = new File(saveDir, "currency_claims.dat_old"); + if (file.exists()) { + loadFromFile(file); + } + } catch (Exception ee) { + logger.error("Error loading backup currency claims", ee); + } + } + + // Clean expired claims + cleanExpiredClaims(); + } + + private void loadFromFile(File file) throws Exception { + FileInputStream fis = new FileInputStream(file); + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + fis.close(); + + NBTTagList claimsList = compound.getTagList("Claims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + NBTTagCompound playerCompound = claimsList.getCompoundTagAt(i); + UUID playerUUID = UUID.fromString(playerCompound.getString("UUID")); + + List playerClaims = new ArrayList<>(); + NBTTagList playerClaimsList = playerCompound.getTagList("PlayerClaims", 10); + for (int j = 0; j < playerClaimsList.tagCount(); j++) { + NBTTagCompound claimCompound = playerClaimsList.getCompoundTagAt(j); + CurrencyClaim claim = new CurrencyClaim(); + claim.readFromNBT(claimCompound); + playerClaims.add(claim); + } + + if (!playerClaims.isEmpty()) { + pendingClaims.put(playerUUID, playerClaims); + } + } + + logger.info("Loaded {} player claim records", pendingClaims.size()); + } + + public void save() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + try { + File file = new File(saveDir, "currency_claims.dat"); + File backup = new File(saveDir, "currency_claims.dat_old"); + + if (file.exists()) { + if (backup.exists()) { + backup.delete(); + } + file.renameTo(backup); + } + + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList claimsList = new NBTTagList(); + + for (Map.Entry> entry : pendingClaims.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + + NBTTagCompound playerCompound = new NBTTagCompound(); + playerCompound.setString("UUID", entry.getKey().toString()); + + NBTTagList playerClaimsList = new NBTTagList(); + for (CurrencyClaim claim : entry.getValue()) { + NBTTagCompound claimCompound = new NBTTagCompound(); + claim.writeToNBT(claimCompound); + playerClaimsList.appendTag(claimCompound); + } + playerCompound.setTag("PlayerClaims", playerClaimsList); + + claimsList.appendTag(playerCompound); + } + + compound.setTag("Claims", claimsList); + + file = new File(saveDir, "currency_claims.dat"); + FileOutputStream fos = new FileOutputStream(file); + CompressedStreamTools.writeCompressed(compound, fos); + fos.close(); + } catch (Exception e) { + logger.error("Error saving currency claims", e); + } + } + + /** + * Clean up expired claims + */ + public void cleanExpiredClaims() { + long now = System.currentTimeMillis(); + boolean changed = false; + + Iterator>> it = pendingClaims.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + List claims = entry.getValue(); + + int before = claims.size(); + claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); + + if (claims.size() != before) { + changed = true; + } + + if (claims.isEmpty()) { + it.remove(); + } + } + + if (changed) { + save(); + } + } + + // ==================== CLAIM DATA CLASS ==================== + + public static class CurrencyClaim { + public long amount; + public String source; // e.g., "Auction Refund", "NPC Payment", "Quest Reward" + public String sourceId; // e.g., auction ID + public long timestamp; // When the claim was created + public long expiresAt; // When the claim expires (0 = never) + + public void writeToNBT(NBTTagCompound compound) { + compound.setLong("Amount", amount); + compound.setString("Source", source != null ? source : ""); + compound.setString("SourceId", sourceId != null ? sourceId : ""); + compound.setLong("Timestamp", timestamp); + compound.setLong("ExpiresAt", expiresAt); + } + + public void readFromNBT(NBTTagCompound compound) { + amount = compound.getLong("Amount"); + source = compound.getString("Source"); + sourceId = compound.getString("SourceId"); + timestamp = compound.getLong("Timestamp"); + expiresAt = compound.getLong("ExpiresAt"); + } + + /** + * Get formatted time remaining until expiration + */ + public String getTimeRemaining() { + if (expiresAt <= 0) { + return "Never"; + } + + long remaining = expiresAt - System.currentTimeMillis(); + if (remaining <= 0) { + return "Expired"; + } + + long days = remaining / (24L * 60L * 60L * 1000L); + long hours = (remaining % (24L * 60L * 60L * 1000L)) / (60L * 60L * 1000L); + + if (days > 0) { + return days + " days, " + hours + " hours"; + } else if (hours > 0) { + return hours + " hours"; + } else { + long minutes = remaining / (60L * 1000L); + return minutes + " minutes"; + } + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java new file mode 100644 index 000000000..ae2c6f97a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/VaultHelper.java @@ -0,0 +1,272 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerDataController; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.Method; + +/** + * Helper class for VaultAPI integration on Bukkit hybrid servers. + * Uses reflection to integrate with Vault when available. + * Falls back to internal currency system when Vault is not present. + */ +public class VaultHelper { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static VaultHelper Instance; + + // Vault reflection references + private boolean vaultAvailable = false; + private Object economy = null; + private Method getBalance; + private Method depositPlayer; + private Method withdrawPlayer; + private Method hasAccount; + private Method createPlayerAccount; + private Method responseSuccess; + + // Bukkit references for getting offline player + private Class bukkitClass; + private Method getOfflinePlayer; + + public VaultHelper() { + Instance = this; + + if (!ConfigMarket.UseVaultIfAvailable) { + logger.info("Vault integration disabled by config"); + return; + } + + try { + // Check for Bukkit first + bukkitClass = Class.forName("org.bukkit.Bukkit"); + getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); + + // Check for Vault + Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); + + // Get the Economy service from Bukkit's ServicesManager + Method getServicesManager = bukkitClass.getMethod("getServicesManager"); + Object servicesManager = getServicesManager.invoke(null); + + Method getRegistration = servicesManager.getClass().getMethod("getRegistration", Class.class); + Object registration = getRegistration.invoke(servicesManager, vaultClass); + + if (registration != null) { + Method getProvider = registration.getClass().getMethod("getProvider"); + economy = getProvider.invoke(registration); + + if (economy != null) { + // Get Economy methods - using OfflinePlayer versions for broader compatibility + Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); + getBalance = economy.getClass().getMethod("getBalance", offlinePlayerClass); + depositPlayer = economy.getClass().getMethod("depositPlayer", offlinePlayerClass, double.class); + withdrawPlayer = economy.getClass().getMethod("withdrawPlayer", offlinePlayerClass, double.class); + hasAccount = economy.getClass().getMethod("hasAccount", offlinePlayerClass); + createPlayerAccount = economy.getClass().getMethod("createPlayerAccount", offlinePlayerClass); + + // EconomyResponse.transactionSuccess() method + Class economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); + responseSuccess = economyResponseClass.getField("transactionSuccess").getClass().getMethod("booleanValue"); + + vaultAvailable = true; + logger.info("Vault Economy integration enabled - using external economy"); + } + } + + if (!vaultAvailable) { + logger.info("Vault found but no Economy provider registered - using internal currency"); + } + } catch (ClassNotFoundException e) { + // Vault not present - use internal currency + logger.info("Vault not found - using internal currency system"); + } catch (Exception e) { + logger.warn("Error initializing Vault integration - using internal currency: " + e.getMessage()); + } + } + + /** + * Check if Vault economy is available and enabled + */ + public boolean isVaultEnabled() { + return vaultAvailable && ConfigMarket.UseVaultIfAvailable; + } + + /** + * Get player's balance + * Uses Vault if available, otherwise internal currency + */ + public long getBalance(EntityPlayer player) { + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + Double balance = (Double) getBalance.invoke(economy, offlinePlayer); + return balance.longValue(); + } catch (Exception e) { + logger.warn("Vault getBalance failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + return data.currencyData.getBalance(); + } + + /** + * Deposit currency to player's balance + * Uses Vault if available, otherwise internal currency + * @return true if successful + */ + public boolean deposit(EntityPlayer player, long amount) { + if (amount <= 0) { + return false; + } + + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + + // Ensure account exists + Boolean hasAcc = (Boolean) hasAccount.invoke(economy, offlinePlayer); + if (!hasAcc) { + createPlayerAccount.invoke(economy, offlinePlayer); + } + + Object response = depositPlayer.invoke(economy, offlinePlayer, (double) amount); + // Check if transaction succeeded by checking the transactionSuccess field + Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); + + if (success) { + // Also update internal tracking for statistics + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(getBalance(player)); + return true; + } + return false; + } catch (Exception e) { + logger.warn("Vault deposit failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + boolean success = data.currencyData.deposit(amount); + if (success) { + data.saveCurrency(); + } + return success; + } + + /** + * Withdraw currency from player's balance + * Uses Vault if available, otherwise internal currency + * @return true if successful + */ + public boolean withdraw(EntityPlayer player, long amount) { + if (amount <= 0) { + return false; + } + + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + + // Check if player has enough + Double currentBalance = (Double) getBalance.invoke(economy, offlinePlayer); + if (currentBalance < amount) { + return false; + } + + Object response = withdrawPlayer.invoke(economy, offlinePlayer, (double) amount); + Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); + + if (success) { + // Also update internal tracking for statistics + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(getBalance(player)); + return true; + } + return false; + } catch (Exception e) { + logger.warn("Vault withdraw failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + boolean success = data.currencyData.withdraw(amount); + if (success) { + data.saveCurrency(); + } + return success; + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(EntityPlayer player, long amount) { + return getBalance(player) >= amount; + } + + /** + * Transfer currency from one player to another + * @return true if successful + */ + public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { + if (amount <= 0) { + return false; + } + + if (!canAfford(from, amount)) { + return false; + } + + // Withdraw from sender first + if (!withdraw(from, amount)) { + return false; + } + + // Deposit to receiver + if (!deposit(to, amount)) { + // Rollback - deposit back to sender + deposit(from, amount); + return false; + } + + return true; + } + + /** + * Set player's balance directly (admin operation) + * Only works with internal currency - Vault manages its own balances + */ + public void setBalance(EntityPlayer player, long amount) { + if (isVaultEnabled()) { + logger.warn("Cannot set balance directly when using Vault - use Vault's economy commands"); + return; + } + + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(amount); + data.saveCurrency(); + } + + /** + * Format an amount for display with currency name + */ + public String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Get the currency name + */ + public String getCurrencyName() { + return ConfigMarket.CurrencyName; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java new file mode 100644 index 000000000..511321dbb --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -0,0 +1,131 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigMarket; + +/** + * Stores player currency balance. + * This data is NOT profile slot bound - all profile slots share the same currency pool. + * When Vault API is available, this serves as a cache/fallback. + */ +public class PlayerCurrencyData { + private long balance; + private long lifetimeEarned; + private long lifetimeSpent; + + public PlayerCurrencyData() { + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + } + + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("CurrencyBalance")) { + this.balance = compound.getLong("CurrencyBalance"); + } else { + this.balance = ConfigMarket.StartingBalance; + } + this.lifetimeEarned = compound.getLong("CurrencyLifetimeEarned"); + this.lifetimeSpent = compound.getLong("CurrencyLifetimeSpent"); + } + + public void writeToNBT(NBTTagCompound compound) { + compound.setLong("CurrencyBalance", this.balance); + compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); + compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); + } + + /** + * Get the current balance + */ + public long getBalance() { + return balance; + } + + /** + * Set the balance directly (use with caution) + */ + public void setBalance(long balance) { + if (balance < 0) { + balance = 0; + } + if (balance > ConfigMarket.MaxBalance) { + balance = ConfigMarket.MaxBalance; + } + this.balance = balance; + } + + /** + * Add currency to balance (deposit) + * @param amount Amount to add + * @return true if successful, false if would exceed max balance + */ + public boolean deposit(long amount) { + if (amount <= 0) { + return false; + } + long newBalance = this.balance + amount; + if (newBalance > ConfigMarket.MaxBalance) { + return false; + } + if (newBalance < this.balance) { + // Overflow protection + return false; + } + this.balance = newBalance; + this.lifetimeEarned += amount; + return true; + } + + /** + * Remove currency from balance (withdraw) + * @param amount Amount to remove + * @return true if successful, false if insufficient funds + */ + public boolean withdraw(long amount) { + if (amount <= 0) { + return false; + } + if (this.balance < amount) { + return false; + } + this.balance -= amount; + this.lifetimeSpent += amount; + return true; + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(long amount) { + return this.balance >= amount; + } + + /** + * Get lifetime earned currency + */ + public long getLifetimeEarned() { + return lifetimeEarned; + } + + /** + * Get lifetime spent currency + */ + public long getLifetimeSpent() { + return lifetimeSpent; + } + + /** + * Format the balance for display + */ + public String formatBalance() { + return formatAmount(this.balance); + } + + /** + * Format an amount for display + */ + public static String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 599f81ff1..25d4c81c8 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -59,6 +59,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public DataSkinOverlays skinOverlays = new DataSkinOverlays(this); public MagicData magicData = new MagicData(); + // Currency data - NOT slot-bound, shared across all profile slots + public PlayerCurrencyData currencyData = new PlayerCurrencyData(); + public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -443,6 +446,69 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { data = getNBT(); } setNBT(data); + + // Load currency data separately (not slot-bound) + loadCurrency(); + } + + /** + * Load currency data from separate file (shared across profile slots) + */ + public void loadCurrency() { + if (player == null) return; + + File saveDir = PlayerDataController.Instance.getSaveDir(); + String filename = player.getPersistentID().toString() + "_currency"; + + try { + File file = new File(saveDir, filename + ".json"); + if (file.exists()) { + NBTTagCompound compound = NBTJsonUtil.LoadFile(file); + currencyData.readFromNBT(compound); + return; + } + + // Try dat format + file = new File(saveDir, filename + ".dat"); + if (file.exists()) { + NBTTagCompound compound = NBTJsonUtil.loadNBTData(file); + currencyData.readFromNBT(compound); + return; + } + + // No currency file exists, use defaults (new player) + currencyData = new PlayerCurrencyData(); + } catch (Exception e) { + LogWriter.error("Error loading currency data for " + player.getCommandSenderName(), e); + currencyData = new PlayerCurrencyData(); + } + } + + /** + * Save currency data to separate file (shared across profile slots) + */ + public synchronized void saveCurrency() { + if (uuid == null || uuid.isEmpty()) return; + + final NBTTagCompound compound = new NBTTagCompound(); + currencyData.writeToNBT(compound); + + final String filename = uuid + "_currency.json"; + + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + File saveDir = PlayerDataController.Instance.getSaveDir(); + File file = new File(saveDir, filename + "_new"); + File file1 = new File(saveDir, filename); + NBTJsonUtil.SaveFile(file, compound); + if (file1.exists()) { + file1.delete(); + } + file.renameTo(file1); + } catch (Exception e) { + LogWriter.except(e); + } + }); } public static PlayerData get(EntityPlayer player) { diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 6206cfbac..5bfaba816 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,11 +49,13 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.PlayerQuestController; import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.data.Dialog; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -895,4 +897,120 @@ public class ScriptPlayer extends ScriptLivingBase } return (IPlayer[]) list.toArray(new IPlayer[list.size()]); } + + // ==================== CURRENCY METHODS ==================== + + /** + * Get the player's current currency balance. + * Uses Vault if available, otherwise uses internal currency system. + * @return The player's currency balance + * @since 1.11 + */ + public long getCurrencyBalance() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.getBalance(player); + } + + /** + * Add currency to the player's balance. + * @param amount The amount to add (must be positive) + * @return true if successful, false if failed (e.g., would exceed max balance) + * @since 1.11 + */ + public boolean addCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.deposit(player, amount); + } + + /** + * Remove currency from the player's balance. + * @param amount The amount to remove (must be positive) + * @return true if successful, false if insufficient funds + * @since 1.11 + */ + public boolean removeCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.withdraw(player, amount); + } + + /** + * Check if the player can afford a specific amount. + * @param amount The amount to check + * @return true if player has sufficient balance + * @since 1.11 + */ + public boolean canAffordCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.canAfford(player, amount); + } + + /** + * Get the configured currency name (e.g., "Gold"). + * @return The currency name + * @since 1.11 + */ + public String getCurrencyName() { + if (VaultHelper.Instance == null) { + return "Currency"; + } + return VaultHelper.Instance.getCurrencyName(); + } + + /** + * Format a currency amount for display (e.g., "1,000 Gold"). + * @param amount The amount to format + * @return Formatted string + * @since 1.11 + */ + public String formatCurrency(long amount) { + if (CurrencyController.Instance == null) { + return String.valueOf(amount); + } + return CurrencyController.Instance.formatAmount(amount); + } + + /** + * Check if the player has pending currency claims. + * Claims are created by auction refunds, NPC payments, etc. + * @return true if there are pending claims + * @since 1.11 + */ + public boolean hasPendingClaims() { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.hasPendingClaims(player); + } + + /** + * Get the total amount of pending currency claims. + * @return Total pending claim amount + * @since 1.11 + */ + public long getPendingClaimsTotal() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.getTotalPendingClaims(player); + } + + /** + * Claim all pending currency claims and add them to balance. + * @return The total amount claimed + * @since 1.11 + */ + public long claimAllCurrency() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.claimAll(player); + } } From 60b985d33d0a3cde23a0f058578450b13031a051 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:23:50 +0000 Subject: [PATCH 06/64] Fix currency storage to use main PlayerData file - Currency now stored in main player save file, not separate file - Currency excluded from getNBT/setNBT (slot-specific data) - Currency saved in save() after getNBT(), loaded before setNBT() - This ensures currency is preserved during profile slot changes - Removed loadCurrency() and saveCurrency() separate file methods - Updated VaultHelper to call save() instead of saveCurrency() --- .../noppes/npcs/controllers/VaultHelper.java | 6 +- .../npcs/controllers/data/PlayerData.java | 69 +++---------------- 2 files changed, 11 insertions(+), 64 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java index ae2c6f97a..f660002fc 100644 --- a/src/main/java/noppes/npcs/controllers/VaultHelper.java +++ b/src/main/java/noppes/npcs/controllers/VaultHelper.java @@ -157,7 +157,7 @@ public class VaultHelper { PlayerData data = PlayerDataController.Instance.getPlayerData(player); boolean success = data.currencyData.deposit(amount); if (success) { - data.saveCurrency(); + data.save(); } return success; } @@ -201,7 +201,7 @@ public class VaultHelper { PlayerData data = PlayerDataController.Instance.getPlayerData(player); boolean success = data.currencyData.withdraw(amount); if (success) { - data.saveCurrency(); + data.save(); } return success; } @@ -253,7 +253,7 @@ public class VaultHelper { PlayerData data = PlayerDataController.Instance.getPlayerData(player); data.currencyData.setBalance(amount); - data.saveCurrency(); + data.save(); } /** diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 25d4c81c8..08fdebdd0 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -409,6 +409,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); + // Add currency data to main save (NOT slot-bound, preserved across profile changes) + currencyData.writeToNBT(compound); + final String filename; if (ConfigMain.DatFormat) { filename = uuid + ".dat"; @@ -445,70 +448,14 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (data.hasNoTags()) { data = getNBT(); } - setNBT(data); - // Load currency data separately (not slot-bound) - loadCurrency(); - } - - /** - * Load currency data from separate file (shared across profile slots) - */ - public void loadCurrency() { - if (player == null) return; - - File saveDir = PlayerDataController.Instance.getSaveDir(); - String filename = player.getPersistentID().toString() + "_currency"; - - try { - File file = new File(saveDir, filename + ".json"); - if (file.exists()) { - NBTTagCompound compound = NBTJsonUtil.LoadFile(file); - currencyData.readFromNBT(compound); - return; - } - - // Try dat format - file = new File(saveDir, filename + ".dat"); - if (file.exists()) { - NBTTagCompound compound = NBTJsonUtil.loadNBTData(file); - currencyData.readFromNBT(compound); - return; - } - - // No currency file exists, use defaults (new player) - currencyData = new PlayerCurrencyData(); - } catch (Exception e) { - LogWriter.error("Error loading currency data for " + player.getCommandSenderName(), e); - currencyData = new PlayerCurrencyData(); + // Load currency BEFORE setNBT so we can preserve it + // Currency is stored in main file but NOT in slot data + if (data.hasKey("CurrencyBalance")) { + currencyData.readFromNBT(data); } - } - /** - * Save currency data to separate file (shared across profile slots) - */ - public synchronized void saveCurrency() { - if (uuid == null || uuid.isEmpty()) return; - - final NBTTagCompound compound = new NBTTagCompound(); - currencyData.writeToNBT(compound); - - final String filename = uuid + "_currency.json"; - - CustomNPCsThreader.customNPCThread.execute(() -> { - try { - File saveDir = PlayerDataController.Instance.getSaveDir(); - File file = new File(saveDir, filename + "_new"); - File file1 = new File(saveDir, filename); - NBTJsonUtil.SaveFile(file, compound); - if (file1.exists()) { - file1.delete(); - } - file.renameTo(file1); - } catch (Exception e) { - LogWriter.except(e); - } - }); + setNBT(data); } public static PlayerData get(EntityPlayer player) { From c9f77bed05fc77fd5fb3ad722b5a5dfc24c934fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:29:03 +0000 Subject: [PATCH 07/64] Implement Phase 2: Trader Enhancements with stock and currency system - Add EnumStockReset.java for stock reset timing types - NONE, MCDAILY, MCWEEKLY, MCCUSTOM (MC time) - RLDAILY, RLWEEKLY, RLCUSTOM (real time) - Helper methods for interval calculation and display formatting - Add TraderStock.java for stock management - Per-slot stock limits with -1 for unlimited - Per-player vs per-server stock tracking modes - Automatic reset timer with configurable intervals - PlayerTraderStock inner class for per-player tracking - NBT serialization for persistence - Modify RoleTrader.java to support stock and currency - Add TraderStock instance for stock management - Add useCurrency flag and slotPrices array for virtual currency - Auto-check for stock reset on interact() - Helper methods: hasStock(), getAvailableStock(), consumeStock() - Helper methods: getSlotPrice(), setSlotPrice(), hasSlotPrice() - getTimeUntilResetFormatted() for GUI display - Modify ContainerNPCTrader.java for transactions - Check stock availability before purchase - Handle virtual currency purchases via CurrencyController - Fall back to item-based currency when useCurrency disabled - Consume stock after successful purchase --- .../noppes/npcs/constants/EnumStockReset.java | 64 ++++ .../npcs/containers/ContainerNPCTrader.java | 37 ++- .../npcs/controllers/data/TraderStock.java | 313 ++++++++++++++++++ .../java/noppes/npcs/roles/RoleTrader.java | 126 +++++++ 4 files changed, 535 insertions(+), 5 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/EnumStockReset.java create mode 100644 src/main/java/noppes/npcs/controllers/data/TraderStock.java diff --git a/src/main/java/noppes/npcs/constants/EnumStockReset.java b/src/main/java/noppes/npcs/constants/EnumStockReset.java new file mode 100644 index 000000000..cdc56cb95 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumStockReset.java @@ -0,0 +1,64 @@ +package noppes.npcs.constants; + +/** + * Stock reset types for Trader NPCs. + * Mirrors EnumQuestRepeat pattern for consistency. + */ +public enum EnumStockReset { + NONE, // Never resets + MCDAILY, // Every 24000 MC ticks (1 MC day) + MCWEEKLY, // Every 168000 MC ticks (7 MC days) + MCCUSTOM, // Custom MC tick interval + RLDAILY, // Every 24 real hours + RLWEEKLY, // Every 7 real days + RLCUSTOM; // Custom real-time interval + + /** + * Get reset interval in milliseconds for real-time types + * or ticks for MC-time types + */ + public long getDefaultInterval() { + switch (this) { + case MCDAILY: + return 24000; // MC ticks + case MCWEEKLY: + return 168000; // MC ticks (7 days) + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + default: + return 0; + } + } + + /** + * Check if this reset type uses real-world time + */ + public boolean isRealTime() { + return this == RLDAILY || this == RLWEEKLY || this == RLCUSTOM; + } + + /** + * Check if this reset type uses Minecraft time + */ + public boolean isMinecraftTime() { + return this == MCDAILY || this == MCWEEKLY || this == MCCUSTOM; + } + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case NONE: return "Never"; + case MCDAILY: return "MC Daily"; + case MCWEEKLY: return "MC Weekly"; + case MCCUSTOM: return "MC Custom"; + case RLDAILY: return "Real Daily"; + case RLWEEKLY: return "Real Weekly"; + case RLCUSTOM: return "Real Custom"; + default: return "Unknown"; + } + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index 71dcd6fa6..936868152 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -4,6 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; @@ -61,17 +62,43 @@ public class ContainerNPCTrader extends ContainerNpcInterface { ItemStack item = slot.getStack(); if (!canGivePlayer(item, entityplayer)) return null; - if (!canBuy(i, entityplayer)) - return null; if (!isSlotEnabled(i, entityplayer)) return null; - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + + // Check stock availability + String playerName = entityplayer.getCommandSenderName(); + if (!role.hasStock(i, playerName, 1)) { + return null; // Out of stock + } + + // Handle currency-based purchase + if (role.hasSlotPrice(i)) { + long price = role.getSlotPrice(i); + if (CurrencyController.Instance == null) { + return null; // Currency system not available + } + if (!CurrencyController.Instance.canAfford(entityplayer, price)) { + return null; // Can't afford + } + // Withdraw currency + if (!CurrencyController.Instance.withdraw(entityplayer, price)) { + return null; // Withdrawal failed + } + } else { + // Item-based currency - existing logic + if (!canBuy(i, entityplayer)) + return null; + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + } + + // Consume stock + role.consumeStock(i, playerName, 1); + ItemStack soldItem = item.copy(); givePlayer(soldItem, entityplayer); role.addPurchase(i, entityplayer.getDisplayName()); return soldItem; - } public boolean isSlotEnabled(int slot, EntityPlayer player) { diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java new file mode 100644 index 000000000..808b06f36 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -0,0 +1,313 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.constants.EnumStockReset; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manages trader stock system including: + * - Per-slot stock limits + * - Per-player vs per-server stock tracking + * - Automatic stock reset timers + */ +public class TraderStock { + // Stock configuration + public boolean enableStock = false; + public boolean perPlayer = false; // false = per-server, true = per-player + + // Reset configuration (mirrors Quest cooldown system) + public EnumStockReset resetType = EnumStockReset.NONE; + public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types (in ticks or ms) + + // Stock quantities per slot (max stock, -1 = unlimited) + public int[] maxStock = new int[18]; + + // Current stock (per-server mode) + public int[] currentStock = new int[18]; + public long lastResetTime = 0; + + // Per-player stock tracking + public HashMap playerStock = new HashMap<>(); + + public TraderStock() { + for (int i = 0; i < 18; i++) { + maxStock[i] = -1; // -1 = unlimited + currentStock[i] = -1; + } + } + + /** + * Get available stock for a slot + * @param slot The slot index + * @param playerName The player name (for per-player mode) + * @return Available stock count, or Integer.MAX_VALUE if unlimited + */ + public int getAvailableStock(int slot, String playerName) { + if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { + return Integer.MAX_VALUE; // Unlimited + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.get(playerName); + if (pStock == null) { + return maxStock[slot]; // Not purchased yet + } + return pStock.getStock(slot, maxStock[slot]); + } + + return Math.max(0, currentStock[slot]); + } + + /** + * Check if stock is available for purchase + */ + public boolean hasStock(int slot, String playerName, int amount) { + return getAvailableStock(slot, playerName) >= amount; + } + + /** + * Consume stock when purchase is made + * @return true if successful, false if insufficient stock + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { + return true; // Unlimited, always succeeds + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.computeIfAbsent( + playerName, k -> new PlayerTraderStock()); + return pStock.consumeStock(slot, maxStock[slot], amount); + } + + if (currentStock[slot] >= amount) { + currentStock[slot] -= amount; + return true; + } + return false; + } + + /** + * Check if stock should reset based on current time + * @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type + */ + public boolean shouldReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return false; + } + + long elapsed = currentTime - lastResetTime; + long resetInterval = getResetInterval(); + + return elapsed >= resetInterval; + } + + /** + * Reset all stock to max values + * @param currentTime Current time to record as reset time + */ + public void resetStock(long currentTime) { + lastResetTime = currentTime; + + // Reset per-server stock + for (int i = 0; i < 18; i++) { + currentStock[i] = maxStock[i]; + } + + // Clear per-player stock + playerStock.clear(); + } + + /** + * Get reset interval based on reset type + * @return Interval in ticks (MC time) or milliseconds (real time) + */ + public long getResetInterval() { + switch (resetType) { + case MCDAILY: + return 24000; // 1 MC day in ticks + case MCWEEKLY: + return 168000; // 7 MC days in ticks + case MCCUSTOM: + return customResetTime; // Custom ticks + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + case RLCUSTOM: + return customResetTime; // Custom ms + default: + return Long.MAX_VALUE; + } + } + + /** + * Get time until next reset + * @param currentTime Current time + * @return Time remaining until reset, or -1 if no reset scheduled + */ + public long getTimeUntilReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return -1; + } + long elapsed = currentTime - lastResetTime; + long interval = getResetInterval(); + return Math.max(0, interval - elapsed); + } + + /** + * Format time until reset for display + * @param currentTime Current time + * @return Formatted string like "2 hours" or "1 day, 5 hours" + */ + public String getTimeUntilResetFormatted(long currentTime) { + long remaining = getTimeUntilReset(currentTime); + if (remaining < 0) { + return ""; + } + + if (resetType.isRealTime()) { + // Convert ms to readable format + long hours = remaining / 3600000L; + long days = hours / 24; + hours = hours % 24; + + if (days > 0) { + return days + " day" + (days != 1 ? "s" : "") + + (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); + } else if (hours > 0) { + return hours + " hour" + (hours != 1 ? "s" : ""); + } else { + long minutes = remaining / 60000L; + return minutes + " minute" + (minutes != 1 ? "s" : ""); + } + } else { + // Convert ticks to MC time format + long mcHours = remaining / 1000; // 1000 ticks = 1 MC hour + long mcDays = mcHours / 24; + mcHours = mcHours % 24; + + if (mcDays > 0) { + return mcDays + " MC day" + (mcDays != 1 ? "s" : "") + + (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); + } else { + return mcHours + " MC hour" + (mcHours != 1 ? "s" : ""); + } + } + } + + /** + * Set stock for a specific slot + */ + public void setMaxStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + maxStock[slot] = amount; + if (currentStock[slot] < 0 || currentStock[slot] > amount) { + currentStock[slot] = amount; + } + } + } + + // ==================== NBT Serialization ==================== + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("EnableStock", enableStock); + compound.setBoolean("PerPlayer", perPlayer); + compound.setInteger("ResetType", resetType.ordinal()); + compound.setLong("CustomResetTime", customResetTime); + compound.setIntArray("MaxStock", maxStock); + compound.setIntArray("CurrentStock", currentStock); + compound.setLong("LastResetTime", lastResetTime); + + // Save per-player stock + NBTTagList playerList = new NBTTagList(); + for (Map.Entry entry : playerStock.entrySet()) { + NBTTagCompound playerTag = new NBTTagCompound(); + playerTag.setString("Player", entry.getKey()); + entry.getValue().writeToNBT(playerTag); + playerList.appendTag(playerTag); + } + compound.setTag("PlayerStock", playerList); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + enableStock = compound.getBoolean("EnableStock"); + perPlayer = compound.getBoolean("PerPlayer"); + + int resetOrdinal = compound.getInteger("ResetType"); + if (resetOrdinal >= 0 && resetOrdinal < EnumStockReset.values().length) { + resetType = EnumStockReset.values()[resetOrdinal]; + } + + customResetTime = compound.getLong("CustomResetTime"); + + int[] loadedMax = compound.getIntArray("MaxStock"); + int[] loadedCurrent = compound.getIntArray("CurrentStock"); + + if (loadedMax != null && loadedMax.length == 18) { + maxStock = loadedMax; + } + if (loadedCurrent != null && loadedCurrent.length == 18) { + currentStock = loadedCurrent; + } + + lastResetTime = compound.getLong("LastResetTime"); + + // Load per-player stock + playerStock.clear(); + NBTTagList playerList = compound.getTagList("PlayerStock", 10); + for (int i = 0; i < playerList.tagCount(); i++) { + NBTTagCompound playerTag = playerList.getCompoundTagAt(i); + String playerName = playerTag.getString("Player"); + PlayerTraderStock pStock = new PlayerTraderStock(); + pStock.readFromNBT(playerTag); + playerStock.put(playerName, pStock); + } + } + + // ==================== Inner Class for Per-Player Stock ==================== + + public static class PlayerTraderStock { + public int[] purchasedAmounts = new int[18]; + public long lastPurchaseTime = 0; + + public PlayerTraderStock() { + for (int i = 0; i < 18; i++) { + purchasedAmounts[i] = 0; + } + } + + public int getStock(int slot, int maxStock) { + if (slot < 0 || slot >= 18) return 0; + return Math.max(0, maxStock - purchasedAmounts[slot]); + } + + public boolean consumeStock(int slot, int maxStock, int amount) { + if (slot < 0 || slot >= 18) return false; + if (getStock(slot, maxStock) >= amount) { + purchasedAmounts[slot] += amount; + lastPurchaseTime = System.currentTimeMillis(); + return true; + } + return false; + } + + public void writeToNBT(NBTTagCompound compound) { + compound.setIntArray("Purchased", purchasedAmounts); + compound.setLong("LastPurchase", lastPurchaseTime); + } + + public void readFromNBT(NBTTagCompound compound) { + int[] loaded = compound.getIntArray("Purchased"); + if (loaded != null && loaded.length == 18) { + purchasedAmounts = loaded; + } + lastPurchaseTime = compound.getLong("LastPurchase"); + } + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 5275c26c0..6215eef2f 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -4,11 +4,13 @@ import foxz.utils.Market; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; import noppes.npcs.NBTTags; import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.NoppesUtilServer; import noppes.npcs.NpcMiscInventory; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.data.TraderStock; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; @@ -30,6 +32,13 @@ public class RoleTrader extends RoleInterface { public HashMap playerPurchases; public HashMap playerDisableSlot; + // Stock system + public TraderStock stock = new TraderStock(); + + // Currency system + public boolean useCurrency = false; // Use virtual currency instead of items + public long[] slotPrices = new long[18]; // Currency prices per slot + public RoleTrader(EntityNPCInterface npc) { super(npc); inventoryCurrency = new NpcMiscInventory(36); @@ -59,6 +68,20 @@ public class RoleTrader extends RoleInterface { nbttagcompound.setIntArray("Purchases", purchases); nbttagcompound.setTag("PlayerPurchases", NBTTags.nbtStringIntegerArrayMap(playerPurchases)); } + + // Stock system + nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); + + // Currency system + nbttagcompound.setBoolean("UseCurrency", useCurrency); + NBTTagList priceList = new NBTTagList(); + for (long price : slotPrices) { + NBTTagCompound priceTag = new NBTTagCompound(); + priceTag.setLong("Price", price); + priceList.appendTag(priceTag); + } + nbttagcompound.setTag("SlotPrices", priceList); + return nbttagcompound; } @@ -94,10 +117,39 @@ public class RoleTrader extends RoleInterface { disableSlot = new int[18]; for (int i = 0; i < disableSlot.length; ++i) disableSlot[i] = 0; } + + // Stock system + if (nbttagcompound.hasKey("Stock")) { + stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); + } + + // Currency system + useCurrency = nbttagcompound.getBoolean("UseCurrency"); + if (nbttagcompound.hasKey("SlotPrices")) { + NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); + for (int i = 0; i < priceList.tagCount() && i < 18; i++) { + slotPrices[i] = priceList.getCompoundTagAt(i).getLong("Price"); + } + } } @Override public void interact(EntityPlayer player) { + // Check for stock reset before opening GUI + if (stock.enableStock) { + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + // Save to market if using shared market + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + } + } + npc.say(player, npc.advanced.getInteractLine()); try { Market.getMarket(this, marketName); @@ -138,4 +190,78 @@ public class RoleTrader extends RoleInterface { return map.get(name); } + // ==================== Stock System Methods ==================== + + /** + * Check if there is stock available for a slot + */ + public boolean hasStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled, always available + } + return stock.hasStock(slot, playerName, amount); + } + + /** + * Get available stock for a slot + */ + public int getAvailableStock(int slot, String playerName) { + if (!stock.enableStock) { + return Integer.MAX_VALUE; // Unlimited + } + return stock.getAvailableStock(slot, playerName); + } + + /** + * Consume stock after purchase + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled + } + boolean consumed = stock.consumeStock(slot, playerName, amount); + if (consumed && !marketName.isEmpty()) { + Market.save(this, marketName); + } + return consumed; + } + + /** + * Get time until stock reset formatted for display + */ + public String getTimeUntilResetFormatted() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return ""; + } + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + return stock.getTimeUntilResetFormatted(currentTime); + } + + // ==================== Currency System Methods ==================== + + /** + * Get the currency price for a slot + */ + public long getSlotPrice(int slot) { + if (slot < 0 || slot >= 18) return 0; + return slotPrices[slot]; + } + + /** + * Set the currency price for a slot + */ + public void setSlotPrice(int slot, long price) { + if (slot >= 0 && slot < 18) { + slotPrices[slot] = Math.max(0, price); + } + } + + /** + * Check if a slot has a currency price set + */ + public boolean hasSlotPrice(int slot) { + return useCurrency && slot >= 0 && slot < 18 && slotPrices[slot] > 0; + } } From dc9d15f3e3e0086535030bc961607368740c7bec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:34:45 +0000 Subject: [PATCH 08/64] Implement Phase 3: Item Attributes & Restrictions - Add ItemTradeAttribute.java for trade restriction utilities - TAG_UNTRADEABLE for items that cannot be traded - Check existing TAG_PROFILE_SLOT and TAG_SOULBIND tags - canTrade() checks all three restriction types - getTradeBlockReason() returns localization key for UI - Utility methods for setting/getting each restriction type - clearAllRestrictions() and getRestrictionSummary() helpers - Add AuctionBlacklist.java controller - Blacklist items by registry name (e.g., "minecraft:diamond_sword") - Blacklist entire mods by mod ID (e.g., "minecraft") - Blacklist items with specific NBT tags (supports dot notation) - isBlacklisted() and getBlacklistReason() for checking - Persistence via auction_blacklist.dat - Add/remove methods for each blacklist type - Initialize AuctionBlacklist in CustomNpcs.java - Add scripting API methods to ScriptItemStack.java - canTrade() - check if item can be traded - getTradeBlockReason() - get block reason - isUntradeable() / setUntradeable() - manage untradeable flag - isProfileSlotBound() / getProfileSlot() / setProfileSlotBound() - isSoulbound() / getSoulbindOwner() / removeSoulbind() - clearTradeRestrictions() - remove all restrictions - getRestrictionSummary() - human-readable summary --- .../data/attribute/ItemTradeAttribute.java | 308 +++++++++++++ src/main/java/noppes/npcs/CustomNpcs.java | 2 + .../npcs/controllers/AuctionBlacklist.java | 419 ++++++++++++++++++ .../npcs/scripted/item/ScriptItemStack.java | 114 +++++ 4 files changed, 843 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionBlacklist.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java b/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java new file mode 100644 index 000000000..180f41d02 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java @@ -0,0 +1,308 @@ +package kamkeel.npcs.controllers.data.attribute; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.data.PlayerData; + +import java.util.UUID; + +/** + * Utility class for managing item trade restrictions. + * Checks and sets attributes that control whether items can be traded/auctioned. + */ +public class ItemTradeAttribute { + + // New untradeable attribute - prevents all trading + public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; + + // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) + public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; + public static final String TAG_SOULBIND = "cnpc_soulbind"; + + // ==================== Trade Check Methods ==================== + + /** + * Check if item can be traded/auctioned by the given player. + * An item cannot be traded if: + * - It has the Untradeable flag + * - It is bound to a specific profile slot (ProfileSlot Bound) + * - It is soulbound to a different player + * + * @param item The item to check + * @param player The player trying to trade (null to check without player context) + * @return true if the item can be traded + */ + public static boolean canTrade(ItemStack item, EntityPlayer player) { + if (item == null) { + return true; + } + if (!item.hasTagCompound()) { + return true; + } + + NBTTagCompound tag = item.getTagCompound(); + + // Check untradeable flag - always blocks trading + if (tag.getBoolean(TAG_UNTRADEABLE)) { + return false; + } + + // Check profile slot bound - blocks trading as it's tied to a slot + if (tag.hasKey(TAG_PROFILE_SLOT)) { + return false; + } + + // Check soulbound - blocks trading as it's tied to a player + if (tag.hasKey(TAG_SOULBIND)) { + // If we have a player context, check if they're the owner + if (player != null) { + String soulbindUUID = tag.getString(TAG_SOULBIND); + if (soulbindUUID != null && !soulbindUUID.isEmpty()) { + // Even if bound to this player, still can't trade + return false; + } + } + return false; + } + + return true; + } + + /** + * Check if item can be traded (without player context) + */ + public static boolean canTrade(ItemStack item) { + return canTrade(item, null); + } + + /** + * Get the reason why an item cannot be traded. + * Returns a translation key for localization. + * + * @param item The item to check + * @return Translation key for the block reason, or null if tradeable + */ + public static String getTradeBlockReason(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return null; + } + + NBTTagCompound tag = item.getTagCompound(); + + if (tag.getBoolean(TAG_UNTRADEABLE)) { + return "gui.auction.untradeable"; + } + if (tag.hasKey(TAG_PROFILE_SLOT)) { + return "gui.auction.profileslotbound"; + } + if (tag.hasKey(TAG_SOULBIND)) { + return "gui.auction.soulbound"; + } + + return null; + } + + // ==================== Untradeable Methods ==================== + + /** + * Check if item is marked as untradeable + */ + public static boolean isUntradeable(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return false; + } + return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); + } + + /** + * Set the untradeable flag on an item + */ + public static void setUntradeable(ItemStack item, boolean untradeable) { + if (item == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + if (untradeable) { + item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); + } else { + item.getTagCompound().removeTag(TAG_UNTRADEABLE); + } + } + + // ==================== Profile Slot Bound Methods ==================== + + /** + * Check if item is bound to a profile slot + */ + public static boolean isProfileSlotBound(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return false; + } + return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); + } + + /** + * Get the profile slot an item is bound to + * @return The profile slot ID, or -1 if not bound + */ + public static int getProfileSlot(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return -1; + } + if (item.getTagCompound().hasKey(TAG_PROFILE_SLOT)) { + return item.getTagCompound().getInteger(TAG_PROFILE_SLOT); + } + return -1; + } + + /** + * Bind an item to a specific profile slot + */ + public static void setProfileSlotBound(ItemStack item, int slot) { + if (item == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + if (slot >= 0) { + item.getTagCompound().setInteger(TAG_PROFILE_SLOT, slot); + } else { + item.getTagCompound().removeTag(TAG_PROFILE_SLOT); + } + } + + /** + * Bind an item to the player's current profile slot + */ + public static void bindToCurrentSlot(ItemStack item, EntityPlayer player) { + if (item == null || player == null) { + return; + } + PlayerData data = PlayerData.get(player); + if (data != null) { + setProfileSlotBound(item, data.profileSlot); + } + } + + // ==================== Soulbind Methods ==================== + + /** + * Check if item is soulbound to any player + */ + public static boolean isSoulbound(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return false; + } + return item.getTagCompound().hasKey(TAG_SOULBIND); + } + + /** + * Check if item is soulbound to a specific player + */ + public static boolean isSoulboundTo(ItemStack item, EntityPlayer player) { + if (item == null || player == null || !item.hasTagCompound()) { + return false; + } + if (!item.getTagCompound().hasKey(TAG_SOULBIND)) { + return false; + } + String soulbindUUID = item.getTagCompound().getString(TAG_SOULBIND); + return player.getUniqueID().toString().equals(soulbindUUID); + } + + /** + * Get the UUID of the player the item is soulbound to + * @return The UUID string, or null if not soulbound + */ + public static String getSoulbindOwner(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return null; + } + if (item.getTagCompound().hasKey(TAG_SOULBIND)) { + return item.getTagCompound().getString(TAG_SOULBIND); + } + return null; + } + + /** + * Soulbind an item to a player + */ + public static void setSoulbound(ItemStack item, EntityPlayer player) { + if (item == null || player == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + item.getTagCompound().setString(TAG_SOULBIND, player.getUniqueID().toString()); + } + + /** + * Soulbind an item to a player by UUID + */ + public static void setSoulbound(ItemStack item, UUID playerUUID) { + if (item == null || playerUUID == null) { + return; + } + if (!item.hasTagCompound()) { + item.setTagCompound(new NBTTagCompound()); + } + item.getTagCompound().setString(TAG_SOULBIND, playerUUID.toString()); + } + + /** + * Remove soulbind from an item + */ + public static void removeSoulbind(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return; + } + item.getTagCompound().removeTag(TAG_SOULBIND); + } + + // ==================== Utility Methods ==================== + + /** + * Clear all trade restriction attributes from an item + */ + public static void clearAllRestrictions(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return; + } + NBTTagCompound tag = item.getTagCompound(); + tag.removeTag(TAG_UNTRADEABLE); + tag.removeTag(TAG_PROFILE_SLOT); + tag.removeTag(TAG_SOULBIND); + } + + /** + * Get a display string describing all trade restrictions on an item + * @return Human-readable string of restrictions, or empty if none + */ + public static String getRestrictionSummary(ItemStack item) { + if (item == null || !item.hasTagCompound()) { + return ""; + } + + StringBuilder summary = new StringBuilder(); + NBTTagCompound tag = item.getTagCompound(); + + if (tag.getBoolean(TAG_UNTRADEABLE)) { + summary.append("Untradeable"); + } + if (tag.hasKey(TAG_PROFILE_SLOT)) { + if (summary.length() > 0) summary.append(", "); + summary.append("Profile Slot Bound (Slot ").append(tag.getInteger(TAG_PROFILE_SLOT)).append(")"); + } + if (tag.hasKey(TAG_SOULBIND)) { + if (summary.length() > 0) summary.append(", "); + summary.append("Soulbound"); + } + + return summary.toString(); + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 422be3e61..dbdee9e67 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -60,6 +60,7 @@ import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.AuctionBlacklist; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -308,6 +309,7 @@ public class CustomNpcs { public void started(FMLServerStartedEvent event) { new VaultHelper(); new CurrencyController(); + new AuctionBlacklist(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java new file mode 100644 index 000000000..020bb0bcb --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java @@ -0,0 +1,419 @@ +package noppes.npcs.controllers; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import noppes.npcs.CustomNpcs; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashSet; +import java.util.Set; + +/** + * Controller for managing auction house item blacklisting. + * Allows administrators to block specific items, entire mods, + * or items with certain NBT tags from being listed. + */ +public class AuctionBlacklist { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static AuctionBlacklist Instance; + + // Blacklist by item registry name (e.g., "minecraft:diamond_sword") + private Set blacklistedItems = new HashSet<>(); + + // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) + private Set blacklistedMods = new HashSet<>(); + + // Blacklist by NBT tag presence (e.g., items with specific custom data) + private Set blacklistedNBTTags = new HashSet<>(); + + public AuctionBlacklist() { + Instance = this; + load(); + } + + // ==================== Blacklist Checking ==================== + + /** + * Check if an item is blacklisted from the auction house. + * + * @param item The item to check + * @return true if the item is blacklisted + */ + public boolean isBlacklisted(ItemStack item) { + if (item == null || item.getItem() == null) { + return false; + } + + // Check item registry name + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null) { + // Direct item blacklist + if (blacklistedItems.contains(itemName)) { + return true; + } + + // Also check with metadata for specific variants + String itemNameWithMeta = itemName + ":" + item.getItemDamage(); + if (blacklistedItems.contains(itemNameWithMeta)) { + return true; + } + + // Check mod ID + String modId = getModIdFromName(itemName); + if (modId != null && blacklistedMods.contains(modId)) { + return true; + } + } + + // Check NBT tags + if (item.hasTagCompound()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtTag : blacklistedNBTTags) { + if (hasNestedTag(tag, nbtTag)) { + return true; + } + } + } + + return false; + } + + /** + * Get the reason why an item is blacklisted + * @return Description of why blacklisted, or null if not blacklisted + */ + public String getBlacklistReason(ItemStack item) { + if (item == null || item.getItem() == null) { + return null; + } + + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null) { + if (blacklistedItems.contains(itemName)) { + return "Item is blacklisted: " + itemName; + } + + String itemNameWithMeta = itemName + ":" + item.getItemDamage(); + if (blacklistedItems.contains(itemNameWithMeta)) { + return "Item variant is blacklisted: " + itemNameWithMeta; + } + + String modId = getModIdFromName(itemName); + if (modId != null && blacklistedMods.contains(modId)) { + return "Mod is blacklisted: " + modId; + } + } + + if (item.hasTagCompound()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtTag : blacklistedNBTTags) { + if (hasNestedTag(tag, nbtTag)) { + return "Item has blacklisted NBT tag: " + nbtTag; + } + } + } + + return null; + } + + /** + * Extract mod ID from registry name (e.g., "minecraft" from "minecraft:stone") + */ + private String getModIdFromName(String registryName) { + if (registryName == null) { + return null; + } + int colonIndex = registryName.indexOf(':'); + if (colonIndex > 0) { + return registryName.substring(0, colonIndex); + } + return null; + } + + /** + * Check for nested NBT tag (supports dot notation like "display.Name") + */ + private boolean hasNestedTag(NBTTagCompound tag, String path) { + if (tag == null || path == null || path.isEmpty()) { + return false; + } + + String[] parts = path.split("\\."); + NBTTagCompound current = tag; + + for (int i = 0; i < parts.length - 1; i++) { + if (!current.hasKey(parts[i])) { + return false; + } + if (current.hasKey(parts[i], 10)) { // 10 = Compound tag + current = current.getCompoundTag(parts[i]); + } else { + return false; + } + } + + return current.hasKey(parts[parts.length - 1]); + } + + // ==================== Item Blacklist Management ==================== + + /** + * Add an item to the blacklist by registry name + * @param itemName Registry name (e.g., "minecraft:diamond_sword" or "minecraft:wool:14") + */ + public void addItem(String itemName) { + if (itemName != null && !itemName.isEmpty()) { + blacklistedItems.add(itemName); + save(); + logger.info("Added item to auction blacklist: " + itemName); + } + } + + /** + * Add an item to the blacklist from an ItemStack + */ + public void addItem(ItemStack item, boolean includeMeta) { + if (item == null || item.getItem() == null) { + return; + } + String itemName = Item.itemRegistry.getNameForObject(item.getItem()); + if (itemName != null) { + if (includeMeta && item.getHasSubtypes()) { + addItem(itemName + ":" + item.getItemDamage()); + } else { + addItem(itemName); + } + } + } + + /** + * Remove an item from the blacklist + */ + public void removeItem(String itemName) { + if (blacklistedItems.remove(itemName)) { + save(); + logger.info("Removed item from auction blacklist: " + itemName); + } + } + + /** + * Check if a specific item is blacklisted + */ + public boolean isItemBlacklisted(String itemName) { + return blacklistedItems.contains(itemName); + } + + /** + * Get all blacklisted items + */ + public Set getBlacklistedItems() { + return new HashSet<>(blacklistedItems); + } + + // ==================== Mod Blacklist Management ==================== + + /** + * Add an entire mod to the blacklist + * @param modId The mod ID (e.g., "minecraft", "customnpcs") + */ + public void addMod(String modId) { + if (modId != null && !modId.isEmpty()) { + blacklistedMods.add(modId); + save(); + logger.info("Added mod to auction blacklist: " + modId); + } + } + + /** + * Remove a mod from the blacklist + */ + public void removeMod(String modId) { + if (blacklistedMods.remove(modId)) { + save(); + logger.info("Removed mod from auction blacklist: " + modId); + } + } + + /** + * Check if a mod is blacklisted + */ + public boolean isModBlacklisted(String modId) { + return blacklistedMods.contains(modId); + } + + /** + * Get all blacklisted mods + */ + public Set getBlacklistedMods() { + return new HashSet<>(blacklistedMods); + } + + // ==================== NBT Tag Blacklist Management ==================== + + /** + * Add an NBT tag to the blacklist + * Items containing this tag will be blocked + * @param tagPath The tag path (e.g., "display.Name" or "CustomData") + */ + public void addNBTTag(String tagPath) { + if (tagPath != null && !tagPath.isEmpty()) { + blacklistedNBTTags.add(tagPath); + save(); + logger.info("Added NBT tag to auction blacklist: " + tagPath); + } + } + + /** + * Remove an NBT tag from the blacklist + */ + public void removeNBTTag(String tagPath) { + if (blacklistedNBTTags.remove(tagPath)) { + save(); + logger.info("Removed NBT tag from auction blacklist: " + tagPath); + } + } + + /** + * Check if an NBT tag is blacklisted + */ + public boolean isNBTTagBlacklisted(String tagPath) { + return blacklistedNBTTags.contains(tagPath); + } + + /** + * Get all blacklisted NBT tags + */ + public Set getBlacklistedNBTTags() { + return new HashSet<>(blacklistedNBTTags); + } + + // ==================== Persistence ==================== + + public void save() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + try { + NBTTagCompound compound = new NBTTagCompound(); + + // Save blacklisted items + NBTTagList itemList = new NBTTagList(); + for (String item : blacklistedItems) { + itemList.appendTag(new NBTTagString(item)); + } + compound.setTag("Items", itemList); + + // Save blacklisted mods + NBTTagList modList = new NBTTagList(); + for (String mod : blacklistedMods) { + modList.appendTag(new NBTTagString(mod)); + } + compound.setTag("Mods", modList); + + // Save blacklisted NBT tags + NBTTagList nbtList = new NBTTagList(); + for (String nbt : blacklistedNBTTags) { + nbtList.appendTag(new NBTTagString(nbt)); + } + compound.setTag("NBTTags", nbtList); + + // Write to file + File file = new File(saveDir, "auction_blacklist.dat"); + File backup = new File(saveDir, "auction_blacklist.dat_old"); + + if (file.exists()) { + if (backup.exists()) { + backup.delete(); + } + file.renameTo(backup); + } + + file = new File(saveDir, "auction_blacklist.dat"); + FileOutputStream fos = new FileOutputStream(file); + CompressedStreamTools.writeCompressed(compound, fos); + fos.close(); + + } catch (Exception e) { + logger.error("Error saving auction blacklist", e); + } + } + + public void load() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + blacklistedItems.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + + try { + File file = new File(saveDir, "auction_blacklist.dat"); + if (!file.exists()) { + // Try backup + file = new File(saveDir, "auction_blacklist.dat_old"); + if (!file.exists()) { + return; + } + } + + FileInputStream fis = new FileInputStream(file); + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + fis.close(); + + // Load blacklisted items + NBTTagList itemList = compound.getTagList("Items", 8); // 8 = String tag + for (int i = 0; i < itemList.tagCount(); i++) { + blacklistedItems.add(itemList.getStringTagAt(i)); + } + + // Load blacklisted mods + NBTTagList modList = compound.getTagList("Mods", 8); + for (int i = 0; i < modList.tagCount(); i++) { + blacklistedMods.add(modList.getStringTagAt(i)); + } + + // Load blacklisted NBT tags + NBTTagList nbtList = compound.getTagList("NBTTags", 8); + for (int i = 0; i < nbtList.tagCount(); i++) { + blacklistedNBTTags.add(nbtList.getStringTagAt(i)); + } + + logger.info("Loaded auction blacklist: {} items, {} mods, {} NBT tags", + blacklistedItems.size(), blacklistedMods.size(), blacklistedNBTTags.size()); + + } catch (Exception e) { + logger.error("Error loading auction blacklist", e); + } + } + + /** + * Clear all blacklist entries + */ + public void clearAll() { + blacklistedItems.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + save(); + logger.info("Cleared all auction blacklist entries"); + } + + /** + * Get total number of blacklist entries + */ + public int getTotalEntries() { + return blacklistedItems.size() + blacklistedMods.size() + blacklistedNBTTags.size(); + } +} diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java index 6736369b9..a2588059f 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java @@ -1,6 +1,7 @@ package noppes.npcs.scripted.item; import com.google.common.collect.Multimap; +import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; import kamkeel.npcs.util.AttributeItemUtil; @@ -592,4 +593,117 @@ public class ScriptItemStack implements IItemStack { Set keys = reqTag.func_150296_c(); return keys.toArray(new String[keys.size()]); } + + // ==================== Trade Restriction Methods ==================== + + /** + * Check if this item can be traded or auctioned. + * An item cannot be traded if it is: + * - Marked as untradeable + * - Bound to a profile slot + * - Soulbound to a player + * @return true if the item can be traded + * @since 1.11 + */ + public boolean canTrade() { + return ItemTradeAttribute.canTrade(item); + } + + /** + * Get the reason why this item cannot be traded. + * @return Localization key for the block reason, or null if tradeable + * @since 1.11 + */ + public String getTradeBlockReason() { + return ItemTradeAttribute.getTradeBlockReason(item); + } + + /** + * Check if this item is marked as untradeable + * @return true if untradeable + * @since 1.11 + */ + public boolean isUntradeable() { + return ItemTradeAttribute.isUntradeable(item); + } + + /** + * Set whether this item is untradeable + * @param untradeable true to mark as untradeable + * @since 1.11 + */ + public void setUntradeable(boolean untradeable) { + ItemTradeAttribute.setUntradeable(item, untradeable); + } + + /** + * Check if this item is bound to a profile slot + * @return true if profile slot bound + * @since 1.11 + */ + public boolean isProfileSlotBound() { + return ItemTradeAttribute.isProfileSlotBound(item); + } + + /** + * Get the profile slot this item is bound to + * @return Profile slot ID, or -1 if not bound + * @since 1.11 + */ + public int getProfileSlot() { + return ItemTradeAttribute.getProfileSlot(item); + } + + /** + * Bind this item to a specific profile slot + * @param slot The profile slot ID, or -1 to unbind + * @since 1.11 + */ + public void setProfileSlotBound(int slot) { + ItemTradeAttribute.setProfileSlotBound(item, slot); + } + + /** + * Check if this item is soulbound to any player + * @return true if soulbound + * @since 1.11 + */ + public boolean isSoulbound() { + return ItemTradeAttribute.isSoulbound(item); + } + + /** + * Get the UUID of the player this item is soulbound to + * @return UUID string, or null if not soulbound + * @since 1.11 + */ + public String getSoulbindOwner() { + return ItemTradeAttribute.getSoulbindOwner(item); + } + + /** + * Remove the soulbind from this item + * @since 1.11 + */ + public void removeSoulbind() { + ItemTradeAttribute.removeSoulbind(item); + } + + /** + * Clear all trade restrictions from this item + * (untradeable, profile slot bound, soulbound) + * @since 1.11 + */ + public void clearTradeRestrictions() { + ItemTradeAttribute.clearAllRestrictions(item); + } + + /** + * Get a summary of all trade restrictions on this item + * @return Human-readable string of restrictions, or empty if none + * @since 1.11 + */ + public String getRestrictionSummary() { + return ItemTradeAttribute.getRestrictionSummary(item); + } } From c1dc58ae54397aedf0d4c5e134f126a86139be64 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:43:55 +0000 Subject: [PATCH 09/64] Implement Phase 4: Auction House core system - Add auction enums: - EnumAuctionDuration.java - configurable auction durations (SHORT/MEDIUM/LONG/VERY_LONG) - EnumAuctionStatus.java - listing statuses (ACTIVE/SOLD/EXPIRED/CANCELLED/CLAIMED) - EnumAuctionCategory.java - auto-categorization of items for browsing - Add AuctionListing.java data class: - Complete auction listing with seller/bidder tracking - Pricing with starting price, buyout price, current bid - Timing with snipe protection (extends TO X minutes, not by) - Item validation (checks trade restrictions and blacklist) - Previous bidder tracking for refund notifications - NBT serialization for persistence - Add AuctionController.java: - Global auction house management - Create/cancel/finalize listings - Bid placement with automatic outbid refunds via claims system - Buyout support with immediate sale - Claim system for sellers (proceeds) and buyers (items) - Search and filter by category/name - Periodic expiration processing - Persistence via auctions.dat - Add RoleAuctioneer.java NPC role: - Configurable tabs (listings, my listings, my bids, claims) - Configurable permissions (create, bid, buyout) - Custom welcome/disabled messages - Opens auction GUI on interaction - Update ConfigMarket.java: - Duration-specific settings (hours and fees) - Removed deprecated AvailableDurations string - Update EnumRoleType.java - add Auctioneer - Update EnumGuiType.java - add PlayerAuction, SetupAuctioneer - Update DataAdvanced.java - handle Auctioneer role creation - Update CustomNpcs.java - initialize AuctionController --- src/main/java/noppes/npcs/CustomNpcs.java | 2 + src/main/java/noppes/npcs/DataAdvanced.java | 3 + .../java/noppes/npcs/config/ConfigMarket.java | 74 +- .../npcs/constants/EnumAuctionCategory.java | 156 +++++ .../npcs/constants/EnumAuctionDuration.java | 68 ++ .../npcs/constants/EnumAuctionStatus.java | 54 ++ .../noppes/npcs/constants/EnumGuiType.java | 4 +- .../noppes/npcs/constants/EnumRoleType.java | 2 +- .../npcs/controllers/AuctionController.java | 662 ++++++++++++++++++ .../npcs/controllers/data/AuctionListing.java | 392 +++++++++++ .../noppes/npcs/roles/RoleAuctioneer.java | 134 ++++ 11 files changed, 1522 insertions(+), 29 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionCategory.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionDuration.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionStatus.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionController.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionListing.java create mode 100644 src/main/java/noppes/npcs/roles/RoleAuctioneer.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index dbdee9e67..c29fa4253 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -61,6 +61,7 @@ import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.AuctionBlacklist; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -310,6 +311,7 @@ public class CustomNpcs { new VaultHelper(); new CurrencyController(); new AuctionBlacklist(); + new AuctionController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/DataAdvanced.java b/src/main/java/noppes/npcs/DataAdvanced.java index 45ecfe4a8..f03714506 100644 --- a/src/main/java/noppes/npcs/DataAdvanced.java +++ b/src/main/java/noppes/npcs/DataAdvanced.java @@ -18,6 +18,7 @@ import noppes.npcs.roles.JobGuard; import noppes.npcs.roles.JobHealer; import noppes.npcs.roles.JobItemGiver; import noppes.npcs.roles.JobSpawner; +import noppes.npcs.roles.RoleAuctioneer; import noppes.npcs.roles.RoleBank; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; @@ -218,6 +219,8 @@ public class DataAdvanced { npc.roleInterface = new RoleCompanion(npc); else if (role == EnumRoleType.Mount && !(npc.roleInterface instanceof RoleMount)) npc.roleInterface = new RoleMount(npc); + else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) + npc.roleInterface = new RoleAuctioneer(npc); } public void setJob(int i) { diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 9c5791929..81eda2c3f 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -40,13 +40,23 @@ public class ConfigMarket { public static double ListingFeePercent = 0.0; public static double SalesTaxPercent = 0.05; public static boolean RequireAuctioneerNPC = true; - public static String AvailableDurations = "2,12,24,48"; - public static int DefaultDurationHours = 24; public static int ClaimExpirationDays = 30; public static double MinBidIncrementPercent = 0.05; public static int SnipeProtectionMinutes = 5; public static int SnipeProtectionThreshold = 5; + // Auction Duration Settings (in hours) + public static int AuctionDurationShort = 2; + public static int AuctionDurationMedium = 12; + public static int AuctionDurationLong = 24; + public static int AuctionDurationVeryLong = 48; + + // Auction Duration Fees (flat currency amount) + public static long AuctionFeeShort = 0; + public static long AuctionFeeMedium = 0; + public static long AuctionFeeLong = 0; + public static long AuctionFeeVeryLong = 0; + // ========================================= // Trader Settings // ========================================= @@ -108,12 +118,6 @@ public class ConfigMarket { RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, "Require an Auctioneer NPC to access auction house").getBoolean(true); - AvailableDurations = config.get(AUCTION, "Available Durations", "2,12,24,48", - "Available auction durations in hours (comma-separated)").getString(); - - DefaultDurationHours = config.get(AUCTION, "Default Duration Hours", 24, - "Default auction duration in hours").getInt(24); - ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); @@ -126,6 +130,32 @@ public class ConfigMarket { SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, "How many minutes before end triggers snipe protection").getInt(5); + // Duration settings + AuctionDurationShort = config.get(AUCTION, "Duration Short Hours", 2, + "Short auction duration in hours").getInt(2); + + AuctionDurationMedium = config.get(AUCTION, "Duration Medium Hours", 12, + "Medium auction duration in hours").getInt(12); + + AuctionDurationLong = config.get(AUCTION, "Duration Long Hours", 24, + "Long auction duration in hours").getInt(24); + + AuctionDurationVeryLong = config.get(AUCTION, "Duration Very Long Hours", 48, + "Very long auction duration in hours").getInt(48); + + // Duration fees + AuctionFeeShort = config.get(AUCTION, "Fee Short Duration", 0, + "Flat fee for short duration auctions").getInt(0); + + AuctionFeeMedium = config.get(AUCTION, "Fee Medium Duration", 0, + "Flat fee for medium duration auctions").getInt(0); + + AuctionFeeLong = config.get(AUCTION, "Fee Long Duration", 0, + "Flat fee for long duration auctions").getInt(0); + + AuctionFeeVeryLong = config.get(AUCTION, "Fee Very Long Duration", 0, + "Flat fee for very long duration auctions").getInt(0); + // ========================================= // Trader Settings // ========================================= @@ -157,12 +187,18 @@ public class ConfigMarket { "Listing Fee Percent", "Sales Tax Percent", "Require Auctioneer NPC", - "Available Durations", - "Default Duration Hours", "Claim Expiration Days", "Min Bid Increment Percent", "Snipe Protection Minutes", - "Snipe Protection Threshold" + "Snipe Protection Threshold", + "Duration Short Hours", + "Duration Medium Hours", + "Duration Long Hours", + "Duration Very Long Hours", + "Fee Short Duration", + "Fee Medium Duration", + "Fee Long Duration", + "Fee Very Long Duration" ))); config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( @@ -198,20 +234,4 @@ public class ConfigMarket { } } } - - /** - * Parse available durations from config string - */ - public static int[] getAvailableDurationsArray() { - String[] parts = AvailableDurations.split(","); - int[] durations = new int[parts.length]; - for (int i = 0; i < parts.length; i++) { - try { - durations[i] = Integer.parseInt(parts[i].trim()); - } catch (NumberFormatException e) { - durations[i] = 24; // Default to 24 hours if parsing fails - } - } - return durations; - } } diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java b/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java new file mode 100644 index 000000000..0906bb6ed --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java @@ -0,0 +1,156 @@ +package noppes.npcs.constants; + +import net.minecraft.init.Items; +import net.minecraft.item.Item; +import net.minecraft.item.ItemArmor; +import net.minecraft.item.ItemAxe; +import net.minecraft.item.ItemBlock; +import net.minecraft.item.ItemBow; +import net.minecraft.item.ItemEnchantedBook; +import net.minecraft.item.ItemFood; +import net.minecraft.item.ItemHoe; +import net.minecraft.item.ItemPickaxe; +import net.minecraft.item.ItemPotion; +import net.minecraft.item.ItemSpade; +import net.minecraft.item.ItemStack; +import net.minecraft.item.ItemSword; +import net.minecraft.item.ItemTool; + +/** + * Categories for auction house items. + * Used for filtering and browsing. + */ +public enum EnumAuctionCategory { + ALL, // Special: shows all categories + WEAPONS, // Swords, bows, etc. + ARMOR, // Helmets, chestplates, leggings, boots + TOOLS, // Pickaxes, axes, shovels, hoes + FOOD, // Edible items + POTIONS, // Potions and splash potions + BLOCKS, // Placeable blocks + MATERIALS, // Crafting materials (ingots, gems, etc.) + ENCHANTED, // Items with enchantments or enchanted books + MISC; // Everything else + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case ALL: return "All Items"; + case WEAPONS: return "Weapons"; + case ARMOR: return "Armor"; + case TOOLS: return "Tools"; + case FOOD: return "Food"; + case POTIONS: return "Potions"; + case BLOCKS: return "Blocks"; + case MATERIALS: return "Materials"; + case ENCHANTED: return "Enchanted"; + case MISC: return "Miscellaneous"; + default: return "Unknown"; + } + } + + /** + * Auto-categorize an item based on its type + */ + public static EnumAuctionCategory categorize(ItemStack itemStack) { + if (itemStack == null || itemStack.getItem() == null) { + return MISC; + } + + Item item = itemStack.getItem(); + + // Check for enchantments first (higher priority) + if (itemStack.isItemEnchanted() || item instanceof ItemEnchantedBook) { + return ENCHANTED; + } + + // Weapons + if (item instanceof ItemSword || item instanceof ItemBow) { + return WEAPONS; + } + + // Armor + if (item instanceof ItemArmor) { + return ARMOR; + } + + // Tools + if (item instanceof ItemTool || item instanceof ItemPickaxe || + item instanceof ItemAxe || item instanceof ItemSpade || + item instanceof ItemHoe) { + return TOOLS; + } + + // Food + if (item instanceof ItemFood) { + return FOOD; + } + + // Potions + if (item instanceof ItemPotion) { + return POTIONS; + } + + // Blocks + if (item instanceof ItemBlock) { + return BLOCKS; + } + + // Materials - common crafting items + if (isMaterial(item)) { + return MATERIALS; + } + + return MISC; + } + + /** + * Check if an item is a common crafting material + */ + private static boolean isMaterial(Item item) { + // Check common material items + return item == Items.iron_ingot || + item == Items.gold_ingot || + item == Items.diamond || + item == Items.emerald || + item == Items.coal || + item == Items.redstone || + item == Items.glowstone_dust || + item == Items.quartz || + item == Items.leather || + item == Items.string || + item == Items.feather || + item == Items.bone || + item == Items.gunpowder || + item == Items.slime_ball || + item == Items.ender_pearl || + item == Items.blaze_rod || + item == Items.ghast_tear || + item == Items.gold_nugget || + item == Items.nether_wart || + item == Items.spider_eye || + item == Items.magma_cream || + item == Items.nether_star; + } + + /** + * Get icon item for category display + */ + public ItemStack getIconItem() { + switch (this) { + case ALL: return new ItemStack(Items.compass); + case WEAPONS: return new ItemStack(Items.iron_sword); + case ARMOR: return new ItemStack(Items.iron_chestplate); + case TOOLS: return new ItemStack(Items.iron_pickaxe); + case FOOD: return new ItemStack(Items.bread); + case POTIONS: return new ItemStack(Items.potionitem); + case BLOCKS: return new ItemStack(Item.getItemFromBlock(net.minecraft.init.Blocks.grass)); + case MATERIALS: return new ItemStack(Items.iron_ingot); + case ENCHANTED: return new ItemStack(Items.enchanted_book); + case MISC: return new ItemStack(Items.chest_minecart); + default: return new ItemStack(Items.paper); + } + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java new file mode 100644 index 000000000..e18416f50 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java @@ -0,0 +1,68 @@ +package noppes.npcs.constants; + +import noppes.npcs.config.ConfigMarket; + +/** + * Duration options for auction listings. + * Values are configurable via ConfigMarket. + */ +public enum EnumAuctionDuration { + SHORT, // Default: 2 hours + MEDIUM, // Default: 12 hours + LONG, // Default: 24 hours + VERY_LONG; // Default: 48 hours + + /** + * Get duration in milliseconds + */ + public long getMillis() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionDurationShort * 60L * 60L * 1000L; + case MEDIUM: + return ConfigMarket.AuctionDurationMedium * 60L * 60L * 1000L; + case LONG: + return ConfigMarket.AuctionDurationLong * 60L * 60L * 1000L; + case VERY_LONG: + return ConfigMarket.AuctionDurationVeryLong * 60L * 60L * 1000L; + default: + return 24 * 60L * 60L * 1000L; + } + } + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionDurationShort + " Hours"; + case MEDIUM: + return ConfigMarket.AuctionDurationMedium + " Hours"; + case LONG: + return ConfigMarket.AuctionDurationLong + " Hours"; + case VERY_LONG: + return ConfigMarket.AuctionDurationVeryLong + " Hours"; + default: + return "Unknown"; + } + } + + /** + * Get listing fee for this duration + */ + public long getListingFee() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionFeeShort; + case MEDIUM: + return ConfigMarket.AuctionFeeMedium; + case LONG: + return ConfigMarket.AuctionFeeLong; + case VERY_LONG: + return ConfigMarket.AuctionFeeVeryLong; + default: + return 0; + } + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java new file mode 100644 index 000000000..9e614e496 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java @@ -0,0 +1,54 @@ +package noppes.npcs.constants; + +/** + * Status of an auction listing. + */ +public enum EnumAuctionStatus { + ACTIVE, // Currently listed and accepting bids + SOLD, // Successfully sold (buyout or auction end with bids) + EXPIRED, // Ended without any bids + CANCELLED, // Cancelled by seller (only allowed before first bid) + CLAIMED; // Item/money has been claimed by respective parties + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case ACTIVE: return "Active"; + case SOLD: return "Sold"; + case EXPIRED: return "Expired"; + case CANCELLED: return "Cancelled"; + case CLAIMED: return "Claimed"; + default: return "Unknown"; + } + } + + /** + * Get display color for GUI (MC color codes) + */ + public char getColorCode() { + switch (this) { + case ACTIVE: return 'a'; // Green + case SOLD: return '6'; // Gold + case EXPIRED: return '7'; // Gray + case CANCELLED: return 'c'; // Red + case CLAIMED: return '8'; // Dark gray + default: return 'f'; // White + } + } + + /** + * Check if listing can still receive bids + */ + public boolean canBid() { + return this == ACTIVE; + } + + /** + * Check if listing needs claiming (item or money) + */ + public boolean needsClaiming() { + return this == SOLD || this == EXPIRED || this == CANCELLED; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index d0919b9b1..adec6d51d 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -63,5 +63,7 @@ public enum EnumGuiType { ScriptItem, ScriptBlock, Paintbrush, - MagicBook; + MagicBook, + PlayerAuction, + SetupAuctioneer; } diff --git a/src/main/java/noppes/npcs/constants/EnumRoleType.java b/src/main/java/noppes/npcs/constants/EnumRoleType.java index 624658fb3..33f36941a 100644 --- a/src/main/java/noppes/npcs/constants/EnumRoleType.java +++ b/src/main/java/noppes/npcs/constants/EnumRoleType.java @@ -2,5 +2,5 @@ package noppes.npcs.constants; public enum EnumRoleType { - None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount + None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount, Auctioneer } diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java new file mode 100644 index 000000000..ec3b6de19 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -0,0 +1,662 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.data.AuctionListing; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Controller for the global auction house. + * Manages all auction listings, bidding, and claims. + */ +public class AuctionController { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static AuctionController Instance; + + // All auction listings by ID + private Map listings = new ConcurrentHashMap<>(); + + // Next available listing ID + private int nextId = 1; + + // Indices for fast lookup + private Map> listingsBySeller = new ConcurrentHashMap<>(); + private Map> listingsByBidder = new ConcurrentHashMap<>(); + + public AuctionController() { + Instance = this; + load(); + } + + // ==================== Listing Management ==================== + + /** + * Create a new auction listing + * @return The new listing, or null if creation failed + */ + public AuctionListing createListing(EntityPlayer seller, ItemStack item, + long startingPrice, long buyoutPrice, + EnumAuctionDuration duration) { + // Validate item + String validation = AuctionListing.validateItem(item); + if (validation != null) { + logger.warn("Auction listing validation failed: " + validation); + return null; + } + + // Check seller's listing limit + int currentListings = getActiveListingsCount(seller.getUniqueID()); + int maxListings = getMaxListings(seller); + if (currentListings >= maxListings) { + logger.info("Player {} has reached max listings ({}/{})", + seller.getCommandSenderName(), currentListings, maxListings); + return null; + } + + // Calculate and charge listing fee + AuctionListing listing = new AuctionListing( + seller.getUniqueID(), + seller.getCommandSenderName(), + item, + startingPrice, + buyoutPrice, + duration + ); + + long listingFee = listing.getListingFee(); + if (listingFee > 0) { + if (!CurrencyController.Instance.canAfford(seller, listingFee)) { + logger.info("Player {} cannot afford listing fee of {}", + seller.getCommandSenderName(), listingFee); + return null; + } + CurrencyController.Instance.withdraw(seller, listingFee); + } + + // Assign ID and store + listing.id = nextId++; + listings.put(listing.id, listing); + + // Update indices + addToSellerIndex(seller.getUniqueID(), listing.id); + + save(); + logger.info("Created auction listing {} by {} for {}", + listing.id, seller.getCommandSenderName(), + item.getDisplayName()); + + return listing; + } + + /** + * Get a listing by ID + */ + public AuctionListing getListing(int id) { + return listings.get(id); + } + + /** + * Get all active listings + */ + public List getActiveListings() { + processExpiredAuctions(); + return listings.values().stream() + .filter(AuctionListing::isActive) + .collect(Collectors.toList()); + } + + /** + * Get active listings by category + */ + public List getActiveListings(EnumAuctionCategory category) { + if (category == EnumAuctionCategory.ALL) { + return getActiveListings(); + } + return getActiveListings().stream() + .filter(l -> l.category == category) + .collect(Collectors.toList()); + } + + /** + * Search listings by item name + */ + public List searchListings(String query) { + String lowerQuery = query.toLowerCase(); + return getActiveListings().stream() + .filter(l -> l.item.getDisplayName().toLowerCase().contains(lowerQuery)) + .collect(Collectors.toList()); + } + + /** + * Get all listings by a seller (all statuses) + */ + public List getListingsBySeller(UUID sellerUUID) { + List ids = listingsBySeller.get(sellerUUID); + if (ids == null) { + return new ArrayList<>(); + } + return ids.stream() + .map(listings::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Get active listing count for a seller + */ + public int getActiveListingsCount(UUID sellerUUID) { + return (int) getListingsBySeller(sellerUUID).stream() + .filter(l -> l.status == EnumAuctionStatus.ACTIVE) + .count(); + } + + /** + * Get listings where player is the high bidder + */ + public List getListingsAsBidder(UUID bidderUUID) { + List ids = listingsByBidder.get(bidderUUID); + if (ids == null) { + return new ArrayList<>(); + } + return ids.stream() + .map(listings::get) + .filter(Objects::nonNull) + .filter(l -> bidderUUID.equals(l.highBidderUUID)) + .collect(Collectors.toList()); + } + + /** + * Get listings that need claiming by a player + * (items won, sales proceeds, cancelled/expired items) + */ + public List getClaimableListings(UUID playerUUID) { + List claimable = new ArrayList<>(); + + for (AuctionListing listing : listings.values()) { + // Seller can claim proceeds from sold items + if (listing.sellerUUID.equals(playerUUID) && + listing.status == EnumAuctionStatus.SOLD && + !listing.sellerClaimed) { + claimable.add(listing); + } + + // Seller can claim back cancelled/expired items + if (listing.sellerUUID.equals(playerUUID) && + (listing.status == EnumAuctionStatus.CANCELLED || + listing.status == EnumAuctionStatus.EXPIRED) && + !listing.sellerClaimed) { + claimable.add(listing); + } + + // Buyer can claim won items + if (listing.highBidderUUID != null && + listing.highBidderUUID.equals(playerUUID) && + listing.status == EnumAuctionStatus.SOLD && + !listing.buyerClaimed) { + claimable.add(listing); + } + } + + return claimable; + } + + // ==================== Bidding ==================== + + /** + * Place a bid on an auction + * @return true if bid was successful + */ + public boolean placeBid(int listingId, EntityPlayer bidder, long amount) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return false; + } + + // Check if bidder can afford + if (!CurrencyController.Instance.canAfford(bidder, amount)) { + return false; + } + + // Store previous bidder for refund + UUID previousBidder = listing.highBidderUUID; + long previousBidAmount = listing.currentBid; + + // Try to place bid + if (!listing.placeBid(bidder.getUniqueID(), bidder.getCommandSenderName(), amount)) { + return false; + } + + // Withdraw bid amount from new bidder + CurrencyController.Instance.withdraw(bidder, amount); + + // Refund previous bidder via claims system + if (previousBidder != null) { + CurrencyController.Instance.addClaim( + previousBidder, + previousBidAmount, + "Auction Outbid", + String.valueOf(listingId) + ); + } + + // Update bidder index + addToBidderIndex(bidder.getUniqueID(), listingId); + + save(); + logger.info("Bid placed on listing {} by {} for {}", + listingId, bidder.getCommandSenderName(), amount); + + return true; + } + + /** + * Execute buyout on an auction + * @return true if buyout was successful + */ + public boolean buyout(int listingId, EntityPlayer buyer) { + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.buyoutPrice <= 0) { + return false; + } + + // Check if buyer can afford + if (!CurrencyController.Instance.canAfford(buyer, listing.buyoutPrice)) { + return false; + } + + // Store previous bidder for refund + UUID previousBidder = listing.highBidderUUID; + long previousBidAmount = listing.currentBid; + + // Execute buyout + if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { + return false; + } + + // Withdraw buyout amount from buyer + CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); + + // Refund previous bidder via claims system + if (previousBidder != null) { + CurrencyController.Instance.addClaim( + previousBidder, + previousBidAmount, + "Auction Outbid (Buyout)", + String.valueOf(listingId) + ); + } + + // Update bidder index + addToBidderIndex(buyer.getUniqueID(), listingId); + + save(); + logger.info("Buyout executed on listing {} by {} for {}", + listingId, buyer.getCommandSenderName(), listing.buyoutPrice); + + return true; + } + + /** + * Cancel an auction listing + * @return true if cancellation was successful + */ + public boolean cancelListing(int listingId, EntityPlayer requester) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return false; + } + + if (!listing.cancel(requester.getUniqueID())) { + return false; + } + + save(); + logger.info("Listing {} cancelled by {}", + listingId, requester.getCommandSenderName()); + + return true; + } + + // ==================== Claiming ==================== + + /** + * Claim seller proceeds from a sold auction + * @return true if claim was successful + */ + public boolean claimSellerProceeds(int listingId, EntityPlayer seller) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return false; + } + + if (!listing.sellerUUID.equals(seller.getUniqueID())) { + return false; // Not the seller + } + + if (listing.status != EnumAuctionStatus.SOLD) { + return false; // Not sold + } + + if (listing.sellerClaimed) { + return false; // Already claimed + } + + // Deposit proceeds to seller + long proceeds = listing.getSellerProceeds(); + CurrencyController.Instance.deposit(seller, proceeds); + + listing.sellerClaimed = true; + checkAndMarkClaimed(listing); + save(); + + logger.info("Seller {} claimed {} from listing {}", + seller.getCommandSenderName(), proceeds, listingId); + + return true; + } + + /** + * Claim item from won auction + * @return The item if successful, null otherwise + */ + public ItemStack claimBuyerItem(int listingId, EntityPlayer buyer) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return null; + } + + if (listing.highBidderUUID == null || + !listing.highBidderUUID.equals(buyer.getUniqueID())) { + return null; // Not the buyer + } + + if (listing.status != EnumAuctionStatus.SOLD) { + return null; // Not sold + } + + if (listing.buyerClaimed) { + return null; // Already claimed + } + + listing.buyerClaimed = true; + checkAndMarkClaimed(listing); + save(); + + logger.info("Buyer {} claimed item from listing {}", + buyer.getCommandSenderName(), listingId); + + return listing.item.copy(); + } + + /** + * Claim item from cancelled or expired auction (seller reclaiming) + * @return The item if successful, null otherwise + */ + public ItemStack claimExpiredItem(int listingId, EntityPlayer seller) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return null; + } + + if (!listing.sellerUUID.equals(seller.getUniqueID())) { + return null; // Not the seller + } + + if (listing.status != EnumAuctionStatus.CANCELLED && + listing.status != EnumAuctionStatus.EXPIRED) { + return null; // Not claimable + } + + if (listing.sellerClaimed) { + return null; // Already claimed + } + + listing.sellerClaimed = true; + listing.status = EnumAuctionStatus.CLAIMED; + save(); + + logger.info("Seller {} reclaimed item from listing {}", + seller.getCommandSenderName(), listingId); + + return listing.item.copy(); + } + + /** + * Mark listing as fully claimed if both parties have claimed + */ + private void checkAndMarkClaimed(AuctionListing listing) { + if (listing.status == EnumAuctionStatus.SOLD) { + if (listing.sellerClaimed && listing.buyerClaimed) { + listing.status = EnumAuctionStatus.CLAIMED; + } + } + } + + // ==================== Expiration Processing ==================== + + /** + * Process all expired auctions + * Call this periodically (e.g., every minute) + */ + public void processExpiredAuctions() { + boolean changed = false; + + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { + listing.finalize(); + changed = true; + + // If sold, add proceeds to seller's claims + if (listing.status == EnumAuctionStatus.SOLD) { + // Proceeds will be claimed manually, no automatic deposit + logger.info("Auction {} ended with sale to {} for {}", + listing.id, listing.highBidderName, listing.currentBid); + } else { + logger.info("Auction {} expired without bids", listing.id); + } + } + } + + if (changed) { + save(); + } + } + + /** + * Clean up old claimed listings + * Call this periodically (e.g., daily) + */ + public void cleanupOldListings() { + long claimExpirationMs = ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L; + if (claimExpirationMs <= 0) { + return; // No expiration + } + + long cutoffTime = System.currentTimeMillis() - claimExpirationMs; + List toRemove = new ArrayList<>(); + + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.CLAIMED && + listing.expirationTime < cutoffTime) { + toRemove.add(listing.id); + } + } + + for (int id : toRemove) { + AuctionListing removed = listings.remove(id); + if (removed != null) { + removeFromSellerIndex(removed.sellerUUID, id); + if (removed.highBidderUUID != null) { + removeFromBidderIndex(removed.highBidderUUID, id); + } + } + } + + if (!toRemove.isEmpty()) { + save(); + logger.info("Cleaned up {} old auction listings", toRemove.size()); + } + } + + // ==================== Index Management ==================== + + private void addToSellerIndex(UUID seller, int listingId) { + listingsBySeller.computeIfAbsent(seller, k -> new ArrayList<>()).add(listingId); + } + + private void removeFromSellerIndex(UUID seller, int listingId) { + List ids = listingsBySeller.get(seller); + if (ids != null) { + ids.remove(Integer.valueOf(listingId)); + } + } + + private void addToBidderIndex(UUID bidder, int listingId) { + listingsByBidder.computeIfAbsent(bidder, k -> new ArrayList<>()).add(listingId); + } + + private void removeFromBidderIndex(UUID bidder, int listingId) { + List ids = listingsByBidder.get(bidder); + if (ids != null) { + ids.remove(Integer.valueOf(listingId)); + } + } + + private void rebuildIndices() { + listingsBySeller.clear(); + listingsByBidder.clear(); + + for (AuctionListing listing : listings.values()) { + addToSellerIndex(listing.sellerUUID, listing.id); + if (listing.highBidderUUID != null) { + addToBidderIndex(listing.highBidderUUID, listing.id); + } + } + } + + // ==================== Permission Helpers ==================== + + /** + * Get maximum listings allowed for a player + */ + public int getMaxListings(EntityPlayer player) { + // TODO: Check for permission-based overrides + // e.g., CustomNpcsPermissions.hasCustomPermission(player, "cnpc.auction.slots.10") + return ConfigMarket.DefaultMaxListings; + } + + // ==================== Persistence ==================== + + public void save() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + try { + NBTTagCompound compound = new NBTTagCompound(); + compound.setInteger("NextId", nextId); + + NBTTagList listingList = new NBTTagList(); + for (AuctionListing listing : listings.values()) { + listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + compound.setTag("Listings", listingList); + + File file = new File(saveDir, "auctions.dat"); + File backup = new File(saveDir, "auctions.dat_old"); + + if (file.exists()) { + if (backup.exists()) { + backup.delete(); + } + file.renameTo(backup); + } + + file = new File(saveDir, "auctions.dat"); + FileOutputStream fos = new FileOutputStream(file); + CompressedStreamTools.writeCompressed(compound, fos); + fos.close(); + + } catch (Exception e) { + logger.error("Error saving auctions", e); + } + } + + public void load() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + listings.clear(); + + try { + File file = new File(saveDir, "auctions.dat"); + if (!file.exists()) { + file = new File(saveDir, "auctions.dat_old"); + if (!file.exists()) { + return; + } + } + + FileInputStream fis = new FileInputStream(file); + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + fis.close(); + + nextId = compound.getInteger("NextId"); + if (nextId < 1) { + nextId = 1; + } + + NBTTagList listingList = compound.getTagList("Listings", 10); + for (int i = 0; i < listingList.tagCount(); i++) { + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(listingList.getCompoundTagAt(i)); + listings.put(listing.id, listing); + } + + rebuildIndices(); + + logger.info("Loaded {} auction listings", listings.size()); + + } catch (Exception e) { + logger.error("Error loading auctions", e); + } + } + + // ==================== Statistics ==================== + + /** + * Get total number of active listings + */ + public int getActiveListingCount() { + return (int) listings.values().stream() + .filter(l -> l.status == EnumAuctionStatus.ACTIVE) + .count(); + } + + /** + * Get total number of all listings + */ + public int getTotalListingCount() { + return listings.size(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java new file mode 100644 index 000000000..e17a08112 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -0,0 +1,392 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.AuctionBlacklist; + +import java.util.UUID; + +/** + * Represents a single auction listing. + * Contains all data about the item, seller, bidding, and timing. + */ +public class AuctionListing { + // Unique identifier + public int id; + + // Seller info + public UUID sellerUUID; + public String sellerName; + + // Item being sold + public ItemStack item; + + // Pricing + public long startingPrice; + public long buyoutPrice; // 0 = no buyout option + public long currentBid; + + // Current high bidder + public UUID highBidderUUID; + public String highBidderName; + + // Previous bidder (for refund notifications) + public UUID previousBidderUUID; + public String previousBidderName; + public long previousBidAmount; + + // Timing + public long listingTime; // When the auction was created + public long expirationTime; // When the auction ends + public EnumAuctionDuration duration; + + // Status + public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; + + // Category for filtering + public EnumAuctionCategory category = EnumAuctionCategory.MISC; + + // Claim tracking + public boolean sellerClaimed = false; // Has seller claimed their proceeds? + public boolean buyerClaimed = false; // Has buyer claimed their item? + + public AuctionListing() { + } + + /** + * Create a new auction listing + */ + public AuctionListing(UUID seller, String sellerName, ItemStack item, + long startPrice, long buyout, EnumAuctionDuration duration) { + this.sellerUUID = seller; + this.sellerName = sellerName; + this.item = item.copy(); + this.startingPrice = startPrice; + this.buyoutPrice = buyout; + this.currentBid = 0; // No bids yet + this.duration = duration; + this.listingTime = System.currentTimeMillis(); + this.expirationTime = listingTime + duration.getMillis(); + this.category = EnumAuctionCategory.categorize(item); + this.status = EnumAuctionStatus.ACTIVE; + } + + // ==================== Status Checks ==================== + + /** + * Check if auction has expired + */ + public boolean isExpired() { + return System.currentTimeMillis() >= expirationTime; + } + + /** + * Check if auction is still active + */ + public boolean isActive() { + return status == EnumAuctionStatus.ACTIVE && !isExpired(); + } + + /** + * Get time remaining in milliseconds + */ + public long getTimeRemaining() { + return Math.max(0, expirationTime - System.currentTimeMillis()); + } + + /** + * Get formatted time remaining + */ + public String getTimeRemainingFormatted() { + long remaining = getTimeRemaining(); + + if (remaining <= 0) { + return "Ended"; + } + + long hours = remaining / (60L * 60L * 1000L); + long minutes = (remaining % (60L * 60L * 1000L)) / (60L * 1000L); + + if (hours > 24) { + long days = hours / 24; + hours = hours % 24; + return days + "d " + hours + "h"; + } else if (hours > 0) { + return hours + "h " + minutes + "m"; + } else { + long seconds = (remaining % (60L * 1000L)) / 1000L; + return minutes + "m " + seconds + "s"; + } + } + + /** + * Check if auction has any bids + */ + public boolean hasBids() { + return highBidderUUID != null; + } + + // ==================== Bidding Methods ==================== + + /** + * Calculate minimum valid bid amount + */ + public long getMinimumBid() { + if (!hasBids()) { + return startingPrice; + } + // Minimum increment is configured percent or 1, whichever is greater + long minIncrement = Math.max(1, (long) (currentBid * ConfigMarket.MinBidIncrementPercent)); + return currentBid + minIncrement; + } + + /** + * Place a bid + * @return true if bid was successful + */ + public boolean placeBid(UUID bidder, String bidderName, long amount) { + if (status != EnumAuctionStatus.ACTIVE) { + return false; + } + if (isExpired()) { + return false; + } + if (amount < getMinimumBid()) { + return false; + } + if (bidder.equals(sellerUUID)) { + return false; // Can't bid on own item + } + + // Store previous bidder for refund notification + if (highBidderUUID != null) { + previousBidderUUID = highBidderUUID; + previousBidderName = highBidderName; + previousBidAmount = currentBid; + } + + // Update high bidder + highBidderUUID = bidder; + highBidderName = bidderName; + currentBid = amount; + + // Snipe protection - extend if bid placed in final minutes + if (ConfigMarket.SnipeProtectionMinutes > 0) { + long timeRemaining = getTimeRemaining(); + long thresholdMs = ConfigMarket.SnipeProtectionThreshold * 60L * 1000L; + + if (timeRemaining < thresholdMs) { + // Extend TO snipe protection time, not by it + long newExpiration = System.currentTimeMillis() + + (ConfigMarket.SnipeProtectionMinutes * 60L * 1000L); + // Only extend if it would actually extend the auction + if (newExpiration > expirationTime) { + expirationTime = newExpiration; + } + } + } + + return true; + } + + /** + * Execute buyout (instant purchase at buyout price) + * @return true if buyout was successful + */ + public boolean buyout(UUID buyer, String buyerName) { + if (status != EnumAuctionStatus.ACTIVE) { + return false; + } + if (isExpired()) { + return false; + } + if (buyoutPrice <= 0) { + return false; // No buyout available + } + if (buyer.equals(sellerUUID)) { + return false; // Can't buy own item + } + + // Store previous bidder for refund + if (highBidderUUID != null) { + previousBidderUUID = highBidderUUID; + previousBidderName = highBidderName; + previousBidAmount = currentBid; + } + + highBidderUUID = buyer; + highBidderName = buyerName; + currentBid = buyoutPrice; + status = EnumAuctionStatus.SOLD; + + return true; + } + + /** + * Cancel listing (seller only, before any bids) + * @return true if cancellation was successful + */ + public boolean cancel(UUID requester) { + if (!requester.equals(sellerUUID)) { + return false; // Only seller can cancel + } + if (hasBids()) { + return false; // Can't cancel with active bids + } + if (status != EnumAuctionStatus.ACTIVE) { + return false; + } + + status = EnumAuctionStatus.CANCELLED; + return true; + } + + /** + * Finalize auction when it expires + * Call this when auction timer runs out + */ + public void finalize() { + if (status != EnumAuctionStatus.ACTIVE) { + return; + } + + if (hasBids()) { + status = EnumAuctionStatus.SOLD; + } else { + status = EnumAuctionStatus.EXPIRED; + } + } + + // ==================== Validation ==================== + + /** + * Validate that the item can be listed + * @return null if valid, error message if invalid + */ + public static String validateItem(ItemStack item) { + if (item == null || item.getItem() == null) { + return "Invalid item"; + } + + // Check trade restrictions (Untradeable, ProfileSlot Bound, Soulbound) + if (!ItemTradeAttribute.canTrade(item)) { + String reason = ItemTradeAttribute.getTradeBlockReason(item); + return reason != null ? reason : "Item cannot be traded"; + } + + // Check blacklist + if (AuctionBlacklist.Instance != null && AuctionBlacklist.Instance.isBlacklisted(item)) { + return "Item is blacklisted from auction"; + } + + return null; // Valid + } + + // ==================== Financial Calculations ==================== + + /** + * Get listing fee for this auction + */ + public long getListingFee() { + long flatFee = duration.getListingFee(); + long percentFee = (long) (startingPrice * ConfigMarket.ListingFeePercent); + return flatFee + percentFee; + } + + /** + * Get seller's proceeds after sales tax + */ + public long getSellerProceeds() { + if (status != EnumAuctionStatus.SOLD) { + return 0; + } + long tax = (long) (currentBid * ConfigMarket.SalesTaxPercent); + return currentBid - tax; + } + + // ==================== NBT Serialization ==================== + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setInteger("Id", id); + compound.setString("SellerUUID", sellerUUID.toString()); + compound.setString("SellerName", sellerName); + compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); + compound.setLong("StartPrice", startingPrice); + compound.setLong("BuyoutPrice", buyoutPrice); + compound.setLong("CurrentBid", currentBid); + + if (highBidderUUID != null) { + compound.setString("BidderUUID", highBidderUUID.toString()); + compound.setString("BidderName", highBidderName); + } + + if (previousBidderUUID != null) { + compound.setString("PrevBidderUUID", previousBidderUUID.toString()); + compound.setString("PrevBidderName", previousBidderName); + compound.setLong("PrevBidAmount", previousBidAmount); + } + + compound.setLong("ListingTime", listingTime); + compound.setLong("ExpirationTime", expirationTime); + compound.setInteger("Duration", duration.ordinal()); + compound.setInteger("Status", status.ordinal()); + compound.setInteger("Category", category.ordinal()); + compound.setBoolean("SellerClaimed", sellerClaimed); + compound.setBoolean("BuyerClaimed", buyerClaimed); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getInteger("Id"); + sellerUUID = UUID.fromString(compound.getString("SellerUUID")); + sellerName = compound.getString("SellerName"); + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + startingPrice = compound.getLong("StartPrice"); + buyoutPrice = compound.getLong("BuyoutPrice"); + currentBid = compound.getLong("CurrentBid"); + + if (compound.hasKey("BidderUUID")) { + highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); + highBidderName = compound.getString("BidderName"); + } + + if (compound.hasKey("PrevBidderUUID")) { + previousBidderUUID = UUID.fromString(compound.getString("PrevBidderUUID")); + previousBidderName = compound.getString("PrevBidderName"); + previousBidAmount = compound.getLong("PrevBidAmount"); + } + + listingTime = compound.getLong("ListingTime"); + expirationTime = compound.getLong("ExpirationTime"); + + int durationOrdinal = compound.getInteger("Duration"); + if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { + duration = EnumAuctionDuration.values()[durationOrdinal]; + } else { + duration = EnumAuctionDuration.LONG; + } + + int statusOrdinal = compound.getInteger("Status"); + if (statusOrdinal >= 0 && statusOrdinal < EnumAuctionStatus.values().length) { + status = EnumAuctionStatus.values()[statusOrdinal]; + } else { + status = EnumAuctionStatus.ACTIVE; + } + + int categoryOrdinal = compound.getInteger("Category"); + if (categoryOrdinal >= 0 && categoryOrdinal < EnumAuctionCategory.values().length) { + category = EnumAuctionCategory.values()[categoryOrdinal]; + } else { + category = EnumAuctionCategory.MISC; + } + + sellerClaimed = compound.getBoolean("SellerClaimed"); + buyerClaimed = compound.getBoolean("BuyerClaimed"); + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java new file mode 100644 index 000000000..0c0f53d07 --- /dev/null +++ b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java @@ -0,0 +1,134 @@ +package noppes.npcs.roles; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * NPC role for Auctioneer - provides access to the global auction house. + */ +public class RoleAuctioneer extends RoleInterface { + + // Configuration + public boolean showActiveListings = true; // Show active listings tab + public boolean showMyListings = true; // Show player's listings tab + public boolean showMyBids = true; // Show player's bids tab + public boolean showClaims = true; // Show claimable items tab + public boolean allowCreatingListings = true; // Allow creating new listings + public boolean allowBidding = true; // Allow bidding on items + public boolean allowBuyout = true; // Allow buyout purchases + + // Custom greeting messages + public String welcomeMessage = ""; + public String noAuctionMessage = ""; + + public RoleAuctioneer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("ShowActiveListings", showActiveListings); + compound.setBoolean("ShowMyListings", showMyListings); + compound.setBoolean("ShowMyBids", showMyBids); + compound.setBoolean("ShowClaims", showClaims); + compound.setBoolean("AllowCreatingListings", allowCreatingListings); + compound.setBoolean("AllowBidding", allowBidding); + compound.setBoolean("AllowBuyout", allowBuyout); + compound.setString("WelcomeMessage", welcomeMessage); + compound.setString("NoAuctionMessage", noAuctionMessage); + return compound; + } + + @Override + public void readFromNBT(NBTTagCompound compound) { + showActiveListings = compound.hasKey("ShowActiveListings") ? + compound.getBoolean("ShowActiveListings") : true; + showMyListings = compound.hasKey("ShowMyListings") ? + compound.getBoolean("ShowMyListings") : true; + showMyBids = compound.hasKey("ShowMyBids") ? + compound.getBoolean("ShowMyBids") : true; + showClaims = compound.hasKey("ShowClaims") ? + compound.getBoolean("ShowClaims") : true; + allowCreatingListings = compound.hasKey("AllowCreatingListings") ? + compound.getBoolean("AllowCreatingListings") : true; + allowBidding = compound.hasKey("AllowBidding") ? + compound.getBoolean("AllowBidding") : true; + allowBuyout = compound.hasKey("AllowBuyout") ? + compound.getBoolean("AllowBuyout") : true; + welcomeMessage = compound.getString("WelcomeMessage"); + noAuctionMessage = compound.getString("NoAuctionMessage"); + } + + @Override + public void interact(EntityPlayer player) { + // Check if auction system is enabled + if (!ConfigMarket.AuctionEnabled) { + if (!noAuctionMessage.isEmpty()) { + npc.say(player, noAuctionMessage); + } else { + npc.say(player, "The auction house is currently closed."); + } + return; + } + + // Process expired auctions before opening GUI + if (AuctionController.Instance != null) { + AuctionController.Instance.processExpiredAuctions(); + } + + // Say welcome message + if (!welcomeMessage.isEmpty()) { + npc.say(player, welcomeMessage); + } else { + npc.say(player, npc.advanced.getInteractLine()); + } + + // Open auction GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); + } + + /** + * Check if player has claims waiting + */ + public boolean hasClaimsWaiting(EntityPlayer player) { + if (AuctionController.Instance == null) { + return false; + } + return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); + } + + /** + * Get number of active listings in the auction house + */ + public int getActiveListingCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getActiveListingCount(); + } + + /** + * Get player's current listing count + */ + public int getPlayerListingCount(EntityPlayer player) { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); + } + + /** + * Get player's maximum allowed listings + */ + public int getPlayerMaxListings(EntityPlayer player) { + if (AuctionController.Instance == null) { + return ConfigMarket.DefaultMaxListings; + } + return AuctionController.Instance.getMaxListings(player); + } +} From 57f9b706bff5278ae6e5c846c294e13f456e3564 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:57:13 +0000 Subject: [PATCH 10/64] Implement Phase 5: Network packets, permissions, and scripting API - Add auction permission nodes (use, list, bid, buyout, admin) - Create AuctionActionPacket for client-server communication - Supports: CreateListing, PlaceBid, Buyout, CancelListing, ClaimItem, ClaimProceeds - Includes permission checks and validation - Register packet in PacketHandler - Add auction scripting API methods to ScriptPlayer: - getAuctionListingCount() - getMaxAuctionListings() - hasAuctionClaims() - getAuctionClaimCount() - getActiveAuctionBidCount() --- .../kamkeel/npcs/network/PacketHandler.java | 4 + .../npcs/network/enums/EnumPlayerPacket.java | 3 + .../packets/player/AuctionActionPacket.java | 441 ++++++++++++++++++ .../noppes/npcs/CustomNpcsPermissions.java | 7 + .../npcs/scripted/entity/ScriptPlayer.java | 67 +++ 5 files changed, 522 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 745962aba..b22537070 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -49,6 +49,7 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; +import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; @@ -611,6 +612,9 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new PartyAcceptInvitePacket()); PLAYER_PACKET.registerPacket(new PartyIgnoreInvitePacket()); PLAYER_PACKET.registerPacket(new PartyLogToServerPacket()); + + // Auction Packets + PLAYER_PACKET.registerPacket(new AuctionActionPacket()); } public void registerChannels() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 0c1db3cd9..15c90fce5 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -33,4 +33,7 @@ public enum EnumPlayerPacket { ProfileGet, ProfileGetInfo, SyncRevisionInfo, + + // Auction + AuctionAction, } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java new file mode 100644 index 000000000..4aa56ab57 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -0,0 +1,441 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.ChatComponentText; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.IOException; + +/** + * Packet for all auction-related player actions. + * Client -> Server for actions, Server -> Client for responses. + */ +public class AuctionActionPacket extends AbstractPacket { + public static final String packetName = "Player|Auction"; + + private Action action; + private int listingId; + private long amount; + private long buyoutPrice; + private int durationOrdinal; + private NBTTagCompound itemData; + private String responseMessage; + private boolean success; + + public AuctionActionPacket() { + } + + private AuctionActionPacket(Action action) { + this.action = action; + } + + @Override + public boolean needsNPC() { + return true; + } + + // ==================== Client -> Server Actions ==================== + + /** + * Create a new auction listing + */ + @SideOnly(Side.CLIENT) + public static void CreateListing(ItemStack item, long startingPrice, long buyoutPrice, EnumAuctionDuration duration) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); + packet.amount = startingPrice; + packet.buyoutPrice = buyoutPrice; + packet.durationOrdinal = duration.ordinal(); + packet.itemData = item.writeToNBT(new NBTTagCompound()); + PacketClient.sendClient(packet); + } + + /** + * Place a bid on an auction + */ + @SideOnly(Side.CLIENT) + public static void PlaceBid(int listingId, long bidAmount) { + AuctionActionPacket packet = new AuctionActionPacket(Action.PlaceBid); + packet.listingId = listingId; + packet.amount = bidAmount; + PacketClient.sendClient(packet); + } + + /** + * Execute buyout on an auction + */ + @SideOnly(Side.CLIENT) + public static void Buyout(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Cancel own listing + */ + @SideOnly(Side.CLIENT) + public static void CancelListing(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CancelListing); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Claim item from won/expired/cancelled auction + */ + @SideOnly(Side.CLIENT) + public static void ClaimItem(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + /** + * Claim proceeds from sold auction + */ + @SideOnly(Side.CLIENT) + public static void ClaimProceeds(int listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimProceeds); + packet.listingId = listingId; + PacketClient.sendClient(packet); + } + + @Override + public Enum getType() { + return EnumPlayerPacket.AuctionAction; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeInt(action.ordinal()); + + switch (action) { + case CreateListing: + out.writeLong(amount); + out.writeLong(buyoutPrice); + out.writeInt(durationOrdinal); + String itemJson = NBTJsonUtil.Convert(itemData); + byte[] itemBytes = itemJson.getBytes("UTF-8"); + out.writeInt(itemBytes.length); + out.writeBytes(itemBytes); + break; + + case PlaceBid: + out.writeInt(listingId); + out.writeLong(amount); + break; + + case Buyout: + case CancelListing: + case ClaimItem: + case ClaimProceeds: + out.writeInt(listingId); + break; + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + Action requestedAction = Action.values()[in.readInt()]; + + if (!(player instanceof EntityPlayerMP)) { + return; + } + + EntityPlayerMP playerMP = (EntityPlayerMP) player; + + // Verify NPC is an Auctioneer + if (npc == null || npc.advanced.role != EnumRoleType.Auctioneer) { + return; + } + + // Check if auction system is enabled + if (!ConfigMarket.AuctionEnabled) { + sendResponse(playerMP, false, "Auction system is disabled"); + return; + } + + // Check controller exists + if (AuctionController.Instance == null) { + sendResponse(playerMP, false, "Auction controller not initialized"); + return; + } + + switch (requestedAction) { + case CreateListing: + handleCreateListing(in, playerMP); + break; + case PlaceBid: + handlePlaceBid(in, playerMP); + break; + case Buyout: + handleBuyout(in, playerMP); + break; + case CancelListing: + handleCancelListing(in, playerMP); + break; + case ClaimItem: + handleClaimItem(in, playerMP); + break; + case ClaimProceeds: + handleClaimProceeds(in, playerMP); + break; + } + } + + private void handleCreateListing(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_LIST)) { + sendResponse(player, false, "You don't have permission to create listings"); + return; + } + + long startingPrice = in.readLong(); + long buyoutPrice = in.readLong(); + int durationOrdinal = in.readInt(); + + int itemBytesLength = in.readInt(); + byte[] itemBytes = new byte[itemBytesLength]; + in.readBytes(itemBytes); + String itemJson = new String(itemBytes, "UTF-8"); + + try { + NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); + ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); + + if (item == null) { + sendResponse(player, false, "Invalid item"); + return; + } + + // Validate duration + EnumAuctionDuration duration; + if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { + duration = EnumAuctionDuration.values()[durationOrdinal]; + } else { + duration = EnumAuctionDuration.MEDIUM; + } + + // Validate prices + if (startingPrice < 1) { + sendResponse(player, false, "Starting price must be at least 1"); + return; + } + + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + sendResponse(player, false, "Buyout price must be higher than starting price"); + return; + } + + // Create the listing + AuctionListing listing = AuctionController.Instance.createListing( + player, item, startingPrice, buyoutPrice, duration); + + if (listing != null) { + // Remove item from player's inventory + // Note: The GUI should have already collected the item, this is validation + sendResponse(player, true, "Listing created successfully"); + } else { + sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); + } + } catch (Exception e) { + sendResponse(player, false, "Error creating listing: " + e.getMessage()); + } + } + + private void handlePlaceBid(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BID)) { + sendResponse(player, false, "You don't have permission to bid"); + return; + } + + int listingId = in.readInt(); + long bidAmount = in.readLong(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (!listing.isActive()) { + sendResponse(player, false, "This auction has ended"); + return; + } + + if (bidAmount < listing.getMinimumBid()) { + sendResponse(player, false, "Bid must be at least " + listing.getMinimumBid()); + return; + } + + if (!CurrencyController.Instance.canAfford(player, bidAmount)) { + sendResponse(player, false, "You cannot afford this bid"); + return; + } + + if (AuctionController.Instance.placeBid(listingId, player, bidAmount)) { + sendResponse(player, true, "Bid placed successfully"); + } else { + sendResponse(player, false, "Failed to place bid"); + } + } + + private void handleBuyout(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BUYOUT)) { + sendResponse(player, false, "You don't have permission to use buyout"); + return; + } + + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (!listing.isActive()) { + sendResponse(player, false, "This auction has ended"); + return; + } + + if (listing.buyoutPrice <= 0) { + sendResponse(player, false, "This listing has no buyout option"); + return; + } + + if (!CurrencyController.Instance.canAfford(player, listing.buyoutPrice)) { + sendResponse(player, false, "You cannot afford the buyout price"); + return; + } + + if (AuctionController.Instance.buyout(listingId, player)) { + sendResponse(player, true, "Buyout successful! Claim your item."); + } else { + sendResponse(player, false, "Failed to execute buyout"); + } + } + + private void handleCancelListing(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + // Admin can cancel any listing + boolean isAdmin = CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_ADMIN); + + if (!listing.sellerUUID.equals(player.getUniqueID()) && !isAdmin) { + sendResponse(player, false, "You can only cancel your own listings"); + return; + } + + if (listing.hasBids() && !isAdmin) { + sendResponse(player, false, "Cannot cancel listing with active bids"); + return; + } + + if (AuctionController.Instance.cancelListing(listingId, player)) { + sendResponse(player, true, "Listing cancelled. Claim your item."); + } else { + sendResponse(player, false, "Failed to cancel listing"); + } + } + + private void handleClaimItem(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + ItemStack claimed = null; + + // Try buyer claim first + if (listing.highBidderUUID != null && listing.highBidderUUID.equals(player.getUniqueID())) { + claimed = AuctionController.Instance.claimBuyerItem(listingId, player); + } + // Then try seller reclaim (for expired/cancelled) + else if (listing.sellerUUID.equals(player.getUniqueID())) { + claimed = AuctionController.Instance.claimExpiredItem(listingId, player); + } + + if (claimed != null) { + // Give item to player + if (!player.inventory.addItemStackToInventory(claimed)) { + // Drop if inventory full + player.entityDropItem(claimed, 0.5f); + } + sendResponse(player, true, "Item claimed"); + } else { + sendResponse(player, false, "Cannot claim item"); + } + } + + private void handleClaimProceeds(ByteBuf in, EntityPlayerMP player) throws IOException { + int listingId = in.readInt(); + + AuctionListing listing = AuctionController.Instance.getListing(listingId); + if (listing == null) { + sendResponse(player, false, "Listing not found"); + return; + } + + if (AuctionController.Instance.claimSellerProceeds(listingId, player)) { + long proceeds = listing.getSellerProceeds(); + sendResponse(player, true, "Claimed " + proceeds + " " + ConfigMarket.CurrencyName); + } else { + sendResponse(player, false, "Cannot claim proceeds"); + } + } + + /** + * Send a response message to the player + */ + private void sendResponse(EntityPlayerMP player, boolean success, String message) { + // Use chat message for now; can be extended to use GUI updates + if (message != null && !message.isEmpty()) { + player.addChatMessage(new ChatComponentText("[Auction] " + message)); + } + } + + private enum Action { + CreateListing, + PlaceBid, + Buyout, + CancelListing, + ClaimItem, + ClaimProceeds + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 3363c0351..1f09e466f 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -90,6 +90,13 @@ public class CustomNpcsPermissions { public static final Permission SOULSTONE_ALL = new Permission("customnpcs.soulstone.all"); + // Auction System + public static final Permission AUCTION_USE = new Permission("customnpcs.auction.use"); + public static final Permission AUCTION_LIST = new Permission("customnpcs.auction.list"); + public static final Permission AUCTION_BID = new Permission("customnpcs.auction.bid"); + public static final Permission AUCTION_BUYOUT = new Permission("customnpcs.auction.buyout"); + public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); + // Modify / Save Scripts public static final Permission SCRIPT = new Permission("customnpcs.script"); public static final Permission SCRIPT_NPC = new Permission("customnpcs.script.npc"); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 5bfaba816..9fc547260 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,6 +49,7 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; @@ -1013,4 +1014,70 @@ public class ScriptPlayer extends ScriptLivingBase } return CurrencyController.Instance.claimAll(player); } + + // ==================== AUCTION METHODS ==================== + + /** + * Get the number of active auction listings the player has. + * @return Number of active listings + * @since 1.11 + */ + public int getAuctionListingCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); + } + + /** + * Get the maximum number of auction listings the player is allowed. + * @return Maximum listing count + * @since 1.11 + */ + public int getMaxAuctionListings() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getMaxListings(player); + } + + /** + * Check if the player has auction items waiting to be claimed. + * This includes won items, expired listings, and sale proceeds. + * @return true if there are claimable items + * @since 1.11 + */ + public boolean hasAuctionClaims() { + if (AuctionController.Instance == null) { + return false; + } + return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); + } + + /** + * Get the number of auction items waiting to be claimed. + * @return Number of claimable items/listings + * @since 1.11 + */ + public int getAuctionClaimCount() { + if (AuctionController.Instance == null) { + return 0; + } + return AuctionController.Instance.getClaimableListings(player.getUniqueID()).size(); + } + + /** + * Get the number of auctions where the player is the current high bidder. + * @return Number of active bids + * @since 1.11 + */ + public int getActiveAuctionBidCount() { + if (AuctionController.Instance == null) { + return 0; + } + return (int) AuctionController.Instance.getListingsAsBidder(player.getUniqueID()) + .stream() + .filter(l -> l.isActive()) + .count(); + } } From d09c6b0c81ff3557cc1cd5b16099c2a54273a9de Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 09:33:29 +0000 Subject: [PATCH 11/64] Add auction filtering and sorting system New files: - EnumAuctionSort: Sorting options (price, time, name, bids, etc.) - AuctionFilter: Search/filter criteria with builder pattern - AuctionDataPacket: Large packet for server->client listing data AuctionController changes: - getFilteredListings(): Main filtering/pagination method - sortListings(): Comprehensive sorting by various criteria - quickSearch(): Convenience method for simple searches - FilterResult: Pagination result container AuctionListing changes: - Added bidCount field with NBT persistence - Added creationTime alias methods AuctionActionPacket changes: - QueryListings: Filter-based search - QueryMyListings: Player's own listings - QueryMyBids: Player's active bids - QueryClaimable: Items ready to claim NoppesUtil changes: - Auction data storage and IAuctionData interface - Client-side data handling for GUI integration --- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumDataPacket.java | 3 + .../packets/data/large/AuctionDataPacket.java | 218 +++++++++++++++ .../packets/player/AuctionActionPacket.java | 127 ++++++++- .../java/noppes/npcs/client/NoppesUtil.java | 61 +++++ .../npcs/constants/EnumAuctionSort.java | 68 +++++ .../npcs/controllers/AuctionController.java | 167 ++++++++++++ .../npcs/controllers/data/AuctionFilter.java | 250 ++++++++++++++++++ .../npcs/controllers/data/AuctionListing.java | 18 ++ 9 files changed, 913 insertions(+), 1 deletion(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionSort.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionFilter.java diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index b22537070..fa8f99de4 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -36,6 +36,7 @@ import kamkeel.npcs.network.packets.data.gui.GuiWaypointPacket; import kamkeel.npcs.network.packets.data.gui.IsGuiOpenPacket; import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.network.packets.data.large.PartyDataPacket; +import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollListPacket; import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; @@ -551,6 +552,7 @@ public class PacketHandler { DATA_PACKET.registerPacket(new SyncEffectPacket()); DATA_PACKET.registerPacket(new GuiDataPacket()); DATA_PACKET.registerPacket(new PartyDataPacket()); + DATA_PACKET.registerPacket(new AuctionDataPacket()); } public void registerPlayerPackets() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 874677af9..2f8cf71bc 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -58,4 +58,7 @@ public enum EnumDataPacket { SWING_PLAYER_ARM, DISABLE_MOUSE_INPUT, LOGIN, + + // Auction + AUCTION_DATA, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java new file mode 100644 index 000000000..f7ca16dd7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java @@ -0,0 +1,218 @@ +package kamkeel.npcs.network.packets.data.large; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import kamkeel.npcs.network.LargeAbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; + +import java.io.IOException; +import java.util.List; + +/** + * Large packet for sending auction listing data from server to client. + * Used for initial load and search/filter queries. + */ +public final class AuctionDataPacket extends LargeAbstractPacket { + public static final String packetName = "Large|AuctionData"; + + private DataType dataType; + private List listings; + private int totalResults; + private int currentPage; + private int totalPages; + private long playerBalance; + + public AuctionDataPacket() { + } + + private AuctionDataPacket(DataType type, List listings, + int totalResults, int currentPage, int totalPages, + long playerBalance) { + this.dataType = type; + this.listings = listings; + this.totalResults = totalResults; + this.currentPage = currentPage; + this.totalPages = totalPages; + this.playerBalance = playerBalance; + } + + /** + * Send filtered listing results to client + */ + public static void sendFilteredListings(EntityPlayerMP player, AuctionFilter filter) { + if (AuctionController.Instance == null) { + return; + } + + AuctionController.FilterResult result = AuctionController.Instance.getFilteredListings(filter); + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.FILTERED_LISTINGS, + result.listings, + result.totalResults, + result.currentPage, + result.totalPages, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + /** + * Send player's own listings + */ + public static void sendMyListings(EntityPlayerMP player) { + if (AuctionController.Instance == null) { + return; + } + + List myListings = AuctionController.Instance + .getListingsBySeller(player.getUniqueID()); + + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.MY_LISTINGS, + myListings, + myListings.size(), + 0, + 1, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + /** + * Send player's active bids + */ + public static void sendMyBids(EntityPlayerMP player) { + if (AuctionController.Instance == null) { + return; + } + + List myBids = AuctionController.Instance + .getListingsAsBidder(player.getUniqueID()); + + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.MY_BIDS, + myBids, + myBids.size(), + 0, + 1, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + /** + * Send player's claimable items + */ + public static void sendClaimable(EntityPlayerMP player) { + if (AuctionController.Instance == null) { + return; + } + + List claimable = AuctionController.Instance + .getClaimableListings(player.getUniqueID()); + + long balance = 0; + if (noppes.npcs.controllers.CurrencyController.Instance != null) { + balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); + } + + AuctionDataPacket packet = new AuctionDataPacket( + DataType.CLAIMABLE, + claimable, + claimable.size(), + 0, + 1, + balance + ); + PacketHandler.Instance.sendToPlayer(packet, player); + } + + @Override + public Enum getType() { + return EnumDataPacket.AUCTION_DATA; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + protected byte[] getData() throws IOException { + ByteBuf buffer = Unpooled.buffer(); + + buffer.writeInt(dataType.ordinal()); + buffer.writeInt(totalResults); + buffer.writeInt(currentPage); + buffer.writeInt(totalPages); + buffer.writeLong(playerBalance); + + // Write listings as NBT + NBTTagList listingList = new NBTTagList(); + if (listings != null) { + for (AuctionListing listing : listings) { + listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + } + + NBTTagCompound compound = new NBTTagCompound(); + compound.setTag("Listings", listingList); + ByteBufUtils.writeNBT(buffer, compound); + + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + return bytes; + } + + @Override + protected void handleCompleteData(ByteBuf data, EntityPlayer player) throws IOException { + // Client-side handling will be done in a client util class + // Similar to NoppesUtil.setScrollData() + int typeOrdinal = data.readInt(); + int totalResults = data.readInt(); + int currentPage = data.readInt(); + int totalPages = data.readInt(); + long playerBalance = data.readLong(); + + NBTTagCompound compound = ByteBufUtils.readNBT(data); + + DataType type = DataType.values()[typeOrdinal]; + + // Store data for client GUI to access + // This will be implemented in the client-side GUI handler + noppes.npcs.client.NoppesUtil.setAuctionData(type.ordinal(), compound, + totalResults, currentPage, totalPages, playerBalance); + } + + public enum DataType { + FILTERED_LISTINGS, + MY_LISTINGS, + MY_BIDS, + CLAIMABLE + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index 4aa56ab57..54bfaef50 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -19,8 +19,10 @@ import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.util.NBTJsonUtil; +import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; import java.io.IOException; @@ -37,6 +39,7 @@ public class AuctionActionPacket extends AbstractPacket { private long buyoutPrice; private int durationOrdinal; private NBTTagCompound itemData; + private NBTTagCompound filterData; // For query operations private String responseMessage; private boolean success; @@ -118,6 +121,43 @@ public class AuctionActionPacket extends AbstractPacket { PacketClient.sendClient(packet); } + /** + * Query filtered listings + */ + @SideOnly(Side.CLIENT) + public static void QueryListings(AuctionFilter filter) { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryListings); + packet.filterData = filter.writeToNBT(new NBTTagCompound()); + PacketClient.sendClient(packet); + } + + /** + * Request player's own listings + */ + @SideOnly(Side.CLIENT) + public static void QueryMyListings() { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyListings); + PacketClient.sendClient(packet); + } + + /** + * Request player's active bids + */ + @SideOnly(Side.CLIENT) + public static void QueryMyBids() { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyBids); + PacketClient.sendClient(packet); + } + + /** + * Request player's claimable items + */ + @SideOnly(Side.CLIENT) + public static void QueryClaimable() { + AuctionActionPacket packet = new AuctionActionPacket(Action.QueryClaimable); + PacketClient.sendClient(packet); + } + @Override public Enum getType() { return EnumPlayerPacket.AuctionAction; @@ -155,6 +195,19 @@ public class AuctionActionPacket extends AbstractPacket { case ClaimProceeds: out.writeInt(listingId); break; + + case QueryListings: + String filterJson = NBTJsonUtil.Convert(filterData); + byte[] filterBytes = filterJson.getBytes("UTF-8"); + out.writeInt(filterBytes.length); + out.writeBytes(filterBytes); + break; + + case QueryMyListings: + case QueryMyBids: + case QueryClaimable: + // No additional data needed + break; } } @@ -204,6 +257,18 @@ public class AuctionActionPacket extends AbstractPacket { case ClaimProceeds: handleClaimProceeds(in, playerMP); break; + case QueryListings: + handleQueryListings(in, playerMP); + break; + case QueryMyListings: + handleQueryMyListings(playerMP); + break; + case QueryMyBids: + handleQueryMyBids(playerMP); + break; + case QueryClaimable: + handleQueryClaimable(playerMP); + break; } } @@ -420,6 +485,62 @@ public class AuctionActionPacket extends AbstractPacket { } } + // ==================== Query Handlers ==================== + + private void handleQueryListings(ByteBuf in, EntityPlayerMP player) throws IOException { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + int filterBytesLength = in.readInt(); + byte[] filterBytes = new byte[filterBytesLength]; + in.readBytes(filterBytes); + String filterJson = new String(filterBytes, "UTF-8"); + + try { + NBTTagCompound filterNBT = NBTJsonUtil.Convert(filterJson); + AuctionFilter filter = new AuctionFilter(); + filter.readFromNBT(filterNBT); + + // Send filtered listings to client + AuctionDataPacket.sendFilteredListings(player, filter); + } catch (Exception e) { + sendResponse(player, false, "Error parsing filter"); + } + } + + private void handleQueryMyListings(EntityPlayerMP player) { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + AuctionDataPacket.sendMyListings(player); + } + + private void handleQueryMyBids(EntityPlayerMP player) { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + AuctionDataPacket.sendMyBids(player); + } + + private void handleQueryClaimable(EntityPlayerMP player) { + // Check permission + if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { + sendResponse(player, false, "You don't have permission to view auctions"); + return; + } + + AuctionDataPacket.sendClaimable(player); + } + /** * Send a response message to the player */ @@ -436,6 +557,10 @@ public class AuctionActionPacket extends AbstractPacket { Buyout, CancelListing, ClaimItem, - ClaimProceeds + ClaimProceeds, + QueryListings, + QueryMyListings, + QueryMyBids, + QueryClaimable } } diff --git a/src/main/java/noppes/npcs/client/NoppesUtil.java b/src/main/java/noppes/npcs/client/NoppesUtil.java index 620142564..64b26fa5d 100644 --- a/src/main/java/noppes/npcs/client/NoppesUtil.java +++ b/src/main/java/noppes/npcs/client/NoppesUtil.java @@ -182,6 +182,67 @@ public class NoppesUtil { data = new HashMap(); } + // ==================== Auction Data ==================== + + // Static storage for auction data that the GUI can access + private static NBTTagCompound auctionListingData; + private static int auctionDataType; + private static int auctionTotalResults; + private static int auctionCurrentPage; + private static int auctionTotalPages; + private static long auctionPlayerBalance; + + public static void setAuctionData(int dataType, NBTTagCompound compound, + int totalResults, int currentPage, int totalPages, + long playerBalance) { + auctionDataType = dataType; + auctionListingData = compound; + auctionTotalResults = totalResults; + auctionCurrentPage = currentPage; + auctionTotalPages = totalPages; + auctionPlayerBalance = playerBalance; + + // Notify the current GUI if it's an auction GUI + GuiScreen gui = Minecraft.getMinecraft().currentScreen; + if (gui instanceof IAuctionData) { + ((IAuctionData) gui).onAuctionDataReceived(dataType, compound, + totalResults, currentPage, totalPages, playerBalance); + } + } + + public static NBTTagCompound getAuctionListingData() { + return auctionListingData; + } + + public static int getAuctionDataType() { + return auctionDataType; + } + + public static int getAuctionTotalResults() { + return auctionTotalResults; + } + + public static int getAuctionCurrentPage() { + return auctionCurrentPage; + } + + public static int getAuctionTotalPages() { + return auctionTotalPages; + } + + public static long getAuctionPlayerBalance() { + return auctionPlayerBalance; + } + + /** + * Interface for GUIs that receive auction data + */ + public interface IAuctionData { + void onAuctionDataReceived(int dataType, NBTTagCompound data, + int totalResults, int currentPage, int totalPages, + long playerBalance); + } + public static void guiQuestCompletion(EntityPlayer player, NBTTagCompound read) { Quest quest = new Quest(); quest.readNBT(read); diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java new file mode 100644 index 000000000..a94d1f1bf --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java @@ -0,0 +1,68 @@ +package noppes.npcs.constants; + +/** + * Sorting options for auction listings. + */ +public enum EnumAuctionSort { + /** Default - newest listings first */ + NEWEST_FIRST("Newest First"), + + /** Oldest listings first */ + OLDEST_FIRST("Oldest First"), + + /** Highest current price first */ + PRICE_HIGH_TO_LOW("Most Expensive"), + + /** Lowest current price first */ + PRICE_LOW_TO_HIGH("Least Expensive"), + + /** Buyout price high to low (listings without buyout at end) */ + BUYOUT_HIGH_TO_LOW("Buyout: High to Low"), + + /** Buyout price low to high (listings without buyout at end) */ + BUYOUT_LOW_TO_HIGH("Buyout: Low to High"), + + /** Ending soonest first */ + ENDING_SOON("Ending Soon"), + + /** Most time remaining first */ + MOST_TIME_LEFT("Most Time Left"), + + /** Alphabetical by item name A-Z */ + NAME_A_TO_Z("Name: A to Z"), + + /** Alphabetical by item name Z-A */ + NAME_Z_TO_A("Name: Z to A"), + + /** Most bids first */ + MOST_BIDS("Most Bids"), + + /** Least bids first */ + LEAST_BIDS("Least Bids"); + + private final String displayName; + + EnumAuctionSort(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Get next sort option (for cycling through in GUI) + */ + public EnumAuctionSort next() { + EnumAuctionSort[] values = values(); + return values[(this.ordinal() + 1) % values.length]; + } + + /** + * Get previous sort option + */ + public EnumAuctionSort previous() { + EnumAuctionSort[] values = values(); + return values[(this.ordinal() - 1 + values.length) % values.length]; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index ec3b6de19..3e0f44a21 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -9,7 +9,9 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -144,6 +146,171 @@ public class AuctionController { .collect(Collectors.toList()); } + // ==================== Filtering & Sorting ==================== + + /** + * Get filtered and sorted auction listings. + * This is the main method for GUI queries. + * @param filter The filter criteria + * @return FilterResult containing matching listings and metadata + */ + public FilterResult getFilteredListings(AuctionFilter filter) { + processExpiredAuctions(); + + // Get all active listings + List results = listings.values().stream() + .filter(AuctionListing::isActive) + .filter(filter::matches) + .collect(Collectors.toList()); + + // Sort the results + sortListings(results, filter.sortOrder); + + // Calculate pagination + int totalResults = results.size(); + int totalPages = (int) Math.ceil((double) totalResults / filter.pageSize); + int startIndex = filter.page * filter.pageSize; + int endIndex = Math.min(startIndex + filter.pageSize, totalResults); + + // Get the page slice + List pageResults; + if (startIndex >= totalResults) { + pageResults = new ArrayList<>(); + } else { + pageResults = results.subList(startIndex, endIndex); + } + + return new FilterResult(pageResults, totalResults, filter.page, totalPages); + } + + /** + * Sort listings according to the specified sort order + */ + public void sortListings(List listings, EnumAuctionSort sortOrder) { + if (listings == null || listings.isEmpty()) { + return; + } + + Comparator comparator; + + switch (sortOrder) { + case NEWEST_FIRST: + comparator = Comparator.comparingLong(l -> -l.listingTime); + break; + + case OLDEST_FIRST: + comparator = Comparator.comparingLong(l -> l.listingTime); + break; + + case PRICE_HIGH_TO_LOW: + comparator = Comparator.comparingLong((AuctionListing l) -> + -(l.currentBid > 0 ? l.currentBid : l.startingPrice)); + break; + + case PRICE_LOW_TO_HIGH: + comparator = Comparator.comparingLong(l -> + l.currentBid > 0 ? l.currentBid : l.startingPrice); + break; + + case BUYOUT_HIGH_TO_LOW: + // Listings with buyout first, sorted high to low + comparator = Comparator + .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) + .thenComparingLong(l -> -l.buyoutPrice); + break; + + case BUYOUT_LOW_TO_HIGH: + // Listings with buyout first, sorted low to high + comparator = Comparator + .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) + .thenComparingLong(l -> l.buyoutPrice); + break; + + case ENDING_SOON: + comparator = Comparator.comparingLong(l -> l.expirationTime); + break; + + case MOST_TIME_LEFT: + comparator = Comparator.comparingLong(l -> -l.expirationTime); + break; + + case NAME_A_TO_Z: + comparator = Comparator.comparing(l -> + l.item.getDisplayName().toLowerCase()); + break; + + case NAME_Z_TO_A: + comparator = Comparator.comparing((AuctionListing l) -> + l.item.getDisplayName().toLowerCase()).reversed(); + break; + + case MOST_BIDS: + comparator = Comparator.comparingInt(l -> -l.bidCount); + break; + + case LEAST_BIDS: + comparator = Comparator.comparingInt(l -> l.bidCount); + break; + + default: + comparator = Comparator.comparingLong(l -> l.expirationTime); + } + + listings.sort(comparator); + } + + /** + * Quick search with default sorting (ending soon) + */ + public List quickSearch(String searchText, EnumAuctionCategory category, int limit) { + AuctionFilter filter = new AuctionFilter() + .withSearch(searchText) + .withCategory(category) + .withSort(EnumAuctionSort.ENDING_SOON) + .withPage(0, limit); + + return getFilteredListings(filter).listings; + } + + /** + * Get listings sorted by a specific order + */ + public List getListingsSorted(EnumAuctionSort sortOrder) { + List results = new ArrayList<>(getActiveListings()); + sortListings(results, sortOrder); + return results; + } + + /** + * Result container for filtered queries + */ + public static class FilterResult { + public final List listings; + public final int totalResults; + public final int currentPage; + public final int totalPages; + + public FilterResult(List listings, int totalResults, + int currentPage, int totalPages) { + this.listings = listings; + this.totalResults = totalResults; + this.currentPage = currentPage; + this.totalPages = totalPages; + } + + public boolean hasNextPage() { + return currentPage < totalPages - 1; + } + + public boolean hasPreviousPage() { + return currentPage > 0; + } + + public boolean isEmpty() { + return listings == null || listings.isEmpty(); + } + } + /** * Get all listings by a seller (all statuses) */ diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java new file mode 100644 index 000000000..a4a3d1d5a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -0,0 +1,250 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionSort; + +/** + * Filter/search criteria for auction listings. + * Used to query the auction controller with specific criteria. + */ +public class AuctionFilter { + + /** Search text to match against item names */ + public String searchText = ""; + + /** Category to filter by (ALL for no category filter) */ + public EnumAuctionCategory category = EnumAuctionCategory.ALL; + + /** How to sort results */ + public EnumAuctionSort sortOrder = EnumAuctionSort.ENDING_SOON; + + /** Minimum current bid/price filter */ + public long minPrice = 0; + + /** Maximum current bid/price filter (0 = no max) */ + public long maxPrice = 0; + + /** Only show listings with buyout option */ + public boolean buyoutOnly = false; + + /** Only show listings without any bids yet */ + public boolean noBidsOnly = false; + + /** Page number for pagination (0-indexed) */ + public int page = 0; + + /** Items per page */ + public int pageSize = 20; + + /** Filter by specific seller UUID string (empty = all sellers) */ + public String sellerUUID = ""; + + /** Filter by specific seller name (partial match, empty = all) */ + public String sellerName = ""; + + public AuctionFilter() { + } + + /** + * Create a filter with search text + */ + public static AuctionFilter search(String text) { + AuctionFilter filter = new AuctionFilter(); + filter.searchText = text != null ? text : ""; + return filter; + } + + /** + * Create a filter with category + */ + public static AuctionFilter category(EnumAuctionCategory category) { + AuctionFilter filter = new AuctionFilter(); + filter.category = category; + return filter; + } + + /** + * Create a filter for a specific seller + */ + public static AuctionFilter seller(String sellerUUID) { + AuctionFilter filter = new AuctionFilter(); + filter.sellerUUID = sellerUUID != null ? sellerUUID : ""; + return filter; + } + + // Builder-style methods for chaining + + public AuctionFilter withSearch(String text) { + this.searchText = text != null ? text : ""; + return this; + } + + public AuctionFilter withCategory(EnumAuctionCategory category) { + this.category = category; + return this; + } + + public AuctionFilter withSort(EnumAuctionSort sort) { + this.sortOrder = sort; + return this; + } + + public AuctionFilter withPriceRange(long min, long max) { + this.minPrice = min; + this.maxPrice = max; + return this; + } + + public AuctionFilter withBuyoutOnly(boolean buyoutOnly) { + this.buyoutOnly = buyoutOnly; + return this; + } + + public AuctionFilter withNoBidsOnly(boolean noBidsOnly) { + this.noBidsOnly = noBidsOnly; + return this; + } + + public AuctionFilter withPage(int page, int pageSize) { + this.page = Math.max(0, page); + this.pageSize = Math.max(1, Math.min(100, pageSize)); + return this; + } + + public AuctionFilter withSeller(String sellerUUID, String sellerName) { + this.sellerUUID = sellerUUID != null ? sellerUUID : ""; + this.sellerName = sellerName != null ? sellerName : ""; + return this; + } + + /** + * Check if a listing matches this filter's criteria + */ + public boolean matches(AuctionListing listing) { + if (listing == null) { + return false; + } + + // Search text filter (case-insensitive) + if (!searchText.isEmpty()) { + String lowerSearch = searchText.toLowerCase(); + String itemName = listing.item.getDisplayName().toLowerCase(); + if (!itemName.contains(lowerSearch)) { + return false; + } + } + + // Category filter + if (category != EnumAuctionCategory.ALL && listing.category != category) { + return false; + } + + // Price range filter (uses current bid or starting price) + long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; + if (minPrice > 0 && price < minPrice) { + return false; + } + if (maxPrice > 0 && price > maxPrice) { + return false; + } + + // Buyout only filter + if (buyoutOnly && listing.buyoutPrice <= 0) { + return false; + } + + // No bids only filter + if (noBidsOnly && listing.hasBids()) { + return false; + } + + // Seller UUID filter + if (!sellerUUID.isEmpty() && !listing.sellerUUID.toString().equals(sellerUUID)) { + return false; + } + + // Seller name filter (partial match) + if (!sellerName.isEmpty()) { + String lowerName = sellerName.toLowerCase(); + if (!listing.sellerName.toLowerCase().contains(lowerName)) { + return false; + } + } + + return true; + } + + /** + * Reset all filters to defaults + */ + public void reset() { + searchText = ""; + category = EnumAuctionCategory.ALL; + sortOrder = EnumAuctionSort.ENDING_SOON; + minPrice = 0; + maxPrice = 0; + buyoutOnly = false; + noBidsOnly = false; + page = 0; + sellerUUID = ""; + sellerName = ""; + } + + /** + * Write filter to NBT for network transmission + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("SearchText", searchText); + compound.setInteger("Category", category.ordinal()); + compound.setInteger("SortOrder", sortOrder.ordinal()); + compound.setLong("MinPrice", minPrice); + compound.setLong("MaxPrice", maxPrice); + compound.setBoolean("BuyoutOnly", buyoutOnly); + compound.setBoolean("NoBidsOnly", noBidsOnly); + compound.setInteger("Page", page); + compound.setInteger("PageSize", pageSize); + compound.setString("SellerUUID", sellerUUID); + compound.setString("SellerName", sellerName); + return compound; + } + + /** + * Read filter from NBT + */ + public void readFromNBT(NBTTagCompound compound) { + searchText = compound.getString("SearchText"); + + int catOrdinal = compound.getInteger("Category"); + if (catOrdinal >= 0 && catOrdinal < EnumAuctionCategory.values().length) { + category = EnumAuctionCategory.values()[catOrdinal]; + } + + int sortOrdinal = compound.getInteger("SortOrder"); + if (sortOrdinal >= 0 && sortOrdinal < EnumAuctionSort.values().length) { + sortOrder = EnumAuctionSort.values()[sortOrdinal]; + } + + minPrice = compound.getLong("MinPrice"); + maxPrice = compound.getLong("MaxPrice"); + buyoutOnly = compound.getBoolean("BuyoutOnly"); + noBidsOnly = compound.getBoolean("NoBidsOnly"); + page = compound.getInteger("Page"); + pageSize = compound.getInteger("PageSize"); + if (pageSize <= 0) pageSize = 20; + sellerUUID = compound.getString("SellerUUID"); + sellerName = compound.getString("SellerName"); + } + + @Override + public String toString() { + return "AuctionFilter{" + + "search='" + searchText + '\'' + + ", category=" + category + + ", sort=" + sortOrder + + ", price=" + minPrice + "-" + maxPrice + + ", buyoutOnly=" + buyoutOnly + + ", page=" + page + + '}'; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index e17a08112..24c778439 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -55,9 +55,24 @@ public class AuctionListing { public boolean sellerClaimed = false; // Has seller claimed their proceeds? public boolean buyerClaimed = false; // Has buyer claimed their item? + // Bid tracking + public int bidCount = 0; // Number of bids placed + public AuctionListing() { } + /** + * Alias for listingTime (when the auction was created) + */ + public long getCreationTime() { + return listingTime; + } + + // For sorting compatibility + public long creationTime() { + return listingTime; + } + /** * Create a new auction listing */ @@ -174,6 +189,7 @@ public class AuctionListing { highBidderUUID = bidder; highBidderName = bidderName; currentBid = amount; + bidCount++; // Snipe protection - extend if bid placed in final minutes if (ConfigMarket.SnipeProtectionMinutes > 0) { @@ -338,6 +354,7 @@ public class AuctionListing { compound.setInteger("Category", category.ordinal()); compound.setBoolean("SellerClaimed", sellerClaimed); compound.setBoolean("BuyerClaimed", buyerClaimed); + compound.setInteger("BidCount", bidCount); return compound; } @@ -388,5 +405,6 @@ public class AuctionListing { sellerClaimed = compound.getBoolean("SellerClaimed"); buyerClaimed = compound.getBoolean("BuyerClaimed"); + bidCount = compound.getInteger("BidCount"); } } From a1de79053b3bd90e8b2412a493270ff8b944d93c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 10:09:40 +0000 Subject: [PATCH 12/64] Implement Phase 6: GUI components, notifications, and logging system Add auction GUI components: - GuiAuction: Main auction browse interface with search, categories, sorting, pagination, and live countdown timers - SubGuiAuctionBid: Bid placement dialog with quick bid buttons - SubGuiAuctionCreate: Listing creation form with duration options - SubGuiAuctionDetails: Item details view with 2x scaled rendering - ContainerAuction: Container class for auction GUI inventory Add notification system: - AuctionNotificationController: Handles real-time notifications - Supports outbid, won, sold, expired, cancelled events - Queues notifications for offline players - Sends pending notifications on player login Add auction logging: - AuctionLogger: Comprehensive logging to CNPC+ log system - Logs all auction events (create, bid, buyout, expire, claim) - Includes system startup and processing tick logs - Admin action tracking Integrate with existing systems: - Initialize AuctionNotificationController on server start - Hook player login for notification delivery - Add logging calls to all AuctionController methods --- src/main/java/noppes/npcs/CustomNpcs.java | 2 + .../java/noppes/npcs/ServerTickHandler.java | 6 + .../npcs/client/gui/player/GuiAuction.java | 489 ++++++++++++++++++ .../client/gui/player/SubGuiAuctionBid.java | 205 ++++++++ .../gui/player/SubGuiAuctionCreate.java | 171 ++++++ .../gui/player/SubGuiAuctionDetails.java | 193 +++++++ .../npcs/containers/ContainerAuction.java | 193 +++++++ .../npcs/controllers/AuctionController.java | 95 +++- .../npcs/controllers/AuctionLogger.java | 269 ++++++++++ .../AuctionNotificationController.java | 226 ++++++++ 10 files changed, 1845 insertions(+), 4 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuction.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java create mode 100644 src/main/java/noppes/npcs/containers/ContainerAuction.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionLogger.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionNotificationController.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index c29fa4253..79a4a1b40 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -62,6 +62,7 @@ import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.AuctionBlacklist; import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.AuctionNotificationController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -312,6 +313,7 @@ public class CustomNpcs { new CurrencyController(); new AuctionBlacklist(); new AuctionController(); + new AuctionNotificationController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 3648f00d7..204e34bb5 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,6 +10,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; +import noppes.npcs.controllers.AuctionNotificationController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; @@ -65,6 +66,11 @@ public class ServerTickHandler { ProfileController.Instance.login(player); SyncController.beginLogin(player); SyncController.syncEffects(player); + + // Send pending auction notifications + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.onPlayerLogin(player); + } } @SubscribeEvent diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java new file mode 100644 index 000000000..1fe089d40 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -0,0 +1,489 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.CustomNpcResourceListener; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.*; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionCategory; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.List; + +/** + * Main auction house browse GUI. + * Displays listings with search, filter, sort, and pagination. + */ +public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, + ICustomScrollListener, NoppesUtil.IAuctionData { + + private static final ResourceLocation background = new ResourceLocation("customnpcs", "textures/gui/auction.png"); + + // GUI Components + private GuiNpcTextField searchField; + private GuiCustomScroll listingScroll; + + // Current state + private AuctionFilter currentFilter = new AuctionFilter(); + private List listings = new ArrayList<>(); + private int totalResults = 0; + private int currentPage = 0; + private int totalPages = 0; + private long playerBalance = 0; + + // Tab state (0=Browse, 1=My Listings, 2=My Bids, 3=Claims, 4=Create) + private int currentTab = 0; + + // Selected listing for details + private int selectedListingIndex = -1; + + // Countdown update timer + private long lastCountdownUpdate = 0; + + public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { + super(npc, container); + this.title = "Auction House"; + this.xSize = 256; + this.ySize = 222; + this.closeOnEsc = true; + this.drawDefaultBackground = true; + + // Request initial data + AuctionActionPacket.QueryListings(currentFilter); + } + + @Override + public void initGui() { + super.initGui(); + + // Tab buttons at top + addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, "Browse")); + addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, "My Listings")); + addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, "My Bids")); + addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, "Claims")); + addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, "Sell")); + getTopButton(currentTab).setEnabled(false); + + // Search field + searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 120, 14, currentFilter.searchText); + searchField.setMaxStringLength(50); + addTextField(searchField); + + // Search button + addButton(new GuiNpcButton(11, guiLeft + 130, guiTop + 5, 30, 16, "Search")); + + // Category buttons (simplified row) + String[] categories = {"All", "Weapons", "Armor", "Tools", "Potions", "Blocks", "Other"}; + for (int i = 0; i < categories.length; i++) { + GuiNpcButton btn = new GuiNpcButton(20 + i, guiLeft + 8 + i * 35, guiTop + 24, 33, 14, categories[i]); + btn.setEnabled(currentFilter.category.ordinal() != getCategoryOrdinal(i)); + addButton(btn); + } + + // Sort dropdown button + addButton(new GuiNpcButton(30, guiLeft + 170, guiTop + 5, 78, 16, currentFilter.sortOrder.getDisplayName())); + + // Listing scroll area + listingScroll = new GuiCustomScroll(this, 0); + listingScroll.guiLeft = guiLeft + 8; + listingScroll.guiTop = guiTop + 42; + listingScroll.setSize(160, 130); + addScroll(listingScroll); + updateListingDisplay(); + + // Pagination buttons + addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, "< Prev")); + addLabel(new GuiNpcLabel(41, getPageLabel(), guiLeft + 90, guiTop + 180, CustomNpcResourceListener.DefaultTextColor)); + addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, "Next >")); + + // Action buttons (right side) + addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, "View Details")); + addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, "Place Bid")); + addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, "Buyout")); + addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, "Refresh")); + + // Balance display + addLabel(new GuiNpcLabel(60, "Balance: " + formatCurrency(playerBalance), + guiLeft + 175, guiTop + 155, 0x00AA00)); + + updateButtonStates(); + } + + private int getCategoryOrdinal(int buttonIndex) { + switch (buttonIndex) { + case 0: return EnumAuctionCategory.ALL.ordinal(); + case 1: return EnumAuctionCategory.WEAPONS.ordinal(); + case 2: return EnumAuctionCategory.ARMOR.ordinal(); + case 3: return EnumAuctionCategory.TOOLS.ordinal(); + case 4: return EnumAuctionCategory.POTIONS.ordinal(); + case 5: return EnumAuctionCategory.BLOCKS.ordinal(); + default: return EnumAuctionCategory.MISC.ordinal(); + } + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update countdowns every second + long now = System.currentTimeMillis(); + if (now - lastCountdownUpdate >= 1000) { + lastCountdownUpdate = now; + updateListingDisplay(); + } + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + int id = button.id; + + // Tab buttons + if (id >= 0 && id <= 4) { + switchTab(id); + } + // Search button + else if (id == 11) { + performSearch(); + } + // Category buttons + else if (id >= 20 && id <= 26) { + setCategory(getCategoryOrdinal(id - 20)); + } + // Sort button + else if (id == 30) { + cycleSortOrder(); + } + // Pagination + else if (id == 40) { + previousPage(); + } else if (id == 42) { + nextPage(); + } + // Actions + else if (id == 50) { + viewDetails(); + } else if (id == 51) { + placeBid(); + } else if (id == 52) { + buyout(); + } else if (id == 53) { + refresh(); + } + } + + private void switchTab(int tab) { + currentTab = tab; + currentPage = 0; + + switch (tab) { + case 0: // Browse + AuctionActionPacket.QueryListings(currentFilter); + break; + case 1: // My Listings + AuctionActionPacket.QueryMyListings(); + break; + case 2: // My Bids + AuctionActionPacket.QueryMyBids(); + break; + case 3: // Claims + AuctionActionPacket.QueryClaimable(); + break; + case 4: // Create - open sub GUI + setSubGui(new SubGuiAuctionCreate(this, (ContainerAuction) inventorySlots)); + break; + } + + initGui(); + } + + private void performSearch() { + currentFilter.searchText = searchField.getText(); + currentFilter.page = 0; + currentPage = 0; + AuctionActionPacket.QueryListings(currentFilter); + } + + private void setCategory(int categoryOrdinal) { + currentFilter.category = EnumAuctionCategory.values()[categoryOrdinal]; + currentFilter.page = 0; + currentPage = 0; + AuctionActionPacket.QueryListings(currentFilter); + initGui(); + } + + private void cycleSortOrder() { + currentFilter.sortOrder = currentFilter.sortOrder.next(); + currentFilter.page = 0; + currentPage = 0; + AuctionActionPacket.QueryListings(currentFilter); + initGui(); + } + + private void previousPage() { + if (currentPage > 0) { + currentPage--; + currentFilter.page = currentPage; + AuctionActionPacket.QueryListings(currentFilter); + } + } + + private void nextPage() { + if (currentPage < totalPages - 1) { + currentPage++; + currentFilter.page = currentPage; + AuctionActionPacket.QueryListings(currentFilter); + } + } + + private void viewDetails() { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + setSubGui(new SubGuiAuctionDetails(this, listing)); + } + } + + private void placeBid() { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + setSubGui(new SubGuiAuctionBid(this, listing, playerBalance)); + } + } + + private void buyout() { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + if (listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice) { + AuctionActionPacket.Buyout(listing.id); + refresh(); + } + } + } + + private void refresh() { + switch (currentTab) { + case 0: + AuctionActionPacket.QueryListings(currentFilter); + break; + case 1: + AuctionActionPacket.QueryMyListings(); + break; + case 2: + AuctionActionPacket.QueryMyBids(); + break; + case 3: + AuctionActionPacket.QueryClaimable(); + break; + } + } + + private void updateListingDisplay() { + if (listingScroll == null) return; + + List displayList = new ArrayList<>(); + for (AuctionListing listing : listings) { + String line = formatListingLine(listing); + displayList.add(line); + } + listingScroll.setList(displayList); + + if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { + listingScroll.selected = selectedListingIndex; + } + } + + private String formatListingLine(AuctionListing listing) { + String itemName = listing.item.getDisplayName(); + if (itemName.length() > 15) { + itemName = itemName.substring(0, 12) + "..."; + } + + long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; + String priceStr = formatCurrency(price); + + String timeStr = formatTimeRemaining(listing.getTimeRemaining()); + + return String.format("%s - %s (%s)", itemName, priceStr, timeStr); + } + + private String formatTimeRemaining(long millis) { + if (millis <= 0) return "Ended"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + if (days > 0) { + return days + "d " + (hours % 24) + "h"; + } else if (hours > 0) { + return hours + "h " + (minutes % 60) + "m"; + } else if (minutes > 0) { + return minutes + "m " + (seconds % 60) + "s"; + } else { + return seconds + "s"; + } + } + + private String formatCurrency(long amount) { + if (amount >= 1000000) { + return String.format("%.1fM", amount / 1000000.0); + } else if (amount >= 1000) { + return String.format("%.1fK", amount / 1000.0); + } + return String.valueOf(amount); + } + + private String getPageLabel() { + if (totalPages == 0) { + return "No results"; + } + return "Page " + (currentPage + 1) + " of " + totalPages; + } + + private void updateButtonStates() { + // Pagination buttons + if (getButton(40) != null) getButton(40).setEnabled(currentPage > 0); + if (getButton(42) != null) getButton(42).setEnabled(currentPage < totalPages - 1); + + // Action buttons based on selection + boolean hasSelection = selectedListingIndex >= 0 && selectedListingIndex < listings.size(); + if (getButton(50) != null) getButton(50).setEnabled(hasSelection); + if (getButton(51) != null) getButton(51).setEnabled(hasSelection); + + if (hasSelection && getButton(52) != null) { + AuctionListing listing = listings.get(selectedListingIndex); + getButton(52).setEnabled(listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice); + } else if (getButton(52) != null) { + getButton(52).setEnabled(false); + } + } + + @Override + protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { + drawDefaultBackground(); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + + // Draw background (using a standard size, can be customized) + mc.renderEngine.bindTexture(new ResourceLocation("customnpcs", "textures/gui/bgfilled.png")); + drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + + // Draw selected item preview + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + AuctionListing listing = listings.get(selectedListingIndex); + drawItemPreview(listing, guiLeft + 180, guiTop + 160); + } + + super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); + } + + private void drawItemPreview(AuctionListing listing, int x, int y) { + if (listing == null || listing.item == null) return; + + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glEnable(GL11.GL_LIGHTING); + + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); + + RenderHelper.disableStandardItemLighting(); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + // Title + String titleText = getTabTitle(); + fontRendererObj.drawString(titleText, (xSize - fontRendererObj.getStringWidth(titleText)) / 2, -8, + CustomNpcResourceListener.DefaultTextColor); + + // Results count + fontRendererObj.drawString(totalResults + " listings", 175, 42, 0x666666); + } + + private String getTabTitle() { + switch (currentTab) { + case 0: return "Browse Auctions"; + case 1: return "My Listings"; + case 2: return "My Active Bids"; + case 3: return "Claim Items"; + case 4: return "Create Listing"; + default: return "Auction House"; + } + } + + // ICustomScrollListener + @Override + public void scrollClicked(int scrollId, int index, boolean doubleClick, String selection) { + selectedListingIndex = index; + updateButtonStates(); + + if (doubleClick && selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + viewDetails(); + } + } + + @Override + public void scrollDoubleClicked(String selection, GuiCustomScroll scroll) { + // Handled above + } + + // IGuiData - Receive NBT data from server + @Override + public void setGuiData(NBTTagCompound compound) { + // This can be used for initial setup data + } + + // IAuctionData - Receive auction listing data + @Override + public void onAuctionDataReceived(int dataType, NBTTagCompound data, + int totalResults, int currentPage, int totalPages, + long playerBalance) { + this.totalResults = totalResults; + this.currentPage = currentPage; + this.totalPages = totalPages; + this.playerBalance = playerBalance; + + // Parse listings from NBT + listings.clear(); + NBTTagList listingList = data.getTagList("Listings", 10); + for (int i = 0; i < listingList.tagCount(); i++) { + NBTTagCompound listingNBT = listingList.getCompoundTagAt(i); + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(listingNBT); + listings.add(listing); + } + + selectedListingIndex = -1; + updateListingDisplay(); + updateButtonStates(); + + // Update labels + if (getLabel(41) != null) { + getLabel(41).label = getPageLabel(); + } + if (getLabel(60) != null) { + getLabel(60).label = "Balance: " + formatCurrency(playerBalance); + } + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java new file mode 100644 index 000000000..36572b14d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -0,0 +1,205 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.AuctionListing; + +/** + * Sub GUI for placing a bid on an auction. + */ +public class SubGuiAuctionBid extends SubGuiInterface { + private AuctionListing listing; + private long playerBalance; + private GuiNpcTextField bidField; + private long lastUpdate = 0; + + public SubGuiAuctionBid(GuiAuction parent, AuctionListing listing, long playerBalance) { + this.listing = listing; + this.playerBalance = playerBalance; + setBackground("menubg.png"); + xSize = 200; + ySize = 150; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + int labelColor = 0x404040; + + // Title + addLabel(new GuiNpcLabel(0, "Place Bid", guiLeft + 70, y, labelColor)); + y += 18; + + // Item name + addLabel(new GuiNpcLabel(1, "Item: " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); + y += 14; + + // Current price + long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; + addLabel(new GuiNpcLabel(2, "Current Price:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 100, y, 0x008800)); + y += 14; + + // Minimum bid + long minBid = listing.getMinimumBid(); + addLabel(new GuiNpcLabel(4, "Minimum Bid:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 100, y, 0x0066AA)); + y += 14; + + // Your balance + addLabel(new GuiNpcLabel(6, "Your Balance:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 100, y, + playerBalance >= minBid ? 0x008800 : 0xAA0000)); + y += 14; + + // Time remaining + addLabel(new GuiNpcLabel(8, "Time Left:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + getTimeColor(listing.getTimeRemaining()))); + y += 18; + + // Bid input + addLabel(new GuiNpcLabel(10, "Your Bid:", guiLeft + 15, y + 3, labelColor)); + bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); + bidField.setNumbersOnly(); + bidField.setMinMaxDefault(minBid, 999999999, minBid); + addTextField(bidField); + y += 24; + + // Quick bid buttons + addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, "Min Bid")); + addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, "+10%")); + addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, "+25%")); + y += 22; + + // Place bid button + GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, "Place Bid"); + bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); + addButton(bidButton); + + // Cancel button + addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, "Cancel")); + + // Error message area + if (playerBalance < minBid) { + addLabel(new GuiNpcLabel(40, "Insufficient funds!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + } else if (!listing.isActive()) { + addLabel(new GuiNpcLabel(40, "Auction has ended!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + } + } + + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + private String formatTimeRemaining(long millis) { + if (millis <= 0) return "Ended"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + + if (hours > 0) { + return String.format("%dh %dm", hours, minutes % 60); + } else if (minutes > 0) { + return String.format("%dm %ds", minutes, seconds % 60); + } else { + return String.format("%ds", seconds); + } + } + + private int getTimeColor(long millis) { + if (millis <= 0) return 0xAA0000; + if (millis < 5 * 60 * 1000) return 0xFF0000; + if (millis < 30 * 60 * 1000) return 0xFF6600; + return 0x008800; + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update time remaining every second + long now = System.currentTimeMillis(); + if (now - lastUpdate >= 1000) { + lastUpdate = now; + + GuiNpcLabel timeLabel = getLabel(9); + if (timeLabel != null) { + long remaining = listing.getTimeRemaining(); + timeLabel.label = formatTimeRemaining(remaining); + timeLabel.color = getTimeColor(remaining); + } + + // Disable bid button if auction ended + GuiNpcButton bidButton = getButton(30); + if (bidButton != null && !listing.isActive()) { + bidButton.setEnabled(false); + } + } + } + + @Override + public void actionPerformed(GuiButton button) { + int id = button.id; + + long minBid = listing.getMinimumBid(); + + if (id == 20) { + // Min bid + bidField.setText(String.valueOf(minBid)); + } else if (id == 21) { + // +10% + long newBid = (long) (minBid * 1.10); + if (newBid <= playerBalance) { + bidField.setText(String.valueOf(newBid)); + } + } else if (id == 22) { + // +25% + long newBid = (long) (minBid * 1.25); + if (newBid <= playerBalance) { + bidField.setText(String.valueOf(newBid)); + } + } else if (id == 30) { + // Place bid + placeBid(); + } else if (id == 31) { + // Cancel + close(); + } + } + + private void placeBid() { + long bidAmount = bidField.getInteger(); + long minBid = listing.getMinimumBid(); + + if (bidAmount < minBid) { + return; + } + + if (bidAmount > playerBalance) { + return; + } + + if (!listing.isActive()) { + return; + } + + // Send bid to server + AuctionActionPacket.PlaceBid(listing.id, bidAmount); + close(); + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java new file mode 100644 index 000000000..4fca552e0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -0,0 +1,171 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.item.ItemStack; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.containers.ContainerAuction; + +/** + * Sub GUI for creating a new auction listing. + */ +public class SubGuiAuctionCreate extends SubGuiInterface { + private ContainerAuction container; + + private GuiNpcTextField startingPriceField; + private GuiNpcTextField buyoutPriceField; + private int selectedDuration = 1; // Default to MEDIUM + + private static final String[] DURATION_NAMES = {"Short", "Medium", "Long", "Very Long"}; + + public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { + this.container = container; + setBackground("menubg.png"); + xSize = 220; + ySize = 180; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + // Title + addLabel(new GuiNpcLabel(0, "Create Auction Listing", guiLeft + 50, guiTop + 8, 0x404040)); + + // Item slot instructions + addLabel(new GuiNpcLabel(1, "Place item in slot below:", guiLeft + 20, guiTop + 28, 0x404040)); + + // Item info (shows when item is in slot) + ItemStack item = container.getListingItem(); + if (item != null) { + addLabel(new GuiNpcLabel(2, "Item: " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); + } else { + addLabel(new GuiNpcLabel(2, "(No item selected)", guiLeft + 20, guiTop + 50, 0x888888)); + } + + // Starting price + addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, guiTop + 70, 0x404040)); + startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); + startingPriceField.setNumbersOnly(); + startingPriceField.setMinMaxDefault(1, 999999999, 100); + addTextField(startingPriceField); + + // Buyout price (optional) + addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, guiTop + 90, 0x404040)); + buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); + buyoutPriceField.setNumbersOnly(); + buyoutPriceField.setMinMaxDefault(0, 999999999, 0); + addTextField(buyoutPriceField); + addLabel(new GuiNpcLabel(22, "(0 = no buyout)", guiLeft + 185, guiTop + 90, 0x888888)); + + // Duration selection + addLabel(new GuiNpcLabel(30, "Duration:", guiLeft + 20, guiTop + 112, 0x404040)); + addButton(new GuiNpcButton(31, guiLeft + 100, guiTop + 108, 100, 20, getDurationDisplay())); + + // Fee display + long fee = getListingFee(); + addLabel(new GuiNpcLabel(40, "Listing Fee: " + fee + " " + ConfigMarket.CurrencyName, + guiLeft + 20, guiTop + 135, fee > 0 ? 0xAA0000 : 0x008800)); + + // Create button + addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, "Create Listing")); + getButton(50).setEnabled(item != null); + + // Cancel button + addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, "Cancel")); + } + + private String getDurationDisplay() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return DURATION_NAMES[selectedDuration] + " (" + duration.getHours() + "h)"; + } + + private long getListingFee() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return duration.getFee(); + } + + @Override + public void actionPerformed(GuiButton button) { + int id = button.id; + + if (id == 31) { + // Cycle duration + selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; + initGui(); + } else if (id == 50) { + // Create listing + createListing(); + } else if (id == 51) { + // Cancel + close(); + } + } + + private void createListing() { + ItemStack item = container.getListingItem(); + if (item == null) { + return; + } + + long startingPrice = startingPriceField.getInteger(); + long buyoutPrice = buyoutPriceField.getInteger(); + + if (startingPrice < 1) { + startingPrice = 1; + } + + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + // Show error - buyout must be higher than starting + return; + } + + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + + // Send to server + AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); + + // Clear slot and close + container.clearListingSlot(); + close(); + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update item display if it changed + ItemStack item = container.getListingItem(); + GuiNpcLabel itemLabel = getLabel(2); + GuiNpcButton createButton = getButton(50); + + if (item != null) { + if (itemLabel != null) { + itemLabel.label = "Item: " + item.getDisplayName(); + itemLabel.color = 0x008800; + } + if (createButton != null) { + createButton.setEnabled(true); + } + } else { + if (itemLabel != null) { + itemLabel.label = "(No item selected)"; + itemLabel.color = 0x888888; + } + if (createButton != null) { + createButton.setEnabled(false); + } + } + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java new file mode 100644 index 000000000..7f226799a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -0,0 +1,193 @@ +package noppes.npcs.client.gui.player; + +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.RenderHelper; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.AuctionListing; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +/** + * Sub GUI for viewing auction listing details. + */ +public class SubGuiAuctionDetails extends SubGuiInterface { + private AuctionListing listing; + private long lastUpdate = 0; + + public SubGuiAuctionDetails(GuiAuction parent, AuctionListing listing) { + this.listing = listing; + setBackground("menubg.png"); + xSize = 240; + ySize = 200; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 8; + int labelColor = 0x404040; + int valueColor = 0x000000; + + // Title + addLabel(new GuiNpcLabel(0, "Auction Details", guiLeft + 80, y, labelColor)); + y += 16; + + // Item name + addLabel(new GuiNpcLabel(1, "Item:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 80, y, valueColor)); + y += 14; + + // Quantity + addLabel(new GuiNpcLabel(3, "Quantity:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 80, y, valueColor)); + y += 14; + + // Seller + addLabel(new GuiNpcLabel(5, "Seller:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 80, y, valueColor)); + y += 14; + + // Starting price + addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 100, y, valueColor)); + y += 14; + + // Current bid + if (listing.currentBid > 0) { + addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 100, y, 0x008800)); + y += 14; + + // High bidder + addLabel(new GuiNpcLabel(14, "High Bidder:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 100, y, valueColor)); + y += 14; + + // Bid count + addLabel(new GuiNpcLabel(16, "Total Bids:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 100, y, valueColor)); + y += 14; + } else { + addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(13, "No bids yet", guiLeft + 100, y, 0x888888)); + y += 14; + } + + // Buyout price + if (listing.buyoutPrice > 0) { + addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 100, y, 0xAA6600)); + y += 14; + } + + // Minimum bid + addLabel(new GuiNpcLabel(22, "Minimum Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 100, y, 0x0066AA)); + y += 14; + + // Time remaining (will be updated) + addLabel(new GuiNpcLabel(30, "Time Left:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + getTimeColor(listing.getTimeRemaining()))); + y += 20; + + // Close button + addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, "Close")); + } + + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + private String formatTimeRemaining(long millis) { + if (millis <= 0) return "Auction Ended"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + if (days > 0) { + return String.format("%dd %dh %dm", days, hours % 24, minutes % 60); + } else if (hours > 0) { + return String.format("%dh %dm %ds", hours, minutes % 60, seconds % 60); + } else if (minutes > 0) { + return String.format("%dm %ds", minutes, seconds % 60); + } else { + return String.format("%ds", seconds); + } + } + + private int getTimeColor(long millis) { + if (millis <= 0) return 0xAA0000; + if (millis < 5 * 60 * 1000) return 0xFF0000; // < 5 minutes - red + if (millis < 30 * 60 * 1000) return 0xFF6600; // < 30 minutes - orange + if (millis < 60 * 60 * 1000) return 0xAAAA00; // < 1 hour - yellow + return 0x008800; // green + } + + @Override + public void updateScreen() { + super.updateScreen(); + + // Update time remaining every second + long now = System.currentTimeMillis(); + if (now - lastUpdate >= 1000) { + lastUpdate = now; + + GuiNpcLabel timeLabel = getLabel(31); + if (timeLabel != null) { + long remaining = listing.getTimeRemaining(); + timeLabel.label = formatTimeRemaining(remaining); + timeLabel.color = getTimeColor(remaining); + } + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw item icon + if (listing.item != null) { + int itemX = guiLeft + 200; + int itemY = guiTop + 30; + + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glScalef(2.0f, 2.0f, 2.0f); + + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, + listing.item, itemX / 2, itemY / 2); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, + listing.item, itemX / 2, itemY / 2); + + GL11.glScalef(0.5f, 0.5f, 0.5f); + RenderHelper.disableStandardItemLighting(); + + // Tooltip on hover + if (mouseX >= itemX && mouseX < itemX + 32 && mouseY >= itemY && mouseY < itemY + 32) { + renderToolTip(listing.item, mouseX, mouseY); + } + } + } + + @Override + public void actionPerformed(GuiButton button) { + if (button.id == 50) { + close(); + } + } + + @Override + public void save() { + // Nothing to save + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java new file mode 100644 index 000000000..61e783d91 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuction.java @@ -0,0 +1,193 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.inventory.Container; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Container for auction house GUI. + * Handles player inventory and item slot for creating listings. + */ +public class ContainerAuction extends Container { + public EntityNPCInterface npc; + public EntityPlayer player; + + // Slot for item being listed + public SlotAuctionItem listingSlot; + private IInventory listingInventory; + + public ContainerAuction(EntityNPCInterface npc, EntityPlayer player) { + this.npc = npc; + this.player = player; + + // Create a single-slot inventory for the item being listed + listingInventory = new InventoryAuctionListing(); + listingSlot = new SlotAuctionItem(listingInventory, 0, 80, 35); + addSlotToContainer(listingSlot); + + // Player inventory slots (standard positioning) + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + addSlotToContainer(new Slot(player.inventory, col + row * 9 + 9, + 8 + col * 18, 140 + row * 18)); + } + } + + // Player hotbar + for (int col = 0; col < 9; col++) { + addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 198)); + } + } + + @Override + public boolean canInteractWith(EntityPlayer player) { + return npc != null && npc.isEntityAlive() && + player.getDistanceSqToEntity(npc) < 64.0D; + } + + @Override + public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { + ItemStack itemstack = null; + Slot slot = (Slot) inventorySlots.get(slotIndex); + + if (slot != null && slot.getHasStack()) { + ItemStack stackInSlot = slot.getStack(); + itemstack = stackInSlot.copy(); + + // If from listing slot, move to player inventory + if (slotIndex == 0) { + if (!mergeItemStack(stackInSlot, 1, 37, true)) { + return null; + } + } + // If from player inventory, move to listing slot + else { + if (!mergeItemStack(stackInSlot, 0, 1, false)) { + return null; + } + } + + if (stackInSlot.stackSize == 0) { + slot.putStack(null); + } else { + slot.onSlotChanged(); + } + } + + return itemstack; + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + + // Return any item in the listing slot to player + ItemStack stack = listingInventory.getStackInSlot(0); + if (stack != null) { + if (!player.inventory.addItemStackToInventory(stack)) { + player.entityDropItem(stack, 0.5f); + } + } + } + + /** + * Get the item currently in the listing slot + */ + public ItemStack getListingItem() { + return listingInventory.getStackInSlot(0); + } + + /** + * Clear the listing slot (after successful listing creation) + */ + public void clearListingSlot() { + listingInventory.setInventorySlotContents(0, null); + } + + /** + * Simple inventory for the listing slot + */ + private class InventoryAuctionListing implements IInventory { + private ItemStack item; + + @Override + public int getSizeInventory() { return 1; } + + @Override + public ItemStack getStackInSlot(int slot) { return item; } + + @Override + public ItemStack decrStackSize(int slot, int amount) { + if (item != null) { + ItemStack result; + if (item.stackSize <= amount) { + result = item; + item = null; + return result; + } else { + result = item.splitStack(amount); + if (item.stackSize == 0) { + item = null; + } + return result; + } + } + return null; + } + + @Override + public ItemStack getStackInSlotOnClosing(int slot) { + ItemStack result = item; + item = null; + return result; + } + + @Override + public void setInventorySlotContents(int slot, ItemStack stack) { + item = stack; + } + + @Override + public String getInventoryName() { return "AuctionListing"; } + + @Override + public boolean hasCustomInventoryName() { return false; } + + @Override + public int getInventoryStackLimit() { return 64; } + + @Override + public void markDirty() {} + + @Override + public boolean isUseableByPlayer(EntityPlayer player) { return true; } + + @Override + public void openInventory() {} + + @Override + public void closeInventory() {} + + @Override + public boolean isItemValidForSlot(int slot, ItemStack stack) { return true; } + } + + /** + * Custom slot for auction listings + */ + public class SlotAuctionItem extends Slot { + public SlotAuctionItem(IInventory inv, int index, int x, int y) { + super(inv, index, x, y); + } + + @Override + public boolean isItemValid(ItemStack stack) { + // Check if item can be traded (not untradeable, soulbound, etc.) + return kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute.canTrade(stack); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 3e0f44a21..76e5f0891 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -104,6 +104,9 @@ public class AuctionController { listing.id, seller.getCommandSenderName(), item.getDisplayName()); + // Log to CNPC+ auction history + AuctionLogger.logListingCreated(listing); + return listing; } @@ -401,8 +404,9 @@ public class AuctionController { return false; } - // Store previous bidder for refund + // Store previous bidder for refund and notification UUID previousBidder = listing.highBidderUUID; + String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; // Try to place bid @@ -413,7 +417,7 @@ public class AuctionController { // Withdraw bid amount from new bidder CurrencyController.Instance.withdraw(bidder, amount); - // Refund previous bidder via claims system + // Refund previous bidder via claims system and notify if (previousBidder != null) { CurrencyController.Instance.addClaim( previousBidder, @@ -421,6 +425,20 @@ public class AuctionController { "Auction Outbid", String.valueOf(listingId) ); + + // Notify previous bidder they were outbid + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyOutbid( + listing, previousBidder, previousBidderName, previousBidAmount, amount); + } + + // Log refund + AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); + } + + // Notify seller of new bid + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyNewBid(listing, bidder.getCommandSenderName(), amount); } // Update bidder index @@ -448,8 +466,9 @@ public class AuctionController { return false; } - // Store previous bidder for refund + // Store previous bidder for refund and notification UUID previousBidder = listing.highBidderUUID; + String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; // Execute buyout @@ -460,7 +479,7 @@ public class AuctionController { // Withdraw buyout amount from buyer CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); - // Refund previous bidder via claims system + // Refund previous bidder via claims system and notify if (previousBidder != null) { CurrencyController.Instance.addClaim( previousBidder, @@ -468,6 +487,15 @@ public class AuctionController { "Auction Outbid (Buyout)", String.valueOf(listingId) ); + + // Notify previous bidder they were outbid + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyOutbid( + listing, previousBidder, previousBidderName, previousBidAmount, listing.buyoutPrice); + } + + // Log refund + AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); } // Update bidder index @@ -477,6 +505,16 @@ public class AuctionController { logger.info("Buyout executed on listing {} by {} for {}", listingId, buyer.getCommandSenderName(), listing.buyoutPrice); + // Log buyout and send notifications + AuctionLogger.logBuyout(listing, buyer.getCommandSenderName()); + + if (AuctionNotificationController.Instance != null) { + // Notify buyer they won + AuctionNotificationController.Instance.notifyAuctionWon(listing); + // Notify seller their item sold + AuctionNotificationController.Instance.notifyAuctionSold(listing); + } + return true; } @@ -490,6 +528,8 @@ public class AuctionController { return false; } + boolean byAdmin = !listing.sellerUUID.equals(requester.getUniqueID()); + if (!listing.cancel(requester.getUniqueID())) { return false; } @@ -498,6 +538,12 @@ public class AuctionController { logger.info("Listing {} cancelled by {}", listingId, requester.getCommandSenderName()); + // Log and notify + AuctionLogger.logAuctionCancelled(listing, byAdmin); + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyAuctionCancelled(listing, byAdmin); + } + return true; } @@ -536,6 +582,9 @@ public class AuctionController { logger.info("Seller {} claimed {} from listing {}", seller.getCommandSenderName(), proceeds, listingId); + // Log the currency claim + AuctionLogger.logCurrencyClaimed(listing, seller.getCommandSenderName(), proceeds); + return true; } @@ -569,6 +618,9 @@ public class AuctionController { logger.info("Buyer {} claimed item from listing {}", buyer.getCommandSenderName(), listingId); + // Log the item claim + AuctionLogger.logItemClaimed(listing, buyer.getCommandSenderName(), false); + return listing.item.copy(); } @@ -602,6 +654,9 @@ public class AuctionController { logger.info("Seller {} reclaimed item from listing {}", seller.getCommandSenderName(), listingId); + // Log the item reclaim + AuctionLogger.logItemClaimed(listing, seller.getCommandSenderName(), true); + return listing.item.copy(); } @@ -624,6 +679,8 @@ public class AuctionController { */ public void processExpiredAuctions() { boolean changed = false; + int expiredCount = 0; + int soldCount = 0; for (AuctionListing listing : listings.values()) { if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { @@ -632,17 +689,32 @@ public class AuctionController { // If sold, add proceeds to seller's claims if (listing.status == EnumAuctionStatus.SOLD) { + soldCount++; // Proceeds will be claimed manually, no automatic deposit logger.info("Auction {} ended with sale to {} for {}", listing.id, listing.highBidderName, listing.currentBid); + + // Send notifications + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyAuctionWon(listing); + AuctionNotificationController.Instance.notifyAuctionSold(listing); + } } else { + expiredCount++; logger.info("Auction {} expired without bids", listing.id); + + // Notify seller about expiration + if (AuctionNotificationController.Instance != null) { + AuctionNotificationController.Instance.notifyAuctionExpired(listing); + } } } } if (changed) { save(); + // Log processing tick if anything happened + AuctionLogger.logProcessingTick(expiredCount, soldCount); } } @@ -804,8 +876,23 @@ public class AuctionController { logger.info("Loaded {} auction listings", listings.size()); + // Log system startup + int activeCount = getActiveListingCount(); + int pendingClaims = 0; + for (AuctionListing listing : listings.values()) { + if (!listing.sellerClaimed || !listing.buyerClaimed) { + if (listing.status == EnumAuctionStatus.SOLD || + listing.status == EnumAuctionStatus.EXPIRED || + listing.status == EnumAuctionStatus.CANCELLED) { + pendingClaims++; + } + } + } + AuctionLogger.logSystemStartup(activeCount, pendingClaims); + } catch (Exception e) { logger.error("Error loading auctions", e); + AuctionLogger.logError("Loading auctions", e); } } diff --git a/src/main/java/noppes/npcs/controllers/AuctionLogger.java b/src/main/java/noppes/npcs/controllers/AuctionLogger.java new file mode 100644 index 000000000..4c5264319 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionLogger.java @@ -0,0 +1,269 @@ +package noppes.npcs.controllers; + +import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.controllers.data.AuctionListing; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Handles auction history logging to the CNPC+ logging system. + * All auction events are recorded for administrative review. + */ +public class AuctionLogger { + private static final String PREFIX = "[Auction] "; + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + /** + * Log when a new auction listing is created + */ + public static void logListingCreated(AuctionListing listing) { + String msg = String.format("%sLISTING_CREATED: ID=%d, Seller=%s (%s), Item=%s x%d, StartPrice=%d, Buyout=%s, Duration=%s", + PREFIX, + listing.id, + listing.sellerName, + listing.sellerUUID.toString(), + listing.item.getDisplayName(), + listing.item.stackSize, + listing.startingPrice, + listing.buyoutPrice > 0 ? String.valueOf(listing.buyoutPrice) : "None", + formatDuration(listing.expirationTime - listing.listingTime) + ); + LogWriter.info(msg); + } + + /** + * Log when a bid is placed + */ + public static void logBidPlaced(AuctionListing listing, String bidderName, long bidAmount) { + String msg = String.format("%sBID_PLACED: ID=%d, Item=%s, Bidder=%s, Amount=%d %s, Previous=%d %s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + bidderName, + bidAmount, + ConfigMarket.CurrencyName, + listing.currentBid, + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log when a player is outbid + */ + public static void logOutbid(AuctionListing listing, String outbidPlayerName, long previousBid, + String newBidderName, long newBid) { + String msg = String.format("%sOUTBID: ID=%d, Item=%s, OutbidPlayer=%s (had %d), NewBidder=%s (bid %d)", + PREFIX, + listing.id, + listing.item.getDisplayName(), + outbidPlayerName, + previousBid, + newBidderName, + newBid + ); + LogWriter.info(msg); + } + + /** + * Log when a buyout occurs + */ + public static void logBuyout(AuctionListing listing, String buyerName) { + String msg = String.format("%sBUYOUT: ID=%d, Item=%s, Buyer=%s, Price=%d %s, Seller=%s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + buyerName, + listing.buyoutPrice, + ConfigMarket.CurrencyName, + listing.sellerName + ); + LogWriter.info(msg); + } + + /** + * Log when an auction is won (ended with bids) + */ + public static void logAuctionWon(AuctionListing listing) { + String msg = String.format("%sAUCTION_WON: ID=%d, Item=%s, Winner=%s (%s), FinalPrice=%d %s, Seller=%s, SellerProceeds=%d %s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.highBidderName, + listing.highBidderUUID != null ? listing.highBidderUUID.toString() : "N/A", + listing.currentBid, + ConfigMarket.CurrencyName, + listing.sellerName, + listing.getSellerProceeds(), + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log when an auction sells (same as won, for notification controller compatibility) + */ + public static void logAuctionSold(AuctionListing listing) { + String msg = String.format("%sAUCTION_SOLD: ID=%d, Item=%s, Buyer=%s, SalePrice=%d %s, Seller=%s (%s), Proceeds=%d %s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.highBidderName, + listing.currentBid, + ConfigMarket.CurrencyName, + listing.sellerName, + listing.sellerUUID.toString(), + listing.getSellerProceeds(), + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log when an auction expires with no bids + */ + public static void logAuctionExpired(AuctionListing listing) { + String msg = String.format("%sAUCTION_EXPIRED: ID=%d, Item=%s, Seller=%s (%s), StartPrice=%d %s, Duration=%s", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.sellerName, + listing.sellerUUID.toString(), + listing.startingPrice, + ConfigMarket.CurrencyName, + formatDuration(listing.expirationTime - listing.listingTime) + ); + LogWriter.info(msg); + } + + /** + * Log when an auction is cancelled + */ + public static void logAuctionCancelled(AuctionListing listing, boolean byAdmin) { + String msg = String.format("%sAUCTION_CANCELLED: ID=%d, Item=%s, Seller=%s, CancelledBy=%s, HadBids=%b", + PREFIX, + listing.id, + listing.item.getDisplayName(), + listing.sellerName, + byAdmin ? "ADMIN" : "SELLER", + listing.currentBid > 0 + ); + LogWriter.info(msg); + } + + /** + * Log when an item is claimed + */ + public static void logItemClaimed(AuctionListing listing, String claimerName, boolean isSeller) { + String claimType = isSeller ? + (listing.status == EnumAuctionStatus.EXPIRED ? "RECLAIMED_EXPIRED" : "CLAIMED_PROCEEDS") : + "CLAIMED_WON_ITEM"; + + String msg = String.format("%s%s: ID=%d, Item=%s, Claimer=%s, Status=%s", + PREFIX, + claimType, + listing.id, + listing.item.getDisplayName(), + claimerName, + listing.status.name() + ); + LogWriter.info(msg); + } + + /** + * Log currency claimed from auction + */ + public static void logCurrencyClaimed(AuctionListing listing, String claimerName, long amount) { + String msg = String.format("%sCURRENCY_CLAIMED: ID=%d, Claimer=%s, Amount=%d %s", + PREFIX, + listing.id, + claimerName, + amount, + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log refund of bid to outbid player + */ + public static void logBidRefunded(AuctionListing listing, String playerName, long amount) { + String msg = String.format("%sBID_REFUNDED: ID=%d, Player=%s, Amount=%d %s", + PREFIX, + listing.id, + playerName, + amount, + ConfigMarket.CurrencyName + ); + LogWriter.info(msg); + } + + /** + * Log admin actions on auctions + */ + public static void logAdminAction(String adminName, String action, AuctionListing listing) { + String msg = String.format("%sADMIN_ACTION: Admin=%s, Action=%s, ListingID=%d, Item=%s, Seller=%s", + PREFIX, + adminName, + action, + listing.id, + listing.item.getDisplayName(), + listing.sellerName + ); + LogWriter.info(msg); + } + + /** + * Log auction system startup + */ + public static void logSystemStartup(int activeListings, int pendingClaims) { + String msg = String.format("%sSYSTEM_STARTUP: ActiveListings=%d, PendingClaims=%d, Timestamp=%s", + PREFIX, + activeListings, + pendingClaims, + dateFormat.format(new Date()) + ); + LogWriter.info(msg); + } + + /** + * Log auction processing tick + */ + public static void logProcessingTick(int expired, int completed) { + if (expired > 0 || completed > 0) { + String msg = String.format("%sPROCESSING_TICK: ExpiredListings=%d, CompletedAuctions=%d", + PREFIX, + expired, + completed + ); + LogWriter.info(msg); + } + } + + /** + * Log errors in the auction system + */ + public static void logError(String context, Exception e) { + String msg = String.format("%sERROR: Context=%s, Message=%s", + PREFIX, + context, + e.getMessage() + ); + LogWriter.error(msg, e); + } + + /** + * Format duration in a human-readable format + */ + private static String formatDuration(long millis) { + long hours = millis / (1000 * 60 * 60); + if (hours >= 24) { + long days = hours / 24; + return days + "d " + (hours % 24) + "h"; + } + return hours + "h"; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java b/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java new file mode 100644 index 000000000..48da6d345 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java @@ -0,0 +1,226 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; +import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.AuctionListing; + +import java.util.*; + +/** + * Handles auction notifications for players. + * Sends messages for outbid, won, sold, expired events. + */ +public class AuctionNotificationController { + public static AuctionNotificationController Instance; + + // Pending notifications for offline players (UUID -> List of notifications) + private Map> pendingNotifications = new HashMap<>(); + + public AuctionNotificationController() { + Instance = this; + } + + /** + * Notify a player they have been outbid + */ + public void notifyOutbid(AuctionListing listing, UUID outbidPlayerUUID, + String outbidPlayerName, long previousBid, long newBid) { + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + + "You have been outbid on " + EnumChatFormatting.WHITE + + listing.item.getDisplayName() + EnumChatFormatting.RED + + "! New bid: " + formatCurrency(newBid) + + " (Your bid: " + formatCurrency(previousBid) + " has been refunded to claims)"; + + sendOrQueueNotification(outbidPlayerUUID, NotificationType.OUTBID, message, listing.id); + + // Log the outbid event + AuctionLogger.logOutbid(listing, outbidPlayerName, previousBid, listing.highBidderName, newBid); + } + + /** + * Notify a player they won an auction + */ + public void notifyAuctionWon(AuctionListing listing) { + if (listing.highBidderUUID == null) return; + + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "Congratulations! You won the auction for " + EnumChatFormatting.WHITE + + listing.item.getDisplayName() + EnumChatFormatting.GREEN + + " for " + formatCurrency(listing.currentBid) + "! Visit an Auctioneer to claim your item."; + + sendOrQueueNotification(listing.highBidderUUID, NotificationType.WON, message, listing.id); + + // Log the win + AuctionLogger.logAuctionWon(listing); + } + + /** + * Notify seller their auction sold + */ + public void notifyAuctionSold(AuctionListing listing) { + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "Your " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.GREEN + " sold for " + formatCurrency(listing.currentBid) + + "! Visit an Auctioneer to claim " + formatCurrency(listing.getSellerProceeds()) + "."; + + sendOrQueueNotification(listing.sellerUUID, NotificationType.SOLD, message, listing.id); + + // Log the sale + AuctionLogger.logAuctionSold(listing); + } + + /** + * Notify seller their auction expired with no bids + */ + public void notifyAuctionExpired(AuctionListing listing) { + String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + + "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.GRAY + " has expired with no bids. Visit an Auctioneer to reclaim your item."; + + sendOrQueueNotification(listing.sellerUUID, NotificationType.EXPIRED, message, listing.id); + + // Log the expiration + AuctionLogger.logAuctionExpired(listing); + } + + /** + * Notify player their auction was cancelled + */ + public void notifyAuctionCancelled(AuctionListing listing, boolean byAdmin) { + String message; + if (byAdmin) { + message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + + "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + " was cancelled by an administrator."; + } else { + message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + + "You cancelled your auction for " + EnumChatFormatting.WHITE + + listing.item.getDisplayName() + EnumChatFormatting.GRAY + "."; + } + + sendOrQueueNotification(listing.sellerUUID, NotificationType.CANCELLED, message, listing.id); + + // Log the cancellation + AuctionLogger.logAuctionCancelled(listing, byAdmin); + } + + /** + * Notify about a new bid placed + */ + public void notifyNewBid(AuctionListing listing, String bidderName, long bidAmount) { + // Notify seller about new bid + String sellerMessage = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.AQUA + + bidderName + " placed a bid of " + formatCurrency(bidAmount) + + " on your " + listing.item.getDisplayName() + "!"; + + sendOrQueueNotification(listing.sellerUUID, NotificationType.NEW_BID, sellerMessage, listing.id); + + // Log the bid + AuctionLogger.logBidPlaced(listing, bidderName, bidAmount); + } + + /** + * Send notification immediately if player online, otherwise queue for later + */ + private void sendOrQueueNotification(UUID playerUUID, NotificationType type, String message, int listingId) { + EntityPlayerMP player = getOnlinePlayer(playerUUID); + + if (player != null) { + player.addChatMessage(new ChatComponentText(message)); + } else { + // Queue for when player logs in + AuctionNotification notification = new AuctionNotification(type, message, listingId); + + if (!pendingNotifications.containsKey(playerUUID)) { + pendingNotifications.put(playerUUID, new ArrayList<>()); + } + pendingNotifications.get(playerUUID).add(notification); + + // Limit queue size per player + List queue = pendingNotifications.get(playerUUID); + while (queue.size() > 50) { + queue.remove(0); + } + } + } + + /** + * Called when a player logs in - send any pending notifications + */ + public void onPlayerLogin(EntityPlayerMP player) { + UUID uuid = player.getUniqueID(); + + if (pendingNotifications.containsKey(uuid)) { + List notifications = pendingNotifications.remove(uuid); + + if (!notifications.isEmpty()) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + + "You have " + notifications.size() + " auction notification(s):")); + + for (AuctionNotification notification : notifications) { + player.addChatMessage(new ChatComponentText(notification.message)); + } + } + } + + // Also check for claimable items + if (AuctionController.Instance != null) { + int claimableCount = AuctionController.Instance.getClaimableListings(uuid).size(); + if (claimableCount > 0) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "You have " + claimableCount + " item(s) waiting to be claimed!")); + } + } + } + + private EntityPlayerMP getOnlinePlayer(UUID uuid) { + if (noppes.npcs.CustomNpcs.getServer() == null) return null; + + for (Object obj : noppes.npcs.CustomNpcs.getServer().getConfigurationManager().playerEntityList) { + EntityPlayerMP player = (EntityPlayerMP) obj; + if (player.getUniqueID().equals(uuid)) { + return player; + } + } + return null; + } + + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Notification types for categorization + */ + public enum NotificationType { + OUTBID, + WON, + SOLD, + EXPIRED, + CANCELLED, + NEW_BID + } + + /** + * Stored notification for offline players + */ + private static class AuctionNotification { + public final NotificationType type; + public final String message; + public final int listingId; + public final long timestamp; + + public AuctionNotification(NotificationType type, String message, int listingId) { + this.type = type; + this.message = message; + this.listingId = listingId; + this.timestamp = System.currentTimeMillis(); + } + } +} From 740c2852e5d805fc6c4e54c353cd51f7aae1c3e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 10:26:56 +0000 Subject: [PATCH 13/64] Refactor auction system: consolidate controllers, add lang keys, remove categories - Consolidate AuctionLogger and AuctionNotificationController into AuctionController - Add inline logging and notification methods to AuctionController - Add comprehensive LANG keys for all auction GUI strings - Update EnumAuctionSort and EnumAuctionDuration to use lang keys - Remove category filtering system (EnumAuctionCategory deleted) - Update AuctionFilter to remove category references - Update GuiAuction and all sub-GUIs to use StatCollector.translateToLocal() - Update AuctionListing to remove category field and NBT serialization --- src/main/java/noppes/npcs/CustomNpcs.java | 2 - .../java/noppes/npcs/ServerTickHandler.java | 6 +- .../npcs/client/gui/player/GuiAuction.java | 109 ++--- .../client/gui/player/SubGuiAuctionBid.java | 31 +- .../gui/player/SubGuiAuctionCreate.java | 31 +- .../gui/player/SubGuiAuctionDetails.java | 31 +- .../npcs/constants/EnumAuctionCategory.java | 156 ------ .../npcs/constants/EnumAuctionDuration.java | 46 +- .../npcs/constants/EnumAuctionSort.java | 61 +-- .../npcs/controllers/AuctionController.java | 457 +++++++----------- .../npcs/controllers/AuctionLogger.java | 269 ----------- .../AuctionNotificationController.java | 226 --------- .../npcs/controllers/data/AuctionFilter.java | 31 -- .../npcs/controllers/data/AuctionListing.java | 13 - .../assets/customnpcs/lang/en_US.lang | 70 +++ 15 files changed, 374 insertions(+), 1165 deletions(-) delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionCategory.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionLogger.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionNotificationController.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 79a4a1b40..c29fa4253 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -62,7 +62,6 @@ import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.AuctionBlacklist; import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.AuctionNotificationController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -313,7 +312,6 @@ public class CustomNpcs { new CurrencyController(); new AuctionBlacklist(); new AuctionController(); - new AuctionNotificationController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 204e34bb5..fe846df19 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,7 +10,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; -import noppes.npcs.controllers.AuctionNotificationController; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; @@ -68,8 +68,8 @@ public class ServerTickHandler { SyncController.syncEffects(player); // Send pending auction notifications - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.onPlayerLogin(player); + if (AuctionController.Instance != null) { + AuctionController.Instance.onPlayerLogin(player); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 1fe089d40..118dde167 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -3,17 +3,13 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.util.*; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionCategory; -import noppes.npcs.constants.EnumAuctionSort; -import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.containers.ContainerAuction; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; @@ -26,13 +22,11 @@ import java.util.List; /** * Main auction house browse GUI. - * Displays listings with search, filter, sort, and pagination. + * Displays listings with search, sort, and pagination. */ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, ICustomScrollListener, NoppesUtil.IAuctionData { - private static final ResourceLocation background = new ResourceLocation("customnpcs", "textures/gui/auction.png"); - // GUI Components private GuiNpcTextField searchField; private GuiCustomScroll listingScroll; @@ -56,7 +50,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { super(npc, container); - this.title = "Auction House"; + this.title = ""; this.xSize = 256; this.ySize = 222; this.closeOnEsc = true; @@ -71,70 +65,50 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, super.initGui(); // Tab buttons at top - addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, "Browse")); - addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, "My Listings")); - addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, "My Bids")); - addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, "Claims")); - addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, "Sell")); + addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse"))); + addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, StatCollector.translateToLocal("auction.myListings"))); + addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, StatCollector.translateToLocal("auction.myBids"))); + addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, StatCollector.translateToLocal("auction.claims"))); + addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, StatCollector.translateToLocal("auction.sell"))); getTopButton(currentTab).setEnabled(false); // Search field - searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 120, 14, currentFilter.searchText); + searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 140, 14, currentFilter.searchText); searchField.setMaxStringLength(50); addTextField(searchField); // Search button - addButton(new GuiNpcButton(11, guiLeft + 130, guiTop + 5, 30, 16, "Search")); + addButton(new GuiNpcButton(11, guiLeft + 150, guiTop + 5, 40, 16, StatCollector.translateToLocal("auction.search"))); - // Category buttons (simplified row) - String[] categories = {"All", "Weapons", "Armor", "Tools", "Potions", "Blocks", "Other"}; - for (int i = 0; i < categories.length; i++) { - GuiNpcButton btn = new GuiNpcButton(20 + i, guiLeft + 8 + i * 35, guiTop + 24, 33, 14, categories[i]); - btn.setEnabled(currentFilter.category.ordinal() != getCategoryOrdinal(i)); - addButton(btn); - } - - // Sort dropdown button - addButton(new GuiNpcButton(30, guiLeft + 170, guiTop + 5, 78, 16, currentFilter.sortOrder.getDisplayName())); + // Sort button + addButton(new GuiNpcButton(30, guiLeft + 8, guiTop + 24, 120, 16, currentFilter.sortOrder.getDisplayName())); // Listing scroll area listingScroll = new GuiCustomScroll(this, 0); listingScroll.guiLeft = guiLeft + 8; - listingScroll.guiTop = guiTop + 42; - listingScroll.setSize(160, 130); + listingScroll.guiTop = guiTop + 44; + listingScroll.setSize(160, 128); addScroll(listingScroll); updateListingDisplay(); // Pagination buttons - addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, "< Prev")); + addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.prevPage"))); addLabel(new GuiNpcLabel(41, getPageLabel(), guiLeft + 90, guiTop + 180, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, "Next >")); + addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.nextPage"))); // Action buttons (right side) - addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, "View Details")); - addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, "Place Bid")); - addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, "Buyout")); - addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, "Refresh")); + addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, StatCollector.translateToLocal("auction.viewDetails"))); + addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, StatCollector.translateToLocal("auction.placeBid"))); + addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, StatCollector.translateToLocal("auction.buyout"))); + addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, StatCollector.translateToLocal("auction.refresh"))); // Balance display - addLabel(new GuiNpcLabel(60, "Balance: " + formatCurrency(playerBalance), + addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), guiLeft + 175, guiTop + 155, 0x00AA00)); updateButtonStates(); } - private int getCategoryOrdinal(int buttonIndex) { - switch (buttonIndex) { - case 0: return EnumAuctionCategory.ALL.ordinal(); - case 1: return EnumAuctionCategory.WEAPONS.ordinal(); - case 2: return EnumAuctionCategory.ARMOR.ordinal(); - case 3: return EnumAuctionCategory.TOOLS.ordinal(); - case 4: return EnumAuctionCategory.POTIONS.ordinal(); - case 5: return EnumAuctionCategory.BLOCKS.ordinal(); - default: return EnumAuctionCategory.MISC.ordinal(); - } - } - @Override public void updateScreen() { super.updateScreen(); @@ -160,10 +134,6 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, else if (id == 11) { performSearch(); } - // Category buttons - else if (id >= 20 && id <= 26) { - setCategory(getCategoryOrdinal(id - 20)); - } // Sort button else if (id == 30) { cycleSortOrder(); @@ -218,14 +188,6 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, AuctionActionPacket.QueryListings(currentFilter); } - private void setCategory(int categoryOrdinal) { - currentFilter.category = EnumAuctionCategory.values()[categoryOrdinal]; - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - initGui(); - } - private void cycleSortOrder() { currentFilter.sortOrder = currentFilter.sortOrder.next(); currentFilter.page = 0; @@ -321,7 +283,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } private String formatTimeRemaining(long millis) { - if (millis <= 0) return "Ended"; + if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); long seconds = millis / 1000; long minutes = seconds / 60; @@ -350,9 +312,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private String getPageLabel() { if (totalPages == 0) { - return "No results"; + return StatCollector.translateToLocal("auction.noResults"); } - return "Page " + (currentPage + 1) + " of " + totalPages; + return String.format(StatCollector.translateToLocal("auction.page"), currentPage + 1, totalPages); } private void updateButtonStates() { @@ -378,7 +340,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, drawDefaultBackground(); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - // Draw background (using a standard size, can be customized) + // Draw background mc.renderEngine.bindTexture(new ResourceLocation("customnpcs", "textures/gui/bgfilled.png")); drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); @@ -413,17 +375,18 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, CustomNpcResourceListener.DefaultTextColor); // Results count - fontRendererObj.drawString(totalResults + " listings", 175, 42, 0x666666); + String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); + fontRendererObj.drawString(listingsLabel, 175, 42, 0x666666); } private String getTabTitle() { switch (currentTab) { - case 0: return "Browse Auctions"; - case 1: return "My Listings"; - case 2: return "My Active Bids"; - case 3: return "Claim Items"; - case 4: return "Create Listing"; - default: return "Auction House"; + case 0: return StatCollector.translateToLocal("auction.browseTitle"); + case 1: return StatCollector.translateToLocal("auction.myListingsTitle"); + case 2: return StatCollector.translateToLocal("auction.myBidsTitle"); + case 3: return StatCollector.translateToLocal("auction.claimsTitle"); + case 4: return StatCollector.translateToLocal("auction.sellTitle"); + default: return StatCollector.translateToLocal("auction.title"); } } @@ -443,13 +406,13 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Handled above } - // IGuiData - Receive NBT data from server + // IGuiData @Override public void setGuiData(NBTTagCompound compound) { // This can be used for initial setup data } - // IAuctionData - Receive auction listing data + // IAuctionData @Override public void onAuctionDataReceived(int dataType, NBTTagCompound data, int totalResults, int currentPage, int totalPages, @@ -478,7 +441,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, getLabel(41).label = getPageLabel(); } if (getLabel(60) != null) { - getLabel(60).label = "Balance: " + formatCurrency(playerBalance); + getLabel(60).label = String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index 36572b14d..7f4665b40 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; @@ -35,39 +36,39 @@ public class SubGuiAuctionBid extends SubGuiInterface { int labelColor = 0x404040; // Title - addLabel(new GuiNpcLabel(0, "Place Bid", guiLeft + 70, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 70, y, labelColor)); y += 18; // Item name - addLabel(new GuiNpcLabel(1, "Item: " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item") + " " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); y += 14; // Current price long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - addLabel(new GuiNpcLabel(2, "Current Price:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.currentPrice"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 100, y, 0x008800)); y += 14; // Minimum bid long minBid = listing.getMinimumBid(); - addLabel(new GuiNpcLabel(4, "Minimum Bid:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 100, y, 0x0066AA)); y += 14; // Your balance - addLabel(new GuiNpcLabel(6, "Your Balance:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(6, StatCollector.translateToLocal("auction.yourBalance"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 100, y, playerBalance >= minBid ? 0x008800 : 0xAA0000)); y += 14; // Time remaining - addLabel(new GuiNpcLabel(8, "Time Left:", guiLeft + 15, y, labelColor)); + addLabel(new GuiNpcLabel(8, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 15, y, labelColor)); addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, getTimeColor(listing.getTimeRemaining()))); y += 18; // Bid input - addLabel(new GuiNpcLabel(10, "Your Bid:", guiLeft + 15, y + 3, labelColor)); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); bidField.setNumbersOnly(); bidField.setMinMaxDefault(minBid, 999999999, minBid); @@ -75,24 +76,24 @@ public class SubGuiAuctionBid extends SubGuiInterface { y += 24; // Quick bid buttons - addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, "Min Bid")); - addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, "+10%")); - addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, "+25%")); + addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, StatCollector.translateToLocal("auction.minBid"))); + addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus10"))); + addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus25"))); y += 22; // Place bid button - GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, "Place Bid"); + GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); addButton(bidButton); // Cancel button - addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, "Cancel")); + addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); // Error message area if (playerBalance < minBid) { - addLabel(new GuiNpcLabel(40, "Insufficient funds!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); } else if (!listing.isActive()) { - addLabel(new GuiNpcLabel(40, "Auction has ended!", guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); } } @@ -101,7 +102,7 @@ public class SubGuiAuctionBid extends SubGuiInterface { } private String formatTimeRemaining(long millis) { - if (millis <= 0) return "Ended"; + if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); long seconds = millis / 1000; long minutes = seconds / 60; diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index 4fca552e0..75329f5e8 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.item.ItemStack; +import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; @@ -21,8 +22,6 @@ public class SubGuiAuctionCreate extends SubGuiInterface { private GuiNpcTextField buyoutPriceField; private int selectedDuration = 1; // Default to MEDIUM - private static final String[] DURATION_NAMES = {"Short", "Medium", "Long", "Very Long"}; - public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { this.container = container; setBackground("menubg.png"); @@ -36,54 +35,54 @@ public class SubGuiAuctionCreate extends SubGuiInterface { super.initGui(); // Title - addLabel(new GuiNpcLabel(0, "Create Auction Listing", guiLeft + 50, guiTop + 8, 0x404040)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 50, guiTop + 8, 0x404040)); // Item slot instructions - addLabel(new GuiNpcLabel(1, "Place item in slot below:", guiLeft + 20, guiTop + 28, 0x404040)); + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.placeItemBelow"), guiLeft + 20, guiTop + 28, 0x404040)); // Item info (shows when item is in slot) ItemStack item = container.getListingItem(); if (item != null) { - addLabel(new GuiNpcLabel(2, "Item: " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); } else { - addLabel(new GuiNpcLabel(2, "(No item selected)", guiLeft + 20, guiTop + 50, 0x888888)); + addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 20, guiTop + 50, 0x888888)); } // Starting price - addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, guiTop + 70, 0x404040)); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 70, 0x404040)); startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); startingPriceField.setNumbersOnly(); startingPriceField.setMinMaxDefault(1, 999999999, 100); addTextField(startingPriceField); // Buyout price (optional) - addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, guiTop + 90, 0x404040)); + addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 90, 0x404040)); buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); buyoutPriceField.setNumbersOnly(); buyoutPriceField.setMinMaxDefault(0, 999999999, 0); addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(22, "(0 = no buyout)", guiLeft + 185, guiTop + 90, 0x888888)); + addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 185, guiTop + 90, 0x888888)); // Duration selection - addLabel(new GuiNpcLabel(30, "Duration:", guiLeft + 20, guiTop + 112, 0x404040)); + addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 112, 0x404040)); addButton(new GuiNpcButton(31, guiLeft + 100, guiTop + 108, 100, 20, getDurationDisplay())); // Fee display long fee = getListingFee(); - addLabel(new GuiNpcLabel(40, "Listing Fee: " + fee + " " + ConfigMarket.CurrencyName, + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, guiLeft + 20, guiTop + 135, fee > 0 ? 0xAA0000 : 0x008800)); // Create button - addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, "Create Listing")); + addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, StatCollector.translateToLocal("auction.create"))); getButton(50).setEnabled(item != null); // Cancel button - addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, "Cancel")); + addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, StatCollector.translateToLocal("gui.cancel"))); } private String getDurationDisplay() { EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return DURATION_NAMES[selectedDuration] + " (" + duration.getHours() + "h)"; + return duration.getDisplayName(); } private long getListingFee() { @@ -147,7 +146,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { if (item != null) { if (itemLabel != null) { - itemLabel.label = "Item: " + item.getDisplayName(); + itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); itemLabel.color = 0x008800; } if (createButton != null) { @@ -155,7 +154,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { } } else { if (itemLabel != null) { - itemLabel.label = "(No item selected)"; + itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); itemLabel.color = 0x888888; } if (createButton != null) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java index 7f226799a..f18046922 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.player; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; @@ -34,70 +35,70 @@ public class SubGuiAuctionDetails extends SubGuiInterface { int valueColor = 0x000000; // Title - addLabel(new GuiNpcLabel(0, "Auction Details", guiLeft + 80, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 80, y, labelColor)); y += 16; // Item name - addLabel(new GuiNpcLabel(1, "Item:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 80, y, valueColor)); y += 14; // Quantity - addLabel(new GuiNpcLabel(3, "Quantity:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("auction.quantity"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 80, y, valueColor)); y += 14; // Seller - addLabel(new GuiNpcLabel(5, "Seller:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("auction.seller"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 80, y, valueColor)); y += 14; // Starting price - addLabel(new GuiNpcLabel(10, "Starting Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 100, y, valueColor)); y += 14; // Current bid if (listing.currentBid > 0) { - addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 100, y, 0x008800)); y += 14; // High bidder - addLabel(new GuiNpcLabel(14, "High Bidder:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(14, StatCollector.translateToLocal("auction.highBidder"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 100, y, valueColor)); y += 14; // Bid count - addLabel(new GuiNpcLabel(16, "Total Bids:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(16, StatCollector.translateToLocal("auction.totalBids"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 100, y, valueColor)); y += 14; } else { - addLabel(new GuiNpcLabel(12, "Current Bid:", guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, "No bids yet", guiLeft + 100, y, 0x888888)); + addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 100, y, 0x888888)); y += 14; } // Buyout price if (listing.buyoutPrice > 0) { - addLabel(new GuiNpcLabel(20, "Buyout Price:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 100, y, 0xAA6600)); y += 14; } // Minimum bid - addLabel(new GuiNpcLabel(22, "Minimum Bid:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 100, y, 0x0066AA)); y += 14; // Time remaining (will be updated) - addLabel(new GuiNpcLabel(30, "Time Left:", guiLeft + 20, y, labelColor)); + addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 20, y, labelColor)); addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, getTimeColor(listing.getTimeRemaining()))); y += 20; // Close button - addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, "Close")); + addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); } private String formatCurrency(long amount) { @@ -105,7 +106,7 @@ public class SubGuiAuctionDetails extends SubGuiInterface { } private String formatTimeRemaining(long millis) { - if (millis <= 0) return "Auction Ended"; + if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); long seconds = millis / 1000; long minutes = seconds / 60; diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java b/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java deleted file mode 100644 index 0906bb6ed..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionCategory.java +++ /dev/null @@ -1,156 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.init.Items; -import net.minecraft.item.Item; -import net.minecraft.item.ItemArmor; -import net.minecraft.item.ItemAxe; -import net.minecraft.item.ItemBlock; -import net.minecraft.item.ItemBow; -import net.minecraft.item.ItemEnchantedBook; -import net.minecraft.item.ItemFood; -import net.minecraft.item.ItemHoe; -import net.minecraft.item.ItemPickaxe; -import net.minecraft.item.ItemPotion; -import net.minecraft.item.ItemSpade; -import net.minecraft.item.ItemStack; -import net.minecraft.item.ItemSword; -import net.minecraft.item.ItemTool; - -/** - * Categories for auction house items. - * Used for filtering and browsing. - */ -public enum EnumAuctionCategory { - ALL, // Special: shows all categories - WEAPONS, // Swords, bows, etc. - ARMOR, // Helmets, chestplates, leggings, boots - TOOLS, // Pickaxes, axes, shovels, hoes - FOOD, // Edible items - POTIONS, // Potions and splash potions - BLOCKS, // Placeable blocks - MATERIALS, // Crafting materials (ingots, gems, etc.) - ENCHANTED, // Items with enchantments or enchanted books - MISC; // Everything else - - /** - * Get display name for GUI - */ - public String getDisplayName() { - switch (this) { - case ALL: return "All Items"; - case WEAPONS: return "Weapons"; - case ARMOR: return "Armor"; - case TOOLS: return "Tools"; - case FOOD: return "Food"; - case POTIONS: return "Potions"; - case BLOCKS: return "Blocks"; - case MATERIALS: return "Materials"; - case ENCHANTED: return "Enchanted"; - case MISC: return "Miscellaneous"; - default: return "Unknown"; - } - } - - /** - * Auto-categorize an item based on its type - */ - public static EnumAuctionCategory categorize(ItemStack itemStack) { - if (itemStack == null || itemStack.getItem() == null) { - return MISC; - } - - Item item = itemStack.getItem(); - - // Check for enchantments first (higher priority) - if (itemStack.isItemEnchanted() || item instanceof ItemEnchantedBook) { - return ENCHANTED; - } - - // Weapons - if (item instanceof ItemSword || item instanceof ItemBow) { - return WEAPONS; - } - - // Armor - if (item instanceof ItemArmor) { - return ARMOR; - } - - // Tools - if (item instanceof ItemTool || item instanceof ItemPickaxe || - item instanceof ItemAxe || item instanceof ItemSpade || - item instanceof ItemHoe) { - return TOOLS; - } - - // Food - if (item instanceof ItemFood) { - return FOOD; - } - - // Potions - if (item instanceof ItemPotion) { - return POTIONS; - } - - // Blocks - if (item instanceof ItemBlock) { - return BLOCKS; - } - - // Materials - common crafting items - if (isMaterial(item)) { - return MATERIALS; - } - - return MISC; - } - - /** - * Check if an item is a common crafting material - */ - private static boolean isMaterial(Item item) { - // Check common material items - return item == Items.iron_ingot || - item == Items.gold_ingot || - item == Items.diamond || - item == Items.emerald || - item == Items.coal || - item == Items.redstone || - item == Items.glowstone_dust || - item == Items.quartz || - item == Items.leather || - item == Items.string || - item == Items.feather || - item == Items.bone || - item == Items.gunpowder || - item == Items.slime_ball || - item == Items.ender_pearl || - item == Items.blaze_rod || - item == Items.ghast_tear || - item == Items.gold_nugget || - item == Items.nether_wart || - item == Items.spider_eye || - item == Items.magma_cream || - item == Items.nether_star; - } - - /** - * Get icon item for category display - */ - public ItemStack getIconItem() { - switch (this) { - case ALL: return new ItemStack(Items.compass); - case WEAPONS: return new ItemStack(Items.iron_sword); - case ARMOR: return new ItemStack(Items.iron_chestplate); - case TOOLS: return new ItemStack(Items.iron_pickaxe); - case FOOD: return new ItemStack(Items.bread); - case POTIONS: return new ItemStack(Items.potionitem); - case BLOCKS: return new ItemStack(Item.getItemFromBlock(net.minecraft.init.Blocks.grass)); - case MATERIALS: return new ItemStack(Items.iron_ingot); - case ENCHANTED: return new ItemStack(Items.enchanted_book); - case MISC: return new ItemStack(Items.chest_minecart); - default: return new ItemStack(Items.paper); - } - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java index e18416f50..c901d21ce 100644 --- a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java +++ b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java @@ -1,5 +1,6 @@ package noppes.npcs.constants; +import net.minecraft.util.StatCollector; import noppes.npcs.config.ConfigMarket; /** @@ -7,10 +8,16 @@ import noppes.npcs.config.ConfigMarket; * Values are configurable via ConfigMarket. */ public enum EnumAuctionDuration { - SHORT, // Default: 2 hours - MEDIUM, // Default: 12 hours - LONG, // Default: 24 hours - VERY_LONG; // Default: 48 hours + SHORT("auction.duration.short"), + MEDIUM("auction.duration.medium"), + LONG("auction.duration.long"), + VERY_LONG("auction.duration.veryLong"); + + private final String langKey; + + EnumAuctionDuration(String langKey) { + this.langKey = langKey; + } /** * Get duration in milliseconds @@ -30,22 +37,29 @@ public enum EnumAuctionDuration { } } + /** + * Get duration in hours + */ + public int getHours() { + switch (this) { + case SHORT: + return ConfigMarket.AuctionDurationShort; + case MEDIUM: + return ConfigMarket.AuctionDurationMedium; + case LONG: + return ConfigMarket.AuctionDurationLong; + case VERY_LONG: + return ConfigMarket.AuctionDurationVeryLong; + default: + return 24; + } + } + /** * Get display name for GUI */ public String getDisplayName() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort + " Hours"; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium + " Hours"; - case LONG: - return ConfigMarket.AuctionDurationLong + " Hours"; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong + " Hours"; - default: - return "Unknown"; - } + return StatCollector.translateToLocal(langKey); } /** diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java index a94d1f1bf..75220861c 100644 --- a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java +++ b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java @@ -1,66 +1,37 @@ package noppes.npcs.constants; +import net.minecraft.util.StatCollector; + /** * Sorting options for auction listings. */ public enum EnumAuctionSort { - /** Default - newest listings first */ - NEWEST_FIRST("Newest First"), + NEWEST_FIRST("auction.sort.newest"), + OLDEST_FIRST("auction.sort.oldest"), + PRICE_HIGH_TO_LOW("auction.sort.priceHigh"), + PRICE_LOW_TO_HIGH("auction.sort.priceLow"), + ENDING_SOON("auction.sort.endingSoon"), + MOST_TIME_LEFT("auction.sort.mostTime"), + NAME_A_TO_Z("auction.sort.nameAZ"), + NAME_Z_TO_A("auction.sort.nameZA"), + MOST_BIDS("auction.sort.mostBids"), + LEAST_BIDS("auction.sort.leastBids"); - /** Oldest listings first */ - OLDEST_FIRST("Oldest First"), + private final String langKey; - /** Highest current price first */ - PRICE_HIGH_TO_LOW("Most Expensive"), - - /** Lowest current price first */ - PRICE_LOW_TO_HIGH("Least Expensive"), - - /** Buyout price high to low (listings without buyout at end) */ - BUYOUT_HIGH_TO_LOW("Buyout: High to Low"), - - /** Buyout price low to high (listings without buyout at end) */ - BUYOUT_LOW_TO_HIGH("Buyout: Low to High"), - - /** Ending soonest first */ - ENDING_SOON("Ending Soon"), - - /** Most time remaining first */ - MOST_TIME_LEFT("Most Time Left"), - - /** Alphabetical by item name A-Z */ - NAME_A_TO_Z("Name: A to Z"), - - /** Alphabetical by item name Z-A */ - NAME_Z_TO_A("Name: Z to A"), - - /** Most bids first */ - MOST_BIDS("Most Bids"), - - /** Least bids first */ - LEAST_BIDS("Least Bids"); - - private final String displayName; - - EnumAuctionSort(String displayName) { - this.displayName = displayName; + EnumAuctionSort(String langKey) { + this.langKey = langKey; } public String getDisplayName() { - return displayName; + return StatCollector.translateToLocal(langKey); } - /** - * Get next sort option (for cycling through in GUI) - */ public EnumAuctionSort next() { EnumAuctionSort[] values = values(); return values[(this.ordinal() + 1) % values.length]; } - /** - * Get previous sort option - */ public EnumAuctionSort previous() { EnumAuctionSort[] values = values(); return values[(this.ordinal() - 1 + values.length) % values.length]; diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 76e5f0891..b6bb41d10 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -1,20 +1,21 @@ package noppes.npcs.controllers; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import java.io.File; import java.io.FileInputStream; @@ -25,10 +26,10 @@ import java.util.stream.Collectors; /** * Controller for the global auction house. - * Manages all auction listings, bidding, and claims. + * Manages all auction listings, bidding, claims, notifications, and logging. */ public class AuctionController { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + private static final String LOG_PREFIX = "[Auction] "; public static AuctionController Instance; @@ -42,6 +43,9 @@ public class AuctionController { private Map> listingsBySeller = new ConcurrentHashMap<>(); private Map> listingsByBidder = new ConcurrentHashMap<>(); + // Pending notifications for offline players + private Map> pendingNotifications = new ConcurrentHashMap<>(); + public AuctionController() { Instance = this; load(); @@ -59,7 +63,7 @@ public class AuctionController { // Validate item String validation = AuctionListing.validateItem(item); if (validation != null) { - logger.warn("Auction listing validation failed: " + validation); + LogWriter.info(LOG_PREFIX + "Listing validation failed: " + validation); return null; } @@ -67,8 +71,6 @@ public class AuctionController { int currentListings = getActiveListingsCount(seller.getUniqueID()); int maxListings = getMaxListings(seller); if (currentListings >= maxListings) { - logger.info("Player {} has reached max listings ({}/{})", - seller.getCommandSenderName(), currentListings, maxListings); return null; } @@ -85,8 +87,6 @@ public class AuctionController { long listingFee = listing.getListingFee(); if (listingFee > 0) { if (!CurrencyController.Instance.canAfford(seller, listingFee)) { - logger.info("Player {} cannot afford listing fee of {}", - seller.getCommandSenderName(), listingFee); return null; } CurrencyController.Instance.withdraw(seller, listingFee); @@ -100,12 +100,11 @@ public class AuctionController { addToSellerIndex(seller.getUniqueID(), listing.id); save(); - logger.info("Created auction listing {} by {} for {}", - listing.id, seller.getCommandSenderName(), - item.getDisplayName()); - // Log to CNPC+ auction history - AuctionLogger.logListingCreated(listing); + // Log to CNPC+ + logEvent("LISTING_CREATED", String.format("ID=%d, Seller=%s, Item=%s x%d, StartPrice=%d, Buyout=%s", + listing.id, listing.sellerName, item.getDisplayName(), item.stackSize, + listing.startingPrice, listing.buyoutPrice > 0 ? listing.buyoutPrice : "None")); return listing; } @@ -127,18 +126,6 @@ public class AuctionController { .collect(Collectors.toList()); } - /** - * Get active listings by category - */ - public List getActiveListings(EnumAuctionCategory category) { - if (category == EnumAuctionCategory.ALL) { - return getActiveListings(); - } - return getActiveListings().stream() - .filter(l -> l.category == category) - .collect(Collectors.toList()); - } - /** * Search listings by item name */ @@ -153,7 +140,6 @@ public class AuctionController { /** * Get filtered and sorted auction listings. - * This is the main method for GUI queries. * @param filter The filter criteria * @return FilterResult containing matching listings and metadata */ @@ -171,7 +157,7 @@ public class AuctionController { // Calculate pagination int totalResults = results.size(); - int totalPages = (int) Math.ceil((double) totalResults / filter.pageSize); + int totalPages = Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); int startIndex = filter.page * filter.pageSize; int endIndex = Math.min(startIndex + filter.pageSize, totalResults); @@ -180,7 +166,7 @@ public class AuctionController { if (startIndex >= totalResults) { pageResults = new ArrayList<>(); } else { - pageResults = results.subList(startIndex, endIndex); + pageResults = new ArrayList<>(results.subList(startIndex, endIndex)); } return new FilterResult(pageResults, totalResults, filter.page, totalPages); @@ -200,61 +186,36 @@ public class AuctionController { case NEWEST_FIRST: comparator = Comparator.comparingLong(l -> -l.listingTime); break; - case OLDEST_FIRST: comparator = Comparator.comparingLong(l -> l.listingTime); break; - case PRICE_HIGH_TO_LOW: comparator = Comparator.comparingLong((AuctionListing l) -> -(l.currentBid > 0 ? l.currentBid : l.startingPrice)); break; - case PRICE_LOW_TO_HIGH: comparator = Comparator.comparingLong(l -> l.currentBid > 0 ? l.currentBid : l.startingPrice); break; - - case BUYOUT_HIGH_TO_LOW: - // Listings with buyout first, sorted high to low - comparator = Comparator - .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) - .thenComparingLong(l -> -l.buyoutPrice); - break; - - case BUYOUT_LOW_TO_HIGH: - // Listings with buyout first, sorted low to high - comparator = Comparator - .comparingInt((AuctionListing l) -> l.buyoutPrice > 0 ? 0 : 1) - .thenComparingLong(l -> l.buyoutPrice); - break; - case ENDING_SOON: comparator = Comparator.comparingLong(l -> l.expirationTime); break; - case MOST_TIME_LEFT: comparator = Comparator.comparingLong(l -> -l.expirationTime); break; - case NAME_A_TO_Z: - comparator = Comparator.comparing(l -> - l.item.getDisplayName().toLowerCase()); + comparator = Comparator.comparing(l -> l.item.getDisplayName().toLowerCase()); break; - case NAME_Z_TO_A: comparator = Comparator.comparing((AuctionListing l) -> l.item.getDisplayName().toLowerCase()).reversed(); break; - case MOST_BIDS: comparator = Comparator.comparingInt(l -> -l.bidCount); break; - case LEAST_BIDS: comparator = Comparator.comparingInt(l -> l.bidCount); break; - default: comparator = Comparator.comparingLong(l -> l.expirationTime); } @@ -262,28 +223,6 @@ public class AuctionController { listings.sort(comparator); } - /** - * Quick search with default sorting (ending soon) - */ - public List quickSearch(String searchText, EnumAuctionCategory category, int limit) { - AuctionFilter filter = new AuctionFilter() - .withSearch(searchText) - .withCategory(category) - .withSort(EnumAuctionSort.ENDING_SOON) - .withPage(0, limit); - - return getFilteredListings(filter).listings; - } - - /** - * Get listings sorted by a specific order - */ - public List getListingsSorted(EnumAuctionSort sortOrder) { - List results = new ArrayList<>(getActiveListings()); - sortListings(results, sortOrder); - return results; - } - /** * Result container for filtered queries */ @@ -315,7 +254,7 @@ public class AuctionController { } /** - * Get all listings by a seller (all statuses) + * Get all listings by a seller */ public List getListingsBySeller(UUID sellerUUID) { List ids = listingsBySeller.get(sellerUUID); @@ -354,7 +293,6 @@ public class AuctionController { /** * Get listings that need claiming by a player - * (items won, sales proceeds, cancelled/expired items) */ public List getClaimableListings(UUID playerUUID) { List claimable = new ArrayList<>(); @@ -391,7 +329,6 @@ public class AuctionController { /** * Place a bid on an auction - * @return true if bid was successful */ public boolean placeBid(int listingId, EntityPlayer bidder, long amount) { AuctionListing listing = listings.get(listingId); @@ -399,61 +336,51 @@ public class AuctionController { return false; } - // Check if bidder can afford if (!CurrencyController.Instance.canAfford(bidder, amount)) { return false; } - // Store previous bidder for refund and notification + // Store previous bidder for refund UUID previousBidder = listing.highBidderUUID; String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; - // Try to place bid if (!listing.placeBid(bidder.getUniqueID(), bidder.getCommandSenderName(), amount)) { return false; } - // Withdraw bid amount from new bidder CurrencyController.Instance.withdraw(bidder, amount); - // Refund previous bidder via claims system and notify + // Refund previous bidder if (previousBidder != null) { - CurrencyController.Instance.addClaim( - previousBidder, - previousBidAmount, - "Auction Outbid", - String.valueOf(listingId) - ); + CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, + "Auction Outbid", String.valueOf(listingId)); - // Notify previous bidder they were outbid - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyOutbid( - listing, previousBidder, previousBidderName, previousBidAmount, amount); - } + // Notify outbid player + notifyPlayer(previousBidder, EnumChatFormatting.RED + + "You have been outbid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + "! New bid: " + formatCurrency(amount)); - // Log refund - AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); + logEvent("OUTBID", String.format("ID=%d, OutbidPlayer=%s, PrevBid=%d, NewBid=%d", + listingId, previousBidderName, previousBidAmount, amount)); } - // Notify seller of new bid - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyNewBid(listing, bidder.getCommandSenderName(), amount); - } + // Notify seller + notifyPlayer(listing.sellerUUID, EnumChatFormatting.AQUA + + bidder.getCommandSenderName() + " placed a bid of " + formatCurrency(amount) + + " on your " + listing.item.getDisplayName() + "!"); - // Update bidder index addToBidderIndex(bidder.getUniqueID(), listingId); - save(); - logger.info("Bid placed on listing {} by {} for {}", - listingId, bidder.getCommandSenderName(), amount); + + logEvent("BID_PLACED", String.format("ID=%d, Bidder=%s, Amount=%d", + listingId, bidder.getCommandSenderName(), amount)); return true; } /** * Execute buyout on an auction - * @return true if buyout was successful */ public boolean buyout(int listingId, EntityPlayer buyer) { AuctionListing listing = listings.get(listingId); @@ -461,66 +388,50 @@ public class AuctionController { return false; } - // Check if buyer can afford if (!CurrencyController.Instance.canAfford(buyer, listing.buyoutPrice)) { return false; } - // Store previous bidder for refund and notification UUID previousBidder = listing.highBidderUUID; String previousBidderName = listing.highBidderName; long previousBidAmount = listing.currentBid; - // Execute buyout if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { return false; } - // Withdraw buyout amount from buyer CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); - // Refund previous bidder via claims system and notify + // Refund previous bidder if (previousBidder != null) { - CurrencyController.Instance.addClaim( - previousBidder, - previousBidAmount, - "Auction Outbid (Buyout)", - String.valueOf(listingId) - ); + CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, + "Auction Outbid (Buyout)", String.valueOf(listingId)); - // Notify previous bidder they were outbid - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyOutbid( - listing, previousBidder, previousBidderName, previousBidAmount, listing.buyoutPrice); - } - - // Log refund - AuctionLogger.logBidRefunded(listing, previousBidderName, previousBidAmount); + notifyPlayer(previousBidder, EnumChatFormatting.RED + + "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + " was outbid by a buyout. Your " + + formatCurrency(previousBidAmount) + " has been refunded."); } - // Update bidder index addToBidderIndex(buyer.getUniqueID(), listingId); - save(); - logger.info("Buyout executed on listing {} by {} for {}", - listingId, buyer.getCommandSenderName(), listing.buyoutPrice); - // Log buyout and send notifications - AuctionLogger.logBuyout(listing, buyer.getCommandSenderName()); + // Notify buyer and seller + notifyPlayer(buyer.getUniqueID(), EnumChatFormatting.GREEN + + "Buyout successful! Claim your " + listing.item.getDisplayName() + " from an Auctioneer."); - if (AuctionNotificationController.Instance != null) { - // Notify buyer they won - AuctionNotificationController.Instance.notifyAuctionWon(listing); - // Notify seller their item sold - AuctionNotificationController.Instance.notifyAuctionSold(listing); - } + notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + + "Your " + listing.item.getDisplayName() + " was bought out for " + + formatCurrency(listing.buyoutPrice) + "! Claim your proceeds."); + + logEvent("BUYOUT", String.format("ID=%d, Buyer=%s, Price=%d, Seller=%s", + listingId, buyer.getCommandSenderName(), listing.buyoutPrice, listing.sellerName)); return true; } /** * Cancel an auction listing - * @return true if cancellation was successful */ public boolean cancelListing(int listingId, EntityPlayer requester) { AuctionListing listing = listings.get(listingId); @@ -535,15 +446,16 @@ public class AuctionController { } save(); - logger.info("Listing {} cancelled by {}", - listingId, requester.getCommandSenderName()); - // Log and notify - AuctionLogger.logAuctionCancelled(listing, byAdmin); - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyAuctionCancelled(listing, byAdmin); + if (byAdmin) { + notifyPlayer(listing.sellerUUID, EnumChatFormatting.RED + + "Your auction for " + listing.item.getDisplayName() + + " was cancelled by an administrator."); } + logEvent("CANCELLED", String.format("ID=%d, CancelledBy=%s, ByAdmin=%b", + listingId, requester.getCommandSenderName(), byAdmin)); + return true; } @@ -551,27 +463,14 @@ public class AuctionController { /** * Claim seller proceeds from a sold auction - * @return true if claim was successful */ public boolean claimSellerProceeds(int listingId, EntityPlayer seller) { AuctionListing listing = listings.get(listingId); - if (listing == null) { + if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || + listing.status != EnumAuctionStatus.SOLD || listing.sellerClaimed) { return false; } - if (!listing.sellerUUID.equals(seller.getUniqueID())) { - return false; // Not the seller - } - - if (listing.status != EnumAuctionStatus.SOLD) { - return false; // Not sold - } - - if (listing.sellerClaimed) { - return false; // Already claimed - } - - // Deposit proceeds to seller long proceeds = listing.getSellerProceeds(); CurrencyController.Instance.deposit(seller, proceeds); @@ -579,95 +478,58 @@ public class AuctionController { checkAndMarkClaimed(listing); save(); - logger.info("Seller {} claimed {} from listing {}", - seller.getCommandSenderName(), proceeds, listingId); - - // Log the currency claim - AuctionLogger.logCurrencyClaimed(listing, seller.getCommandSenderName(), proceeds); + logEvent("PROCEEDS_CLAIMED", String.format("ID=%d, Seller=%s, Amount=%d", + listingId, seller.getCommandSenderName(), proceeds)); return true; } /** * Claim item from won auction - * @return The item if successful, null otherwise */ public ItemStack claimBuyerItem(int listingId, EntityPlayer buyer) { AuctionListing listing = listings.get(listingId); - if (listing == null) { + if (listing == null || listing.highBidderUUID == null || + !listing.highBidderUUID.equals(buyer.getUniqueID()) || + listing.status != EnumAuctionStatus.SOLD || listing.buyerClaimed) { return null; } - if (listing.highBidderUUID == null || - !listing.highBidderUUID.equals(buyer.getUniqueID())) { - return null; // Not the buyer - } - - if (listing.status != EnumAuctionStatus.SOLD) { - return null; // Not sold - } - - if (listing.buyerClaimed) { - return null; // Already claimed - } - listing.buyerClaimed = true; checkAndMarkClaimed(listing); save(); - logger.info("Buyer {} claimed item from listing {}", - buyer.getCommandSenderName(), listingId); - - // Log the item claim - AuctionLogger.logItemClaimed(listing, buyer.getCommandSenderName(), false); + logEvent("ITEM_CLAIMED", String.format("ID=%d, Buyer=%s, Item=%s", + listingId, buyer.getCommandSenderName(), listing.item.getDisplayName())); return listing.item.copy(); } /** - * Claim item from cancelled or expired auction (seller reclaiming) - * @return The item if successful, null otherwise + * Claim item from cancelled or expired auction */ public ItemStack claimExpiredItem(int listingId, EntityPlayer seller) { AuctionListing listing = listings.get(listingId); - if (listing == null) { + if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || + (listing.status != EnumAuctionStatus.CANCELLED && listing.status != EnumAuctionStatus.EXPIRED) || + listing.sellerClaimed) { return null; } - if (!listing.sellerUUID.equals(seller.getUniqueID())) { - return null; // Not the seller - } - - if (listing.status != EnumAuctionStatus.CANCELLED && - listing.status != EnumAuctionStatus.EXPIRED) { - return null; // Not claimable - } - - if (listing.sellerClaimed) { - return null; // Already claimed - } - listing.sellerClaimed = true; listing.status = EnumAuctionStatus.CLAIMED; save(); - logger.info("Seller {} reclaimed item from listing {}", - seller.getCommandSenderName(), listingId); - - // Log the item reclaim - AuctionLogger.logItemClaimed(listing, seller.getCommandSenderName(), true); + logEvent("ITEM_RECLAIMED", String.format("ID=%d, Seller=%s, Item=%s", + listingId, seller.getCommandSenderName(), listing.item.getDisplayName())); return listing.item.copy(); } - /** - * Mark listing as fully claimed if both parties have claimed - */ private void checkAndMarkClaimed(AuctionListing listing) { - if (listing.status == EnumAuctionStatus.SOLD) { - if (listing.sellerClaimed && listing.buyerClaimed) { - listing.status = EnumAuctionStatus.CLAIMED; - } + if (listing.status == EnumAuctionStatus.SOLD && + listing.sellerClaimed && listing.buyerClaimed) { + listing.status = EnumAuctionStatus.CLAIMED; } } @@ -675,58 +537,49 @@ public class AuctionController { /** * Process all expired auctions - * Call this periodically (e.g., every minute) */ public void processExpiredAuctions() { boolean changed = false; - int expiredCount = 0; - int soldCount = 0; for (AuctionListing listing : listings.values()) { if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { listing.finalize(); changed = true; - // If sold, add proceeds to seller's claims if (listing.status == EnumAuctionStatus.SOLD) { - soldCount++; - // Proceeds will be claimed manually, no automatic deposit - logger.info("Auction {} ended with sale to {} for {}", - listing.id, listing.highBidderName, listing.currentBid); + // Notify winner and seller + notifyPlayer(listing.highBidderUUID, EnumChatFormatting.GREEN + + "You won the auction for " + listing.item.getDisplayName() + + " for " + formatCurrency(listing.currentBid) + "! Claim your item."); - // Send notifications - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyAuctionWon(listing); - AuctionNotificationController.Instance.notifyAuctionSold(listing); - } + notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + + "Your " + listing.item.getDisplayName() + " sold for " + + formatCurrency(listing.currentBid) + "! Claim your proceeds."); + + logEvent("AUCTION_SOLD", String.format("ID=%d, Winner=%s, Price=%d, Seller=%s", + listing.id, listing.highBidderName, listing.currentBid, listing.sellerName)); } else { - expiredCount++; - logger.info("Auction {} expired without bids", listing.id); + notifyPlayer(listing.sellerUUID, EnumChatFormatting.GRAY + + "Your auction for " + listing.item.getDisplayName() + + " has expired with no bids. Claim your item."); - // Notify seller about expiration - if (AuctionNotificationController.Instance != null) { - AuctionNotificationController.Instance.notifyAuctionExpired(listing); - } + logEvent("AUCTION_EXPIRED", String.format("ID=%d, Item=%s, Seller=%s", + listing.id, listing.item.getDisplayName(), listing.sellerName)); } } } if (changed) { save(); - // Log processing tick if anything happened - AuctionLogger.logProcessingTick(expiredCount, soldCount); } } /** * Clean up old claimed listings - * Call this periodically (e.g., daily) */ public void cleanupOldListings() { long claimExpirationMs = ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L; - if (claimExpirationMs <= 0) { - return; // No expiration - } + if (claimExpirationMs <= 0) return; long cutoffTime = System.currentTimeMillis() - claimExpirationMs; List toRemove = new ArrayList<>(); @@ -750,10 +603,71 @@ public class AuctionController { if (!toRemove.isEmpty()) { save(); - logger.info("Cleaned up {} old auction listings", toRemove.size()); + logEvent("CLEANUP", "Removed " + toRemove.size() + " old listings"); } } + // ==================== Notifications ==================== + + /** + * Send notification to player (or queue if offline) + */ + private void notifyPlayer(UUID playerUUID, String message) { + if (playerUUID == null) return; + + EntityPlayerMP player = getOnlinePlayer(playerUUID); + String fullMessage = EnumChatFormatting.YELLOW + "[Auction] " + message; + + if (player != null) { + player.addChatMessage(new ChatComponentText(fullMessage)); + } else { + pendingNotifications.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(fullMessage); + // Limit queue size + List queue = pendingNotifications.get(playerUUID); + while (queue.size() > 50) { + queue.remove(0); + } + } + } + + /** + * Send pending notifications on player login + */ + public void onPlayerLogin(EntityPlayerMP player) { + UUID uuid = player.getUniqueID(); + + List notifications = pendingNotifications.remove(uuid); + if (notifications != null && !notifications.isEmpty()) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + + "You have " + notifications.size() + " auction notification(s):")); + + for (String msg : notifications) { + player.addChatMessage(new ChatComponentText(msg)); + } + } + + // Check claimable items + int claimableCount = getClaimableListings(uuid).size(); + if (claimableCount > 0) { + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + + "You have " + claimableCount + " item(s) waiting to be claimed!")); + } + } + + private EntityPlayerMP getOnlinePlayer(UUID uuid) { + if (CustomNpcs.getServer() == null) return null; + + for (Object obj : CustomNpcs.getServer().getConfigurationManager().playerEntityList) { + EntityPlayerMP player = (EntityPlayerMP) obj; + if (player.getUniqueID().equals(uuid)) { + return player; + } + } + return null; + } + // ==================== Index Management ==================== private void addToSellerIndex(UUID seller, int listingId) { @@ -790,24 +704,25 @@ public class AuctionController { } } - // ==================== Permission Helpers ==================== + // ==================== Helpers ==================== - /** - * Get maximum listings allowed for a player - */ public int getMaxListings(EntityPlayer player) { - // TODO: Check for permission-based overrides - // e.g., CustomNpcsPermissions.hasCustomPermission(player, "cnpc.auction.slots.10") return ConfigMarket.DefaultMaxListings; } + private String formatCurrency(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + private void logEvent(String event, String details) { + LogWriter.info(LOG_PREFIX + event + ": " + details); + } + // ==================== Persistence ==================== public void save() { File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } + if (saveDir == null) return; try { NBTTagCompound compound = new NBTTagCompound(); @@ -823,9 +738,7 @@ public class AuctionController { File backup = new File(saveDir, "auctions.dat_old"); if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } + if (backup.exists()) backup.delete(); file.renameTo(backup); } @@ -835,15 +748,13 @@ public class AuctionController { fos.close(); } catch (Exception e) { - logger.error("Error saving auctions", e); + LogWriter.error(LOG_PREFIX + "Error saving auctions", e); } } public void load() { File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } + if (saveDir == null) return; listings.clear(); @@ -851,9 +762,7 @@ public class AuctionController { File file = new File(saveDir, "auctions.dat"); if (!file.exists()) { file = new File(saveDir, "auctions.dat_old"); - if (!file.exists()) { - return; - } + if (!file.exists()) return; } FileInputStream fis = new FileInputStream(file); @@ -861,9 +770,7 @@ public class AuctionController { fis.close(); nextId = compound.getInteger("NextId"); - if (nextId < 1) { - nextId = 1; - } + if (nextId < 1) nextId = 1; NBTTagList listingList = compound.getTagList("Listings", 10); for (int i = 0; i < listingList.tagCount(); i++) { @@ -874,42 +781,22 @@ public class AuctionController { rebuildIndices(); - logger.info("Loaded {} auction listings", listings.size()); - - // Log system startup - int activeCount = getActiveListingCount(); - int pendingClaims = 0; - for (AuctionListing listing : listings.values()) { - if (!listing.sellerClaimed || !listing.buyerClaimed) { - if (listing.status == EnumAuctionStatus.SOLD || - listing.status == EnumAuctionStatus.EXPIRED || - listing.status == EnumAuctionStatus.CANCELLED) { - pendingClaims++; - } - } - } - AuctionLogger.logSystemStartup(activeCount, pendingClaims); + logEvent("SYSTEM_STARTUP", String.format("Loaded %d listings, %d active", + listings.size(), getActiveListingCount())); } catch (Exception e) { - logger.error("Error loading auctions", e); - AuctionLogger.logError("Loading auctions", e); + LogWriter.error(LOG_PREFIX + "Error loading auctions", e); } } // ==================== Statistics ==================== - /** - * Get total number of active listings - */ public int getActiveListingCount() { return (int) listings.values().stream() .filter(l -> l.status == EnumAuctionStatus.ACTIVE) .count(); } - /** - * Get total number of all listings - */ public int getTotalListingCount() { return listings.size(); } diff --git a/src/main/java/noppes/npcs/controllers/AuctionLogger.java b/src/main/java/noppes/npcs/controllers/AuctionLogger.java deleted file mode 100644 index 4c5264319..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionLogger.java +++ /dev/null @@ -1,269 +0,0 @@ -package noppes.npcs.controllers; - -import noppes.npcs.LogWriter; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionListing; - -import java.text.SimpleDateFormat; -import java.util.Date; - -/** - * Handles auction history logging to the CNPC+ logging system. - * All auction events are recorded for administrative review. - */ -public class AuctionLogger { - private static final String PREFIX = "[Auction] "; - private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - /** - * Log when a new auction listing is created - */ - public static void logListingCreated(AuctionListing listing) { - String msg = String.format("%sLISTING_CREATED: ID=%d, Seller=%s (%s), Item=%s x%d, StartPrice=%d, Buyout=%s, Duration=%s", - PREFIX, - listing.id, - listing.sellerName, - listing.sellerUUID.toString(), - listing.item.getDisplayName(), - listing.item.stackSize, - listing.startingPrice, - listing.buyoutPrice > 0 ? String.valueOf(listing.buyoutPrice) : "None", - formatDuration(listing.expirationTime - listing.listingTime) - ); - LogWriter.info(msg); - } - - /** - * Log when a bid is placed - */ - public static void logBidPlaced(AuctionListing listing, String bidderName, long bidAmount) { - String msg = String.format("%sBID_PLACED: ID=%d, Item=%s, Bidder=%s, Amount=%d %s, Previous=%d %s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - bidderName, - bidAmount, - ConfigMarket.CurrencyName, - listing.currentBid, - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log when a player is outbid - */ - public static void logOutbid(AuctionListing listing, String outbidPlayerName, long previousBid, - String newBidderName, long newBid) { - String msg = String.format("%sOUTBID: ID=%d, Item=%s, OutbidPlayer=%s (had %d), NewBidder=%s (bid %d)", - PREFIX, - listing.id, - listing.item.getDisplayName(), - outbidPlayerName, - previousBid, - newBidderName, - newBid - ); - LogWriter.info(msg); - } - - /** - * Log when a buyout occurs - */ - public static void logBuyout(AuctionListing listing, String buyerName) { - String msg = String.format("%sBUYOUT: ID=%d, Item=%s, Buyer=%s, Price=%d %s, Seller=%s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - buyerName, - listing.buyoutPrice, - ConfigMarket.CurrencyName, - listing.sellerName - ); - LogWriter.info(msg); - } - - /** - * Log when an auction is won (ended with bids) - */ - public static void logAuctionWon(AuctionListing listing) { - String msg = String.format("%sAUCTION_WON: ID=%d, Item=%s, Winner=%s (%s), FinalPrice=%d %s, Seller=%s, SellerProceeds=%d %s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.highBidderName, - listing.highBidderUUID != null ? listing.highBidderUUID.toString() : "N/A", - listing.currentBid, - ConfigMarket.CurrencyName, - listing.sellerName, - listing.getSellerProceeds(), - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log when an auction sells (same as won, for notification controller compatibility) - */ - public static void logAuctionSold(AuctionListing listing) { - String msg = String.format("%sAUCTION_SOLD: ID=%d, Item=%s, Buyer=%s, SalePrice=%d %s, Seller=%s (%s), Proceeds=%d %s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.highBidderName, - listing.currentBid, - ConfigMarket.CurrencyName, - listing.sellerName, - listing.sellerUUID.toString(), - listing.getSellerProceeds(), - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log when an auction expires with no bids - */ - public static void logAuctionExpired(AuctionListing listing) { - String msg = String.format("%sAUCTION_EXPIRED: ID=%d, Item=%s, Seller=%s (%s), StartPrice=%d %s, Duration=%s", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.sellerName, - listing.sellerUUID.toString(), - listing.startingPrice, - ConfigMarket.CurrencyName, - formatDuration(listing.expirationTime - listing.listingTime) - ); - LogWriter.info(msg); - } - - /** - * Log when an auction is cancelled - */ - public static void logAuctionCancelled(AuctionListing listing, boolean byAdmin) { - String msg = String.format("%sAUCTION_CANCELLED: ID=%d, Item=%s, Seller=%s, CancelledBy=%s, HadBids=%b", - PREFIX, - listing.id, - listing.item.getDisplayName(), - listing.sellerName, - byAdmin ? "ADMIN" : "SELLER", - listing.currentBid > 0 - ); - LogWriter.info(msg); - } - - /** - * Log when an item is claimed - */ - public static void logItemClaimed(AuctionListing listing, String claimerName, boolean isSeller) { - String claimType = isSeller ? - (listing.status == EnumAuctionStatus.EXPIRED ? "RECLAIMED_EXPIRED" : "CLAIMED_PROCEEDS") : - "CLAIMED_WON_ITEM"; - - String msg = String.format("%s%s: ID=%d, Item=%s, Claimer=%s, Status=%s", - PREFIX, - claimType, - listing.id, - listing.item.getDisplayName(), - claimerName, - listing.status.name() - ); - LogWriter.info(msg); - } - - /** - * Log currency claimed from auction - */ - public static void logCurrencyClaimed(AuctionListing listing, String claimerName, long amount) { - String msg = String.format("%sCURRENCY_CLAIMED: ID=%d, Claimer=%s, Amount=%d %s", - PREFIX, - listing.id, - claimerName, - amount, - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log refund of bid to outbid player - */ - public static void logBidRefunded(AuctionListing listing, String playerName, long amount) { - String msg = String.format("%sBID_REFUNDED: ID=%d, Player=%s, Amount=%d %s", - PREFIX, - listing.id, - playerName, - amount, - ConfigMarket.CurrencyName - ); - LogWriter.info(msg); - } - - /** - * Log admin actions on auctions - */ - public static void logAdminAction(String adminName, String action, AuctionListing listing) { - String msg = String.format("%sADMIN_ACTION: Admin=%s, Action=%s, ListingID=%d, Item=%s, Seller=%s", - PREFIX, - adminName, - action, - listing.id, - listing.item.getDisplayName(), - listing.sellerName - ); - LogWriter.info(msg); - } - - /** - * Log auction system startup - */ - public static void logSystemStartup(int activeListings, int pendingClaims) { - String msg = String.format("%sSYSTEM_STARTUP: ActiveListings=%d, PendingClaims=%d, Timestamp=%s", - PREFIX, - activeListings, - pendingClaims, - dateFormat.format(new Date()) - ); - LogWriter.info(msg); - } - - /** - * Log auction processing tick - */ - public static void logProcessingTick(int expired, int completed) { - if (expired > 0 || completed > 0) { - String msg = String.format("%sPROCESSING_TICK: ExpiredListings=%d, CompletedAuctions=%d", - PREFIX, - expired, - completed - ); - LogWriter.info(msg); - } - } - - /** - * Log errors in the auction system - */ - public static void logError(String context, Exception e) { - String msg = String.format("%sERROR: Context=%s, Message=%s", - PREFIX, - context, - e.getMessage() - ); - LogWriter.error(msg, e); - } - - /** - * Format duration in a human-readable format - */ - private static String formatDuration(long millis) { - long hours = millis / (1000 * 60 * 60); - if (hours >= 24) { - long days = hours / 24; - return days + "d " + (hours % 24) + "h"; - } - return hours + "h"; - } -} diff --git a/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java b/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java deleted file mode 100644 index 48da6d345..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionNotificationController.java +++ /dev/null @@ -1,226 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.util.ChatComponentText; -import net.minecraft.util.EnumChatFormatting; -import noppes.npcs.LogWriter; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; - -import java.util.*; - -/** - * Handles auction notifications for players. - * Sends messages for outbid, won, sold, expired events. - */ -public class AuctionNotificationController { - public static AuctionNotificationController Instance; - - // Pending notifications for offline players (UUID -> List of notifications) - private Map> pendingNotifications = new HashMap<>(); - - public AuctionNotificationController() { - Instance = this; - } - - /** - * Notify a player they have been outbid - */ - public void notifyOutbid(AuctionListing listing, UUID outbidPlayerUUID, - String outbidPlayerName, long previousBid, long newBid) { - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + - "You have been outbid on " + EnumChatFormatting.WHITE + - listing.item.getDisplayName() + EnumChatFormatting.RED + - "! New bid: " + formatCurrency(newBid) + - " (Your bid: " + formatCurrency(previousBid) + " has been refunded to claims)"; - - sendOrQueueNotification(outbidPlayerUUID, NotificationType.OUTBID, message, listing.id); - - // Log the outbid event - AuctionLogger.logOutbid(listing, outbidPlayerName, previousBid, listing.highBidderName, newBid); - } - - /** - * Notify a player they won an auction - */ - public void notifyAuctionWon(AuctionListing listing) { - if (listing.highBidderUUID == null) return; - - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "Congratulations! You won the auction for " + EnumChatFormatting.WHITE + - listing.item.getDisplayName() + EnumChatFormatting.GREEN + - " for " + formatCurrency(listing.currentBid) + "! Visit an Auctioneer to claim your item."; - - sendOrQueueNotification(listing.highBidderUUID, NotificationType.WON, message, listing.id); - - // Log the win - AuctionLogger.logAuctionWon(listing); - } - - /** - * Notify seller their auction sold - */ - public void notifyAuctionSold(AuctionListing listing) { - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "Your " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.GREEN + " sold for " + formatCurrency(listing.currentBid) + - "! Visit an Auctioneer to claim " + formatCurrency(listing.getSellerProceeds()) + "."; - - sendOrQueueNotification(listing.sellerUUID, NotificationType.SOLD, message, listing.id); - - // Log the sale - AuctionLogger.logAuctionSold(listing); - } - - /** - * Notify seller their auction expired with no bids - */ - public void notifyAuctionExpired(AuctionListing listing) { - String message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + - "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.GRAY + " has expired with no bids. Visit an Auctioneer to reclaim your item."; - - sendOrQueueNotification(listing.sellerUUID, NotificationType.EXPIRED, message, listing.id); - - // Log the expiration - AuctionLogger.logAuctionExpired(listing); - } - - /** - * Notify player their auction was cancelled - */ - public void notifyAuctionCancelled(AuctionListing listing, boolean byAdmin) { - String message; - if (byAdmin) { - message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.RED + - "Your auction for " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was cancelled by an administrator."; - } else { - message = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GRAY + - "You cancelled your auction for " + EnumChatFormatting.WHITE + - listing.item.getDisplayName() + EnumChatFormatting.GRAY + "."; - } - - sendOrQueueNotification(listing.sellerUUID, NotificationType.CANCELLED, message, listing.id); - - // Log the cancellation - AuctionLogger.logAuctionCancelled(listing, byAdmin); - } - - /** - * Notify about a new bid placed - */ - public void notifyNewBid(AuctionListing listing, String bidderName, long bidAmount) { - // Notify seller about new bid - String sellerMessage = EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.AQUA + - bidderName + " placed a bid of " + formatCurrency(bidAmount) + - " on your " + listing.item.getDisplayName() + "!"; - - sendOrQueueNotification(listing.sellerUUID, NotificationType.NEW_BID, sellerMessage, listing.id); - - // Log the bid - AuctionLogger.logBidPlaced(listing, bidderName, bidAmount); - } - - /** - * Send notification immediately if player online, otherwise queue for later - */ - private void sendOrQueueNotification(UUID playerUUID, NotificationType type, String message, int listingId) { - EntityPlayerMP player = getOnlinePlayer(playerUUID); - - if (player != null) { - player.addChatMessage(new ChatComponentText(message)); - } else { - // Queue for when player logs in - AuctionNotification notification = new AuctionNotification(type, message, listingId); - - if (!pendingNotifications.containsKey(playerUUID)) { - pendingNotifications.put(playerUUID, new ArrayList<>()); - } - pendingNotifications.get(playerUUID).add(notification); - - // Limit queue size per player - List queue = pendingNotifications.get(playerUUID); - while (queue.size() > 50) { - queue.remove(0); - } - } - } - - /** - * Called when a player logs in - send any pending notifications - */ - public void onPlayerLogin(EntityPlayerMP player) { - UUID uuid = player.getUniqueID(); - - if (pendingNotifications.containsKey(uuid)) { - List notifications = pendingNotifications.remove(uuid); - - if (!notifications.isEmpty()) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + - "You have " + notifications.size() + " auction notification(s):")); - - for (AuctionNotification notification : notifications) { - player.addChatMessage(new ChatComponentText(notification.message)); - } - } - } - - // Also check for claimable items - if (AuctionController.Instance != null) { - int claimableCount = AuctionController.Instance.getClaimableListings(uuid).size(); - if (claimableCount > 0) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "You have " + claimableCount + " item(s) waiting to be claimed!")); - } - } - } - - private EntityPlayerMP getOnlinePlayer(UUID uuid) { - if (noppes.npcs.CustomNpcs.getServer() == null) return null; - - for (Object obj : noppes.npcs.CustomNpcs.getServer().getConfigurationManager().playerEntityList) { - EntityPlayerMP player = (EntityPlayerMP) obj; - if (player.getUniqueID().equals(uuid)) { - return player; - } - } - return null; - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - /** - * Notification types for categorization - */ - public enum NotificationType { - OUTBID, - WON, - SOLD, - EXPIRED, - CANCELLED, - NEW_BID - } - - /** - * Stored notification for offline players - */ - private static class AuctionNotification { - public final NotificationType type; - public final String message; - public final int listingId; - public final long timestamp; - - public AuctionNotification(NotificationType type, String message, int listingId) { - this.type = type; - this.message = message; - this.listingId = listingId; - this.timestamp = System.currentTimeMillis(); - } - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java index a4a3d1d5a..70242179a 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -1,7 +1,6 @@ package noppes.npcs.controllers.data; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionSort; /** @@ -13,9 +12,6 @@ public class AuctionFilter { /** Search text to match against item names */ public String searchText = ""; - /** Category to filter by (ALL for no category filter) */ - public EnumAuctionCategory category = EnumAuctionCategory.ALL; - /** How to sort results */ public EnumAuctionSort sortOrder = EnumAuctionSort.ENDING_SOON; @@ -55,15 +51,6 @@ public class AuctionFilter { return filter; } - /** - * Create a filter with category - */ - public static AuctionFilter category(EnumAuctionCategory category) { - AuctionFilter filter = new AuctionFilter(); - filter.category = category; - return filter; - } - /** * Create a filter for a specific seller */ @@ -80,11 +67,6 @@ public class AuctionFilter { return this; } - public AuctionFilter withCategory(EnumAuctionCategory category) { - this.category = category; - return this; - } - public AuctionFilter withSort(EnumAuctionSort sort) { this.sortOrder = sort; return this; @@ -135,11 +117,6 @@ public class AuctionFilter { } } - // Category filter - if (category != EnumAuctionCategory.ALL && listing.category != category) { - return false; - } - // Price range filter (uses current bid or starting price) long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; if (minPrice > 0 && price < minPrice) { @@ -180,7 +157,6 @@ public class AuctionFilter { */ public void reset() { searchText = ""; - category = EnumAuctionCategory.ALL; sortOrder = EnumAuctionSort.ENDING_SOON; minPrice = 0; maxPrice = 0; @@ -196,7 +172,6 @@ public class AuctionFilter { */ public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("SearchText", searchText); - compound.setInteger("Category", category.ordinal()); compound.setInteger("SortOrder", sortOrder.ordinal()); compound.setLong("MinPrice", minPrice); compound.setLong("MaxPrice", maxPrice); @@ -215,11 +190,6 @@ public class AuctionFilter { public void readFromNBT(NBTTagCompound compound) { searchText = compound.getString("SearchText"); - int catOrdinal = compound.getInteger("Category"); - if (catOrdinal >= 0 && catOrdinal < EnumAuctionCategory.values().length) { - category = EnumAuctionCategory.values()[catOrdinal]; - } - int sortOrdinal = compound.getInteger("SortOrder"); if (sortOrdinal >= 0 && sortOrdinal < EnumAuctionSort.values().length) { sortOrder = EnumAuctionSort.values()[sortOrdinal]; @@ -240,7 +210,6 @@ public class AuctionFilter { public String toString() { return "AuctionFilter{" + "search='" + searchText + '\'' + - ", category=" + category + ", sort=" + sortOrder + ", price=" + minPrice + "-" + maxPrice + ", buyoutOnly=" + buyoutOnly + diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index 24c778439..5366e367b 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -4,7 +4,6 @@ import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionCategory; import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.controllers.AuctionBlacklist; @@ -48,9 +47,6 @@ public class AuctionListing { // Status public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; - // Category for filtering - public EnumAuctionCategory category = EnumAuctionCategory.MISC; - // Claim tracking public boolean sellerClaimed = false; // Has seller claimed their proceeds? public boolean buyerClaimed = false; // Has buyer claimed their item? @@ -87,7 +83,6 @@ public class AuctionListing { this.duration = duration; this.listingTime = System.currentTimeMillis(); this.expirationTime = listingTime + duration.getMillis(); - this.category = EnumAuctionCategory.categorize(item); this.status = EnumAuctionStatus.ACTIVE; } @@ -351,7 +346,6 @@ public class AuctionListing { compound.setLong("ExpirationTime", expirationTime); compound.setInteger("Duration", duration.ordinal()); compound.setInteger("Status", status.ordinal()); - compound.setInteger("Category", category.ordinal()); compound.setBoolean("SellerClaimed", sellerClaimed); compound.setBoolean("BuyerClaimed", buyerClaimed); compound.setInteger("BidCount", bidCount); @@ -396,13 +390,6 @@ public class AuctionListing { status = EnumAuctionStatus.ACTIVE; } - int categoryOrdinal = compound.getInteger("Category"); - if (categoryOrdinal >= 0 && categoryOrdinal < EnumAuctionCategory.values().length) { - category = EnumAuctionCategory.values()[categoryOrdinal]; - } else { - category = EnumAuctionCategory.MISC; - } - sellerClaimed = compound.getBoolean("SellerClaimed"); buyerClaimed = compound.getBoolean("BuyerClaimed"); bidCount = compound.getInteger("BidCount"); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 466acfb99..88c10066c 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1437,3 +1437,73 @@ trail.heart=Heart trail.angryvillager=AngryVillager trail.happyvillager=HappyVillager trail.custom=Custom + +auction.title=Auction House +auction.browse=Browse +auction.myListings=My Listings +auction.myBids=My Bids +auction.claims=Claims +auction.sell=Sell +auction.search=Search +auction.refresh=Refresh +auction.viewDetails=View Details +auction.placeBid=Place Bid +auction.buyout=Buyout +auction.cancel=Cancel +auction.prevPage=< Prev +auction.nextPage=Next > +auction.noResults=No results +auction.page=Page %d of %d +auction.balance=Balance: %s +auction.listings=%d listings +auction.browseTitle=Browse Auctions +auction.myListingsTitle=My Listings +auction.myBidsTitle=My Active Bids +auction.claimsTitle=Claim Items +auction.sellTitle=Create Listing +auction.currentPrice=Current Price: +auction.minimumBid=Minimum Bid: +auction.yourBalance=Your Balance: +auction.timeLeft=Time Left: +auction.yourBid=Your Bid: +auction.minBid=Min Bid +auction.bidPlus10=+10% +auction.bidPlus25=+25% +auction.insufficientFunds=Insufficient funds! +auction.auctionEnded=Auction has ended! +auction.ended=Ended +auction.item=Item: +auction.seller=Seller: +auction.startingPrice=Starting Price: +auction.buyoutPrice=Buyout Price: +auction.noBuyout=None +auction.bidCount=Bid Count: +auction.highBidder=High Bidder: +auction.noBids=No bids +auction.claim=Claim +auction.createListing=Create Listing +auction.duration=Duration: +auction.listingFee=Listing Fee: +auction.create=Create +auction.sort.newest=Newest First +auction.sort.oldest=Oldest First +auction.sort.priceHigh=Most Expensive +auction.sort.priceLow=Least Expensive +auction.sort.endingSoon=Ending Soon +auction.sort.mostTime=Most Time Left +auction.sort.nameAZ=Name: A to Z +auction.sort.nameZA=Name: Z to A +auction.sort.mostBids=Most Bids +auction.sort.leastBids=Least Bids +auction.duration.short=Short (12h) +auction.duration.medium=Medium (24h) +auction.duration.long=Long (48h) +auction.duration.veryLong=Very Long (72h) +auction.placeItemBelow=Place item in slot below: +auction.noItemSelected=(No item selected) +auction.noBuyoutHint=(0 = no buyout) +auction.quantity=Quantity: +auction.totalBids=Total Bids: +auction.noBidsYet=No bids yet +auction.auctionDetails=Auction Details +auction.currentBid=Current Bid: From 77c8e418ebdc3bc3d7c65be74c13f2b766b65f9b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 10:29:57 +0000 Subject: [PATCH 14/64] Update market plan doc: convert to implementation status recap - Changed from planning document to status tracking document - Documented completed phases (2-6) - Documented not yet implemented phases (1, 7) - Documented removed features (categories, separate controllers) - Added file inventory and localization keys reference - Added design decisions summary - Added next steps section --- docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 2759 +++----------------------- 1 file changed, 262 insertions(+), 2497 deletions(-) diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md index e9b4cd0ce..4d62a7772 100644 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -1,2559 +1,324 @@ -# CustomNPC+ 1.11 Market Update - Implementation Plan +# CustomNPC+ 1.11 Market Update - Implementation Status ## Overview -This document outlines the implementation plan for the CustomNPC+ 1.11 update, focusing on three major features: +This document tracks the implementation status of the CustomNPC+ 1.11 Market Update, which includes: -1. **Auction House** - A new NPC role allowing players to access a global auction system -2. **Currency System** - Built-in currency integration with VaultAPI for Bukkit hybrid servers -3. **Trader Enhancements** - Stock/Inventory system with per-player/per-server options and reset timers +1. **Auction House** - Global auction system with bidding, buyout, and claims +2. **Currency System** - Built-in currency with VaultAPI integration for Bukkit hybrid servers +3. **Trader Enhancements** - Stock/inventory system with reset timers --- -## Design Decisions (Confirmed) +## Implementation Status Summary -### 1. Auction Refund System -**Decision:** When a player is outbid, the refund goes to **claims** (not direct to balance). -- Players can manage their bids from claims -- If auction ends while player is offline, they can claim their money later -- This keeps the flow consistent with item claims -- **Modify Bid Feature**: Players can use a "Modify Bid" button that auto-reclaims their money and applies it to a higher bid amount - -### 2. Permission System -**Decision:** Use the **existing CustomNPC+ permission system** used for Profiles. -- Pattern: `customnpcs.auction.slots.X` where X is the max slot count -- Implementation mirrors `ProfileController.allowSlotPermission()` approach -- Uses `CustomNpcsPermissions.hasCustomPermission(player, permissionString)` -- **Default**: 5 auction slots per player (configurable) - -### 3. Virtual Currency Storage -**Decision:** Currency is **NOT profile slot bound** - all profile slots share the same currency pool per player. -- Stored in PlayerData outside of slot components -- Balance persists across profile slot switches -- Balance viewing system to be added later (for now, visible via GUI only) - -### 4. Currency Display Icon -**Decision:** Use the **existing gold coin** from the mod (`CustomItems.coinGold`). -- Already has textures and localization for all supported languages -- Available coin types: Wood, Stone, Bronze, Iron, Gold, Diamond, Emerald - -### 5. Trader Stock Reset Timer Display -**Decision:** Show "Stocks reset in X hours" as a **banner ABOVE the GUI**. -- Positioned at a higher Y level than the main GUI -- Not overlayed on the GUI content -- Displayed when opening the trader interface - -### 6. Item Trading Restrictions -**Decision:** Implement the following restrictions for auction listings: -- **Untradeable Attribute**: New item attribute (`CNPC_Untradeable`) that prevents auction listing -- **ProfileSlot Bound Items**: Use existing `cnpc_profile_slot` NBT tag (from `ProfileSlotRequirement.java`) -- **Soulbound Items**: Use existing `cnpc_soulbind` NBT tag (from `SoulbindRequirement.java`) -- **Auction House Blacklist**: Configurable list of items that cannot be auctioned - -### 7. Admin Auction Management -**Decision:** Create an **Auction Management system** with a Global Admin button. -- Admins can modify, remove, and edit auction listings in real-time -- Accessible via special admin GUI -- Permission-gated for server operators - -### 8. Auction Timing & Bidding Rules -**Decision:** Configurable auction timing with snipe protection. -- **Claim Expiration**: 30 days by default (configurable) - claims auto-expire after this period -- **Bid Increment**: Percentage-based minimum increment (configurable, e.g., 5%) -- **Snipe Protection**: When a bid is placed near auction end, extend time TO 5 minutes remaining (never higher) - - Extension amount is configurable - - Example: If 2 minutes left and bid placed, extend to 5 minutes. If 10 minutes left, no extension. -- **Auction Durations**: Configurable default options (2h, 12h, 24h, 48h) +| Phase | Feature | Status | +|-------|---------|--------| +| Phase 1 | Currency System Foundation | **NOT STARTED** | +| Phase 2 | Trader Enhancements | **COMPLETED** | +| Phase 3 | Item Attributes & Restrictions | **COMPLETED** | +| Phase 4 | Auction House Core | **COMPLETED** | +| Phase 5 | Network Packets & Permissions | **COMPLETED** | +| Phase 6 | Auction GUI & Notifications | **COMPLETED** | +| Phase 7 | Admin Management | **NOT STARTED** | --- -## Table of Contents +## Completed Features -1. [Design Decisions](#design-decisions-confirmed) -2. [Architecture Overview](#architecture-overview) -3. [Feature 1: Currency System](#feature-1-currency-system) -4. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) -5. [Feature 3: Auction House](#feature-3-auction-house) -6. [Feature 4: Item Attributes & Restrictions](#feature-4-item-attributes--restrictions) -7. [Feature 5: Admin Auction Management](#feature-5-admin-auction-management) -8. [Configuration Reference](#configuration-reference) -9. [Implementation Order](#implementation-order) -10. [File Summary](#file-summary) -11. [Testing Strategy](#testing-strategy) +### Phase 2: Trader Enhancements (COMPLETED) +- `EnumStockReset.java` - Stock reset timing options +- `TraderStock.java` - Stock tracking with per-player and per-server modes +- Modified `RoleTrader.java` with stock and currency support +- Modified `ContainerNPCTrader.java` for stock/currency transactions +- Stock persistence via NBT + +### Phase 3: Item Attributes & Restrictions (COMPLETED) +- `ItemTradeAttribute.java` - Untradeable, ProfileSlot Bound, Soulbound checks +- `AuctionBlacklist.java` - Item/mod/NBT blacklist system +- Integration with AuctionListing validation + +### Phase 4: Auction House Core (COMPLETED) +- `EnumAuctionDuration.java` - Configurable durations with lang keys +- `EnumAuctionStatus.java` - Auction state management +- `EnumAuctionSort.java` - Sort options with lang keys +- `AuctionListing.java` - Complete listing data structure with bidding logic +- `AuctionFilter.java` - Search/filter criteria (categories removed) +- `AuctionController.java` - Consolidated controller with: + - Listing creation/management + - Bidding and buyout logic + - Claim system for items and currency refunds + - Inline logging (via LogWriter) + - Inline player notifications (via chat messages) + - Pending notification queue for offline players + - Snipe protection + - Sales tax and listing fees +- `AuctionClaim.java` - Claim data for items/money +- `RoleAuctioneer.java` - Auctioneer NPC role +- `ContainerAuction.java` - Auction container +- `ConfigMarket.java` - All market configuration options + +### Phase 5: Network Packets & Permissions (COMPLETED) +- `AuctionActionPacket.java` - Client-to-server auction actions +- `AuctionDataPacket.java` - Server-to-client data sync +- Packet registration in `PacketHandler.java` +- Permission nodes added to `CustomNpcsPermissions.java` +- API methods added to `ICustomNpc.java` + +### Phase 6: Auction GUI & Notifications (COMPLETED) +- `GuiAuction.java` - Main auction browser with tabs +- `SubGuiAuctionCreate.java` - Create listing sub-GUI +- `SubGuiAuctionBid.java` - Place bid sub-GUI +- `SubGuiAuctionDetails.java` - View listing details sub-GUI +- All GUI strings use lang keys via `StatCollector.translateToLocal()` +- Localization keys added to `en_US.lang` --- -## Architecture Overview +## Removed/Changed Features -### Current Codebase Patterns +### Removed: Category System +Originally planned to have item categories (Weapons, Armor, Tools, etc.) for filtering. This was **removed** to simplify the initial implementation: +- Deleted `EnumAuctionCategory.java` +- Removed category field from `AuctionListing.java` +- Removed category filtering from `AuctionFilter.java` +- Removed category buttons from `GuiAuction.java` -The implementation will follow existing patterns found in the codebase: +**Rationale:** Categories can be added in a future update. Current implementation uses search-based filtering. -| Pattern | Used By | Will Be Applied To | -|---------|---------|-------------------| -| Singleton Controller | `BankController`, `QuestController` | `AuctionController`, `CurrencyController` | -| Role Interface | `RoleTrader`, `RoleBank` | `RoleAuctioneer` | -| Player Data Extension | `PlayerBankData`, `PlayerQuestData` | `PlayerAuctionData`, `PlayerCurrencyData` | -| Cooldown System | `Quest.repeat`, `JobItemGiver` | Trader Stock Reset | -| Optional Addon | `PixelmonHelper`, `DBCAddon` | `VaultHelper` | -| NBT Serialization | All data classes | All new data classes | -| Cache System | `Market.java`, `CacheHashMap` | Auction listings cache | +### Consolidated: Controller Architecture +Originally planned separate controllers: +- ~~`AuctionLogger.java`~~ - **CONSOLIDATED** into `AuctionController.logEvent()` +- ~~`AuctionNotificationController.java`~~ - **CONSOLIDATED** into `AuctionController.notifyPlayer()` -### Package Structure for New Features +**Rationale:** Reduces complexity, all auction logic in one place. -``` -noppes/npcs/ -├── controllers/ -│ ├── AuctionController.java # Manages global auction house -│ ├── CurrencyController.java # Manages currency settings -│ └── data/ -│ ├── AuctionListing.java # Single auction listing -│ ├── AuctionData.java # World auction data -│ ├── PlayerAuctionData.java # Per-player auction history -│ ├── PlayerCurrencyData.java # Per-player currency balance -│ └── TraderStock.java # Trader stock configuration -├── roles/ -│ └── RoleAuctioneer.java # Auctioneer NPC role -├── containers/ -│ ├── ContainerAuction*.java # Auction GUI containers -│ └── ContainerNPCTraderStock.java # Stock management container -├── client/gui/ -│ ├── player/ -│ │ └── GuiAuction*.java # Player-facing auction GUIs -│ └── roles/ -│ └── GuiNpcAuctioneerSetup.java # Auctioneer configuration GUI - -kamkeel/npcs/ -├── controllers/ -│ └── CurrencyController.java # Currency management -├── network/packets/ -│ ├── data/ -│ │ ├── AuctionSyncPacket.java # Sync auction data -│ │ └── CurrencySyncPacket.java # Sync currency data -│ └── player/ -│ ├── AuctionActionPacket.java # Auction actions (bid, buy, sell) -│ └── CurrencyActionPacket.java # Currency transactions -└── compat/ - └── VaultHelper.java # VaultAPI integration -``` +### Changed: Localization Approach +- All client GUI strings now use `StatCollector.translateToLocal()` with lang keys +- `EnumAuctionSort` uses lang keys for display names +- `EnumAuctionDuration` uses lang keys for display names --- -## Feature 1: Currency System - -### 1.1 Overview - -The currency system provides a unified economy layer that can: -- Use VaultAPI when available on Bukkit hybrid servers (Cauldron, Thermos, Mohist) -- Fall back to an internal item-based currency when VaultAPI is unavailable -- Be configured per-world or globally - -### 1.2 VaultAPI Integration - -#### 1.2.1 Dependency Setup - -**File: `dependencies.gradle`** -```gradle -// VaultAPI - compile-only for optional integration -compileOnly("net.milkbowl.vault:VaultAPI:1.4") { - transitive = false -} -``` - -**File: `repositories.gradle`** -```gradle -maven { - name = "vault-repo" - url = "https://nexus.hc.to/content/repositories/pub_releases/" -} -``` - -> **Note:** VaultAPI 1.4.1 works with 1.7.10 Bukkit hybrid servers. The dependency is compile-only so the mod works without Vault present. - -#### 1.2.2 VaultHelper Class - -**File: `src/main/java/kamkeel/npcs/compat/VaultHelper.java`** - -```java -package kamkeel.npcs.compat; - -import cpw.mods.fml.common.Loader; -import net.minecraft.entity.player.EntityPlayer; -import noppes.npcs.LogWriter; -import org.bukkit.Bukkit; -import org.bukkit.plugin.RegisteredServiceProvider; - -public class VaultHelper { - public static boolean Enabled = false; - public static boolean Available = false; - - // Cached via reflection - private static Object economy = null; - private static Class economyClass = null; - private static java.lang.reflect.Method getBalance; - private static java.lang.reflect.Method withdrawPlayer; - private static java.lang.reflect.Method depositPlayer; - private static java.lang.reflect.Method hasMethod; - private static java.lang.reflect.Method format; - - public static void load() { - // Check if we're on a Bukkit hybrid server - try { - Class.forName("org.bukkit.Bukkit"); - Available = true; - } catch (ClassNotFoundException e) { - Available = false; - return; - } - - // Try to hook into Vault - try { - Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); - economyClass = vaultClass; - - RegisteredServiceProvider rsp = Bukkit.getServicesManager() - .getRegistration(vaultClass); - - if (rsp != null) { - economy = rsp.getProvider(); - - // Cache reflection methods - getBalance = economyClass.getMethod("getBalance", String.class); - withdrawPlayer = economyClass.getMethod("withdrawPlayer", - String.class, double.class); - depositPlayer = economyClass.getMethod("depositPlayer", - String.class, double.class); - hasMethod = economyClass.getMethod("has", String.class, double.class); - format = economyClass.getMethod("format", double.class); - - Enabled = true; - LogWriter.info("VaultAPI Economy hooked successfully!"); - } - } catch (Exception e) { - LogWriter.info("VaultAPI not available: " + e.getMessage()); - Enabled = false; - } - } - - // Economy Methods - public static double getBalance(EntityPlayer player) { - if (!Enabled) return 0.0; - try { - return (Double) getBalance.invoke(economy, player.getCommandSenderName()); - } catch (Exception e) { - LogWriter.except(e); - return 0.0; - } - } - - public static boolean withdraw(EntityPlayer player, double amount) { - if (!Enabled) return false; - try { - Object response = withdrawPlayer.invoke(economy, - player.getCommandSenderName(), amount); - return (Boolean) response.getClass() - .getMethod("transactionSuccess").invoke(response); - } catch (Exception e) { - LogWriter.except(e); - return false; - } - } - - public static boolean deposit(EntityPlayer player, double amount) { - if (!Enabled) return false; - try { - Object response = depositPlayer.invoke(economy, - player.getCommandSenderName(), amount); - return (Boolean) response.getClass() - .getMethod("transactionSuccess").invoke(response); - } catch (Exception e) { - LogWriter.except(e); - return false; - } - } - - public static boolean has(EntityPlayer player, double amount) { - if (!Enabled) return false; - try { - return (Boolean) hasMethod.invoke(economy, - player.getCommandSenderName(), amount); - } catch (Exception e) { - LogWriter.except(e); - return false; - } - } - - public static String formatCurrency(double amount) { - if (!Enabled) return String.format("%.2f", amount); - try { - return (String) format.invoke(economy, amount); - } catch (Exception e) { - return String.format("%.2f", amount); - } - } -} -``` - -#### 1.2.3 Initialization Hook - -**File: `src/main/java/noppes/npcs/CustomNpcs.java`** - -Add to `load()` method after AddonManager: -```java -// Initialize VaultAPI integration (for Bukkit hybrid servers) -VaultHelper.load(); -``` - -### 1.3 Currency Controller - -**File: `src/main/java/kamkeel/npcs/controllers/CurrencyController.java`** - -```java -package kamkeel.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import kamkeel.npcs.compat.VaultHelper; -import noppes.npcs.NoppesUtilPlayer; - -public class CurrencyController { - private static CurrencyController instance; - - // Configuration - public boolean useVaultIfAvailable = true; - public ItemStack fallbackCurrency = null; // Item to use when Vault unavailable - public double itemToVaultRatio = 1.0; // 1 item = X vault currency - - public static CurrencyController getInstance() { - if (instance == null) { - instance = new CurrencyController(); - } - return instance; - } - - /** - * Check if Vault economy is being used - */ - public boolean isUsingVault() { - return useVaultIfAvailable && VaultHelper.Enabled; - } - - /** - * Get player's balance (Vault or item count) - */ - public double getBalance(EntityPlayer player) { - if (isUsingVault()) { - return VaultHelper.getBalance(player); - } - // Fall back to counting items - if (fallbackCurrency != null) { - return NoppesUtilPlayer.countItems(player, fallbackCurrency, false, false); - } - return 0.0; - } - - /** - * Check if player can afford amount - */ - public boolean canAfford(EntityPlayer player, double amount) { - if (isUsingVault()) { - return VaultHelper.has(player, amount); - } - return getBalance(player) >= amount; - } - - /** - * Withdraw currency from player - */ - public boolean withdraw(EntityPlayer player, double amount) { - if (isUsingVault()) { - return VaultHelper.withdraw(player, amount); - } - // Remove items - if (fallbackCurrency != null) { - ItemStack toRemove = fallbackCurrency.copy(); - toRemove.stackSize = (int) Math.ceil(amount / itemToVaultRatio); - return NoppesUtilPlayer.consumeItem(player, toRemove, false, false); - } - return false; - } - - /** - * Deposit currency to player - */ - public boolean deposit(EntityPlayer player, double amount) { - if (isUsingVault()) { - return VaultHelper.deposit(player, amount); - } - // Give items - if (fallbackCurrency != null) { - ItemStack toGive = fallbackCurrency.copy(); - toGive.stackSize = (int) Math.floor(amount / itemToVaultRatio); - return player.inventory.addItemStackToInventory(toGive); - } - return false; - } - - /** - * Format currency for display - */ - public String format(double amount) { - if (isUsingVault()) { - return VaultHelper.formatCurrency(amount); - } - return String.format("%.0f", amount); - } - - // NBT Persistence - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setBoolean("UseVault", useVaultIfAvailable); - compound.setDouble("ItemRatio", itemToVaultRatio); - if (fallbackCurrency != null) { - compound.setTag("FallbackItem", fallbackCurrency.writeToNBT(new NBTTagCompound())); - } - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - useVaultIfAvailable = compound.getBoolean("UseVault"); - itemToVaultRatio = compound.getDouble("ItemRatio"); - if (compound.hasKey("FallbackItem")) { - fallbackCurrency = ItemStack.loadItemStackFromNBT( - compound.getCompoundTag("FallbackItem")); - } - } -} -``` - -### 1.4 Player Currency Data - -**File: `src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java`** - -```java -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import java.util.HashMap; - -public class PlayerCurrencyData { - private final PlayerData parent; - - // Transaction history (for auditing/display) - public HashMap transactionHistory = new HashMap<>(); - public double totalEarned = 0.0; - public double totalSpent = 0.0; - - public PlayerCurrencyData(PlayerData parent) { - this.parent = parent; - } - - public void recordTransaction(double amount, boolean isEarning) { - long timestamp = System.currentTimeMillis(); - transactionHistory.put(timestamp, isEarning ? amount : -amount); - if (isEarning) { - totalEarned += amount; - } else { - totalSpent += amount; - } - // Limit history size - if (transactionHistory.size() > 100) { - // Remove oldest entries - Long oldest = transactionHistory.keySet().stream() - .min(Long::compare).orElse(null); - if (oldest != null) { - transactionHistory.remove(oldest); - } - } - } - - public void loadNBTData(NBTTagCompound compound) { - if (compound.hasKey("CurrencyData")) { - NBTTagCompound data = compound.getCompoundTag("CurrencyData"); - totalEarned = data.getDouble("TotalEarned"); - totalSpent = data.getDouble("TotalSpent"); - } - } - - public void saveNBTData(NBTTagCompound compound) { - NBTTagCompound data = new NBTTagCompound(); - data.setDouble("TotalEarned", totalEarned); - data.setDouble("TotalSpent", totalSpent); - compound.setTag("CurrencyData", data); - } -} -``` - -### 1.5 Configuration UI - -Add currency configuration to the global settings GUI: - -**File: `src/main/java/noppes/npcs/client/gui/global/GuiNPCManageCurrency.java`** - -This GUI will allow server operators to: -- Enable/disable Vault integration -- Set fallback currency item -- Configure item-to-Vault ratio -- View server economy statistics - ---- - -## Feature 2: Trader Enhancements - -### 2.1 Overview - -Enhance the existing `RoleTrader` with: -- **Stock System**: Limited inventory that depletes when players buy -- **Per-Player vs Per-Server Stock**: Configure whether stock is shared or individual -- **Stock Reset Timer**: Automatic restocking based on configurable cooldown (like quest cooldowns) -- **Currency Integration**: Use Vault currency alongside or instead of item-based trading - -### 2.2 Trader Stock Configuration - -#### 2.2.1 Stock Data Class - -**File: `src/main/java/noppes/npcs/controllers/data/TraderStock.java`** - -```java -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import java.util.HashMap; - -public class TraderStock { - // Stock configuration - public boolean enableStock = false; - public boolean perPlayer = false; // false = per-server, true = per-player - - // Reset configuration (mirrors Quest cooldown system) - public EnumStockReset resetType = EnumStockReset.NONE; - public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types - - // Stock quantities per slot (max stock) - public int[] maxStock = new int[18]; - - // Current stock (per-server mode) - public int[] currentStock = new int[18]; - public long lastResetTime = 0; - - // Per-player stock tracking - public HashMap playerStock = new HashMap<>(); - - public TraderStock() { - for (int i = 0; i < 18; i++) { - maxStock[i] = -1; // -1 = unlimited - currentStock[i] = -1; - } - } - - /** - * Get available stock for a slot - */ - public int getAvailableStock(int slot, String playerName) { - if (!enableStock || maxStock[slot] < 0) { - return Integer.MAX_VALUE; // Unlimited - } - - if (perPlayer) { - PlayerTraderStock pStock = playerStock.get(playerName); - if (pStock == null) { - return maxStock[slot]; // Not purchased yet - } - return pStock.getStock(slot, maxStock[slot]); - } - - return currentStock[slot]; - } - - /** - * Consume stock when purchase is made - */ - public boolean consumeStock(int slot, String playerName, int amount) { - if (!enableStock || maxStock[slot] < 0) { - return true; // Unlimited - } - - if (perPlayer) { - PlayerTraderStock pStock = playerStock.computeIfAbsent( - playerName, k -> new PlayerTraderStock()); - return pStock.consumeStock(slot, maxStock[slot], amount); - } - - if (currentStock[slot] >= amount) { - currentStock[slot] -= amount; - return true; - } - return false; - } - - /** - * Check if stock should reset - */ - public boolean shouldReset(long currentTime) { - if (resetType == EnumStockReset.NONE) { - return false; - } - - long elapsed = currentTime - lastResetTime; - long resetInterval = getResetInterval(); - - return elapsed >= resetInterval; - } - - /** - * Reset all stock to max values - */ - public void resetStock(long currentTime) { - lastResetTime = currentTime; - - for (int i = 0; i < 18; i++) { - currentStock[i] = maxStock[i]; - } - - // Clear per-player stock - playerStock.clear(); - } - - /** - * Get reset interval in milliseconds - */ - public long getResetInterval() { - switch (resetType) { - case MCDAILY: - return 24000 * 50; // 24000 ticks * 50ms - case MCWEEKLY: - return 168000 * 50; - case MCCUSTOM: - return customResetTime * 50; - case RLDAILY: - return 86400000L; // 24 hours in ms - case RLWEEKLY: - return 604800000L; // 7 days in ms - case RLCUSTOM: - return customResetTime; - default: - return Long.MAX_VALUE; - } - } - - /** - * Get time until next reset (for display) - */ - public long getTimeUntilReset(long currentTime) { - if (resetType == EnumStockReset.NONE) { - return -1; - } - long elapsed = currentTime - lastResetTime; - long interval = getResetInterval(); - return Math.max(0, interval - elapsed); - } - - // NBT Serialization - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setBoolean("EnableStock", enableStock); - compound.setBoolean("PerPlayer", perPlayer); - compound.setInteger("ResetType", resetType.ordinal()); - compound.setLong("CustomResetTime", customResetTime); - compound.setIntArray("MaxStock", maxStock); - compound.setIntArray("CurrentStock", currentStock); - compound.setLong("LastResetTime", lastResetTime); - - // Save per-player stock - NBTTagList playerList = new NBTTagList(); - for (Map.Entry entry : playerStock.entrySet()) { - NBTTagCompound playerTag = new NBTTagCompound(); - playerTag.setString("Player", entry.getKey()); - entry.getValue().writeToNBT(playerTag); - playerList.appendTag(playerTag); - } - compound.setTag("PlayerStock", playerList); - - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - enableStock = compound.getBoolean("EnableStock"); - perPlayer = compound.getBoolean("PerPlayer"); - resetType = EnumStockReset.values()[compound.getInteger("ResetType")]; - customResetTime = compound.getLong("CustomResetTime"); - maxStock = compound.getIntArray("MaxStock"); - currentStock = compound.getIntArray("CurrentStock"); - lastResetTime = compound.getLong("LastResetTime"); - - // Load per-player stock - NBTTagList playerList = compound.getTagList("PlayerStock", 10); - for (int i = 0; i < playerList.tagCount(); i++) { - NBTTagCompound playerTag = playerList.getCompoundTagAt(i); - String playerName = playerTag.getString("Player"); - PlayerTraderStock pStock = new PlayerTraderStock(); - pStock.readFromNBT(playerTag); - playerStock.put(playerName, pStock); - } - } -} - -// Inner class for per-player stock tracking -class PlayerTraderStock { - public int[] purchasedAmounts = new int[18]; - public long lastPurchaseTime = 0; - - public int getStock(int slot, int maxStock) { - return Math.max(0, maxStock - purchasedAmounts[slot]); - } - - public boolean consumeStock(int slot, int maxStock, int amount) { - if (getStock(slot, maxStock) >= amount) { - purchasedAmounts[slot] += amount; - lastPurchaseTime = System.currentTimeMillis(); - return true; - } - return false; - } - - public void writeToNBT(NBTTagCompound compound) { - compound.setIntArray("Purchased", purchasedAmounts); - compound.setLong("LastPurchase", lastPurchaseTime); - } - - public void readFromNBT(NBTTagCompound compound) { - purchasedAmounts = compound.getIntArray("Purchased"); - lastPurchaseTime = compound.getLong("LastPurchase"); - } -} -``` - -#### 2.2.2 Stock Reset Enum - -**File: `src/main/java/noppes/npcs/constants/EnumStockReset.java`** - -```java -package noppes.npcs.constants; - -public enum EnumStockReset { - NONE, // Never resets - MCDAILY, // Every 24000 MC ticks (1 MC day) - MCWEEKLY, // Every 168000 MC ticks (7 MC days) - MCCUSTOM, // Custom MC tick interval - RLDAILY, // Every 24 real hours - RLWEEKLY, // Every 7 real days - RLCUSTOM // Custom real-time interval -} -``` - -### 2.3 RoleTrader Modifications - -**File: `src/main/java/noppes/npcs/roles/RoleTrader.java`** - -Add the following to `RoleTrader`: - -```java -// Add new fields -public TraderStock stock = new TraderStock(); -public boolean useCurrency = false; // Use Vault/Currency system -public double[] slotPrices = new double[18]; // Currency prices per slot - -// Modify interact() to check stock reset -@Override -public void interact(EntityPlayer player) { - // Check for stock reset before opening GUI - if (stock.enableStock) { - long currentTime = stock.resetType.name().startsWith("RL") - ? System.currentTimeMillis() - : player.worldObj.getTotalWorldTime(); - - if (stock.shouldReset(currentTime)) { - stock.resetStock(currentTime); - // Save to market if using shared market - if (!marketName.isEmpty()) { - Market.save(this, marketName); - } - } - } - - npc.say(player, npc.advanced.getInteractLine()); - NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); -} - -// Modify writeToNBT -@Override -public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) { - // ... existing code ... - - // Add stock data - nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); - nbttagcompound.setBoolean("UseCurrency", useCurrency); - - // Save currency prices - NBTTagList priceList = new NBTTagList(); - for (double price : slotPrices) { - NBTTagCompound priceTag = new NBTTagCompound(); - priceTag.setDouble("Price", price); - priceList.appendTag(priceTag); - } - nbttagcompound.setTag("SlotPrices", priceList); - - return nbttagcompound; -} - -// Modify readFromNBT -@Override -public void readFromNBT(NBTTagCompound nbttagcompound) { - // ... existing code ... - - // Load stock data - if (nbttagcompound.hasKey("Stock")) { - stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); - } - useCurrency = nbttagcompound.getBoolean("UseCurrency"); - - // Load currency prices - if (nbttagcompound.hasKey("SlotPrices")) { - NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); - for (int i = 0; i < priceList.tagCount() && i < 18; i++) { - slotPrices[i] = priceList.getCompoundTagAt(i).getDouble("Price"); - } - } -} -``` - -### 2.4 ContainerNPCTrader Modifications - -**File: `src/main/java/noppes/npcs/containers/ContainerNPCTrader.java`** - -Modify `slotClick()` to handle stock and currency: - -```java -public ItemStack slotClick(int slotIndex, EntityPlayer player) { - RoleTrader role = (RoleTrader) npc.roleInterface; - String playerName = player.getCommandSenderName(); - - // ... existing validation ... - - // Check stock availability - if (role.stock.enableStock) { - int available = role.stock.getAvailableStock(slotIndex, playerName); - if (available <= 0) { - // Out of stock - send message to player - return null; - } - } - - // Handle currency-based purchase - if (role.useCurrency && role.slotPrices[slotIndex] > 0) { - double price = role.slotPrices[slotIndex]; - CurrencyController currency = CurrencyController.getInstance(); - - if (!currency.canAfford(player, price)) { - // Can't afford - send message - return null; - } - - // Withdraw currency - if (!currency.withdraw(player, price)) { - return null; - } - } else { - // ... existing item-based currency logic ... - } - - // Consume stock - if (role.stock.enableStock) { - role.stock.consumeStock(slotIndex, playerName, 1); - } - - // ... existing item delivery logic ... - - // Save if using market - if (!role.marketName.isEmpty()) { - Market.save(role, role.marketName); - } - - return soldItem; -} -``` - -### 2.5 Trader Setup GUI Additions - -**File: `src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java`** - -Add new configuration options: - -```java -// Add buttons/fields for: -// - "Enable Stock" toggle -// - "Per Player" / "Per Server" radio buttons -// - "Reset Type" dropdown (None, MC Daily, MC Weekly, etc.) -// - "Custom Reset Time" field -// - "Use Currency" toggle -// - Per-slot stock quantity inputs -// - Per-slot currency price inputs - -private void initStockGui() { - addButton(new GuiNpcButton(20, x, y, 80, 20, - "Enable Stock: " + (role.stock.enableStock ? "Yes" : "No"))); - - addButton(new GuiNpcButton(21, x, y + 22, 80, 20, - role.stock.perPlayer ? "Per Player" : "Per Server")); - - String[] resetTypes = {"None", "MC Daily", "MC Weekly", "MC Custom", - "RL Daily", "RL Weekly", "RL Custom"}; - addButton(new GuiNpcButton(22, x, y + 44, 80, 20, resetTypes, - role.stock.resetType.ordinal())); - - addButton(new GuiNpcButton(23, x, y + 66, 80, 20, - "Use Currency: " + (role.useCurrency ? "Yes" : "No"))); -} -``` - ---- - -## Feature 3: Auction House - -### 3.1 Overview - -The Auction House is a global trading system where players can: -- **List items for sale** at fixed prices or as auctions with bidding -- **Browse listings** by category, search, and filter -- **Bid on auctions** with automatic outbid notifications -- **Buy instantly** for buy-it-now prices -- **Manage their listings** (cancel, modify) -- **Collect proceeds and expired items** via mailbox or direct claim - -### 3.2 Auction Data Structures - -#### 3.2.1 AuctionListing - -**File: `src/main/java/noppes/npcs/controllers/data/AuctionListing.java`** - -```java -package noppes.npcs.controllers.data; - -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import java.util.UUID; - -public class AuctionListing { - public int id; - public UUID sellerUUID; - public String sellerName; - public ItemStack item; - - // Pricing - public double startingPrice; - public double buyoutPrice; // 0 = no buyout - public double currentBid; - public UUID highBidderUUID; - public String highBidderName; - - // Timing - public long listingTime; - public long expirationTime; - public EnumAuctionDuration duration; - - // Status - public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; - - // Category - public EnumAuctionCategory category = EnumAuctionCategory.MISC; - - public AuctionListing() {} - - public AuctionListing(UUID seller, String sellerName, ItemStack item, - double startPrice, double buyout, EnumAuctionDuration duration) { - this.sellerUUID = seller; - this.sellerName = sellerName; - this.item = item.copy(); - this.startingPrice = startPrice; - this.buyoutPrice = buyout; - this.currentBid = startPrice; - this.duration = duration; - this.listingTime = System.currentTimeMillis(); - this.expirationTime = listingTime + duration.getMillis(); - this.category = categorizeItem(item); - } - - /** - * Check if auction has expired - */ - public boolean isExpired() { - return System.currentTimeMillis() >= expirationTime; - } - - /** - * Get time remaining in milliseconds - */ - public long getTimeRemaining() { - return Math.max(0, expirationTime - System.currentTimeMillis()); - } - - /** - * Place a bid - */ - public boolean placeBid(UUID bidder, String bidderName, double amount) { - if (status != EnumAuctionStatus.ACTIVE) return false; - if (amount <= currentBid) return false; - if (bidder.equals(sellerUUID)) return false; // Can't bid on own item - - // Minimum bid increment (5% or 1, whichever is greater) - double minIncrement = Math.max(1, currentBid * 0.05); - if (amount < currentBid + minIncrement) return false; - - highBidderUUID = bidder; - highBidderName = bidderName; - currentBid = amount; - - return true; - } - - /** - * Execute buyout - */ - public boolean buyout(UUID buyer, String buyerName) { - if (status != EnumAuctionStatus.ACTIVE) return false; - if (buyoutPrice <= 0) return false; // No buyout available - if (buyer.equals(sellerUUID)) return false; - - highBidderUUID = buyer; - highBidderName = buyerName; - currentBid = buyoutPrice; - status = EnumAuctionStatus.SOLD; - - return true; - } - - /** - * Cancel listing (seller only, before any bids) - */ - public boolean cancel(UUID requester) { - if (!requester.equals(sellerUUID)) return false; - if (highBidderUUID != null) return false; // Has bids, can't cancel - - status = EnumAuctionStatus.CANCELLED; - return true; - } - - /** - * Auto-categorize item based on type - */ - private EnumAuctionCategory categorizeItem(ItemStack item) { - // Implement item categorization logic - // Check for weapons, armor, tools, food, blocks, etc. - return EnumAuctionCategory.MISC; - } - - // NBT Serialization - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("Id", id); - compound.setString("SellerUUID", sellerUUID.toString()); - compound.setString("SellerName", sellerName); - compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); - compound.setDouble("StartPrice", startingPrice); - compound.setDouble("BuyoutPrice", buyoutPrice); - compound.setDouble("CurrentBid", currentBid); - if (highBidderUUID != null) { - compound.setString("BidderUUID", highBidderUUID.toString()); - compound.setString("BidderName", highBidderName); - } - compound.setLong("ListingTime", listingTime); - compound.setLong("ExpirationTime", expirationTime); - compound.setInteger("Duration", duration.ordinal()); - compound.setInteger("Status", status.ordinal()); - compound.setInteger("Category", category.ordinal()); - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - id = compound.getInteger("Id"); - sellerUUID = UUID.fromString(compound.getString("SellerUUID")); - sellerName = compound.getString("SellerName"); - item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); - startingPrice = compound.getDouble("StartPrice"); - buyoutPrice = compound.getDouble("BuyoutPrice"); - currentBid = compound.getDouble("CurrentBid"); - if (compound.hasKey("BidderUUID")) { - highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); - highBidderName = compound.getString("BidderName"); - } - listingTime = compound.getLong("ListingTime"); - expirationTime = compound.getLong("ExpirationTime"); - duration = EnumAuctionDuration.values()[compound.getInteger("Duration")]; - status = EnumAuctionStatus.values()[compound.getInteger("Status")]; - category = EnumAuctionCategory.values()[compound.getInteger("Category")]; - } -} -``` - -#### 3.2.2 Supporting Enums - -**File: `src/main/java/noppes/npcs/constants/EnumAuctionDuration.java`** - -```java -package noppes.npcs.constants; - -public enum EnumAuctionDuration { - SHORT(2 * 60 * 60 * 1000L), // 2 hours - MEDIUM(12 * 60 * 60 * 1000L), // 12 hours - LONG(24 * 60 * 60 * 1000L), // 24 hours - VERY_LONG(48 * 60 * 60 * 1000L); // 48 hours - - private final long millis; - - EnumAuctionDuration(long millis) { - this.millis = millis; - } - - public long getMillis() { - return millis; - } - - public String getDisplayName() { - switch (this) { - case SHORT: return "2 Hours"; - case MEDIUM: return "12 Hours"; - case LONG: return "24 Hours"; - case VERY_LONG: return "48 Hours"; - default: return "Unknown"; - } - } -} -``` - -**File: `src/main/java/noppes/npcs/constants/EnumAuctionStatus.java`** - -```java -package noppes.npcs.constants; - -public enum EnumAuctionStatus { - ACTIVE, // Currently listed - SOLD, // Successfully sold (buyout or auction end with bids) - EXPIRED, // Ended without bids - CANCELLED, // Cancelled by seller - CLAIMED // Item/money has been claimed -} -``` - -**File: `src/main/java/noppes/npcs/constants/EnumAuctionCategory.java`** - -```java -package noppes.npcs.constants; - -public enum EnumAuctionCategory { - WEAPONS, - ARMOR, - TOOLS, - FOOD, - POTIONS, - BLOCKS, - MATERIALS, - ENCHANTED, - MISC -} -``` - -### 3.3 Auction Controller - -**File: `src/main/java/noppes/npcs/controllers/AuctionController.java`** - -```java -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.constants.*; - -import java.io.File; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public class AuctionController { - private static AuctionController instance; - - // All listings (thread-safe for async operations) - public ConcurrentHashMap listings = new ConcurrentHashMap<>(); - - // Index by seller for quick lookup - private Map> sellerIndex = new ConcurrentHashMap<>(); - - // Pending claims (seller -> items/money to claim) - public Map> pendingClaims = new ConcurrentHashMap<>(); - - private int nextId = 1; - private long lastSaveTime = 0; - private static final long SAVE_INTERVAL = 60000; // Save every minute - - // Configuration - public double listingFee = 0.0; // Fee to list item - public double saleTax = 0.05; // 5% tax on sales - public int maxListingsPerPlayer = 20; - public boolean requireAuctioneer = true; // Require Auctioneer NPC to access - - public static AuctionController getInstance() { - if (instance == null) { - instance = new AuctionController(); - instance.load(); - } - return instance; - } - - /** - * Create a new auction listing - */ - public AuctionListing createListing(EntityPlayer seller, ItemStack item, - double startPrice, double buyout, - EnumAuctionDuration duration) { - UUID sellerUUID = seller.getUniqueID(); - - // Check max listings - Set sellerListings = sellerIndex.get(sellerUUID); - if (sellerListings != null && sellerListings.size() >= maxListingsPerPlayer) { - return null; - } - - // Charge listing fee - if (listingFee > 0) { - CurrencyController currency = CurrencyController.getInstance(); - if (!currency.withdraw(seller, listingFee)) { - return null; - } - } - - // Create listing - AuctionListing listing = new AuctionListing( - sellerUUID, seller.getCommandSenderName(), - item, startPrice, buyout, duration - ); - listing.id = nextId++; - - // Add to indexes - listings.put(listing.id, listing); - sellerIndex.computeIfAbsent(sellerUUID, k -> new HashSet<>()).add(listing.id); - - // Remove item from seller inventory - item.stackSize = 0; - - scheduleSave(); - return listing; - } - - /** - * Place a bid on an auction - */ - public boolean placeBid(EntityPlayer bidder, int listingId, double amount) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.isExpired()) return false; - - CurrencyController currency = CurrencyController.getInstance(); - - // Check if bidder can afford - if (!currency.canAfford(bidder, amount)) return false; - - // Refund previous bidder - if (listing.highBidderUUID != null) { - addClaim(listing.highBidderUUID, listing.highBidderName, - null, listing.currentBid, "Outbid refund for: " + - listing.item.getDisplayName()); - } - - // Place bid - if (!listing.placeBid(bidder.getUniqueID(), - bidder.getCommandSenderName(), amount)) { - return false; - } - - // Hold currency from bidder - currency.withdraw(bidder, amount); - - scheduleSave(); - return true; - } - - /** - * Buyout an auction - */ - public boolean buyout(EntityPlayer buyer, int listingId) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.buyoutPrice <= 0) return false; - - CurrencyController currency = CurrencyController.getInstance(); - - // Check if buyer can afford - if (!currency.canAfford(buyer, listing.buyoutPrice)) return false; - - // Refund previous bidder if any - if (listing.highBidderUUID != null) { - addClaim(listing.highBidderUUID, listing.highBidderName, - null, listing.currentBid, "Buyout refund for: " + - listing.item.getDisplayName()); - } - - // Execute buyout - if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { - return false; - } - - // Withdraw from buyer - currency.withdraw(buyer, listing.buyoutPrice); - - // Calculate seller proceeds (minus tax) - double proceeds = listing.buyoutPrice * (1 - saleTax); - - // Add claims for both parties - addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, - "Sale of: " + listing.item.getDisplayName()); - addClaim(buyer.getUniqueID(), buyer.getCommandSenderName(), - listing.item, 0, "Purchased: " + listing.item.getDisplayName()); - - scheduleSave(); - return true; - } - - /** - * Cancel a listing (seller only) - */ - public boolean cancelListing(EntityPlayer player, int listingId) { - AuctionListing listing = listings.get(listingId); - if (listing == null) return false; - - if (!listing.cancel(player.getUniqueID())) return false; - - // Return item to seller - addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, - "Cancelled listing: " + listing.item.getDisplayName()); - - scheduleSave(); - return true; - } - - /** - * Process expired auctions (called periodically) - */ - public void processExpiredAuctions() { - long now = System.currentTimeMillis(); - - for (AuctionListing listing : listings.values()) { - if (listing.status != EnumAuctionStatus.ACTIVE) continue; - if (!listing.isExpired()) continue; - - if (listing.highBidderUUID != null) { - // Auction successful - has bids - listing.status = EnumAuctionStatus.SOLD; - - double proceeds = listing.currentBid * (1 - saleTax); - addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, - "Auction sold: " + listing.item.getDisplayName()); - addClaim(listing.highBidderUUID, listing.highBidderName, - listing.item, 0, "Won auction: " + listing.item.getDisplayName()); - } else { - // No bids - return to seller - listing.status = EnumAuctionStatus.EXPIRED; - addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, - "Expired listing: " + listing.item.getDisplayName()); - } - } - - scheduleSave(); - } - - /** - * Get active listings with optional filters - */ - public List getActiveListings(EnumAuctionCategory category, - String search, - int page, int pageSize) { - return listings.values().stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .filter(l -> !l.isExpired()) - .filter(l -> category == null || l.category == category) - .filter(l -> search == null || search.isEmpty() || - l.item.getDisplayName().toLowerCase() - .contains(search.toLowerCase())) - .sorted(Comparator.comparingLong(l -> l.expirationTime)) - .skip((long) page * pageSize) - .limit(pageSize) - .collect(Collectors.toList()); - } - - /** - * Get player's own listings - */ - public List getPlayerListings(UUID playerUUID) { - Set ids = sellerIndex.get(playerUUID); - if (ids == null) return Collections.emptyList(); - - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Get pending claims for a player - */ - public List getPendingClaims(UUID playerUUID) { - return pendingClaims.getOrDefault(playerUUID, Collections.emptyList()); - } - - /** - * Claim pending items/money - */ - public boolean claim(EntityPlayer player, int claimIndex) { - List claims = pendingClaims.get(player.getUniqueID()); - if (claims == null || claimIndex >= claims.size()) return false; - - AuctionClaim claim = claims.get(claimIndex); - - // Give item or money - if (claim.item != null) { - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { - return false; // Inventory full - } - } - if (claim.money > 0) { - CurrencyController.getInstance().deposit(player, claim.money); - } - - claims.remove(claimIndex); - scheduleSave(); - return true; - } - - private void addClaim(UUID playerUUID, String playerName, - ItemStack item, double money, String description) { - AuctionClaim claim = new AuctionClaim(item, money, description); - pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(claim); - } - - // Persistence - private void scheduleSave() { - long now = System.currentTimeMillis(); - if (now - lastSaveTime > SAVE_INTERVAL) { - save(); - lastSaveTime = now; - } - } - - public void save() { - try { - NBTTagCompound compound = new NBTTagCompound(); - compound.setInteger("NextId", nextId); - compound.setDouble("ListingFee", listingFee); - compound.setDouble("SaleTax", saleTax); - compound.setInteger("MaxListings", maxListingsPerPlayer); - - // Save listings - NBTTagList listingList = new NBTTagList(); - for (AuctionListing listing : listings.values()) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - compound.setTag("Listings", listingList); - - // Save claims - NBTTagList claimList = new NBTTagList(); - for (Map.Entry> entry : pendingClaims.entrySet()) { - NBTTagCompound playerClaims = new NBTTagCompound(); - playerClaims.setString("UUID", entry.getKey().toString()); - NBTTagList claims = new NBTTagList(); - for (AuctionClaim claim : entry.getValue()) { - claims.appendTag(claim.writeToNBT(new NBTTagCompound())); - } - playerClaims.setTag("Claims", claims); - claimList.appendTag(playerClaims); - } - compound.setTag("PendingClaims", claimList); - - File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); - CompressedStreamTools.writeCompressed(compound, - new java.io.FileOutputStream(file)); - - } catch (Exception e) { - LogWriter.except(e); - } - } - - public void load() { - try { - File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); - if (!file.exists()) return; - - NBTTagCompound compound = CompressedStreamTools.readCompressed( - new java.io.FileInputStream(file)); - - nextId = compound.getInteger("NextId"); - listingFee = compound.getDouble("ListingFee"); - saleTax = compound.getDouble("SaleTax"); - maxListingsPerPlayer = compound.getInteger("MaxListings"); - - // Load listings - NBTTagList listingList = compound.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingList.getCompoundTagAt(i)); - listings.put(listing.id, listing); - sellerIndex.computeIfAbsent(listing.sellerUUID, k -> new HashSet<>()) - .add(listing.id); - } - - // Load claims - NBTTagList claimList = compound.getTagList("PendingClaims", 10); - for (int i = 0; i < claimList.tagCount(); i++) { - NBTTagCompound playerClaims = claimList.getCompoundTagAt(i); - UUID playerUUID = UUID.fromString(playerClaims.getString("UUID")); - NBTTagList claims = playerClaims.getTagList("Claims", 10); - List claimsList = new ArrayList<>(); - for (int j = 0; j < claims.tagCount(); j++) { - AuctionClaim claim = new AuctionClaim(); - claim.readFromNBT(claims.getCompoundTagAt(j)); - claimsList.add(claim); - } - pendingClaims.put(playerUUID, claimsList); - } - - } catch (Exception e) { - LogWriter.except(e); - } - } -} - -// Claim data structure -class AuctionClaim { - public ItemStack item; - public double money; - public String description; - public long timestamp; - - public AuctionClaim() {} - - public AuctionClaim(ItemStack item, double money, String description) { - this.item = item != null ? item.copy() : null; - this.money = money; - this.description = description; - this.timestamp = System.currentTimeMillis(); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - if (item != null) { - compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); - } - compound.setDouble("Money", money); - compound.setString("Description", description); - compound.setLong("Timestamp", timestamp); - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Item")) { - item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); - } - money = compound.getDouble("Money"); - description = compound.getString("Description"); - timestamp = compound.getLong("Timestamp"); - } -} -``` - -### 3.4 Auctioneer Role - -**File: `src/main/java/noppes/npcs/roles/RoleAuctioneer.java`** - -```java -package noppes.npcs.roles; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NoppesUtilServer; -import noppes.npcs.constants.EnumGuiType; -import noppes.npcs.entity.EntityNPCInterface; - -public class RoleAuctioneer extends RoleInterface { - - // Configuration - public String auctionHouseName = "Global"; // For future multi-auction support - public boolean allowSelling = true; - public boolean allowBuying = true; - public boolean showExpiredListings = true; - - // Custom messages - public String welcomeMessage = ""; - public String sellDisabledMessage = "This auctioneer does not accept listings."; - public String buyDisabledMessage = "This auctioneer does not sell items."; - - public RoleAuctioneer(EntityNPCInterface npc) { - super(npc); - } - - @Override - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("AuctionHouse", auctionHouseName); - compound.setBoolean("AllowSelling", allowSelling); - compound.setBoolean("AllowBuying", allowBuying); - compound.setBoolean("ShowExpired", showExpiredListings); - compound.setString("WelcomeMsg", welcomeMessage); - compound.setString("SellDisabledMsg", sellDisabledMessage); - compound.setString("BuyDisabledMsg", buyDisabledMessage); - return compound; - } - - @Override - public void readFromNBT(NBTTagCompound compound) { - auctionHouseName = compound.getString("AuctionHouse"); - allowSelling = compound.getBoolean("AllowSelling"); - allowBuying = compound.getBoolean("AllowBuying"); - showExpiredListings = compound.getBoolean("ShowExpired"); - welcomeMessage = compound.getString("WelcomeMsg"); - sellDisabledMessage = compound.getString("SellDisabledMsg"); - buyDisabledMessage = compound.getString("BuyDisabledMsg"); - } - - @Override - public void interact(EntityPlayer player) { - // Show welcome message if set - if (!welcomeMessage.isEmpty()) { - npc.say(player, npc.advanced.getInteractLine()); - } - - // Open auction house GUI - NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); - } -} -``` - -### 3.5 Update EnumRoleType - -**File: `src/main/java/noppes/npcs/constants/EnumRoleType.java`** - -```java -public enum EnumRoleType { - None, - Trader, - Follower, - Bank, - Transporter, - Postman, - Companion, - Mount, - Auctioneer // Add this -} -``` - -### 3.6 Update DataAdvanced - -**File: `src/main/java/noppes/npcs/DataAdvanced.java`** - -Add to `setRole()` method: - -```java -else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) - npc.roleInterface = new RoleAuctioneer(npc); -``` - -### 3.7 Update EnumGuiType - -**File: `src/main/java/noppes/npcs/constants/EnumGuiType.java`** - -Add new GUI types: - -```java -// Auction House GUIs -PlayerAuction, // Main auction browse/buy interface -PlayerAuctionSell, // Create new listing -PlayerAuctionMyListings, // View own listings -PlayerAuctionClaims, // Claim items/money -AuctioneerSetup, // NPC configuration -ManageAuctions // Admin management -``` - -### 3.8 Auction GUI System - -The auction GUI system will consist of multiple interconnected screens: - -#### 3.8.1 Main Auction Browser - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionBrowse.java`** - -Features: -- Category filter tabs (Weapons, Armor, Tools, etc.) -- Search bar for item names -- Sort options (Time remaining, Price, Recently listed) -- Paginated listing display -- Quick buy/bid buttons - -#### 3.8.2 Create Listing GUI - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java`** - -Features: -- Item slot for the item to sell -- Starting price input -- Buyout price input (optional) -- Duration selector -- Listing fee display -- Confirm button - -#### 3.8.3 My Listings GUI - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionMyListings.java`** - -Features: -- List of player's active listings -- Current bid status -- Time remaining -- Cancel button (if no bids) - -#### 3.8.4 Claims GUI - -**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionClaims.java`** - -Features: -- List of pending claims (items won, money from sales, refunds) -- Claim button for each item -- Claim all button - -### 3.9 Network Packets - -#### 3.9.1 Auction Action Packet - -**File: `src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java`** - -```java -package kamkeel.npcs.network.packets.player; - -public class AuctionActionPacket extends AbstractPacket { - - public enum Action { - CREATE_LISTING, - PLACE_BID, - BUYOUT, - CANCEL, - CLAIM, - CLAIM_ALL, - BROWSE, - MY_LISTINGS - } - - private Action action; - private int listingId; - private double amount; - private NBTTagCompound itemData; - private int duration; - private double buyoutPrice; - private int category; - private String search; - private int page; - - // Implement sendData/receiveData... -} -``` - -#### 3.9.2 Auction Sync Packet - -**File: `src/main/java/kamkeel/npcs/network/packets/data/AuctionSyncPacket.java`** - -Used to send auction listing data to clients for GUI display. - -### 3.10 Periodic Tasks - -Add to server tick handler to process expired auctions: - -**File: `src/main/java/noppes/npcs/ServerTickHandler.java`** - -```java -private int auctionCheckCounter = 0; - -@SubscribeEvent -public void onServerTick(TickEvent.ServerTickEvent event) { - // ... existing code ... - - // Check expired auctions every 5 minutes (6000 ticks) - if (++auctionCheckCounter >= 6000) { - auctionCheckCounter = 0; - AuctionController.getInstance().processExpiredAuctions(); - } -} -``` - ---- - -## Feature 4: Item Attributes & Restrictions - -### 4.1 Overview - -Implement item attribute system to control which items can be traded/auctioned. Items with certain attributes will be blocked from the auction house. - -### 4.2 Existing Item Requirements (Reuse) - -The codebase already has requirement types that we can leverage: - -| Requirement | NBT Key | File | Purpose | -|-------------|---------|------|---------| -| ProfileSlot Bound | `cnpc_profile_slot` | `ProfileSlotRequirement.java` | Item bound to specific profile slot | -| Soulbound | `cnpc_soulbind` | `SoulbindRequirement.java` | Item bound to specific player UUID | - -### 4.3 New Untradeable Attribute - -A new NBT-based item attribute that marks items as untradeable. - -**File: `src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java`** - -```java -package kamkeel.npcs.controllers.data.attribute; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; - -public class ItemTradeAttribute { - // New untradeable attribute - public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; - - // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) - public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; - public static final String TAG_SOULBIND = "cnpc_soulbind"; - - /** - * Check if item can be traded/auctioned - */ - public static boolean canTrade(ItemStack item, EntityPlayer player) { - if (item == null || !item.hasTagCompound()) { - return true; - } - NBTTagCompound tag = item.getTagCompound(); - - // Check untradeable flag - if (tag.getBoolean(TAG_UNTRADEABLE)) return false; - - // Check profile slot bound (item tied to specific profile slot) - if (tag.hasKey(TAG_PROFILE_SLOT)) return false; - - // Check soulbound (item tied to specific player) - if (tag.hasKey(TAG_SOULBIND)) return false; - - return true; - } - - /** - * Mark item as untradeable - */ - public static void setUntradeable(ItemStack item, boolean untradeable) { - if (item == null) return; - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (untradeable) { - item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); - } else { - item.getTagCompound().removeTag(TAG_UNTRADEABLE); - } - } - - public static boolean isUntradeable(ItemStack item) { - if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); - } - - public static boolean isProfileSlotBound(ItemStack item) { - if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); - } - - public static boolean isSoulbound(ItemStack item) { - if (item == null || !item.hasTagCompound()) return false; - return item.getTagCompound().hasKey(TAG_SOULBIND); - } - - /** - * Get reason why item cannot be traded (for UI display) - */ - public static String getTradeBlockReason(ItemStack item) { - if (item == null || !item.hasTagCompound()) return null; - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) return "gui.auction.untradeable"; - if (tag.hasKey(TAG_PROFILE_SLOT)) return "gui.auction.profileslotbound"; - if (tag.hasKey(TAG_SOULBIND)) return "gui.auction.soulbound"; - - return null; - } -} -``` - -### 4.4 Auction House Item Blacklist - -A configurable system to blacklist specific items from being auctioned. - -**File: `src/main/java/noppes/npcs/controllers/AuctionBlacklist.java`** - -```java -package noppes.npcs.controllers; - -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; - -import java.io.File; -import java.util.HashSet; -import java.util.Set; - -public class AuctionBlacklist { - private static AuctionBlacklist instance; - - // Blacklist by item registry name (e.g., "minecraft:diamond_sword") - private Set blacklistedItems = new HashSet<>(); - - // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) - private Set blacklistedMods = new HashSet<>(); - - // Blacklist by NBT tag presence (e.g., items with specific custom data) - private Set blacklistedNBTTags = new HashSet<>(); - - public static AuctionBlacklist getInstance() { - if (instance == null) { - instance = new AuctionBlacklist(); - instance.load(); - } - return instance; - } - - /** - * Check if item is blacklisted - */ - public boolean isBlacklisted(ItemStack item) { - if (item == null) return false; - - // Check item registry name - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null && blacklistedItems.contains(itemName)) { - return true; - } - - // Check mod ID - if (itemName != null) { - String modId = itemName.split(":")[0]; - if (blacklistedMods.contains(modId)) { - return true; - } - } - - // Check NBT tags - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (tag.hasKey(nbtTag)) { - return true; - } - } - } - - return false; - } - - // Blacklist management methods - public void addItem(String itemName) { - blacklistedItems.add(itemName); - save(); - } - - public void removeItem(String itemName) { - blacklistedItems.remove(itemName); - save(); - } - - public void addMod(String modId) { - blacklistedMods.add(modId); - save(); - } - - public void removeMod(String modId) { - blacklistedMods.remove(modId); - save(); - } - - public void addNBTTag(String tag) { - blacklistedNBTTags.add(tag); - save(); - } - - public void removeNBTTag(String tag) { - blacklistedNBTTags.remove(tag); - save(); - } - - public Set getBlacklistedItems() { - return new HashSet<>(blacklistedItems); - } - - public Set getBlacklistedMods() { - return new HashSet<>(blacklistedMods); - } - - public Set getBlacklistedNBTTags() { - return new HashSet<>(blacklistedNBTTags); - } - - // Persistence - public void save() { - // Save to auction_blacklist.dat - } - - public void load() { - // Load from auction_blacklist.dat - } -} -``` - -### 4.5 Integration with Auction Listing - -Modify `AuctionController.createListing()` to check restrictions: - -```java -public AuctionListing createListing(EntityPlayer seller, ItemStack item, ...) { - // Check item attribute restrictions - if (!ItemTradeAttribute.canTrade(item)) { - String reason = ItemTradeAttribute.getTradeBlockReason(item); - // Send message to player with reason - return null; - } - - // Check blacklist - if (AuctionBlacklist.getInstance().isBlacklisted(item)) { - // Send "This item cannot be auctioned" message - return null; - } - - // ... rest of listing creation -} -``` - ---- - -## Feature 5: Admin Auction Management - -### 5.1 Overview - -A comprehensive admin system for managing the auction house in real-time. - -### 5.2 Admin Permissions - -**File: `src/main/java/noppes/npcs/CustomNpcsPermissions.java`** - -Add new permissions: - -```java -// Auction Administration -public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); -public static final Permission AUCTION_MANAGE = new Permission("customnpcs.auction.manage"); -public static final Permission AUCTION_BLACKLIST = new Permission("customnpcs.auction.blacklist"); -``` - -### 5.3 Admin GUI - -**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionAdmin.java`** - -Features: -- **View All Listings**: See all active, expired, and cancelled listings -- **Search/Filter**: Find listings by seller, item, status -- **Cancel Any Listing**: Force-cancel listings (returns item to seller) -- **Modify Listing**: Edit prices, duration (with warning) -- **End Auction Early**: Force-end with current highest bidder winning -- **Refund Bidders**: Manually refund specific bidders -- **View Blacklist**: Manage item blacklist -- **Audit Log**: View recent auction actions - -```java -public class GuiAuctionAdmin extends GuiNPCInterface { - private List allListings; - private int currentPage = 0; - private EnumAuctionStatus filterStatus = null; - private String searchQuery = ""; - - // Admin action buttons - private static final int BTN_CANCEL = 10; - private static final int BTN_END_EARLY = 11; - private static final int BTN_EDIT = 12; - private static final int BTN_REFUND = 13; - private static final int BTN_BLACKLIST = 14; - private static final int BTN_AUDIT = 15; - - @Override - public void initGui() { - // Tab buttons: All | Active | Sold | Expired | Cancelled - // Search bar - // Listing table with columns: ID, Seller, Item, Price, Bids, Status, Time - // Action buttons for selected listing - // Blacklist management button - } - - @Override - protected void actionPerformed(GuiButton button) { - switch (button.id) { - case BTN_CANCEL: - adminCancelListing(selectedListing); - break; - case BTN_END_EARLY: - adminEndAuction(selectedListing); - break; - case BTN_EDIT: - openEditDialog(selectedListing); - break; - case BTN_REFUND: - openRefundDialog(selectedListing); - break; - case BTN_BLACKLIST: - openBlacklistGui(); - break; - case BTN_AUDIT: - openAuditLog(); - break; - } - } -} -``` - -### 5.4 Admin Actions in AuctionController - -```java -// Admin-only methods -public boolean adminCancelListing(EntityPlayer admin, int listingId, String reason) { - if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { - return false; - } - - AuctionListing listing = listings.get(listingId); - if (listing == null) return false; - - // Refund current bidder if any - if (listing.highBidderUUID != null) { - addClaim(listing.highBidderUUID, listing.highBidderName, - null, listing.currentBid, "Admin cancelled auction: " + reason); - } - - // Return item to seller - addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, - "Listing cancelled by admin: " + reason); - - listing.status = EnumAuctionStatus.CANCELLED; - - // Log action - logAdminAction(admin, "CANCEL", listingId, reason); - - return true; -} - -public boolean adminEndAuction(EntityPlayer admin, int listingId) { - if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { - return false; - } - - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { - return false; - } - - // End as if time expired - listing.expirationTime = System.currentTimeMillis(); - processExpiredAuctions(); - - // Log action - logAdminAction(admin, "END_EARLY", listingId, ""); - - return true; -} - -public boolean adminEditListing(EntityPlayer admin, int listingId, - double newStartPrice, double newBuyout, - long newExpiration) { - if (!CustomNpcsPermissions.hasPermission(admin, AUCTION_MANAGE)) { - return false; - } - - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.status != EnumAuctionStatus.ACTIVE) { - return false; - } - - // Only allow if no bids placed - if (listing.highBidderUUID != null) { - return false; // Cannot edit listing with bids - } - - listing.startingPrice = newStartPrice; - listing.currentBid = newStartPrice; - listing.buyoutPrice = newBuyout; - listing.expirationTime = newExpiration; - - // Log action - logAdminAction(admin, "EDIT", listingId, - String.format("price=%.2f,buyout=%.2f", newStartPrice, newBuyout)); - - return true; -} - -// Audit logging -private void logAdminAction(EntityPlayer admin, String action, int listingId, String details) { - // Log to file: timestamp, admin name, action, listing id, details - LogWriter.info(String.format("[AUCTION ADMIN] %s performed %s on listing %d: %s", - admin.getCommandSenderName(), action, listingId, details)); -} -``` - -### 5.5 Blacklist Management GUI - -**File: `src/main/java/noppes/npcs/client/gui/global/GuiAuctionBlacklist.java`** - -Features: -- Add item by holding it and clicking "Add" -- Add by typing item registry name -- Add entire mod by mod ID -- Add NBT tag pattern -- View current blacklist with remove buttons -- Test item against blacklist - -### 5.6 Access Points - -The Admin GUI can be accessed via: -1. **Global Menu**: New "Auction Management" button (requires permission) -2. **Auctioneer NPC**: Admin gear icon when editing NPC role (requires permission) -3. **Command**: `/cnpc auction admin` (requires OP or permission) +## Not Yet Implemented + +### Phase 1: Currency System (NOT STARTED) +Requires implementation: +- [ ] `VaultHelper.java` - VaultAPI integration via reflection +- [ ] `CurrencyController.java` - Currency management +- [ ] `PlayerCurrencyData.java` - Per-player currency (shared across profile slots) +- [ ] Vault dependency in build files +- [ ] Currency configuration GUI + +**Note:** Currently auctions use `long` for prices but the actual currency deduction/deposit is not connected to any economy system. + +### Phase 7: Admin Management (NOT STARTED) +Requires implementation: +- [ ] `GuiAuctionAdmin.java` - Admin auction management GUI +- [ ] `GuiAuctionBlacklist.java` - Blacklist management GUI +- [ ] Admin methods in `AuctionController.java` (adminCancelListing, adminEditListing, etc.) +- [ ] Audit logging for admin actions +- [ ] `/cnpc auction admin` command + +### Additional Missing Features +- [ ] Auctioneer NPC setup GUI (`GuiNpcAuctioneerSetup.java`) +- [ ] Trader stock reset timer banner display +- [ ] Comprehensive testing +- [ ] Player balance display (requires Currency System) --- ## Configuration Reference -All auction and market settings will be added to the **main configuration file** under dedicated categories. +All settings are in `ConfigMarket.java`: -### Location +```java +// Currency +public static String CurrencyName = "Gold"; -**File: `config/CustomNPCs/main.cfg`** (existing main config) +// Auction Durations (in hours) +public static int AuctionDurationShort = 12; +public static int AuctionDurationMedium = 24; +public static int AuctionDurationLong = 48; +public static int AuctionDurationVeryLong = 72; -New categories to add: -- `market` - General market/economy settings -- `market.currency` - Currency system settings -- `market.auction` - Auction house settings -- `market.trader` - Trader stock settings +// Auction Fees (per duration) +public static long AuctionFeeShort = 10; +public static long AuctionFeeMedium = 25; +public static long AuctionFeeLong = 50; +public static long AuctionFeeVeryLong = 100; -### Market Configuration (Main Category) +// Listing Configuration +public static int DefaultMaxListings = 5; +public static double ListingFeePercent = 0.0; +public static double SalesTaxPercent = 0.05; +public static double MinBidIncrementPercent = 0.05; +public static int ClaimExpirationDays = 30; +public static boolean RequireAuctioneerNPC = true; -```properties -# =========================================== -# Market & Economy Configuration -# =========================================== -market { - # Enable the market/economy system - B:Enabled=true - - # =========================================== - # Currency Settings - # =========================================== - currency { - # Use VaultAPI if available on Bukkit hybrid servers - B:UseVaultIfAvailable=true - - # Fallback currency item (registry name) when Vault unavailable - # Leave empty to disable fallback currency - S:FallbackCurrencyItem=customnpcs:npcCoinGold - - # Conversion ratio: 1 fallback item = X virtual currency - D:FallbackItemRatio=1.0 - } - - # =========================================== - # Auction House Settings - # =========================================== - auction { - # Enable/disable the auction house system - B:Enabled=true - - # Default maximum auction listings per player (without permission override) - I:DefaultMaxListings=5 - - # Listing fee as percentage of starting price (0.0 = no fee) - D:ListingFeePercent=0.0 - - # Sales tax as percentage taken from final sale price (0.05 = 5%) - D:SalesTaxPercent=0.05 - - # Require an Auctioneer NPC to access auction house - B:RequireAuctioneerNPC=true - - # Available auction durations in hours (comma-separated) - S:AvailableDurations=2,12,24,48 - - # Default auction duration in hours - I:DefaultDurationHours=24 - - # Claim expiration in days (claims auto-expire after this period, 0 = never) - I:ClaimExpirationDays=30 - - # Minimum bid increment as percentage (0.05 = 5% minimum increase) - D:MinBidIncrementPercent=0.05 - - # Snipe protection: extend auction TO this many minutes when bid placed near end - # Set to 0 to disable snipe protection - I:SnipeProtectionMinutes=5 - - # How many minutes before end triggers snipe protection - # (bids placed with less than this remaining will extend the auction) - I:SnipeProtectionThreshold=5 - } - - # =========================================== - # Trader Stock Settings - # =========================================== - trader { - # Enable stock system by default for new traders - B:EnableStockByDefault=false - - # Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY) - S:DefaultResetType=NONE - } -} +// Snipe Protection +public static int SnipeProtectionMinutes = 5; +public static int SnipeProtectionThreshold = 5; ``` -### Permission Nodes - -| Permission | Description | Default | -|------------|-------------|---------| -| `customnpcs.auction.use` | Access auction house | true | -| `customnpcs.auction.sell` | Create auction listings | true | -| `customnpcs.auction.buy` | Bid and buyout auctions | true | -| `customnpcs.auction.slots.X` | Have X maximum listings | - | -| `customnpcs.auction.admin` | Access admin management | op | -| `customnpcs.auction.manage` | Modify/cancel any listing | op | -| `customnpcs.auction.blacklist` | Manage item blacklist | op | -| `customnpcs.auction.bypass.fee` | Bypass listing fees | op | -| `customnpcs.auction.bypass.tax` | Bypass sales tax | op | - --- -## Implementation Order +## File Inventory -The features should be implemented in this order due to dependencies: +### New Files Created -### Phase 1: Currency System (Foundation) -1. Add VaultAPI dependency to build files -2. Create `VaultHelper.java` with reflection-based integration -3. Create `CurrencyController.java` -4. Add initialization hook in `CustomNpcs.java` -5. Create `PlayerCurrencyData.java` and integrate with `PlayerData` -6. Create currency configuration GUI -7. Test with and without Vault present +| File | Purpose | +|------|---------| +| `constants/EnumAuctionDuration.java` | Auction duration options | +| `constants/EnumAuctionSort.java` | Sort options with lang keys | +| `constants/EnumAuctionStatus.java` | Auction state enum | +| `constants/EnumStockReset.java` | Trader stock reset types | +| `controllers/AuctionController.java` | Main auction controller | +| `controllers/AuctionBlacklist.java` | Item blacklist system | +| `controllers/data/AuctionListing.java` | Auction listing data | +| `controllers/data/AuctionFilter.java` | Search/filter criteria | +| `controllers/data/AuctionClaim.java` | Claim data structure | +| `controllers/data/TraderStock.java` | Trader stock system | +| `controllers/data/attribute/ItemTradeAttribute.java` | Trade restriction attributes | +| `roles/RoleAuctioneer.java` | Auctioneer NPC role | +| `containers/ContainerAuction.java` | Auction GUI container | +| `client/gui/player/GuiAuction.java` | Main auction GUI | +| `client/gui/player/SubGuiAuctionCreate.java` | Create listing GUI | +| `client/gui/player/SubGuiAuctionBid.java` | Place bid GUI | +| `client/gui/player/SubGuiAuctionDetails.java` | Listing details GUI | +| `config/ConfigMarket.java` | Market configuration | +| `network/packets/player/AuctionActionPacket.java` | Auction action packet | +| `network/packets/data/AuctionDataPacket.java` | Auction data sync packet | -### Phase 2: Trader Enhancements -1. Create `EnumStockReset.java` -2. Create `TraderStock.java` -3. Modify `RoleTrader.java` to add stock and currency fields -4. Modify `ContainerNPCTrader.java` to handle stock/currency -5. Update `GuiNpcTraderSetup.java` with new options -6. Update network packets for stock synchronization -7. Test stock depletion and reset timers +### Files Deleted +| File | Reason | +|------|--------| +| ~~`constants/EnumAuctionCategory.java`~~ | Categories removed | +| ~~`controllers/AuctionLogger.java`~~ | Consolidated into AuctionController | +| ~~`controllers/AuctionNotificationController.java`~~ | Consolidated into AuctionController | -### Phase 3: Item Attributes & Restrictions -1. Create `ItemTradeAttribute.java` with Untradeable, SlotBound, SoulBound tags -2. Create `AuctionBlacklist.java` controller -3. Add API methods to scripting interface for setting item attributes -4. Create blacklist persistence (load/save to file) -5. Test attribute blocking in listing creation - -### Phase 4: Auction House -1. Create auction enums (`EnumAuctionDuration`, `EnumAuctionStatus`, `EnumAuctionCategory`) -2. Create `AuctionListing.java` and `AuctionClaim` classes -3. Create `AuctionController.java` with item restriction checks -4. Update `EnumRoleType.java` to add Auctioneer -5. Create `RoleAuctioneer.java` -6. Update `DataAdvanced.java` for role instantiation -7. Update `EnumGuiType.java` with auction GUIs -8. Create auction containers -9. Create auction GUI classes -10. Create network packets -11. Add periodic task for expired auctions -12. Create auctioneer setup GUI -13. Comprehensive testing - -### Phase 5: Admin Auction Management -1. Add auction admin permissions to `CustomNpcsPermissions.java` -2. Create `GuiAuctionAdmin.java` admin interface -3. Create `GuiAuctionBlacklist.java` blacklist management -4. Add admin methods to `AuctionController.java` -5. Add audit logging -6. Create admin network packets -7. Add command handler for `/cnpc auction admin` -8. Test admin operations - ---- - -## File Summary - -### New Files to Create - -| File | Description | -|------|-------------| -| `kamkeel/npcs/compat/VaultHelper.java` | VaultAPI integration | -| `kamkeel/npcs/controllers/CurrencyController.java` | Currency management | -| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data (NOT slot-bound) | -| `noppes/npcs/controllers/data/TraderStock.java` | Trader stock configuration | -| `noppes/npcs/constants/EnumStockReset.java` | Stock reset types | -| `kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java` | Untradeable/SlotBound/SoulBound item attributes | -| `noppes/npcs/controllers/AuctionBlacklist.java` | Auction item blacklist controller | -| `noppes/npcs/controllers/AuctionController.java` | Auction house management | -| `noppes/npcs/controllers/data/AuctionListing.java` | Auction listing data | -| `noppes/npcs/constants/EnumAuctionDuration.java` | Auction duration options | -| `noppes/npcs/constants/EnumAuctionStatus.java` | Auction status states | -| `noppes/npcs/constants/EnumAuctionCategory.java` | Item categories | -| `noppes/npcs/roles/RoleAuctioneer.java` | Auctioneer NPC role | -| `noppes/npcs/containers/ContainerAuction*.java` | Auction containers | -| `noppes/npcs/client/gui/player/GuiAuction*.java` | Auction GUIs | -| `noppes/npcs/client/gui/roles/GuiNpcAuctioneerSetup.java` | Auctioneer config | -| `noppes/npcs/client/gui/global/GuiAuctionAdmin.java` | Admin auction management | -| `noppes/npcs/client/gui/global/GuiAuctionBlacklist.java` | Blacklist management GUI | -| `kamkeel/npcs/network/packets/player/AuctionActionPacket.java` | Auction actions | -| `kamkeel/npcs/network/packets/data/AuctionSyncPacket.java` | Auction data sync | -| `kamkeel/npcs/network/packets/admin/AuctionAdminPacket.java` | Admin auction actions | - -### Files to Modify +### Files Modified | File | Changes | |------|---------| -| `dependencies.gradle` | Add VaultAPI dependency | -| `repositories.gradle` | Add Vault repository | -| `CustomNpcs.java` | Initialize VaultHelper, AuctionController, AuctionBlacklist | -| `CustomNpcsPermissions.java` | Add auction admin permissions | -| `EnumRoleType.java` | Add Auctioneer | -| `EnumGuiType.java` | Add auction GUI types, admin GUIs | -| `DataAdvanced.java` | Handle Auctioneer role | -| `RoleTrader.java` | Add stock and currency support | -| `ContainerNPCTrader.java` | Handle stock/currency transactions | -| `GuiNpcTrader.java` | Add stock reset timer banner above GUI | -| `GuiNpcTraderSetup.java` | Add stock/currency UI | -| `PlayerData.java` | Add PlayerCurrencyData (NOT in slot components - shared across slots) | -| `CommonProxy.java` | Register auction containers | -| `ServerTickHandler.java` | Add auction expiration check | -| `PacketHandler.java` | Register auction packets and admin packets | -| `CommandKamkeel.java` | Add `/cnpc auction admin` command | +| `CustomNpcs.java` | Initialize AuctionController, AuctionBlacklist | +| `ServerTickHandler.java` | Call AuctionController.onPlayerLogin() | +| `constants/EnumRoleType.java` | Added Auctioneer | +| `constants/EnumGuiType.java` | Added auction GUI types | +| `DataAdvanced.java` | Handle Auctioneer role creation | +| `roles/RoleTrader.java` | Added stock system | +| `containers/ContainerNPCTrader.java` | Stock handling | +| `CustomNpcsPermissions.java` | Auction permission nodes | +| `api/ICustomNpc.java` | Auction API methods | +| `PacketHandler.java` | Register auction packets | +| `en_US.lang` | All auction lang keys | --- -## Testing Strategy +## Localization Keys Added -### Unit Testing -- Currency operations (deposit, withdraw, balance) -- Currency shared across profile slots (not slot-bound) -- Stock depletion and reset calculations -- Auction bid validation -- Auction expiration handling -- Item attribute checks (Untradeable, SlotBound, SoulBound) -- Blacklist matching (by item, mod, NBT tag) +```properties +# Main GUI +auction.title=Auction House +auction.browse=Browse +auction.myListings=My Listings +auction.myBids=My Bids +auction.claims=Claims +auction.sell=Sell +auction.search=Search +auction.refresh=Refresh +auction.viewDetails=View Details +auction.placeBid=Place Bid +auction.buyout=Buyout +auction.cancel=Cancel +auction.prevPage=< Prev +auction.nextPage=Next > +auction.noResults=No results +auction.page=Page %d of %d +auction.balance=Balance: %s +auction.listings=%d listings -### Integration Testing -- VaultAPI integration on hybrid servers -- Fallback to item currency when Vault unavailable -- Stock persistence across server restarts -- Auction persistence and recovery -- Profile slot switching with shared currency -- Outbid refunds going to claims correctly -- Admin actions affecting live auctions +# Tab Titles +auction.browseTitle=Browse Auctions +auction.myListingsTitle=My Listings +auction.myBidsTitle=My Active Bids +auction.claimsTitle=Claim Items +auction.sellTitle=Create Listing -### User Acceptance Testing -- Complete trading flow with stock limits -- Complete auction flow (list → bid → win → claim) -- Currency display formatting with gold coin icon -- Stock reset timer banner display above trader GUI -- GUI usability -- Admin management workflow +# Listing Details +auction.item=Item: +auction.seller=Seller: +auction.startingPrice=Starting Price: +auction.buyoutPrice=Buyout Price: +auction.currentPrice=Current Price: +auction.currentBid=Current Bid: +auction.minimumBid=Minimum Bid: +auction.highBidder=High Bidder: +auction.totalBids=Total Bids: +auction.quantity=Quantity: +auction.timeLeft=Time Left: +auction.yourBid=Your Bid: +auction.yourBalance=Your Balance: +auction.ended=Ended +auction.noBidsYet=No bids yet -### Edge Cases -- Negative currency amounts -- Concurrent bids on same auction -- Server crash during transaction -- Player offline when auction ends (claim on next login) -- Maximum listings per player (permission-based) -- Inventory full when claiming -- Trying to auction Untradeable/SlotBound/SoulBound items -- Trying to auction blacklisted items -- Admin cancelling auction with active bids -- Profile slot switch while having pending claims +# Create Listing +auction.createListing=Create Listing +auction.placeItemBelow=Place item in slot below: +auction.noItemSelected=(No item selected) +auction.noBuyoutHint=(0 = no buyout) +auction.duration=Duration: +auction.listingFee=Listing Fee: +auction.create=Create ---- +# Bid GUI +auction.minBid=Min Bid +auction.bidPlus10=+10% +auction.bidPlus25=+25% +auction.insufficientFunds=Insufficient funds! +auction.auctionEnded=Auction has ended! +auction.auctionDetails=Auction Details -## API/Scripting Extensions +# Sort Options +auction.sort.newest=Newest First +auction.sort.oldest=Oldest First +auction.sort.priceHigh=Most Expensive +auction.sort.priceLow=Least Expensive +auction.sort.endingSoon=Ending Soon +auction.sort.mostTime=Most Time Left +auction.sort.nameAZ=Name: A to Z +auction.sort.nameZA=Name: Z to A +auction.sort.mostBids=Most Bids +auction.sort.leastBids=Least Bids -Consider adding these scripting hooks: - -```java -// ICurrency interface for scripts -double getBalance(IPlayer player); -boolean withdraw(IPlayer player, double amount); -boolean deposit(IPlayer player, double amount); -String format(double amount); - -// IItemStack extensions for trade attributes -boolean isUntradeable(); -void setUntradeable(boolean value); -boolean isSlotBound(); -void setSlotBound(boolean value); -boolean isSoulBound(); -void setSoulBound(boolean value); -boolean canTrade(); // Returns false if any blocking attribute is set -String getTradeBlockReason(); // Returns localization key for reason - -// IAuction interface for scripts -IAuctionListing createListing(IPlayer seller, IItemStack item, - double startPrice, double buyout, int durationHours); -boolean cancelListing(int listingId); -List getPlayerListings(IPlayer player); -boolean isItemBlacklisted(IItemStack item); - -// IAuctionBlacklist interface for scripts (admin) -void addItemToBlacklist(String registryName); -void removeItemFromBlacklist(String registryName); -void addModToBlacklist(String modId); -void removeModFromBlacklist(String modId); -boolean isBlacklisted(IItemStack item); - -// Events -onAuctionCreated(IAuctionListing listing); -onAuctionBid(IAuctionListing listing, IPlayer bidder, double amount); -onAuctionOutbid(IAuctionListing listing, IPlayer previousBidder, double refundAmount); -onAuctionSold(IAuctionListing listing, IPlayer buyer, double price); -onAuctionExpired(IAuctionListing listing); -onAuctionCancelled(IAuctionListing listing, boolean byAdmin); -onTraderPurchase(ITrader trader, IPlayer buyer, int slot, int stockRemaining); -onTraderStockReset(ITrader trader); -onCurrencyTransaction(IPlayer player, double amount, boolean isDeposit); +# Duration Options +auction.duration.short=Short (12h) +auction.duration.medium=Medium (24h) +auction.duration.long=Long (48h) +auction.duration.veryLong=Very Long (72h) ``` --- -## Conclusion +## Design Decisions (Confirmed) -This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The five features are designed to work together seamlessly: +1. **Outbid Refunds** → Go to claims (not direct to balance) +2. **Currency** → Shared across all profile slots per player (NOT slot-bound) +3. **Permissions** → Uses existing CustomNPC+ permission pattern (default: 5 slots) +4. **Categories** → Removed for initial implementation (can add later) +5. **Controller Architecture** → Single consolidated AuctionController +6. **Localization** → All client strings use StatCollector with lang keys +7. **StatCollector Usage** → Safe - only called from client-side GUI code +8. **Claim Expiration** → 30 days default (configurable) +9. **Snipe Protection** → Extend TO 5 minutes when bid placed near end +10. **Logging** → Inline in AuctionController using LogWriter -1. **Currency System** provides the foundation for monetary transactions (shared across profile slots) -2. **Trader Enhancements** modernize the existing trader with stock, currency support, and reset timer banners -3. **Item Attributes & Restrictions** control which items can be traded via Untradeable, ProfileSlot Bound, and Soulbound attributes plus blacklists -4. **Auction House** creates a global marketplace with claims-based refunds, bid modification, and permission-based slot limits -5. **Admin Management** provides comprehensive tools for server operators to manage the auction house in real-time +--- -The implementation follows existing codebase patterns (permission system from Profiles, gold coin from CustomItems, existing requirement types) and can be developed incrementally, with each phase building on the previous one. +## Next Steps -### Key Design Decisions Summary -- Outbid refunds → Claims (not direct to balance) with **Modify Bid** feature -- Currency → Shared across all profile slots per player -- Permissions → Uses existing CustomNPC+ permission pattern (default: 5 slots) -- Display icon → Existing `CustomItems.coinGold` -- Stock reset timer → Banner displayed ABOVE the trader GUI -- Trade restrictions → New Untradeable + existing ProfileSlotRequirement/SoulbindRequirement + Blacklist -- Claim expiration → 30 days default (configurable) -- Bid increment → Percentage-based minimum (configurable, e.g., 5%) -- Snipe protection → Extend TO 5 minutes when bid placed near end (configurable) -- Auction durations → 2h, 12h, 24h, 48h (configurable) -- Admin tools → Full management GUI with audit logging +1. **Currency System** - Implement VaultHelper and CurrencyController to connect actual economy +2. **Admin Management** - Implement admin GUI and commands +3. **Testing** - Comprehensive testing of all auction flows +4. **Auctioneer Setup GUI** - Create NPC configuration interface +5. **Trader Banner** - Add stock reset timer display above trader GUI From d657bab73c707590122253f58a8dce73a0e48c15 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 15:21:11 -0500 Subject: [PATCH 15/64] Harden auction display and trader stock init --- .../packets/player/AuctionActionPacket.java | 27 ++++++++--- .../npcs/client/gui/player/GuiAuction.java | 32 +++++++++---- .../client/gui/player/SubGuiAuctionBid.java | 21 +++++++-- .../gui/player/SubGuiAuctionCreate.java | 18 +++++-- .../gui/player/SubGuiAuctionDetails.java | 6 +++ .../java/noppes/npcs/config/ConfigMarket.java | 12 ++++- .../npcs/controllers/AuctionController.java | 47 +++++++++++++++---- .../noppes/npcs/controllers/VaultHelper.java | 2 +- .../npcs/controllers/data/AuctionFilter.java | 11 ++++- .../npcs/controllers/data/TraderStock.java | 6 +++ .../noppes/npcs/roles/RoleAuctioneer.java | 7 +-- 11 files changed, 148 insertions(+), 41 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index 54bfaef50..a63312d80 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -17,6 +17,7 @@ import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.containers.ContainerAuction; import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.data.AuctionFilter; @@ -290,10 +291,23 @@ public class AuctionActionPacket extends AbstractPacket { try { NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); - ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); + ItemStack requestedItem = ItemStack.loadItemStackFromNBT(itemNBT); - if (item == null) { - sendResponse(player, false, "Invalid item"); + if (!(player.openContainer instanceof ContainerAuction)) { + sendResponse(player, false, "Open the auction listing screen to create a listing"); + return; + } + + ContainerAuction container = (ContainerAuction) player.openContainer; + ItemStack slotItem = container.getListingItem(); + + if (requestedItem == null || slotItem == null) { + sendResponse(player, false, "Place an item in the listing slot"); + return; + } + + if (!ItemStack.areItemStacksEqual(slotItem, requestedItem) || slotItem.stackSize != requestedItem.stackSize) { + sendResponse(player, false, "Listing item mismatch"); return; } @@ -318,11 +332,10 @@ public class AuctionActionPacket extends AbstractPacket { // Create the listing AuctionListing listing = AuctionController.Instance.createListing( - player, item, startingPrice, buyoutPrice, duration); + player, slotItem, startingPrice, buyoutPrice, duration); if (listing != null) { - // Remove item from player's inventory - // Note: The GUI should have already collected the item, this is validation + container.clearListingSlot(); sendResponse(player, true, "Listing created successfully"); } else { sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); @@ -429,7 +442,7 @@ public class AuctionActionPacket extends AbstractPacket { return; } - if (AuctionController.Instance.cancelListing(listingId, player)) { + if (AuctionController.Instance.cancelListing(listingId, player, isAdmin)) { sendResponse(player, true, "Listing cancelled. Claim your item."); } else { sendResponse(player, false, "Failed to cancel listing"); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 118dde167..507368c7d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -215,6 +215,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private void viewDetails() { if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { AuctionListing listing = listings.get(selectedListingIndex); + if (listing.item == null) { + return; + } setSubGui(new SubGuiAuctionDetails(this, listing)); } } @@ -222,6 +225,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private void placeBid() { if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { AuctionListing listing = listings.get(selectedListingIndex); + if (listing.item == null) { + return; + } setSubGui(new SubGuiAuctionBid(this, listing, playerBalance)); } } @@ -261,7 +267,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, String line = formatListingLine(listing); displayList.add(line); } - listingScroll.setList(displayList); + listingScroll.setList(displayList, true, false); if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { listingScroll.selected = selectedListingIndex; @@ -269,7 +275,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } private String formatListingLine(AuctionListing listing) { - String itemName = listing.item.getDisplayName(); + String itemName = listing.item == null + ? "Unknown" + : listing.item.getDisplayName(); if (itemName.length() > 15) { itemName = itemName.substring(0, 12) + "..."; } @@ -324,12 +332,17 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Action buttons based on selection boolean hasSelection = selectedListingIndex >= 0 && selectedListingIndex < listings.size(); + if (hasSelection && listings.get(selectedListingIndex).item == null) { + hasSelection = false; + } if (getButton(50) != null) getButton(50).setEnabled(hasSelection); if (getButton(51) != null) getButton(51).setEnabled(hasSelection); if (hasSelection && getButton(52) != null) { AuctionListing listing = listings.get(selectedListingIndex); - getButton(52).setEnabled(listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice); + getButton(52).setEnabled(listing.item != null && + listing.buyoutPrice > 0 && + playerBalance >= listing.buyoutPrice); } else if (getButton(52) != null) { getButton(52).setEnabled(false); } @@ -392,18 +405,17 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // ICustomScrollListener @Override - public void scrollClicked(int scrollId, int index, boolean doubleClick, String selection) { + public void customScrollClicked(int mouseX, int mouseY, int mouseButton, GuiCustomScroll scroll) { + int index = scroll.selected; selectedListingIndex = index; updateButtonStates(); - - if (doubleClick && selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - viewDetails(); - } } @Override - public void scrollDoubleClicked(String selection, GuiCustomScroll scroll) { - // Handled above + public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { + if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { + viewDetails(); + } } // IGuiData diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index 7f4665b40..fbf62fc1d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -35,6 +35,12 @@ public class SubGuiAuctionBid extends SubGuiInterface { int y = guiTop + 10; int labelColor = 0x404040; + if (listing.item == null) { + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 40, y, 0xAA0000)); + addButton(new GuiNpcButton(31, guiLeft + 60, guiTop + 110, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + return; + } + // Title addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 70, y, labelColor)); y += 18; @@ -70,8 +76,9 @@ public class SubGuiAuctionBid extends SubGuiInterface { // Bid input addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); - bidField.setNumbersOnly(); - bidField.setMinMaxDefault(minBid, 999999999, minBid); + bidField.setIntegersOnly(); + int minBidInt = (int) Math.min(Integer.MAX_VALUE, Math.max(1L, minBid)); + bidField.setMinMaxDefault(minBidInt, Integer.MAX_VALUE, minBidInt); addTextField(bidField); y += 24; @@ -179,7 +186,7 @@ public class SubGuiAuctionBid extends SubGuiInterface { } private void placeBid() { - long bidAmount = bidField.getInteger(); + long bidAmount = parseBid(bidField); long minBid = listing.getMinimumBid(); if (bidAmount < minBid) { @@ -199,6 +206,14 @@ public class SubGuiAuctionBid extends SubGuiInterface { close(); } + private long parseBid(GuiNpcTextField field) { + try { + return Long.parseLong(field.getText()); + } catch (NumberFormatException e) { + return 0L; + } + } + @Override public void save() { // Nothing to save diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index 75329f5e8..d95dc229a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -51,14 +51,14 @@ public class SubGuiAuctionCreate extends SubGuiInterface { // Starting price addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 70, 0x404040)); startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); - startingPriceField.setNumbersOnly(); + startingPriceField.setIntegersOnly(); startingPriceField.setMinMaxDefault(1, 999999999, 100); addTextField(startingPriceField); // Buyout price (optional) addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 90, 0x404040)); buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); - buyoutPriceField.setNumbersOnly(); + buyoutPriceField.setIntegersOnly(); buyoutPriceField.setMinMaxDefault(0, 999999999, 0); addTextField(buyoutPriceField); addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 185, guiTop + 90, 0x888888)); @@ -87,7 +87,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { private long getListingFee() { EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getFee(); + return duration.getListingFee(); } @Override @@ -113,8 +113,8 @@ public class SubGuiAuctionCreate extends SubGuiInterface { return; } - long startingPrice = startingPriceField.getInteger(); - long buyoutPrice = buyoutPriceField.getInteger(); + long startingPrice = parsePrice(startingPriceField, 1); + long buyoutPrice = parsePrice(buyoutPriceField, 0); if (startingPrice < 1) { startingPrice = 1; @@ -135,6 +135,14 @@ public class SubGuiAuctionCreate extends SubGuiInterface { close(); } + private long parsePrice(GuiNpcTextField field, long defaultValue) { + try { + return Long.parseLong(field.getText()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + @Override public void updateScreen() { super.updateScreen(); diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java index f18046922..4054d04b1 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -34,6 +34,12 @@ public class SubGuiAuctionDetails extends SubGuiInterface { int labelColor = 0x404040; int valueColor = 0x000000; + if (listing.item == null) { + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); + addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); + return; + } + // Title addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 80, y, labelColor)); y += 16; diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 81eda2c3f..f0f019290 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -95,8 +95,16 @@ public class ConfigMarket { StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, "Starting balance for new players (only used when Vault is not available)").getInt(1000); - MaxBalance = config.get(CURRENCY, "Maximum Balance", 999999999999L, - "Maximum balance a player can have (only used when Vault is not available)").getLong(999999999999L); + Property maxBalanceProp = config.get(CURRENCY, "Maximum Balance", String.valueOf(MaxBalance), + "Maximum balance a player can have (only used when Vault is not available)"); + try { + MaxBalance = Long.parseLong(maxBalanceProp.getString()); + } catch (NumberFormatException e) { + MaxBalance = 999999999999L; + } + if (MaxBalance < 0) { + MaxBalance = 0; + } // ========================================= // Auction Settings diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index b6bb41d10..8333fd01a 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -132,6 +132,7 @@ public class AuctionController { public List searchListings(String query) { String lowerQuery = query.toLowerCase(); return getActiveListings().stream() + .filter(l -> l.item != null) .filter(l -> l.item.getDisplayName().toLowerCase().contains(lowerQuery)) .collect(Collectors.toList()); } @@ -157,8 +158,14 @@ public class AuctionController { // Calculate pagination int totalResults = results.size(); - int totalPages = Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); - int startIndex = filter.page * filter.pageSize; + int totalPages = totalResults == 0 + ? 0 + : Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); + int currentPage = Math.max(0, filter.page); + if (totalPages > 0 && currentPage >= totalPages) { + currentPage = totalPages - 1; + } + int startIndex = currentPage * filter.pageSize; int endIndex = Math.min(startIndex + filter.pageSize, totalResults); // Get the page slice @@ -169,7 +176,7 @@ public class AuctionController { pageResults = new ArrayList<>(results.subList(startIndex, endIndex)); } - return new FilterResult(pageResults, totalResults, filter.page, totalPages); + return new FilterResult(pageResults, totalResults, currentPage, totalPages); } /** @@ -204,11 +211,11 @@ public class AuctionController { comparator = Comparator.comparingLong(l -> -l.expirationTime); break; case NAME_A_TO_Z: - comparator = Comparator.comparing(l -> l.item.getDisplayName().toLowerCase()); + comparator = Comparator.comparing(l -> getListingName(l).toLowerCase()); break; case NAME_Z_TO_A: comparator = Comparator.comparing((AuctionListing l) -> - l.item.getDisplayName().toLowerCase()).reversed(); + getListingName(l).toLowerCase()).reversed(); break; case MOST_BIDS: comparator = Comparator.comparingInt(l -> -l.bidCount); @@ -287,6 +294,7 @@ public class AuctionController { return ids.stream() .map(listings::get) .filter(Objects::nonNull) + .filter(AuctionListing::isActive) .filter(l -> bidderUUID.equals(l.highBidderUUID)) .collect(Collectors.toList()); } @@ -433,17 +441,33 @@ public class AuctionController { /** * Cancel an auction listing */ - public boolean cancelListing(int listingId, EntityPlayer requester) { + public boolean cancelListing(int listingId, EntityPlayer requester, boolean forceCancel) { AuctionListing listing = listings.get(listingId); if (listing == null) { return false; } boolean byAdmin = !listing.sellerUUID.equals(requester.getUniqueID()); - - if (!listing.cancel(requester.getUniqueID())) { + if (byAdmin && !forceCancel) { return false; } + if (!forceCancel && listing.hasBids()) { + return false; + } + if (listing.status != EnumAuctionStatus.ACTIVE) { + return false; + } + + if (forceCancel && listing.hasBids() && listing.highBidderUUID != null) { + CurrencyController.Instance.addClaim(listing.highBidderUUID, listing.currentBid, + "Auction Cancelled", String.valueOf(listingId)); + notifyPlayer(listing.highBidderUUID, EnumChatFormatting.RED + + "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + + EnumChatFormatting.RED + " was cancelled. Your " + + formatCurrency(listing.currentBid) + " has been refunded."); + } + + listing.status = EnumAuctionStatus.CANCELLED; save(); @@ -718,6 +742,13 @@ public class AuctionController { LogWriter.info(LOG_PREFIX + event + ": " + details); } + private String getListingName(AuctionListing listing) { + if (listing == null || listing.item == null) { + return ""; + } + return listing.item.getDisplayName(); + } + // ==================== Persistence ==================== public void save() { diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java index f660002fc..54b9235ef 100644 --- a/src/main/java/noppes/npcs/controllers/VaultHelper.java +++ b/src/main/java/noppes/npcs/controllers/VaultHelper.java @@ -4,7 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.CustomNpcs; import noppes.npcs.config.ConfigMarket; import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.controllers.data.PlayerDataController; +import noppes.npcs.controllers.PlayerDataController; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java index 70242179a..176896fae 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -107,6 +107,9 @@ public class AuctionFilter { if (listing == null) { return false; } + if (listing.item == null) { + return false; + } // Search text filter (case-insensitive) if (!searchText.isEmpty()) { @@ -199,9 +202,13 @@ public class AuctionFilter { maxPrice = compound.getLong("MaxPrice"); buyoutOnly = compound.getBoolean("BuyoutOnly"); noBidsOnly = compound.getBoolean("NoBidsOnly"); - page = compound.getInteger("Page"); + page = Math.max(0, compound.getInteger("Page")); pageSize = compound.getInteger("PageSize"); - if (pageSize <= 0) pageSize = 20; + if (pageSize <= 0) { + pageSize = 20; + } else { + pageSize = Math.min(100, pageSize); + } sellerUUID = compound.getString("SellerUUID"); sellerName = compound.getString("SellerName"); } diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index 808b06f36..f3c5f4db9 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -58,6 +58,9 @@ public class TraderStock { return pStock.getStock(slot, maxStock[slot]); } + if (currentStock[slot] < 0) { + return maxStock[slot]; + } return Math.max(0, currentStock[slot]); } @@ -83,6 +86,9 @@ public class TraderStock { return pStock.consumeStock(slot, maxStock[slot], amount); } + if (currentStock[slot] < 0) { + currentStock[slot] = maxStock[slot]; + } if (currentStock[slot] >= amount) { currentStock[slot] -= amount; return true; diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java index 0c0f53d07..4d8febaf1 100644 --- a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java +++ b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java @@ -6,6 +6,7 @@ import noppes.npcs.NoppesUtilServer; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.Line; import noppes.npcs.entity.EntityNPCInterface; /** @@ -69,9 +70,9 @@ public class RoleAuctioneer extends RoleInterface { // Check if auction system is enabled if (!ConfigMarket.AuctionEnabled) { if (!noAuctionMessage.isEmpty()) { - npc.say(player, noAuctionMessage); + npc.say(player, new Line(noAuctionMessage)); } else { - npc.say(player, "The auction house is currently closed."); + npc.say(player, new Line("The auction house is currently closed.")); } return; } @@ -83,7 +84,7 @@ public class RoleAuctioneer extends RoleInterface { // Say welcome message if (!welcomeMessage.isEmpty()) { - npc.say(player, welcomeMessage); + npc.say(player, new Line(welcomeMessage)); } else { npc.say(player, npc.advanced.getInteractLine()); } From e7f6177be89084345123db0a5d1810fc072c8c9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 20:36:40 +0000 Subject: [PATCH 16/64] Add Auctioneer role to NPC GUI and connect auction system - Add Auctioneer to role dropdown in GuiNpcAdvanced - Create GuiNpcAuctioneer setup GUI for configuring: - Tab visibility (browse, my listings, my bids, claims) - Player actions (create, bid, buyout) - Custom welcome/disabled messages - Add PlayerAuction container handler in CommonProxy - Add PlayerAuction GUI handler in ClientProxy - Add all role.auctioneer lang keys to en_US.lang --- src/main/java/noppes/npcs/CommonProxy.java | 4 + .../java/noppes/npcs/client/ClientProxy.java | 5 + .../client/gui/mainmenu/GuiNpcAdvanced.java | 5 +- .../client/gui/roles/GuiNpcAuctioneer.java | 144 ++++++++++++++++++ .../assets/customnpcs/lang/en_US.lang | 13 ++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index 58f931ab8..a73c179e4 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -15,6 +15,7 @@ import noppes.npcs.api.IWorld; import noppes.npcs.blocks.tiles.TileNpcContainer; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; +import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -131,6 +132,9 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.CompanionInv) return new ContainerNPCCompanion(npc, player); + if (gui == EnumGuiType.PlayerAuction) + return new ContainerAuction(npc, player); + return null; } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 7cc3d7b76..cbe2d3da7 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -104,6 +104,7 @@ import noppes.npcs.client.gui.player.GuiCrate; import noppes.npcs.client.gui.player.GuiMailbox; import noppes.npcs.client.gui.player.GuiMailmanWrite; import noppes.npcs.client.gui.player.GuiNPCBankChest; +import noppes.npcs.client.gui.player.GuiAuction; import noppes.npcs.client.gui.player.GuiNPCTrader; import noppes.npcs.client.gui.player.GuiNpcAnvil; import noppes.npcs.client.gui.player.GuiNpcCarpentryBench; @@ -177,6 +178,7 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.config.StringCache; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; +import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -568,6 +570,9 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.MagicBook) return new GuiNpcMagicBook(); + else if (gui == EnumGuiType.PlayerAuction) + return new GuiAuction(npc, (ContainerAuction) container); + else if (gui == EnumGuiType.GlobalRemote) return new GuiNPCGlobalMainMenu(null); diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 6452145a6..8c6248e40 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -25,6 +25,7 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; import noppes.npcs.client.gui.roles.GuiNpcSpawner; +import noppes.npcs.client.gui.roles.GuiNpcAuctioneer; import noppes.npcs.client.gui.roles.GuiNpcMount; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; @@ -48,7 +49,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { super.initGui(); int y = guiTop + 10; this.addButton(new GuiNpcButton(3, guiLeft + 85 + 160, y, 52, 20, "selectServer.edit")); - this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount"}, npc.advanced.role.ordinal())); + this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount", "role.auctioneer"}, npc.advanced.role.ordinal())); getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); this.addButton(new GuiNpcButton(4, guiLeft + 85 + 160, y += 22, 52, 20, "selectServer.edit")); @@ -151,6 +152,8 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { displayGuiScreen(new GuiNpcCompanion(npc)); else if (npc.advanced.role == EnumRoleType.Mount) displayGuiScreen(new GuiNpcMount(npc)); + else if (npc.advanced.role == EnumRoleType.Auctioneer) + displayGuiScreen(new GuiNpcAuctioneer(npc)); } else if (compound.hasKey("JobData")) { if (npc.jobInterface != null) npc.jobInterface.readFromNBT(compound); diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java new file mode 100644 index 000000000..d5fb8e962 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java @@ -0,0 +1,144 @@ +package noppes.npcs.client.gui.roles; + +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.role.RoleSavePacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.gui.util.GuiNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.roles.RoleAuctioneer; + +public class GuiNpcAuctioneer extends GuiNPCInterface2 implements ITextfieldListener { + + private final RoleAuctioneer role; + + public GuiNpcAuctioneer(EntityNPCInterface npc) { + super(npc); + this.role = npc.roleInterface instanceof RoleAuctioneer ? (RoleAuctioneer) npc.roleInterface : null; + } + + @Override + public void initGui() { + super.initGui(); + if (role == null) { + close(); + return; + } + + int y = guiTop + 10; + + // Left column - Tab visibility + addLabel(new GuiNpcLabel(0, "role.auctioneer.tabs", guiLeft + 10, y, 0x404040)); + y += 18; + + addLabel(new GuiNpcLabel(1, "role.auctioneer.showBrowse", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(1, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showActiveListings ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(2, "role.auctioneer.showMyListings", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(2, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyListings ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(3, "role.auctioneer.showMyBids", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(3, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyBids ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(4, "role.auctioneer.showClaims", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(4, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showClaims ? 1 : 0)); + y += 30; + + // Actions + addLabel(new GuiNpcLabel(10, "role.auctioneer.actions", guiLeft + 10, y, 0x404040)); + y += 18; + + addLabel(new GuiNpcLabel(11, "role.auctioneer.allowCreate", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(11, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowCreatingListings ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(12, "role.auctioneer.allowBid", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(12, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBidding ? 1 : 0)); + y += 22; + + addLabel(new GuiNpcLabel(13, "role.auctioneer.allowBuyout", guiLeft + 10, y + 5, 0x404040)); + addButton(new GuiNpcButton(13, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBuyout ? 1 : 0)); + + // Right column - Messages + int rightX = guiLeft + 210; + int rightY = guiTop + 10; + + addLabel(new GuiNpcLabel(20, "role.auctioneer.messages", rightX, rightY, 0x404040)); + rightY += 18; + + addLabel(new GuiNpcLabel(21, "role.auctioneer.welcome", rightX, rightY + 5, 0x404040)); + rightY += 18; + GuiNpcTextField welcomeField = new GuiNpcTextField(21, this, rightX, rightY, 180, 20, role.welcomeMessage); + welcomeField.setMaxStringLength(200); + addTextField(welcomeField); + rightY += 28; + + addLabel(new GuiNpcLabel(22, "role.auctioneer.noAuction", rightX, rightY + 5, 0x404040)); + rightY += 18; + GuiNpcTextField noAuctionField = new GuiNpcTextField(22, this, rightX, rightY, 180, 20, role.noAuctionMessage); + noAuctionField.setMaxStringLength(200); + addTextField(noAuctionField); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (role == null) return; + + if (guibutton instanceof GuiNpcButton) { + GuiNpcButton button = (GuiNpcButton) guibutton; + boolean value = button.getValue() == 1; + + switch (button.id) { + case 1: + role.showActiveListings = value; + break; + case 2: + role.showMyListings = value; + break; + case 3: + role.showMyBids = value; + break; + case 4: + role.showClaims = value; + break; + case 11: + role.allowCreatingListings = value; + break; + case 12: + role.allowBidding = value; + break; + case 13: + role.allowBuyout = value; + break; + } + } + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (role == null) return; + + switch (textfield.id) { + case 21: + role.welcomeMessage = textfield.getText(); + break; + case 22: + role.noAuctionMessage = textfield.getText(); + break; + } + } + + @Override + public void save() { + if (role != null) { + PacketClient.sendClient(new RoleSavePacket(role.writeToNBT(new NBTTagCompound()))); + } + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 88c10066c..efec9bd2f 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -429,6 +429,19 @@ role.mount.hover=Hover role.mount.ascendspeed=Ascend Speed role.mount.descendspeed=Descend Speed role.mount.walkspeed=Walk Speed: %s +role.auctioneer=Auctioneer +role.auctioneer.tabs=Tab Visibility +role.auctioneer.showBrowse=Show Browse +role.auctioneer.showMyListings=Show My Listings +role.auctioneer.showMyBids=Show My Bids +role.auctioneer.showClaims=Show Claims +role.auctioneer.actions=Player Actions +role.auctioneer.allowCreate=Allow Create +role.auctioneer.allowBid=Allow Bid +role.auctioneer.allowBuyout=Allow Buyout +role.auctioneer.messages=Messages +role.auctioneer.welcome=Welcome Message +role.auctioneer.noAuction=Auction Disabled Message trader.insufficient=Insufficient Currency trader.sufficient=Sufficient Currency From bc7e768d5fbf14205075020c52423b433cd6872e Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 15:59:01 -0500 Subject: [PATCH 17/64] Show auction inventory only on create tab --- .../npcs/client/gui/player/GuiAuction.java | 274 +++++++++++++++--- .../client/gui/player/SubGuiAuctionBid.java | 34 +-- .../gui/player/SubGuiAuctionCreate.java | 26 +- .../gui/player/SubGuiAuctionDetails.java | 38 +-- .../npcs/containers/ContainerAuction.java | 6 +- 5 files changed, 284 insertions(+), 94 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 507368c7d..0a93e98ed 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -5,12 +5,15 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.util.*; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.constants.EnumAuctionDuration; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.entity.EntityNPCInterface; @@ -27,8 +30,25 @@ import java.util.List; public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, ICustomScrollListener, NoppesUtil.IAuctionData { + private static final ResourceLocation BG_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/standardbg.png"); + private static final int LEFT_PANEL_X = 8; + private static final int LEFT_PANEL_WIDTH = 190; + private static final int RIGHT_PANEL_X = 210; + private static final int ACTION_BUTTON_WIDTH = 98; + private static final int LISTING_SCROLL_HEIGHT = 100; + private static final int LISTING_SCROLL_WIDTH = 190; + private static final int PAGE_BUTTON_WIDTH = 60; + private static final int SEARCH_FIELD_WIDTH = 140; + private static final int SEARCH_BUTTON_WIDTH = 46; + private static final int HIDDEN_SLOT_POSITION = -1000; + private static final int INVENTORY_START_X = 8; + private static final int INVENTORY_START_Y = 166; + private static final int HOTBAR_Y_OFFSET = 58; + // GUI Components private GuiNpcTextField searchField; + private GuiNpcTextField startingPriceField; + private GuiNpcTextField buyoutPriceField; private GuiCustomScroll listingScroll; // Current state @@ -41,6 +61,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Tab state (0=Browse, 1=My Listings, 2=My Bids, 3=Claims, 4=Create) private int currentTab = 0; + private int selectedDuration = 1; // Selected listing for details private int selectedListingIndex = -1; @@ -51,8 +72,8 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { super(npc, container); this.title = ""; - this.xSize = 256; - this.ySize = 222; + this.xSize = 319; + this.ySize = 238; this.closeOnEsc = true; this.drawDefaultBackground = true; @@ -64,47 +85,66 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, public void initGui() { super.initGui(); + int leftX = guiLeft + LEFT_PANEL_X; + int rightX = guiLeft + RIGHT_PANEL_X; + int actionY = guiTop + 44; + boolean isCreateTab = currentTab == 4; + + updateListingSlotPosition(isCreateTab); + // Tab buttons at top - addTopButton(new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse"))); - addTopButton(new GuiMenuTopButton(1, guiLeft + 54, guiTop - 17, StatCollector.translateToLocal("auction.myListings"))); - addTopButton(new GuiMenuTopButton(2, guiLeft + 114, guiTop - 17, StatCollector.translateToLocal("auction.myBids"))); - addTopButton(new GuiMenuTopButton(3, guiLeft + 164, guiTop - 17, StatCollector.translateToLocal("auction.claims"))); - addTopButton(new GuiMenuTopButton(4, guiLeft + 214, guiTop - 17, StatCollector.translateToLocal("auction.sell"))); + GuiMenuTopButton browseButton = new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse")); + addTopButton(browseButton); + GuiMenuTopButton myListingsButton = new GuiMenuTopButton(1, browseButton, StatCollector.translateToLocal("auction.myListings")); + addTopButton(myListingsButton); + GuiMenuTopButton myBidsButton = new GuiMenuTopButton(2, myListingsButton, StatCollector.translateToLocal("auction.myBids")); + addTopButton(myBidsButton); + GuiMenuTopButton claimsButton = new GuiMenuTopButton(3, myBidsButton, StatCollector.translateToLocal("auction.claims")); + addTopButton(claimsButton); + GuiMenuTopButton sellButton = new GuiMenuTopButton(4, claimsButton, StatCollector.translateToLocal("auction.sell")); + addTopButton(sellButton); getTopButton(currentTab).setEnabled(false); - // Search field - searchField = new GuiNpcTextField(10, this, guiLeft + 8, guiTop + 6, 140, 14, currentFilter.searchText); - searchField.setMaxStringLength(50); - addTextField(searchField); + if (isCreateTab) { + addCreateControls(leftX, rightX); + } else { + // Search field + searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); + searchField.setMaxStringLength(50); + addTextField(searchField); - // Search button - addButton(new GuiNpcButton(11, guiLeft + 150, guiTop + 5, 40, 16, StatCollector.translateToLocal("auction.search"))); + // Search button + addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.search"))); - // Sort button - addButton(new GuiNpcButton(30, guiLeft + 8, guiTop + 24, 120, 16, currentFilter.sortOrder.getDisplayName())); + // Sort button + addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); - // Listing scroll area - listingScroll = new GuiCustomScroll(this, 0); - listingScroll.guiLeft = guiLeft + 8; - listingScroll.guiTop = guiTop + 44; - listingScroll.setSize(160, 128); - addScroll(listingScroll); - updateListingDisplay(); + // Listing scroll area + listingScroll = new GuiCustomScroll(this, 0); + listingScroll.guiLeft = leftX; + listingScroll.guiTop = guiTop + 44; + listingScroll.setSize(LISTING_SCROLL_WIDTH, LISTING_SCROLL_HEIGHT); + addScroll(listingScroll); + updateListingDisplay(); - // Pagination buttons - addButton(new GuiNpcButton(40, guiLeft + 8, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.prevPage"))); - addLabel(new GuiNpcLabel(41, getPageLabel(), guiLeft + 90, guiTop + 180, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, guiLeft + 128, guiTop + 176, 40, 16, StatCollector.translateToLocal("auction.nextPage"))); + // Pagination buttons + addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.prevPage"))); + addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); + addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.nextPage"))); - // Action buttons (right side) - addButton(new GuiNpcButton(50, guiLeft + 175, guiTop + 50, 73, 20, StatCollector.translateToLocal("auction.viewDetails"))); - addButton(new GuiNpcButton(51, guiLeft + 175, guiTop + 75, 73, 20, StatCollector.translateToLocal("auction.placeBid"))); - addButton(new GuiNpcButton(52, guiLeft + 175, guiTop + 100, 73, 20, StatCollector.translateToLocal("auction.buyout"))); - addButton(new GuiNpcButton(53, guiLeft + 175, guiTop + 125, 73, 20, StatCollector.translateToLocal("auction.refresh"))); + // Action buttons (right side) + addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.viewDetails"))); + addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.placeBid"))); + addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.buyout"))); + addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.refresh"))); - // Balance display - addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), - guiLeft + 175, guiTop + 155, 0x00AA00)); + // Balance display + addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), + rightX, guiTop + 170, 0x00AA00)); + } updateButtonStates(); } @@ -119,6 +159,29 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, lastCountdownUpdate = now; updateListingDisplay(); } + + if (currentTab == 4 && inventorySlots instanceof ContainerAuction) { + ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); + GuiNpcLabel itemLabel = getLabel(102); + GuiNpcButton createButton = getButton(71); + if (item != null) { + if (itemLabel != null) { + itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); + itemLabel.color = 0x008800; + } + if (createButton != null) { + createButton.setEnabled(true); + } + } else { + if (itemLabel != null) { + itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); + itemLabel.color = 0x888888; + } + if (createButton != null) { + createButton.setEnabled(false); + } + } + } } @Override @@ -153,6 +216,13 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, buyout(); } else if (id == 53) { refresh(); + } else if (id == 70) { + selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; + initGui(); + } else if (id == 71) { + createListing(); + } else if (id == 72) { + switchTab(0); } } @@ -173,8 +243,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, case 3: // Claims AuctionActionPacket.QueryClaimable(); break; - case 4: // Create - open sub GUI - setSubGui(new SubGuiAuctionCreate(this, (ContainerAuction) inventorySlots)); + case 4: break; } @@ -348,19 +417,139 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } } + private void updateListingSlotPosition(boolean showInventory) { + if (!(inventorySlots instanceof ContainerAuction)) { + return; + } + + ContainerAuction container = (ContainerAuction) inventorySlots; + if (showInventory) { + container.listingSlot.xDisplayPosition = RIGHT_PANEL_X + 41; + container.listingSlot.yDisplayPosition = 64; + + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int slotIndex = 1 + col + row * 9; + container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; + container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + row * 18; + } + } + + for (int col = 0; col < 9; col++) { + int slotIndex = 28 + col; + container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; + container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; + } + } else { + container.listingSlot.xDisplayPosition = HIDDEN_SLOT_POSITION; + container.listingSlot.yDisplayPosition = HIDDEN_SLOT_POSITION; + + for (int slotIndex = 1; slotIndex < container.inventorySlots.size(); slotIndex++) { + container.inventorySlots.get(slotIndex).xDisplayPosition = HIDDEN_SLOT_POSITION; + container.inventorySlots.get(slotIndex).yDisplayPosition = HIDDEN_SLOT_POSITION; + } + } + } + + private void addCreateControls(int leftX, int rightX) { + addLabel(new GuiNpcLabel(100, StatCollector.translateToLocal("auction.createListing"), leftX, guiTop + 8, 0x404040)); + addLabel(new GuiNpcLabel(101, StatCollector.translateToLocal("auction.placeItemBelow"), rightX, guiTop + 32, 0x404040)); + + ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); + if (item != null) { + addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), + rightX, guiTop + 86, 0x008800)); + } else { + addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.noItemSelected"), rightX, guiTop + 86, 0x888888)); + } + + addLabel(new GuiNpcLabel(110, StatCollector.translateToLocal("auction.startingPrice"), leftX, guiTop + 36, 0x404040)); + startingPriceField = new GuiNpcTextField(111, this, leftX + 120, guiTop + 34, 90, 14, "100"); + startingPriceField.setIntegersOnly(); + startingPriceField.setMinMaxDefault(1, 999999999, 100); + addTextField(startingPriceField); + + addLabel(new GuiNpcLabel(120, StatCollector.translateToLocal("auction.buyoutPrice"), leftX, guiTop + 58, 0x404040)); + buyoutPriceField = new GuiNpcTextField(121, this, leftX + 120, guiTop + 56, 90, 14, "0"); + buyoutPriceField.setIntegersOnly(); + buyoutPriceField.setMinMaxDefault(0, 999999999, 0); + addTextField(buyoutPriceField); + addLabel(new GuiNpcLabel(122, StatCollector.translateToLocal("auction.noBuyoutHint"), leftX + 214, guiTop + 58, 0x888888)); + + addLabel(new GuiNpcLabel(130, StatCollector.translateToLocal("auction.duration"), leftX, guiTop + 80, 0x404040)); + addButton(new GuiNpcButton(70, leftX + 120, guiTop + 76, 110, 20, getDurationDisplay())); + + long fee = getListingFee(); + addLabel(new GuiNpcLabel(140, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, + leftX, guiTop + 104, fee > 0 ? 0xAA0000 : 0x008800)); + + addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(72, leftX + 100, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.cancel"))); + + if (getButton(71) != null) { + getButton(71).setEnabled(item != null); + } + } + + private String getDurationDisplay() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return duration.getDisplayName(); + } + + private long getListingFee() { + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + return duration.getListingFee(); + } + + private void createListing() { + ContainerAuction container = (ContainerAuction) inventorySlots; + ItemStack item = container.getListingItem(); + if (item == null) { + return; + } + + long startingPrice = parsePrice(startingPriceField, 1); + long buyoutPrice = parsePrice(buyoutPriceField, 0); + + if (startingPrice < 1) { + startingPrice = 1; + } + + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return; + } + + EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; + AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); + container.clearListingSlot(); + switchTab(0); + } + + private long parsePrice(GuiNpcTextField field, long defaultValue) { + if (field == null) { + return defaultValue; + } + try { + return Long.parseLong(field.getText()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + @Override protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { drawDefaultBackground(); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); // Draw background - mc.renderEngine.bindTexture(new ResourceLocation("customnpcs", "textures/gui/bgfilled.png")); - drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + mc.renderEngine.bindTexture(BG_TEXTURE); + drawTexturedModalRect(guiLeft, guiTop, 0, 0, 252, ySize); + drawTexturedModalRect(guiLeft + 252, guiTop, 188, 0, xSize - 252, ySize); // Draw selected item preview if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { AuctionListing listing = listings.get(selectedListingIndex); - drawItemPreview(listing, guiLeft + 180, guiTop + 160); + drawItemPreview(listing, guiLeft + RIGHT_PANEL_X + 41, guiTop + 192); } super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); @@ -387,9 +576,10 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, fontRendererObj.drawString(titleText, (xSize - fontRendererObj.getStringWidth(titleText)) / 2, -8, CustomNpcResourceListener.DefaultTextColor); - // Results count - String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); - fontRendererObj.drawString(listingsLabel, 175, 42, 0x666666); + if (currentTab != 4) { + String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); + fontRendererObj.drawString(listingsLabel, LEFT_PANEL_X, 36, 0x666666); + } } private String getTabTitle() { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index fbf62fc1d..99f1e00f1 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -23,8 +23,8 @@ public class SubGuiAuctionBid extends SubGuiInterface { this.listing = listing; this.playerBalance = playerBalance; setBackground("menubg.png"); - xSize = 200; - ySize = 150; + xSize = 240; + ySize = 165; closeOnEsc = true; } @@ -36,13 +36,13 @@ public class SubGuiAuctionBid extends SubGuiInterface { int labelColor = 0x404040; if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 40, y, 0xAA0000)); - addButton(new GuiNpcButton(31, guiLeft + 60, guiTop + 110, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); + addButton(new GuiNpcButton(31, guiLeft + 80, guiTop + 125, 80, 20, StatCollector.translateToLocal("gui.cancel"))); return; } // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 70, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 90, y, labelColor)); y += 18; // Item name @@ -52,30 +52,30 @@ public class SubGuiAuctionBid extends SubGuiInterface { // Current price long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.currentPrice"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 100, y, 0x008800)); + addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 120, y, 0x008800)); y += 14; // Minimum bid long minBid = listing.getMinimumBid(); addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 100, y, 0x0066AA)); + addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 120, y, 0x0066AA)); y += 14; // Your balance addLabel(new GuiNpcLabel(6, StatCollector.translateToLocal("auction.yourBalance"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 100, y, + addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 120, y, playerBalance >= minBid ? 0x008800 : 0xAA0000)); y += 14; // Time remaining addLabel(new GuiNpcLabel(8, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, getTimeColor(listing.getTimeRemaining()))); y += 18; // Bid input addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); - bidField = new GuiNpcTextField(11, this, guiLeft + 80, y, 100, 16, String.valueOf(minBid)); + bidField = new GuiNpcTextField(11, this, guiLeft + 100, y, 110, 16, String.valueOf(minBid)); bidField.setIntegersOnly(); int minBidInt = (int) Math.min(Integer.MAX_VALUE, Math.max(1L, minBid)); bidField.setMinMaxDefault(minBidInt, Integer.MAX_VALUE, minBidInt); @@ -83,24 +83,24 @@ public class SubGuiAuctionBid extends SubGuiInterface { y += 24; // Quick bid buttons - addButton(new GuiNpcButton(20, guiLeft + 15, y, 55, 16, StatCollector.translateToLocal("auction.minBid"))); - addButton(new GuiNpcButton(21, guiLeft + 73, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus10"))); - addButton(new GuiNpcButton(22, guiLeft + 131, y, 55, 16, StatCollector.translateToLocal("auction.bidPlus25"))); + addButton(new GuiNpcButton(20, guiLeft + 15, y, 60, 16, StatCollector.translateToLocal("auction.minBid"))); + addButton(new GuiNpcButton(21, guiLeft + 79, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus10"))); + addButton(new GuiNpcButton(22, guiLeft + 143, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus25"))); y += 22; // Place bid button - GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 15, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); + GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 35, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); addButton(bidButton); // Cancel button - addButton(new GuiNpcButton(31, guiLeft + 105, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + addButton(new GuiNpcButton(31, guiLeft + 125, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); // Error message area if (playerBalance < minBid) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); } else if (!listing.isActive()) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 50, guiTop + ySize - 15, 0xAA0000)); + addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index d95dc229a..6d7f0a756 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -25,8 +25,8 @@ public class SubGuiAuctionCreate extends SubGuiInterface { public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { this.container = container; setBackground("menubg.png"); - xSize = 220; - ySize = 180; + xSize = 260; + ySize = 190; closeOnEsc = true; } @@ -35,7 +35,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { super.initGui(); // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 50, guiTop + 8, 0x404040)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 80, guiTop + 8, 0x404040)); // Item slot instructions addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.placeItemBelow"), guiLeft + 20, guiTop + 28, 0x404040)); @@ -49,35 +49,35 @@ public class SubGuiAuctionCreate extends SubGuiInterface { } // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 70, 0x404040)); - startingPriceField = new GuiNpcTextField(11, this, guiLeft + 100, guiTop + 68, 80, 14, "100"); + addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 74, 0x404040)); + startingPriceField = new GuiNpcTextField(11, this, guiLeft + 120, guiTop + 72, 90, 14, "100"); startingPriceField.setIntegersOnly(); startingPriceField.setMinMaxDefault(1, 999999999, 100); addTextField(startingPriceField); // Buyout price (optional) - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 90, 0x404040)); - buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 100, guiTop + 88, 80, 14, "0"); + addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 96, 0x404040)); + buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 120, guiTop + 94, 90, 14, "0"); buyoutPriceField.setIntegersOnly(); buyoutPriceField.setMinMaxDefault(0, 999999999, 0); addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 185, guiTop + 90, 0x888888)); + addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 215, guiTop + 96, 0x888888)); // Duration selection - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 112, 0x404040)); - addButton(new GuiNpcButton(31, guiLeft + 100, guiTop + 108, 100, 20, getDurationDisplay())); + addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 118, 0x404040)); + addButton(new GuiNpcButton(31, guiLeft + 120, guiTop + 114, 110, 20, getDurationDisplay())); // Fee display long fee = getListingFee(); addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - guiLeft + 20, guiTop + 135, fee > 0 ? 0xAA0000 : 0x008800)); + guiLeft + 20, guiTop + 142, fee > 0 ? 0xAA0000 : 0x008800)); // Create button - addButton(new GuiNpcButton(50, guiLeft + 20, guiTop + 155, 80, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("auction.create"))); getButton(50).setEnabled(item != null); // Cancel button - addButton(new GuiNpcButton(51, guiLeft + 120, guiTop + 155, 80, 20, StatCollector.translateToLocal("gui.cancel"))); + addButton(new GuiNpcButton(51, guiLeft + 140, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.cancel"))); } private String getDurationDisplay() { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java index 4054d04b1..e4131fe94 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java @@ -21,8 +21,8 @@ public class SubGuiAuctionDetails extends SubGuiInterface { public SubGuiAuctionDetails(GuiAuction parent, AuctionListing listing) { this.listing = listing; setBackground("menubg.png"); - xSize = 240; - ySize = 200; + xSize = 280; + ySize = 210; closeOnEsc = true; } @@ -35,76 +35,76 @@ public class SubGuiAuctionDetails extends SubGuiInterface { int valueColor = 0x000000; if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); - addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 80, y, 0xAA0000)); + addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); return; } // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 80, y, labelColor)); + addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 100, y, labelColor)); y += 16; // Item name addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 80, y, valueColor)); + addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 100, y, valueColor)); y += 14; // Quantity addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("auction.quantity"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 80, y, valueColor)); + addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 100, y, valueColor)); y += 14; // Seller addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("auction.seller"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 80, y, valueColor)); + addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 100, y, valueColor)); y += 14; // Starting price addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 100, y, valueColor)); + addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 120, y, valueColor)); y += 14; // Current bid if (listing.currentBid > 0) { addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 100, y, 0x008800)); + addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 120, y, 0x008800)); y += 14; // High bidder addLabel(new GuiNpcLabel(14, StatCollector.translateToLocal("auction.highBidder"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 100, y, valueColor)); + addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 120, y, valueColor)); y += 14; // Bid count addLabel(new GuiNpcLabel(16, StatCollector.translateToLocal("auction.totalBids"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 100, y, valueColor)); + addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 120, y, valueColor)); y += 14; } else { addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 100, y, 0x888888)); + addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 120, y, 0x888888)); y += 14; } // Buyout price if (listing.buyoutPrice > 0) { addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 100, y, 0xAA6600)); + addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 120, y, 0xAA6600)); y += 14; } // Minimum bid addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 100, y, 0x0066AA)); + addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 120, y, 0x0066AA)); y += 14; // Time remaining (will be updated) addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 100, y, + addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, getTimeColor(listing.getTimeRemaining()))); y += 20; // Close button - addButton(new GuiNpcButton(50, guiLeft + 80, guiTop + 175, 80, 20, StatCollector.translateToLocal("gui.close"))); + addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); } private String formatCurrency(long amount) { @@ -162,8 +162,8 @@ public class SubGuiAuctionDetails extends SubGuiInterface { // Draw item icon if (listing.item != null) { - int itemX = guiLeft + 200; - int itemY = guiTop + 30; + int itemX = guiLeft + 230; + int itemY = guiTop + 32; RenderHelper.enableGUIStandardItemLighting(); GL11.glEnable(GL12.GL_RESCALE_NORMAL); diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java index 61e783d91..2efd2b1b5 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuction.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuction.java @@ -26,20 +26,20 @@ public class ContainerAuction extends Container { // Create a single-slot inventory for the item being listed listingInventory = new InventoryAuctionListing(); - listingSlot = new SlotAuctionItem(listingInventory, 0, 80, 35); + listingSlot = new SlotAuctionItem(listingInventory, 0, 251, 146); addSlotToContainer(listingSlot); // Player inventory slots (standard positioning) for (int row = 0; row < 3; row++) { for (int col = 0; col < 9; col++) { addSlotToContainer(new Slot(player.inventory, col + row * 9 + 9, - 8 + col * 18, 140 + row * 18)); + 8 + col * 18, 166 + row * 18)); } } // Player hotbar for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 198)); + addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 224)); } } From db14662e90cd90196fe39ec80da6ead1fd1e63aa Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 4 Jan 2026 20:10:58 -0500 Subject: [PATCH 18/64] Fix auction GUI tab actions --- .../npcs/client/gui/player/GuiAuction.java | 273 +++++++++++++++--- 1 file changed, 230 insertions(+), 43 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 0a93e98ed..8dcbcfcbe 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.player; import kamkeel.npcs.network.packets.player.AuctionActionPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.inventory.Slot; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.item.ItemStack; @@ -14,6 +15,7 @@ import noppes.npcs.client.gui.util.*; import noppes.npcs.config.ConfigMarket; import noppes.npcs.containers.ContainerAuction; import noppes.npcs.constants.EnumAuctionDuration; +import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; import noppes.npcs.entity.EntityNPCInterface; @@ -22,6 +24,7 @@ import org.lwjgl.opengl.GL12; import java.util.ArrayList; import java.util.List; +import java.util.UUID; /** * Main auction house browse GUI. @@ -69,6 +72,12 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, // Countdown update timer private long lastCountdownUpdate = 0; + private enum ActionMode { + BID, + CANCEL, + CLAIM + } + public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { super(npc, container); this.title = ""; @@ -89,6 +98,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, int rightX = guiLeft + RIGHT_PANEL_X; int actionY = guiTop + 44; boolean isCreateTab = currentTab == 4; + boolean isBrowseTab = currentTab == 0; updateListingSlotPosition(isCreateTab); @@ -108,17 +118,19 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, if (isCreateTab) { addCreateControls(leftX, rightX); } else { - // Search field - searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); - searchField.setMaxStringLength(50); - addTextField(searchField); + if (isBrowseTab) { + // Search field + searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); + searchField.setMaxStringLength(50); + addTextField(searchField); - // Search button - addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.search"))); + // Search button + addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.search"))); - // Sort button - addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); + // Sort button + addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); + } // Listing scroll area listingScroll = new GuiCustomScroll(this, 0); @@ -128,18 +140,17 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, addScroll(listingScroll); updateListingDisplay(); - // Pagination buttons - addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.prevPage"))); - addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.nextPage"))); + if (isBrowseTab) { + // Pagination buttons + addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.prevPage"))); + addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); + addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, + StatCollector.translateToLocal("auction.nextPage"))); + } // Action buttons (right side) - addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.viewDetails"))); - addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.placeBid"))); - addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.buyout"))); - addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, StatCollector.translateToLocal("auction.refresh"))); + addActionButtons(rightX, actionY); // Balance display addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), @@ -195,25 +206,42 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } // Search button else if (id == 11) { - performSearch(); + if (currentTab == 0) { + performSearch(); + } } // Sort button else if (id == 30) { - cycleSortOrder(); + if (currentTab == 0) { + cycleSortOrder(); + } } // Pagination else if (id == 40) { - previousPage(); + if (currentTab == 0) { + previousPage(); + } } else if (id == 42) { - nextPage(); + if (currentTab == 0) { + nextPage(); + } } // Actions else if (id == 50) { viewDetails(); } else if (id == 51) { - placeBid(); + ActionMode mode = getActionMode(); + if (mode == ActionMode.BID) { + placeBid(); + } else if (mode == ActionMode.CANCEL) { + cancelListing(); + } else if (mode == ActionMode.CLAIM) { + claimListing(); + } } else if (id == 52) { - buyout(); + if (getActionMode() == ActionMode.BID) { + buyout(); + } } else if (id == 53) { refresh(); } else if (id == 70) { @@ -229,6 +257,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, private void switchTab(int tab) { currentTab = tab; currentPage = 0; + selectedListingIndex = -1; switch (tab) { case 0: // Browse @@ -340,6 +369,9 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { listingScroll.selected = selectedListingIndex; + } else { + selectedListingIndex = -1; + listingScroll.selected = -1; } } @@ -395,25 +427,34 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, } private void updateButtonStates() { + ActionMode mode = getActionMode(); + AuctionListing listing = getSelectedListing(); + boolean hasSelection = listing != null; + // Pagination buttons if (getButton(40) != null) getButton(40).setEnabled(currentPage > 0); if (getButton(42) != null) getButton(42).setEnabled(currentPage < totalPages - 1); // Action buttons based on selection - boolean hasSelection = selectedListingIndex >= 0 && selectedListingIndex < listings.size(); - if (hasSelection && listings.get(selectedListingIndex).item == null) { - hasSelection = false; - } if (getButton(50) != null) getButton(50).setEnabled(hasSelection); - if (getButton(51) != null) getButton(51).setEnabled(hasSelection); + if (getButton(51) != null) { + if (!hasSelection) { + getButton(51).setEnabled(false); + } else if (mode == ActionMode.BID) { + getButton(51).setEnabled(canPlaceBid(listing)); + } else if (mode == ActionMode.CANCEL) { + getButton(51).setEnabled(canCancelListing(listing)); + } else { + getButton(51).setEnabled(getClaimAction(listing) != ClaimAction.NONE); + } + } - if (hasSelection && getButton(52) != null) { - AuctionListing listing = listings.get(selectedListingIndex); - getButton(52).setEnabled(listing.item != null && - listing.buyoutPrice > 0 && - playerBalance >= listing.buyoutPrice); - } else if (getButton(52) != null) { - getButton(52).setEnabled(false); + if (getButton(52) != null) { + if (!hasSelection || mode != ActionMode.BID) { + getButton(52).setEnabled(false); + } else { + getButton(52).setEnabled(canBuyout(listing)); + } } } @@ -430,23 +471,26 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, for (int row = 0; row < 3; row++) { for (int col = 0; col < 9; col++) { int slotIndex = 1 + col + row * 9; - container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; - container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + row * 18; + Slot slot = (Slot) container.inventorySlots.get(slotIndex); + slot.xDisplayPosition = INVENTORY_START_X + col * 18; + slot.yDisplayPosition = INVENTORY_START_Y + row * 18; } } for (int col = 0; col < 9; col++) { int slotIndex = 28 + col; - container.inventorySlots.get(slotIndex).xDisplayPosition = INVENTORY_START_X + col * 18; - container.inventorySlots.get(slotIndex).yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; + Slot slot = (Slot) container.inventorySlots.get(slotIndex); + slot.xDisplayPosition = INVENTORY_START_X + col * 18; + slot.yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; } } else { container.listingSlot.xDisplayPosition = HIDDEN_SLOT_POSITION; container.listingSlot.yDisplayPosition = HIDDEN_SLOT_POSITION; for (int slotIndex = 1; slotIndex < container.inventorySlots.size(); slotIndex++) { - container.inventorySlots.get(slotIndex).xDisplayPosition = HIDDEN_SLOT_POSITION; - container.inventorySlots.get(slotIndex).yDisplayPosition = HIDDEN_SLOT_POSITION; + Slot slot = (Slot) container.inventorySlots.get(slotIndex); + slot.xDisplayPosition = HIDDEN_SLOT_POSITION; + slot.yDisplayPosition = HIDDEN_SLOT_POSITION; } } } @@ -525,6 +569,149 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, switchTab(0); } + private void cancelListing() { + AuctionListing listing = getSelectedListing(); + if (!canCancelListing(listing)) { + return; + } + + AuctionActionPacket.CancelListing(listing.id); + refresh(); + } + + private void claimListing() { + AuctionListing listing = getSelectedListing(); + if (listing == null) { + return; + } + + ClaimAction action = getClaimAction(listing); + if (action == ClaimAction.ITEM) { + AuctionActionPacket.ClaimItem(listing.id); + } else if (action == ClaimAction.PROCEEDS) { + AuctionActionPacket.ClaimProceeds(listing.id); + } else { + return; + } + + refresh(); + } + + private ActionMode getActionMode() { + if (currentTab == 1) { + return ActionMode.CANCEL; + } + if (currentTab == 3) { + return ActionMode.CLAIM; + } + return ActionMode.BID; + } + + private String getPrimaryActionLabel(ActionMode mode) { + if (mode == ActionMode.CANCEL) { + return StatCollector.translateToLocal("auction.cancel"); + } + if (mode == ActionMode.CLAIM) { + return StatCollector.translateToLocal("auction.claim"); + } + return StatCollector.translateToLocal("auction.placeBid"); + } + + private void addActionButtons(int rightX, int actionY) { + ActionMode mode = getActionMode(); + addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.viewDetails"))); + addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, + getPrimaryActionLabel(mode))); + if (mode == ActionMode.BID) { + addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.buyout"))); + addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.refresh"))); + } else { + addButton(new GuiNpcButton(53, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, + StatCollector.translateToLocal("auction.refresh"))); + } + } + + private AuctionListing getSelectedListing() { + if (selectedListingIndex < 0 || selectedListingIndex >= listings.size()) { + return null; + } + AuctionListing listing = listings.get(selectedListingIndex); + if (listing == null || listing.item == null) { + return null; + } + return listing; + } + + private boolean canPlaceBid(AuctionListing listing) { + if (listing == null || !listing.isActive()) { + return false; + } + if (isPlayerSeller(listing)) { + return false; + } + return playerBalance >= listing.getMinimumBid(); + } + + private boolean canBuyout(AuctionListing listing) { + if (listing == null || !listing.isActive()) { + return false; + } + if (listing.buyoutPrice <= 0) { + return false; + } + if (isPlayerSeller(listing)) { + return false; + } + return playerBalance >= listing.buyoutPrice; + } + + private boolean canCancelListing(AuctionListing listing) { + if (listing == null || listing.item == null) { + return false; + } + return listing.status == EnumAuctionStatus.ACTIVE && !listing.hasBids(); + } + + private boolean isPlayerSeller(AuctionListing listing) { + if (mc == null || mc.thePlayer == null || listing == null || listing.sellerUUID == null) { + return false; + } + UUID playerId = mc.thePlayer.getUniqueID(); + return listing.sellerUUID.equals(playerId); + } + + private enum ClaimAction { + ITEM, + PROCEEDS, + NONE + } + + private ClaimAction getClaimAction(AuctionListing listing) { + if (listing == null || mc == null || mc.thePlayer == null) { + return ClaimAction.NONE; + } + + UUID playerId = mc.thePlayer.getUniqueID(); + if (listing.sellerUUID != null && listing.sellerUUID.equals(playerId) && !listing.sellerClaimed) { + if (listing.status == EnumAuctionStatus.SOLD) { + return ClaimAction.PROCEEDS; + } + if (listing.status == EnumAuctionStatus.CANCELLED || listing.status == EnumAuctionStatus.EXPIRED) { + return ClaimAction.ITEM; + } + } + + if (listing.highBidderUUID != null && listing.highBidderUUID.equals(playerId) + && listing.status == EnumAuctionStatus.SOLD && !listing.buyerClaimed) { + return ClaimAction.ITEM; + } + + return ClaimAction.NONE; + } + private long parsePrice(GuiNpcTextField field, long defaultValue) { if (field == null) { return defaultValue; From a44cc4f1bbfa44e37501b2e102af04f2568d34a4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 02:49:20 +0200 Subject: [PATCH 19/64] Passed default imports to JaninoScript --- .../java/noppes/npcs/janino/JaninoScript.java | 11 +++++---- .../npcs/janino/impl/JaninoNpcScript.java | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 0f9d5c9f1..22fb9ee95 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -58,11 +58,14 @@ public abstract class JaninoScript implements IScriptUnit { // Map from EnumScriptType to Method for OLD tab-based script system private Map hookTypeMap = new HashMap<>(); - protected JaninoScript(Class type, Consumer> buildSettings) { + // Default imports for this script type (e.g., noppes.npcs.api.*) + private final String[] defaultImports; + + protected JaninoScript(Class type, String[] defaultImports) { this.type = type; - this.builder = IScriptBodyBuilder - .getBuilder(type, CustomNpcs.getClientCompiler()); - buildSettings.accept(builder); + this.defaultImports = defaultImports != null ? defaultImports : new String[0]; + this.builder = IScriptBodyBuilder.getBuilder(type, CustomNpcs.getClientCompiler()) + .setDefaultImports(defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index b41e2aac8..4b051b37e 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -14,18 +14,19 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoNpcScript extends JaninoScript { + /** Default imports available to NPC scripts without explicit import statements. */ + public static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.*", + "noppes.npcs.api.entity.*", + "noppes.npcs.api.event.*", + "noppes.npcs.api.handler.*", + "noppes.npcs.api.handler.data.*", + "noppes.npcs.api.item.*", + "noppes.npcs.api.ability.*" + }; + public JaninoNpcScript() { - super(Functions.class, (builder) -> builder - .setDefaultImports( - "noppes.npcs.api.*", - "noppes.npcs.api.entity.*", - "noppes.npcs.api.event.*", - "noppes.npcs.api.handler.*", - "noppes.npcs.api.handler.data.*", - "noppes.npcs.api.item.*", - "noppes.npcs.api.ability.*" - ) - ); + super(Functions.class, DEFAULT_IMPORTS); } /** From d17534120b7f501ccae88551755f8ac5eca7d7d0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 03:41:24 +0200 Subject: [PATCH 20/64] Merged GuiJaninoScript with GuiScriptInterface and moved usages --- .../client/gui/script/GuiScriptInterface.java | 75 +++++++++++++++++-- .../npcs/controllers/ScriptContainer.java | 5 +- .../npcs/controllers/data/IScriptUnit.java | 6 ++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 3e16377f4..0dc4036b0 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -1,9 +1,9 @@ package noppes.npcs.client.gui.script; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; @@ -29,7 +29,6 @@ import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; -import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.Date; @@ -50,6 +49,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; + /** Single script being edited (handler-less mode) */ + protected IScriptUnit singleScript; + public boolean singleContainer = false; /** If true, uses scroll-based tab selection instead of top buttons */ @@ -88,6 +90,39 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setBackground("menubg.png"); } + /** + * Constructor for single JaninoScript editing (handler-less mode). + * Used for scripts like OverlayScript that don't have an IScriptHandler. + * + * @param parent The parent GUI to return to when closing + * @param script The JaninoScript to edit + */ + public GuiScriptInterface(GuiScreen parent, IScriptUnit script) { + this(); + this.parent = parent; + this.singleScript = script; + this.singleContainer = true; + this.loaded = true; + + // Start on script tab (tab 1) + this.activeTab = 1; + + // Build hook list from the script's type using reflection + // this.hookList = script.getHookList(); + } + + /** + * Create and initialize a GuiScriptInterface for a JaninoScript. + * This is the main entry point for handler-less script editing. + */ + public static GuiScriptInterface create(GuiScreen parent, IScriptUnit script) { + GuiScriptInterface gui = new GuiScriptInterface(parent, script); + gui.setWorldAndResolution(Minecraft.getMinecraft(), gui.width, gui.height); + Minecraft.getMinecraft().currentScreen = gui; + gui.initGui(); + return gui; + } + public void initGui() { // ==================== BASE LAYOUT CALCULATION ==================== this.ySize = (int) ((double) this.xSize * 0.56D); @@ -302,6 +337,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); } + public GuiScriptInterface setDimensions(int x, int y) { + this.xSize = x; + this.ySize = y; + return this; + } + // ==================== RENDERING ==================== @Override @@ -448,6 +489,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Override in subclasses with different container access patterns. */ protected IScriptUnit getCurrentContainer() { + // Handler-less single JaninoScript mode + if (singleScript != null) + return singleScript; + int idx = getActiveScriptIndex(); if (idx >= 0 && idx < this.handler.getScripts().size()) return this.handler.getScripts().get(idx); @@ -475,6 +520,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setScript(); this.activeTab = guibutton.id; this.initGui(); + + // Ensure JaninoScript is compiled after editing + IScriptUnit current = getCurrentContainer(); + if (current != null) + current.ensureCompiled(); } if (guibutton.id == scriptLimit) { @@ -590,9 +640,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } protected void setScript() { - if (this.activeTab > 0 || useScrollTabs) { + if (this.activeTab > 0 || useScrollTabs || singleScript != null) { IScriptUnit container = getCurrentContainer(); - if (container == null) { + + // Skip container creation for single JaninoScript mode + if (container == null && singleScript == null) { container = new ScriptContainer(this.handler); if (singleContainer) setHandlerContainer(container); @@ -600,10 +652,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.handler.getScripts().add(container); } - String text = (this.getTextField(2)).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.setScript(text); + if (container != null) { + String text = (this.getTextField(2)).getText(); + text = text.replace("\r\n", "\n"); + text = text.replace("\r", "\n"); + container.setScript(text); + } } } @@ -753,6 +807,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb @Override public void close() { + // Ensure JaninoScript is compiled after editing + IScriptUnit current = getCurrentContainer(); + if (current != null) + current.ensureCompiled(); + if (parent != null) { this.save(); parent.setWorldAndResolution(mc, width, height); diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index b85bb1407..eb0b8b1c4 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -328,7 +328,10 @@ public class ScriptContainer implements IScriptUnit { // ECMAScript function template return "function " + hookName + "(event) {\n \n}\n"; } - + + @Override + public void ensureCompiled() {} + @Override public boolean hasErrored() { return this.errored; diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index 392901ede..1cd661a6e 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -135,6 +135,12 @@ public interface IScriptUnit { void readFromNBT(NBTTagCompound compound); // ==================== STATE ==================== + + /** + * Ensure this script unit is compiled (for Janino). + * No-op for ScriptContainer. + */ + void ensureCompiled(); /** * Check if this script unit has any code (script or external scripts). From 2c2e13c2f74557b2625f4c51aa478036ad2b7a3b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 03:50:47 +0200 Subject: [PATCH 21/64] Morea ScriptContainer -> IScriptUnit abstraction --- .../client/gui/script/EventGuiScriptList.java | 24 +++++++++++-------- .../client/gui/script/GuiScriptInterface.java | 9 ++++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java index 530a3a2be..cd47fed55 100644 --- a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java @@ -5,7 +5,7 @@ import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.Iterator; @@ -14,10 +14,10 @@ import java.util.List; public class EventGuiScriptList extends SubGuiInterface { private GuiCustomScroll scroll1; private GuiCustomScroll scroll2; - private ScriptContainer container; + private IScriptUnit container; private List scripts; - public EventGuiScriptList(List scripts, ScriptContainer container) { + public EventGuiScriptList(List scripts, IScriptUnit container) { this.container = container; this.setBackground("menubg.png"); this.xSize = 346; @@ -50,9 +50,9 @@ public class EventGuiScriptList extends SubGuiInterface { this.addScroll(this.scroll2); this.addLabel(new GuiNpcLabel(2, "script.loadedScripts", this.guiLeft + 200, this.guiTop + 4)); ArrayList temp = new ArrayList(this.scripts); - temp.removeAll(this.container.scripts); + temp.removeAll(getScripts()); this.scroll1.setList(temp); - this.scroll2.setList(this.container.scripts); + this.scroll2.setList(getScripts()); this.addButton(new GuiNpcButton(1, this.guiLeft + 145, this.guiTop + 40, 55, 20, ">")); this.addButton(new GuiNpcButton(2, this.guiLeft + 145, this.guiTop + 62, 55, 20, "<")); this.addButton(new GuiNpcButton(3, this.guiLeft + 145, this.guiTop + 90, 55, 20, ">>")); @@ -63,25 +63,25 @@ public class EventGuiScriptList extends SubGuiInterface { protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; if (button.id == 1 && this.scroll1.hasSelected()) { - this.container.scripts.add(this.scroll1.getSelected()); + getScripts().add(this.scroll1.getSelected()); this.scroll1.selected = -1; this.scroll2.selected = -1; this.initGui(); } if (button.id == 2 && this.scroll2.hasSelected()) { - this.container.scripts.remove(this.scroll2.getSelected()); + getScripts().remove(this.scroll2.getSelected()); this.scroll2.selected = -1; this.initGui(); } if (button.id == 3) { - this.container.scripts.clear(); + getScripts().clear(); Iterator var3 = this.scripts.iterator(); while (var3.hasNext()) { String script = (String) var3.next(); - this.container.scripts.add(script); + getScripts().add(script); } this.scroll1.selected = -1; @@ -90,7 +90,7 @@ public class EventGuiScriptList extends SubGuiInterface { } if (button.id == 4) { - this.container.scripts.clear(); + getScripts().clear(); this.scroll1.selected = -1; this.scroll2.selected = -1; this.initGui(); @@ -102,6 +102,10 @@ public class EventGuiScriptList extends SubGuiInterface { } + public List getScripts() { + return this.container.getExternalScripts(); + } + public void save() { } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 0dc4036b0..c20c653d5 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -452,10 +452,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setScript(); } if (i == 102) { - ScriptContainer container; + IScriptUnit container; if (this.activeTab > 0) { - container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); - container.script = ""; + container = this.handler.getScripts().get(this.activeTab - 1); + container.setScript(""); } else { this.handler.clearConsole(); if (this.handler instanceof ScriptCustomItem) { @@ -566,7 +566,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb displayGuiScreen(guiyesno); } - ScriptContainer container; if (guibutton.id == 102) { GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); displayGuiScreen(guiyesno); @@ -592,7 +591,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == 107) { - container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + IScriptUnit container = this.handler.getScripts().get(this.activeTab - 1); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) From 23c44cc5d4f3fff64474b8a1f9791edc79fbaad5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 03:52:04 +0200 Subject: [PATCH 22/64] Refactor --- .../npcs/client/gui/script/GuiScriptEffect.java | 2 +- .../npcs/client/gui/script/GuiScriptInterface.java | 12 ++++++------ .../npcs/client/gui/script/GuiScriptLinkedItem.java | 2 +- .../npcs/client/gui/script/GuiScriptRecipe.java | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index e50b31ade..22c51b207 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -36,7 +36,7 @@ public class GuiScriptEffect extends GuiScriptInterface { return ScriptContext.GLOBAL; } - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { this.scriptHandler.container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index c20c653d5..69dcbb1b2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -442,7 +442,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (i == 10) { if (singleContainer) - setHandlerContainer(null); + setHandlerUnit(null); else this.handler.getScripts().remove(this.activeTab - 1); this.activeTab = 0; @@ -530,7 +530,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (guibutton.id == scriptLimit) { if (singleContainer) { if (getFirst() == null) - setHandlerContainer(new ScriptContainer(handler)); + setHandlerUnit(new ScriptContainer(handler)); else this.setScript(); this.activeTab = 1; @@ -595,7 +595,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) - setHandlerContainer(container); + setHandlerUnit(container); else this.handler.getScripts().add(container); } @@ -646,7 +646,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (container == null && singleScript == null) { container = new ScriptContainer(this.handler); if (singleContainer) - setHandlerContainer(container); + setHandlerUnit(container); else this.handler.getScripts().add(container); } @@ -711,7 +711,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb int tab = compound.getInteger("Tab"); NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, this.handler); - this.setHandlerContainer(container); + this.setHandlerUnit(container); this.initGui(); } } @@ -743,7 +743,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb /** * Set the handler's container. Override if handler is not IScriptHandler. */ - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { // Default implementation - subclasses may need to cast and set differently // e.g., ((LinkedItemScript) handler).container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index 2a001b732..881e3dd6b 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -35,7 +35,7 @@ public class GuiScriptLinkedItem extends GuiScriptInterface { return ScriptContext.ITEM; } - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { ((LinkedItemScript) handler).container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index 176294f7e..83316a48d 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -49,7 +49,7 @@ public class GuiScriptRecipe extends GuiScriptInterface { return ScriptContext.GLOBAL; } - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { ((RecipeScript) handler).container = container; } From f6e589ac67936b4734fe4f2a53fdaa40eb97cdc8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 15 Jan 2026 04:13:59 +0200 Subject: [PATCH 23/64] temp SingleScriptHandlers tuff --- .../client/gui/script/GuiScriptInterface.java | 47 ++------- .../controllers/data/SingleScriptHandler.java | 95 +++++++++++++++++++ 2 files changed, 105 insertions(+), 37 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 69dcbb1b2..8da3d089c 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,6 +27,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.controllers.data.SingleScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -49,9 +50,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - /** Single script being edited (handler-less mode) */ - protected IScriptUnit singleScript; - public boolean singleContainer = false; /** If true, uses scroll-based tab selection instead of top buttons */ @@ -91,35 +89,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } /** - * Constructor for single JaninoScript editing (handler-less mode). - * Used for scripts like OverlayScript that don't have an IScriptHandler. - * - * @param parent The parent GUI to return to when closing - * @param script The JaninoScript to edit - */ - public GuiScriptInterface(GuiScreen parent, IScriptUnit script) { - this(); - this.parent = parent; - this.singleScript = script; - this.singleContainer = true; - this.loaded = true; - - // Start on script tab (tab 1) - this.activeTab = 1; - - // Build hook list from the script's type using reflection - // this.hookList = script.getHookList(); - } - - /** - * Create and initialize a GuiScriptInterface for a JaninoScript. - * This is the main entry point for handler-less script editing. + * Create a GuiScriptInterface for a single script without a handler. + * Wraps the script in a SingleScriptHandler automatically. */ public static GuiScriptInterface create(GuiScreen parent, IScriptUnit script) { - GuiScriptInterface gui = new GuiScriptInterface(parent, script); - gui.setWorldAndResolution(Minecraft.getMinecraft(), gui.width, gui.height); - Minecraft.getMinecraft().currentScreen = gui; - gui.initGui(); + GuiScriptInterface gui = new GuiScriptInterface(); + gui.handler = new SingleScriptHandler(script); + gui.parent = parent; + gui.singleContainer = true; + gui.displayGuiScreen(gui); return gui; } @@ -489,10 +467,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Override in subclasses with different container access patterns. */ protected IScriptUnit getCurrentContainer() { - // Handler-less single JaninoScript mode - if (singleScript != null) - return singleScript; - int idx = getActiveScriptIndex(); if (idx >= 0 && idx < this.handler.getScripts().size()) return this.handler.getScripts().get(idx); @@ -639,11 +613,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } protected void setScript() { - if (this.activeTab > 0 || useScrollTabs || singleScript != null) { + if (this.activeTab > 0 || useScrollTabs) { IScriptUnit container = getCurrentContainer(); - // Skip container creation for single JaninoScript mode - if (container == null && singleScript == null) { + if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) setHandlerUnit(container); diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java new file mode 100644 index 000000000..165952090 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -0,0 +1,95 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.Event; +import noppes.npcs.constants.EnumScriptType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Simple IScriptHandler implementation for single scripts that don't have their own handler. + * Useful for standalone scripts like OverlayScript that need to be edited in GuiScriptInterface. + */ +public class SingleScriptHandler implements IScriptHandler { + private final IScriptUnit script; + private final List scripts = new ArrayList<>(); + + public SingleScriptHandler(IScriptUnit script) { + this.script = script; + this.scripts.add(script); + } + + @Override + public void callScript(EnumScriptType type, Event event) { + // No-op: single scripts don't respond to standard hook calls + } + + @Override + public void callScript(String hookName, Event event) { + // No-op: single scripts don't respond to standard hook calls + } + + public boolean isClient() { + return FMLCommonHandler.instance() + .getEffectiveSide() + .isClient(); + } + + @Override + public boolean getEnabled() { + return true; + } + + @Override + public void setEnabled(boolean enabled) { + // No-op: single scripts are always enabled + } + + @Override + public String getLanguage() { + return script.getLanguage(); + } + + @Override + public void setLanguage(String language) { + // No-op: language is determined by script type + } + + @Override + public void setScripts(List list) { + // No-op: single script handler only manages one script + } + + @Override + public List getScripts() { + return scripts; + } + + @Override + public String noticeString() { + return ""; + } + + @Override + public Map getConsoleText() { + return script.getConsole(); + } + + @Override + public void clearConsole() { + script.clearConsole(); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return null; // Single script handlers don't create new scripts + } + + @Override + public boolean supportsJanino() { + return script.isJanino(); + } +} From 68bc02a5d4b6ad05442c88c192308a99f2510b62 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:01:55 +0200 Subject: [PATCH 24/64] Removed SingleScriptHandler --- .../client/gui/script/GuiScriptInterface.java | 5 +- .../npcs/controllers/data/DataScript.java | 18 ---- .../controllers/data/SingleScriptHandler.java | 95 ------------------- 3 files changed, 2 insertions(+), 116 deletions(-) delete mode 100644 src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 8da3d089c..7e1bfa0e1 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,7 +27,6 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.SingleScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -92,9 +91,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Create a GuiScriptInterface for a single script without a handler. * Wraps the script in a SingleScriptHandler automatically. */ - public static GuiScriptInterface create(GuiScreen parent, IScriptUnit script) { + public static GuiScriptInterface create(GuiScreen parent, IScriptHandler handler) { GuiScriptInterface gui = new GuiScriptInterface(); - gui.handler = new SingleScriptHandler(script); + gui.handler = handler; gui.parent = parent; gui.singleContainer = true; gui.displayGuiScreen(gui); diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 6e07223a8..cd6f4bb54 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -222,24 +222,6 @@ public class DataScript implements IScriptHandler { return map; } - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - - for (Map.Entry longStringEntry : script.getConsole().entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } @Override public void callScript(EnumScriptType type, Event event) { diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java deleted file mode 100644 index 165952090..000000000 --- a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -package noppes.npcs.controllers.data; - -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.constants.EnumScriptType; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Simple IScriptHandler implementation for single scripts that don't have their own handler. - * Useful for standalone scripts like OverlayScript that need to be edited in GuiScriptInterface. - */ -public class SingleScriptHandler implements IScriptHandler { - private final IScriptUnit script; - private final List scripts = new ArrayList<>(); - - public SingleScriptHandler(IScriptUnit script) { - this.script = script; - this.scripts.add(script); - } - - @Override - public void callScript(EnumScriptType type, Event event) { - // No-op: single scripts don't respond to standard hook calls - } - - @Override - public void callScript(String hookName, Event event) { - // No-op: single scripts don't respond to standard hook calls - } - - public boolean isClient() { - return FMLCommonHandler.instance() - .getEffectiveSide() - .isClient(); - } - - @Override - public boolean getEnabled() { - return true; - } - - @Override - public void setEnabled(boolean enabled) { - // No-op: single scripts are always enabled - } - - @Override - public String getLanguage() { - return script.getLanguage(); - } - - @Override - public void setLanguage(String language) { - // No-op: language is determined by script type - } - - @Override - public void setScripts(List list) { - // No-op: single script handler only manages one script - } - - @Override - public List getScripts() { - return scripts; - } - - @Override - public String noticeString() { - return ""; - } - - @Override - public Map getConsoleText() { - return script.getConsole(); - } - - @Override - public void clearConsole() { - script.clearConsole(); - } - - @Override - public IScriptUnit createJaninoScriptUnit() { - return null; // Single script handlers don't create new scripts - } - - @Override - public boolean supportsJanino() { - return script.isJanino(); - } -} From cda43fe67a4f64ebb517393a08d11e7524b5292d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:05:02 +0200 Subject: [PATCH 25/64] Defaulted IScriptHandler methods to clean up duplicate code across implementations --- .../npcs/blocks/tiles/TileScripted.java | 21 +---------- .../controllers/data/ForgeDataScript.java | 28 --------------- .../controllers/data/GlobalNPCDataScript.java | 21 ----------- .../npcs/controllers/data/IScriptHandler.java | 35 ++++++++++++++++--- .../controllers/data/LinkedItemScript.java | 29 --------------- .../controllers/data/PlayerDataScript.java | 29 +++++---------- 6 files changed, 39 insertions(+), 124 deletions(-) diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 7174bac6e..969ea0bac 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -416,26 +416,7 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { public List getScripts() { return scripts; } - - @Override - public Map getConsoleText() { - Map map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : getScripts()) { - tab++; - for (Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : getScripts()) { - script.clearConsole(); - } - } + @SideOnly(Side.CLIENT) @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index 043e56efe..c7f345d73 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -5,15 +5,11 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; public class ForgeDataScript implements IScriptHandler { private List scripts = new ArrayList<>(); @@ -56,10 +52,6 @@ public class ForgeDataScript implements IScriptHandler { return compound; } - @Override - public void callScript(EnumScriptType var1, Event var2) { - this.callScript(var1.function, var2); - } public void callScript(String type, Event event) { if (this.isEnabled()) { @@ -117,25 +109,5 @@ public class ForgeDataScript implements IScriptHandler { return this.scripts; } - public String noticeString() { - return "ForgeScript"; - } - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index c4dc15913..f8580963b 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -7,7 +7,6 @@ import net.minecraft.util.math.BlockPos; import noppes.npcs.NBTTags; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; @@ -76,9 +75,6 @@ public class GlobalNPCDataScript implements IScriptHandler { return ConfigScript.GlobalNPCScripts && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; } - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } @Override public void callScript(String hookName, Event event) { @@ -145,23 +141,6 @@ public class GlobalNPCDataScript implements IScriptHandler { return this.npcAPI; } - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } public static final class ToStringHelper { private final String className; diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index 6b551feb9..69d17beb2 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -1,17 +1,25 @@ package noppes.npcs.controllers.data; +import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import noppes.npcs.constants.EnumScriptType; import java.util.List; import java.util.Map; +import java.util.TreeMap; public interface IScriptHandler { - void callScript(EnumScriptType type, Event event); + default void callScript(EnumScriptType type, Event event) { + if (type != null) + callScript(type.function, event); + + } void callScript(String hookName, Event event); - boolean isClient(); + default boolean isClient() { + return FMLCommonHandler.instance().getEffectiveSide().isClient(); + } boolean getEnabled(); @@ -25,11 +33,28 @@ public interface IScriptHandler { List getScripts(); - String noticeString(); + default String noticeString() { + return ""; + } - Map getConsoleText(); + default Map getConsoleText() { + Map map = new TreeMap<>(); + int tab = 0; + + for (IScriptUnit script : getScripts()) { + ++tab; + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); + } + } + return map; + } - void clearConsole(); + default void clearConsole() { + for (IScriptUnit script : getScripts()) + script.clearConsole(); + + } /** * Create a new Janino (Java) script unit for this handler. diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index a2f3a3025..2796586a2 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -1,21 +1,12 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; 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.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; public class LinkedItemScript implements IScriptHandler { public IScriptUnit container; @@ -106,26 +97,6 @@ public class LinkedItemScript implements IScriptHandler { return "LinkedItem"; } - @Override - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } - public void saveScript(ByteBuf buffer) throws IOException { int tab = buffer.readInt(); int totalScripts = buffer.readInt(); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index c8dd007f7..1fd06d927 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -89,9 +89,6 @@ public class PlayerDataScript implements IScriptHandler { return ConfigScript.GlobalPlayerScripts && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); } - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } @Override public void callScript(String hookName, Event event) { @@ -187,10 +184,9 @@ public class PlayerDataScript implements IScriptHandler { public String noticeString() { if (this.player == null) { return "Global script"; - } else { - BlockPos pos = new BlockPos(this.player); - return PlayerDataScript.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } + BlockPos pos = new BlockPos(this.player); + return PlayerDataScript.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } public IPlayer getPlayer() { @@ -201,28 +197,19 @@ public class PlayerDataScript implements IScriptHandler { } public Map getConsoleText() { - if (ConfigScript.IndividualPlayerScripts) + if (ConfigScript.IndividualPlayerScripts) { return console; - - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } } - return map; + return IScriptHandler.super.getConsoleText(); } + @Override public void clearConsole() { - if (ConfigScript.IndividualPlayerScripts) + if (ConfigScript.IndividualPlayerScripts) { console.clear(); - else { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } + return; } + IScriptHandler.super.clearConsole(); } public static final class ToStringHelper { From f829926f00260eb962b255a38a58f258f8a98e83 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:19:30 +0200 Subject: [PATCH 26/64] New ScriptHandler, which is an abstract IScriptHandler implementation --- .../npcs/controllers/data/EffectScript.java | 98 +------------------ .../controllers/data/LinkedItemScript.java | 65 +----------- .../npcs/controllers/data/RecipeScript.java | 93 +----------------- .../npcs/controllers/data/ScriptHandler.java | 64 ++++++++++++ 4 files changed, 67 insertions(+), 253 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/ScriptHandler.java diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index dcd497fc8..ee5f63d90 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -1,27 +1,15 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; 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.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.scripted.event.player.PlayerEvent; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -public class EffectScript implements IScriptHandler { - public IScriptUnit container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; +public class EffectScript extends ScriptHandler { public NBTTagCompound writeToNBT(NBTTagCompound compound) { @@ -47,90 +35,6 @@ public class EffectScript implements IScriptHandler { callScript(type.function, event); } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; - } - - @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) { - return; - } - if (container != null) { - container.run(s, event); - } - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); - } - - @Override - public String noticeString() { - return ""; - } - - @Override - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } public void saveScript(ByteBuf buffer) throws IOException { int tab = buffer.readInt(); diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 2796586a2..3b09c5766 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -8,10 +8,7 @@ import noppes.npcs.controllers.ScriptController; import java.io.IOException; -public class LinkedItemScript implements IScriptHandler { - public IScriptUnit container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; +public class LinkedItemScript extends ScriptHandler { public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ScriptLanguage", scriptLanguage); @@ -32,66 +29,6 @@ public class LinkedItemScript implements IScriptHandler { return this; } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; - } - - @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) { - return; - } - if (container != null) { - container.run(s, event); - } - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); - } - @Override public String noticeString() { return "LinkedItem"; diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index ee1b32c08..b498d233c 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -1,26 +1,14 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; 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.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -public class RecipeScript implements IScriptHandler { - public IScriptUnit container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; +public class RecipeScript extends ScriptHandler { public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ScriptLanguage", scriptLanguage); @@ -39,90 +27,11 @@ public class RecipeScript implements IScriptHandler { return this; } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; - } - - @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) - return; - if (container != null) { - container.run(s, event); - } - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); - } - @Override public String noticeString() { return "RecipeScript"; } - @Override - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } - public void saveScript(ByteBuf buffer) throws IOException { int tab = buffer.readInt(); int totalScripts = buffer.readInt(); diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java new file mode 100644 index 000000000..f5ea44383 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -0,0 +1,64 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class ScriptHandler implements IScriptHandler { + public IScriptUnit container; + protected String scriptLanguage = "ECMAScript"; + protected boolean enabled = false; + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + container.run(hookName, event); + } + + @Override + public boolean getEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getLanguage() { + return scriptLanguage; + } + + @Override + public void setLanguage(String language) { + this.scriptLanguage = language; + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty()) { + container = null; + return; + } + container = list.get(0); + } + + @Override + public List getScripts() { + if (container == null) { + return new ArrayList<>(); + } + return Collections.singletonList(container); + } +} From 86285ceff5ae21d5a92a502034bdb998be934a9b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:26:31 +0200 Subject: [PATCH 27/64] - New JaninoScriptHandler thats a concrete IScriptHandler impl and replaces JaninoScriptable - Removed ScriptHandler based methods from JaninoScript --- .../client/gui/script/GuiJaninoScript.java | 7 +- .../controllers/data/JaninoScriptHandler.java | 121 ++++++++++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 57 ++------- .../noppes/npcs/janino/JaninoScriptable.java | 38 ------ 4 files changed, 139 insertions(+), 84 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java delete mode 100644 src/main/java/noppes/npcs/janino/JaninoScriptable.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index 2f609b205..04d078fda 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -166,7 +166,8 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback this.getScriptIndex())); // this.getButton(103).enabled = !this.languages.isEmpty(); this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.container.getEnabled() ? 1 : 0)); + // Note: GuiJaninoScript is deprecated; enabled button disabled + this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, 0)); this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); @@ -280,7 +281,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback } //Enable else if (guibutton.id == 104) { - this.container.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + // Deprecated GUI - enabled is now per-handler, not per-unit } //Remove else if (guibutton.id == 105) { @@ -347,7 +348,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback } private String getConsoleText() { - Map map = this.container.getConsoleText(); + Map map = this.container.getConsole(); StringBuilder builder = new StringBuilder(); Iterator var3 = map.entrySet() .iterator(); diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java new file mode 100644 index 000000000..81046844e --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -0,0 +1,121 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.JaninoScript; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * Reusable IScriptHandler implementation for objects that own an optional single JaninoScript unit. + * + * This is meant for "function provider" scripts (not EnumScriptType driven). Execution is typically + * initiated by the owning system calling script.call(...) directly. + * + * NBT storage is kept under the parent compound key "Script" (same convention as JaninoScriptable). + */ +public class JaninoScriptHandler> extends ScriptHandler { + + protected S script; + + private final Supplier factory; + private final Class scriptClass; + + public JaninoScriptHandler(Supplier factory, Class scriptClass) { + this.factory = factory; + this.scriptClass = scriptClass; + scriptLanguage = "Java"; + } + + // -------------------- Script ownership (JaninoScriptable) -------------------- + + public boolean hasScript() { + return script != null; + } + + public S getScript() { + return script; + } + + public JaninoScript createScript() { + if (script == null) + script = factory.get(); + + return script; + } + + public void deleteScript() { + unloadScript(); + script = null; + } + + public void unloadScript() { + if (script != null) { + script.unload(); + } + } + + // -------------------- NBT helpers -------------------- + + public void readFromNBT(NBTTagCompound compound) { + // JaninoScriptable's helper generics assume JaninoScript, but most of our scripts are JaninoScript. + // Keep it simple and safe: treat it as raw. + script = (S) JaninoScript.readFromNBT(compound, (JaninoScript) script, (Supplier) factory); + } + + public void writeToNBT(NBTTagCompound compound) { + JaninoScript.writeToNBT(compound, (JaninoScript) script); + } + + // -------------------- IScriptHandler -------------------- + + @Override + public void callScript(String hookName, Event event) { + // No-op: these scripts are not executed through the global hook dispatcher. + } + + @Override + public void callScript(EnumScriptType type, Event event) { + // No-op + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty() || list.get(0) == null) { + deleteScript(); + return; + } + + IScriptUnit unit = list.get(0); + if (scriptClass.isInstance(unit)) { + this.script = scriptClass.cast(unit); + return; + } + + // Fallback: copy code into a real instance of this script type. + S target = scriptClass.cast(createScript()); + target.setScript(unit.getScript()); + target.setExternalScripts(unit.getExternalScripts()); + } + + @Override + public List getScripts() { + if (script == null) + return Collections.emptyList(); + + return Collections.singletonList(script); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return factory.get(); + } + + @Override + public boolean supportsJanino() { + return true; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 22fb9ee95..bc3d8e915 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -22,10 +22,10 @@ import java.security.*; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; public abstract class JaninoScript implements IScriptUnit { - public boolean enabled; public boolean errored = false; private String language = "Java"; public TreeMap console = new TreeMap(); @@ -157,14 +157,6 @@ public abstract class JaninoScript implements IScriptUnit { } public void ensureCompiled() { - if (!isEnabled()) { - if (!evaluated) { - compileScript(""); - evaluated = true; - } - return; - } - int global = ScriptController.Instance.globalRevision; if (!evaluated || global != lastSeenGlobalRevision) { @@ -182,9 +174,6 @@ public abstract class JaninoScript implements IScriptUnit { private int lastSeenGlobalRevision; private String getFullCode() { - if (!isEnabled()) - return ""; - StringBuilder sb = new StringBuilder(); if (ConfigScript.RunLoadedScriptsFirst) @@ -306,6 +295,8 @@ public abstract class JaninoScript implements IScriptUnit { public void setExternalScripts(List externalScripts) { this.externalScripts = externalScripts; this.evaluated = false; + this.hookNameMap.clear(); + this.missingHookNames.clear(); } @Override @@ -470,7 +461,6 @@ public abstract class JaninoScript implements IScriptUnit { @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_JANINO); - compound.setBoolean("enabled", enabled); compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); compound.setTag("externalScripts", NBTTags.nbtStringList(this.externalScripts)); compound.setString("script", script); @@ -479,46 +469,27 @@ public abstract class JaninoScript implements IScriptUnit { @Override public void readFromNBT(NBTTagCompound compound) { - this.enabled = compound.getBoolean("enabled"); + // Read legacy enabled field but ignore it (handler owns enabled now) this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); setScript(compound.getString("script")); } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled; - } + public static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { + if (compound.hasKey("Script")) { + if (script == null) + script = factory.get(); - public boolean isClient() { - return FMLCommonHandler.instance() - .getEffectiveSide() - .isClient(); - } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean b) { - if (this.enabled != b) { - this.enabled = b; - this.evaluated = false; + script.readFromNBT(compound.getCompoundTag("Script")); } + return script; } - public Map getConsoleText() { - TreeMap map = new TreeMap(); - int tab = 0; + public static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { + if (script != null) + compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); - Iterator var5 = console.entrySet() - .iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); - } - - return map; + return compound; } @Override diff --git a/src/main/java/noppes/npcs/janino/JaninoScriptable.java b/src/main/java/noppes/npcs/janino/JaninoScriptable.java deleted file mode 100644 index 9700bf893..000000000 --- a/src/main/java/noppes/npcs/janino/JaninoScriptable.java +++ /dev/null @@ -1,38 +0,0 @@ -package noppes.npcs.janino; - -import net.minecraft.nbt.NBTTagCompound; - -import java.util.function.Supplier; - -/** - - */ -public interface JaninoScriptable> { - - boolean hasScript(); - - JaninoScript getScript(); - - JaninoScript createScript(); - - void deleteScript(); - - void unloadScript(); - - static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { - if (compound.hasKey("Script")) { - if (script == null) - script = factory.get(); - - script.readFromNBT(compound.getCompoundTag("Script")); - } - return script; - } - - static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { - if (script != null) - compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); - - return compound; - } -} From 44f71ddb814d6fd7f44d5a4808bd3ca653a6c12c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 04:04:17 +0200 Subject: [PATCH 28/64] Handled script language correctly for external script selector list + added forgotten singleContainer logic --- .../client/gui/script/GuiScriptInterface.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 7e1bfa0e1..49d4e56f7 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -564,7 +564,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == 107) { - IScriptUnit container = this.handler.getScripts().get(this.activeTab - 1); + IScriptUnit container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) @@ -573,7 +573,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.handler.getScripts().add(container); } - this.setSubGui(new EventGuiScriptList((List) this.languages.get(this.handler.getLanguage()), container)); + String language = container != null ? container.getLanguage() : this.handler.getLanguage(); + this.setSubGui(new EventGuiScriptList((List) this.languages.get(language), container)); } // Language toggle button - switch between ECMAScript and Java @@ -598,10 +599,13 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return; // Handler doesn't support Janino } } - - // Replace the script unit in the list - handler.getScripts().set(idx, newUnit); - + + if (singleContainer) { + setHandlerUnit(newUnit); + } else { + // Replace the script unit in the list + handler.getScripts().set(idx, newUnit); + } // Clear the cached text area so it recreates with new language textAreas.remove(idx); From eecb5b0b3bf0e13bfbdb862c53bbbda7994fc502 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 17:50:04 +0200 Subject: [PATCH 29/64] ScriptHandler refactor: - ScriptHandler now holds a list of containers - Delegated single container handling to SingleScriptHandler IScriptHandler refactor: - Moved ScriptContext/Hook logic to it - More default methods + hooked them to GuIScriptInterface --- .../client/gui/script/GuiScriptInterface.java | 69 ++++---- .../npcs/controllers/data/IScriptHandler.java | 88 ++++++++++ .../npcs/controllers/data/ScriptHandler.java | 35 ++-- .../controllers/data/SingleScriptHandler.java | 153 ++++++++++++++++++ 4 files changed, 294 insertions(+), 51 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 49d4e56f7..4eb7ccca2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,6 +27,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -49,7 +50,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - public boolean singleContainer = false; /** If true, uses scroll-based tab selection instead of top buttons */ protected boolean useScrollTabs = false; @@ -97,6 +97,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb gui.parent = parent; gui.singleContainer = true; gui.displayGuiScreen(gui); + + // Initialize hooks from handler + gui.hookList = new ArrayList<>(handler.getHooks()); + + + Minecraft.getMinecraft().displayGuiScreen(gui); return gui; } @@ -113,6 +119,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.guiTop += 10; int yoffset = (int) ((double) this.ySize * 0.02D); + if (hookList.isEmpty()) + hookList = new ArrayList<>(handler.getHooks()); + // ==================== TAB BUTTONS ==================== // Skip standard tab creation for GUIs that use custom toggle/scroll tabs if (!useSettingsToggle) { @@ -123,7 +132,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb int topXoffset = 0; int topYoffset = 0; - if(!singleContainer) { + boolean isSingle = handler.isSingleContainer(); + if (!isSingle) { for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { if (ta % 20 == 0 && ta > 0) { topYoffset -= 20; @@ -137,7 +147,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } } - if (singleContainer && getFirst() != null) + if (isSingle && handler.getSingleScript() != null) this.addTopButton( top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); @@ -233,10 +243,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); - activeArea.setLanguage(language); - - // Set the script context for context-aware hook autocomplete - activeArea.setScriptContext(getScriptContext()); + activeArea.setLanguage(language); // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); @@ -320,6 +327,16 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return this; } + /** + * Get the script context for this GUI. + * Override in subclasses to return a different context. + * + * @return The script context (default: GLOBAL, or handler's context if available) + */ + protected ScriptContext getScriptContext() { + return handler.getContext(); + } + // ==================== RENDERING ==================== @Override @@ -418,10 +435,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); } if (i == 10) { - if (singleContainer) - setHandlerUnit(null); - else - this.handler.getScripts().remove(this.activeTab - 1); + handler.removeScriptUnit(this.activeTab - 1); this.activeTab = 0; } if (i == 101) { @@ -482,11 +496,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return super.getTextField(id); } - public IScriptUnit getFirst() { - if (this.handler.getScripts().isEmpty()) - return null; - return this.handler.getScripts().get(0); - } @Override protected void actionPerformed(GuiButton guibutton) { if (guibutton.id >= 0 && guibutton.id < scriptLimit) { @@ -501,14 +510,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == scriptLimit) { - if (singleContainer) { - if (getFirst() == null) - setHandlerUnit(new ScriptContainer(handler)); - else + if (handler.isSingleContainer()) { + if (handler.getSingleScript() == null) { + handler.addScriptUnit(new ScriptContainer(handler)); + } else { this.setScript(); + } this.activeTab = 1; } else { - this.handler.getScripts().add(new ScriptContainer(this.handler)); + handler.addScriptUnit(new ScriptContainer(this.handler)); this.activeTab = this.handler.getScripts().size(); } this.initGui(); @@ -567,10 +577,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb IScriptUnit container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); - if (singleContainer) - setHandlerUnit(container); - else - this.handler.getScripts().add(container); + handler.addScriptUnit(container); } String language = container != null ? container.getLanguage() : this.handler.getLanguage(); @@ -600,12 +607,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } } - if (singleContainer) { - setHandlerUnit(newUnit); - } else { - // Replace the script unit in the list - handler.getScripts().set(idx, newUnit); - } + handler.replaceScriptUnit(idx, newUnit); // Clear the cached text area so it recreates with new language textAreas.remove(idx); @@ -621,10 +623,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (container == null) { container = new ScriptContainer(this.handler); - if (singleContainer) - setHandlerUnit(container); - else - this.handler.getScripts().add(container); + handler.addScriptUnit(container); } if (container != null) { diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index 69d17beb2..cc47eb1c8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -3,7 +3,10 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -74,4 +77,89 @@ public interface IScriptHandler { default boolean supportsJanino() { return createJaninoScriptUnit() != null; } + + /** + * Get the script context for autocomplete and type resolution. + * + * @return The script context (default: GLOBAL) + */ + default ScriptContext getContext() { + return ScriptContext.GLOBAL; + } + + /** + * Get the hook context identifier for this handler. + * Used by ScriptHookController to look up available hooks. + * + * @return The context string (e.g., IScriptHookHandler.CONTEXT_EFFECT), or empty if none + */ + default String getHookContext() { + return ""; + } + + /** + * Get the list of available hooks for this handler. + * Uses getHookContext() to look up hooks from ScriptHookController. + * + * @return List of hook names, or empty list if no context + */ + default List getHooks() { + String context = getHookContext(); + if (context == null || context.isEmpty()) + return Collections.emptyList(); + + return ScriptHookController.Instance.getAllHooks(context); + } + + /** + * Check if this handler manages only a single script container. + * Single-container handlers use simplified UI (no numbered tabs). + * + * @return true if this handler uses a single container + */ + default boolean isSingleContainer() { + return false; + } + + /** + * Get the single script unit for single-container handlers. + * Convenience method that returns the first script or null. + * + * @return The single script unit, or null if none exists + */ + default IScriptUnit getSingleScript() { + List scripts = getScripts(); + return (scripts != null && !scripts.isEmpty()) ? scripts.get(0) : null; + } + + /** + * Add a script unit to this handler. + * For single-container handlers, replaces the current unit. + */ + default void addScriptUnit(IScriptUnit unit) { + if (unit != null) + getScripts().add(unit); + } + + /** + * Replace a script unit at the given index. + * For single-container handlers, replaces the current unit. + */ + default void replaceScriptUnit(int index, IScriptUnit unit) { + List scripts = getScripts(); + if (index < 0 || index >= scripts.size()) + scripts.add(unit); + else + scripts.set(index, unit); + } + + /** + * Remove a script unit at the given index. + * For single-container handlers, clears the unit. + */ + default void removeScriptUnit(int index) { + List scripts = getScripts(); + if (index >= 0 && index < scripts.size()) + scripts.remove(index); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index f5ea44383..def6ca7cf 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -5,24 +5,36 @@ import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public abstract class ScriptHandler implements IScriptHandler { - public IScriptUnit container; + public List scripts = new ArrayList<>(); protected String scriptLanguage = "ECMAScript"; protected boolean enabled = false; + protected long lastInited = -1; protected boolean canRunScripts() { - return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && scripts != null && !scripts.isEmpty(); } @Override public void callScript(String hookName, Event event) { - if (!canRunScripts()) { + if (!canRunScripts()) return; + + if (ScriptController.Instance.lastLoaded > lastInited) { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : scripts) { + if (script != null) + script.setErrored(false); + } + } + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(hookName, event); } - container.run(hookName, event); } @Override @@ -45,20 +57,11 @@ public abstract class ScriptHandler implements IScriptHandler { this.scriptLanguage = language; } - @Override public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); + this.scripts = list; } - @Override public List getScripts() { - if (container == null) { - return new ArrayList<>(); - } - return Collections.singletonList(container); + return this.scripts; } } diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java new file mode 100644 index 000000000..a047b41b6 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -0,0 +1,153 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +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.controllers.ScriptController; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Base class for script handlers that manage a single script container. + * Provides shared NBT/ByteBuf serialization and packet handling. + * + * Subclasses should: + * - Store their parent object (e.g., CustomEffect, LinkedItem) + * - Override getHookContext() to return the appropriate context string + * - Override getContext() if needed (default is GLOBAL) + * - Override requestData() and sendSavePacket() for packet communication + */ +public abstract class SingleScriptHandler extends ScriptHandler { + public IScriptUnit container; + + @Override + public boolean isSingleContainer() { + return true; + } + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + container.run(hookName, event); + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty()) { + container = null; + this.scripts = new ArrayList<>(); + } else { + container = list.get(0); + this.scripts = new ArrayList<>(1); + this.scripts.add(container); + } + } + + @Override + public List getScripts() { + if(container == null) + return new ArrayList<>(); + + return Collections.singletonList(container); + } + + public void addScriptUnit(IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void replaceScriptUnit(int index, IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void removeScriptUnit(int index) { + this.container = null; + this.scripts.clear(); + } + + /** + * Write script data to NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ScriptLanguage", scriptLanguage); + compound.setBoolean("ScriptEnabled", enabled); + + if (container != null) { + compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); + } + return compound; + } + + /** + * Read script data from NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public void readFromNBT(NBTTagCompound compound) { + scriptLanguage = compound.getString("ScriptLanguage"); + enabled = compound.getBoolean("ScriptEnabled"); + + if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { + container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); + } + } + + /** + * Save script data from a ByteBuf packet. + * Handles the tab/totalScripts protocol used by script packets. + * + * Protocol: + * - tab == 0: Script content NBT follows + * - tab != 0 (typically -1): Script metadata (language, enabled) follows + */ + public void saveScript(ByteBuf buffer) throws IOException { + int tab = buffer.readInt(); + int totalScripts = buffer.readInt(); + + if (totalScripts == 0) { + this.container = null; + } + + if (tab == 0) { + // Script content + NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); + this.container = IScriptUnit.createFromNBT(tabCompound, this); + } else { + // Metadata (language, enabled) + NBTTagCompound compound = ByteBufUtils.readNBT(buffer); + this.setLanguage(compound.getString("ScriptLanguage")); + normalizeLanguage(); + this.setEnabled(compound.getBoolean("ScriptEnabled")); + } + } + + /** + * 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 (!ScriptController.Instance.languages.isEmpty()) { + this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); + } else { + this.setLanguage("ECMAScript"); + } + } + } +} From f5dbe19de2010404a5f939c9a473e2795d2b51ee Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 17:57:10 +0200 Subject: [PATCH 30/64] =?UTF-8?q?New=20IScriptHandlerPacket:=20-=20Defines?= =?UTF-8?q?=20the=20packet-backed=20script=20GUI=20contract=20used=20by=20?= =?UTF-8?q?GuiScriptInterface=20for=20=E2=80=9Cstreamed=E2=80=9D=20script?= =?UTF-8?q?=20loading/saving.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated EffectScript, LinkedItemScript and RecipeScript to implement IScriptHandlerPacket - Removed GuiScriptEffect, GuiScriptLinkedItem and GuiScriptRecipe as their functionality got replaced with new interface handled by GuiScriptInterface --- .../npcs/client/gui/SubGuiEffectGeneral.java | 8 +- .../gui/global/GuiNpcManageRecipes.java | 13 +- .../client/gui/item/SubGuiLinkedItem.java | 8 +- .../client/gui/script/GuiScriptEffect.java | 56 -------- .../client/gui/script/GuiScriptInterface.java | 126 +++++------------- .../gui/script/GuiScriptLinkedItem.java | 55 -------- .../client/gui/script/GuiScriptRecipe.java | 69 ---------- .../npcs/controllers/data/EffectScript.java | 94 ++++++------- .../data/IScriptHandlerPacket.java | 86 ++++++++++++ .../controllers/data/LinkedItemScript.java | 94 +++++++------ .../npcs/controllers/data/RecipeScript.java | 88 ++++++------ 11 files changed, 260 insertions(+), 437 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java create mode 100644 src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java index 19903229b..db1b516ea 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java @@ -7,7 +7,8 @@ import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.client.gui.script.GuiScriptEffect; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -202,10 +203,7 @@ public class SubGuiEffectGeneral extends SubGuiInterface implements ITextfieldLi } if (id == -2) { PacketClient.sendClient(new EffectSavePacket(effect.writeToNBT(false), originalName)); - GuiScriptEffect scriptGUI = new GuiScriptEffect(parent, effect); - scriptGUI.setWorldAndResolution(mc, width, height); - scriptGUI.initGui(); - mc.currentScreen = scriptGUI; + GuiScriptInterface.open(parent, new EffectScript(effect.id)); } if (id == 10) { GuiNpcButtonYesNo button = (GuiNpcButtonYesNo) guibutton; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java index 1b83b0639..f246cb417 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java @@ -12,7 +12,8 @@ import net.minecraft.util.StatCollector; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.SubGuiNpcAvailability; -import noppes.npcs.client.gui.script.GuiScriptRecipe; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -222,15 +223,9 @@ public class GuiNpcManageRecipes extends GuiContainerNPCInterface2 implements IS if (button.id == 16) { save(); if (container.width == 1) { - GuiScriptRecipe gui = new GuiScriptRecipe(this, container.recipeAnvil); - gui.setWorldAndResolution(mc, width, height); - gui.initGui(); - mc.currentScreen = gui; + GuiScriptInterface.open(this, new RecipeScript(container.recipeAnvil.id, true)); } else if (!container.recipe.isGlobal) { - GuiScriptRecipe gui = new GuiScriptRecipe(this, container.recipe); - gui.setWorldAndResolution(mc, width, height); - gui.initGui(); - mc.currentScreen = gui; + GuiScriptInterface.open(this, new RecipeScript(container.recipe.id, false)); } } } diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index 227f7cecc..fd57c0171 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -12,7 +12,8 @@ import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.client.gui.script.GuiScriptLinkedItem; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.LinkedItemScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButton; @@ -307,10 +308,7 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe return; } else if (id == -3) { PacketClient.sendClient(new LinkedItemSavePacket(linkedItem.writeToNBT(false), originalName)); - GuiScriptLinkedItem scriptGUI = new GuiScriptLinkedItem((GuiNPCManageLinked) this.parent, linkedItem); - scriptGUI.setWorldAndResolution(mc, width, height); - scriptGUI.initGui(); - mc.currentScreen = scriptGUI; + GuiScriptInterface.open(this.parent, new LinkedItemScript(linkedItem.id)); return; } else if (id == 20) { // Open confirmation for version bump. diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java deleted file mode 100644 index 22c51b207..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ /dev/null @@ -1,56 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.CustomEffect; -import noppes.npcs.controllers.data.EffectScript; -import noppes.npcs.controllers.data.IScriptUnit; - -import java.util.ArrayList; - -public class GuiScriptEffect extends GuiScriptInterface { - - public final CustomEffect effect; - private final EffectScript scriptHandler; - - public GuiScriptEffect(GuiNPCManageEffects parent, CustomEffect effect) { - super(); - this.parent = parent; - this.effect = effect; - this.scriptHandler = new EffectScript(); - this.handler = this.scriptHandler; - this.singleContainer = true; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_EFFECT)); - - EffectScriptPacket.Get(effect.id); - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.GLOBAL; - } - - protected void setHandlerUnit(IScriptUnit container) { - this.scriptHandler.container = container; - } - - @Override - public void setGuiData(NBTTagCompound compound) { - setGuiDataWithOldContainer(compound); - } - - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - EffectScriptPacket.Save(effect.id, index, totalCount, scriptNBT); - } - - @Override - public void save() { - saveWithPackets(); - } -} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 4eb7ccca2..76819ab56 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -91,16 +91,17 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Create a GuiScriptInterface for a single script without a handler. * Wraps the script in a SingleScriptHandler automatically. */ - public static GuiScriptInterface create(GuiScreen parent, IScriptHandler handler) { + public static GuiScriptInterface open(GuiScreen parent, IScriptHandler handler) { GuiScriptInterface gui = new GuiScriptInterface(); gui.handler = handler; gui.parent = parent; - gui.singleContainer = true; - gui.displayGuiScreen(gui); // Initialize hooks from handler gui.hookList = new ArrayList<>(handler.getHooks()); + // Request data from server + if (handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) handler).requestData(); Minecraft.getMinecraft().displayGuiScreen(gui); return gui; @@ -637,60 +638,29 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } public void setGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); + if (handler instanceof IScriptHandlerPacket) { + IScriptHandlerPacket.GuiDataResult result = ((IScriptHandlerPacket) handler).setGuiData(compound); + if (result.kind == IScriptHandlerPacket.GuiDataKind.LOAD_COMPLETE) { + loaded = true; + return; + } + if (result.kind == IScriptHandlerPacket.GuiDataKind.METADATA) { + loadLanguagesData(compound); + return; + } + if (result.kind == IScriptHandlerPacket.GuiDataKind.TAB) { + initGui(); + return; } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - /** - * Get the script context for this GUI. - * Override in subclasses to return the appropriate context. - * - * @return The script context (default: GLOBAL) - */ - protected ScriptContext getScriptContext() { - return ScriptContext.GLOBAL; - } - - // ==================== UNIFIED SCRIPT DATA HANDLING ==================== - - /** - * Unified setGuiData for script GUIs with old container system. - * Handles both language data and tab-specific script loading. - */ - protected void setGuiDataWithOldContainer(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; return; } - - if (!compound.hasKey("Tab")) { - this.handler.setLanguage(compound.getString("ScriptLanguage")); - this.handler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.loadLanguagesData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, this.handler); - this.setHandlerUnit(container); - this.initGui(); - } + + loadLanguagesData(compound); } + + // ==================== UNIFIED SCRIPT DATA HANDLING ==================== + /** * Load languages data from NBT. * Separated for potential override in subclasses if needed. @@ -715,50 +685,16 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.initGui(); } - /** - * Set the handler's container. Override if handler is not IScriptHandler. - */ - protected void setHandlerUnit(IScriptUnit container) { - // Default implementation - subclasses may need to cast and set differently - // e.g., ((LinkedItemScript) handler).container = container; - } - - /** - * Unified save method for script GUIs with packet-based saving. - * Subclasses only need to override sendScriptPackets() and sendMetadataPacket(). - */ - protected void saveWithPackets() { - if (loaded) { - this.setScript(); - - List containers = this.handler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.handler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.handler.getEnabled()); - scriptData.setTag("ScriptConsole", noppes.npcs.NBTTags.NBTLongStringMap(this.handler.getConsoleText())); - - sendSavePacket(-1, containers.size(), scriptData); - } - } - - /** - * Send a script container packet. Override in subclasses. - * @param index The index of this script - * @param totalCount Total number of scripts - * @param scriptNBT The script container NBT data - */ - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - // Default: do nothing (for non-packet-based GUIs like GuiScriptItem) - } - public void save() { - if (loaded) - this.setScript(); + if (!loaded) + return; + + this.setScript(); + + if (handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) handler).sync(); + + } public void textUpdate(String text) { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java deleted file mode 100644 index 881e3dd6b..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ /dev/null @@ -1,55 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.LinkedItem; -import noppes.npcs.controllers.data.LinkedItemScript; - -import java.util.ArrayList; - -public class GuiScriptLinkedItem extends GuiScriptInterface { - - public final LinkedItem linkedItem; - - public GuiScriptLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { - super(); - this.parent = parent; - this.linkedItem = linkedItem; - this.handler = new LinkedItemScript(); - this.singleContainer = true; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_LINKED_ITEM)); - - LinkedItemScriptPacket.Get(linkedItem.id); - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.ITEM; - } - - protected void setHandlerUnit(IScriptUnit container) { - ((LinkedItemScript) handler).container = container; - } - - @Override - public void setGuiData(NBTTagCompound compound) { - setGuiDataWithOldContainer(compound); - } - - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - LinkedItemScriptPacket.Save(linkedItem.id, index, totalCount, scriptNBT); - } - - @Override - public void save() { - saveWithPackets(); - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java deleted file mode 100644 index 83316a48d..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ /dev/null @@ -1,69 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.client.gui.global.GuiNpcManageRecipes; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.RecipeAnvil; -import noppes.npcs.controllers.data.RecipeCarpentry; -import noppes.npcs.controllers.data.RecipeScript; - -import java.util.ArrayList; - -public class GuiScriptRecipe extends GuiScriptInterface { - - public final int recipeId; - public final boolean anvil; - - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeCarpentry recipe) { - super(); - this.parent = parent; - this.handler = new RecipeScript(); - this.anvil = false; - this.recipeId = recipe.id; - this.singleContainer = true; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE)); - - RecipeScriptPacket.Get(false, recipeId); - } - - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeAnvil recipe) { - super(); - this.parent = parent; - this.handler = new RecipeScript(); - this.anvil = true; - this.recipeId = recipe.id; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE)); - - RecipeScriptPacket.Get(true, recipeId); - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.GLOBAL; - } - - protected void setHandlerUnit(IScriptUnit container) { - ((RecipeScript) handler).container = container; - } - - @Override - public void setGuiData(NBTTagCompound compound) { - setGuiDataWithOldContainer(compound); - } - - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, scriptNBT); - } - - @Override - public void save() { - saveWithPackets(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index ee5f63d90..8169dbbd7 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -1,63 +1,55 @@ package noppes.npcs.controllers.data; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.scripted.event.player.PlayerEvent; -import java.io.IOException; - -public class EffectScript extends ScriptHandler { - - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; +/** + * Script handler for CustomEffect scripts. + * Manages a single script container for effect hooks (onEffectAdd, onEffectTick, onEffectRemove). + */ +public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** The effect ID for packet communication. -1 if not bound to an effect. */ + private int effectId = -1; + + /** + * Create an unbound EffectScript (for server-side use). + */ + public EffectScript() { } - - public EffectScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); - } - return this; + + /** + * Create an EffectScript bound to a specific effect (for GUI use). + * @param effectId The ID of the CustomEffect + */ + public EffectScript(int effectId) { + this.effectId = effectId; } - + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_EFFECT; + } + + /** + * Convenience method to call a script with a typed event. + */ public void callScript(ScriptType type, PlayerEvent.EffectEvent event) { callScript(type.function, event); } - - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) { - this.container = null; - } - - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - this.container = IScriptUnit.createFromNBT(tabCompound, this); - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } + + @Override + public void requestData() { + if (effectId >= 0) + EffectScriptPacket.Get(effectId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (effectId >= 0) + EffectScriptPacket.Save(effectId, index, totalCount, nbt); } public enum ScriptType { @@ -71,4 +63,4 @@ public class EffectScript extends ScriptHandler { this.function = functionName; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java new file mode 100644 index 000000000..c307f508d --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java @@ -0,0 +1,86 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NBTTags; + +import java.util.List; + +public interface IScriptHandlerPacket extends IScriptHandler { + + /** + * Client-side: request initial script data from the server (typically a {@code ...Packet.Get(...)}). + */ + void requestData(); + + /** + * Client-side: send a script container packet to the server. + * + * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) + * @param totalCount Total number of scripts for this handler + * @param nbt The script container NBT (tab content or metadata) + */ + void sendSavePacket(int index, int totalCount, NBTTagCompound nbt); + + enum GuiDataKind { + LOAD_COMPLETE, + METADATA, + TAB + } + + class GuiDataResult { + public final GuiDataKind kind; + public final int tabIndex; + + public GuiDataResult(GuiDataKind kind, int tabIndex) { + this.kind = kind; + this.tabIndex = tabIndex; + } + } + + /** + * Apply one incoming gui-data payload to this handler. + *

+ * Notes: + *

    + *
  • {@code Languages} / {@code ScriptConsole} are not consumed here; GuiScriptInterface owns those UI concerns.
  • + *
  • Tab payloads are converted to an {@link IScriptUnit} via {@link IScriptUnit#createFromNBT}.
  • + *
+ * + * @return A small classification result so GuiScriptInterface can update GUI state. + */ + default GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + if (!compound.hasKey("Tab")) { + setLanguage(compound.getString("ScriptLanguage")); + setEnabled(compound.getBoolean("ScriptEnabled")); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + int tab = compound.getInteger("Tab"); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit unit = IScriptUnit.createFromNBT(scriptCompound, this); + replaceScriptUnit(tab, unit); + return new GuiDataResult(GuiDataKind.TAB, tab); + } + + /** + * Emit save packets for all tabs plus one metadata packet. + * GuiScriptInterface is responsible for updating the active {@link IScriptUnit} with current editor text first. + */ + default void sync() { + List containers = getScripts(); + for (int i = 0; i < containers.size(); i++) { + IScriptUnit container = containers.get(i); + sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); + } + + NBTTagCompound scriptData = new NBTTagCompound(); + scriptData.setString("ScriptLanguage", getLanguage()); + scriptData.setBoolean("ScriptEnabled", getEnabled()); + scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(getConsoleText())); + sendSavePacket(-1, containers.size(), scriptData); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 3b09c5766..4d01e1c46 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -1,60 +1,58 @@ package noppes.npcs.controllers.data; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; -import java.io.IOException; - -public class LinkedItemScript extends ScriptHandler { - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; +/** + * Script handler for LinkedItem scripts. + * Manages a single script container for linked item hooks. + */ +public class LinkedItemScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** The linked item ID for packet communication. -1 if not bound. */ + private int linkedItemId = -1; + + /** + * Create an unbound LinkedItemScript (for server-side use). + */ + public LinkedItemScript() { } - - public LinkedItemScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); - } - return this; + + /** + * Create a LinkedItemScript bound to a specific linked item (for GUI use). + * @param linkedItemId The ID of the LinkedItem + */ + public LinkedItemScript(int linkedItemId) { + this.linkedItemId = linkedItemId; } - + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_LINKED_ITEM; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.ITEM; + } + @Override public String noticeString() { return "LinkedItem"; } - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) { - this.container = null; - } - - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - this.container = IScriptUnit.createFromNBT(tabCompound, this); - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } + + @Override + public void requestData() { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Get(linkedItemId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Save(linkedItemId, index, totalCount, nbt); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index b498d233c..ab5c67fb5 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -1,57 +1,57 @@ package noppes.npcs.controllers.data; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.api.handler.IScriptHookHandler; -import java.io.IOException; - -public class RecipeScript extends ScriptHandler { - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; +/** + * Script handler for Recipe scripts (both carpentry and anvil). + * Manages a single script container for recipe hooks (pre, post). + */ +public class RecipeScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** The recipe ID for packet communication. -1 if not bound. */ + private int recipeId = -1; + + /** Whether this is an anvil recipe (vs carpentry). */ + private boolean anvil = false; + + /** + * Create an unbound RecipeScript (for server-side use). + */ + public RecipeScript() { } - - public RecipeScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); - } - return this; + + /** + * Create a RecipeScript bound to a specific recipe (for GUI use). + * @param recipeId The ID of the recipe + * @param anvil true for anvil recipe, false for carpentry recipe + */ + public RecipeScript(int recipeId, boolean anvil) { + this.recipeId = recipeId; + this.anvil = anvil; } - + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_RECIPE; + } + @Override public String noticeString() { return "RecipeScript"; } - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) - this.container = null; - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - this.container = IScriptUnit.createFromNBT(tabCompound, this); - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } + + @Override + public void requestData() { + if (recipeId >= 0) + RecipeScriptPacket.Get(anvil, recipeId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (recipeId >= 0) + RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, nbt); } public enum ScriptType { From 4a8e6df034ed48d1c53069fd22791be4af553f5a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 18:20:13 +0200 Subject: [PATCH 31/64] Add gradle-plugins submodule --- .gitignore | 1 - .gitmodules | 3 +++ gradle-plugins | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 160000 gradle-plugins diff --git a/.gitignore b/.gitignore index ed907b3d2..f24532312 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ /out/ /build/ /eclipse/ -/gradle-plugins/ .classpath .project /bin/ diff --git a/.gitmodules b/.gitmodules index d8252cb6a..d3428cb3d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = src/api url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git branch = dev +[submodule "gradle-plugins"] + path = gradle-plugins + url = https://github.com/bigguy345/dts-gradle-plugin diff --git a/gradle-plugins b/gradle-plugins new file mode 160000 index 000000000..e193079bf --- /dev/null +++ b/gradle-plugins @@ -0,0 +1 @@ +Subproject commit e193079bf7392e83d233ce2e120ee0f2b645021f From a032d24d6576f40ac6f20aa42c646ab748fc143d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 03:18:41 +0200 Subject: [PATCH 32/64] Fixed crash --- .../controllers/data/JaninoScriptHandler.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index 81046844e..4dfd5540f 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -109,6 +109,21 @@ public class JaninoScriptHandler> extends ScriptHandle return Collections.singletonList(script); } + @Override + public void addScriptUnit(IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void replaceScriptUnit(int index, IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void removeScriptUnit(int index) { + deleteScript(); + } + @Override public IScriptUnit createJaninoScriptUnit() { return factory.get(); From af62868387eb2cca9182b87b8e25a5980d3c7f80 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 03:26:02 +0200 Subject: [PATCH 33/64] 1. JaninoHookResolver 2. Janino reflection based function calls like: Color script = scriptHandler.callFunction("getColor", Color.class, ctx); Compared to current: Color script = scriptHandler.getScript().call(f -> f.getColor(ctx)); --- .../npcs/controllers/data/IScriptHandler.java | 45 ++- .../npcs/controllers/data/IScriptUnit.java | 29 +- .../controllers/data/JaninoScriptHandler.java | 7 + .../npcs/janino/JaninoHookResolver.java | 319 ++++++++++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 144 +++----- 5 files changed, 435 insertions(+), 109 deletions(-) create mode 100644 src/main/java/noppes/npcs/janino/JaninoHookResolver.java diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index cc47eb1c8..b89c725a9 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -17,9 +17,52 @@ public interface IScriptHandler { callScript(type.function, event); } - + void callScript(String hookName, Event event); + /** + * Call a function by name and return its result. + * + * Default implementation calls the function on each script unit (in order) and returns + * the first non-null value. + */ + default Object callFunction(String hookName, Object... args) { + if (hookName == null || hookName.isEmpty()) + return null; + + if (!getEnabled()) + return null; + + List scripts = getScripts(); + if (scripts == null || scripts.isEmpty()) + return null; + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + + Object result = script.callFunction(hookName, args); + if (result != null) + return result; + } + + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } + default boolean isClient() { return FMLCommonHandler.instance().getEffectiveSide().isClient(); } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index 1cd661a6e..258b584b8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -161,8 +161,6 @@ public interface IScriptUnit { /** * Execute this script unit for the given hook type and event. - * For ScriptContainer: calls run(EnumScriptType, Event) - * For JaninoScript: calls callByHookType(EnumScriptType, Event) * * @param type The script hook type * @param event The event object @@ -171,11 +169,32 @@ public interface IScriptUnit { /** * Execute this script unit for the given hook name and event. - * For ScriptContainer: calls run(String, Event) - * For JaninoScript: converts hookName to EnumScriptType and calls callByHookType - * + * * @param hookName The hook name (e.g., "init", "interact") * @param event The event object */ void run(String hookName, Object event); + + /** + * Call a script function by name and return its result. + * + * Default implementation returns null (not supported). + */ + default Object callFunction(String hookName, Object... args) { + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index 4dfd5540f..28bd3af4f 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -124,6 +124,13 @@ public class JaninoScriptHandler> extends ScriptHandle deleteScript(); } + public List getHooks() { + if (script != null) + return script.getHookList(); + + return Collections.emptyList(); + } + @Override public IScriptUnit createJaninoScriptUnit() { return factory.get(); diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java new file mode 100644 index 000000000..2c8eeb543 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -0,0 +1,319 @@ +package noppes.npcs.janino; + +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.annotations.ScriptHook; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Resolves script hook methods for a given interface type using runtime + * argument values. Resolution favors exact matches and then the "closest" + * assignable parameter types (measured by inheritance distance). + * + * The resolver keeps simple caches for previously resolved signatures and + * supports mapping enum-based hook names (via the {@code @ScriptHook} + * annotation) to methods. + */ +public final class JaninoHookResolver { + private final Class interfaceType; + + /* Methods explicitly mapped from EnumScriptType via @ScriptHook */ + private final Map enumHookLookup = new HashMap<>(); + + /** + * Cache for resolved hook methods. Keys are generated by {@link #buildSignatureKey} + * and represent the unique combination of hook name and runtime argument types. + * This avoids expensive recursive reflection lookups on every hook call. + */ + private final Map signatureCache = new HashMap<>(); + + /* Lazily built list of available hooks and a mapping for display names */ + private boolean hookListInitialized; + /* List of names displayed in GUI hooks list (accounts for overload names) */ + private final List displayHookList = new ArrayList<>(); + /* Display name -> Method map */ + private final Map displayHookMethods = new HashMap<>(); + + /** + * Create a resolver for the provided script interface type. + * @param interfaceType non-null interface class that scripts implement + */ + public JaninoHookResolver(Class interfaceType) { + this.interfaceType = Objects.requireNonNull(interfaceType, "interfaceType"); + initializeEnumHookMethods(); + } + + /** + * Clears internal resolution caches so future calls perform fresh + * method resolution. Call when scripts or available methods change. + */ + public void clearResolutionCaches() { + signatureCache.clear(); + } + + /** + * Resolve a hook method by name and runtime argument values. + * + * This first checks any enum-mapped methods (via {@code @ScriptHook}), + * then a signature-based cache and finally attempts full candidate + * resolution using {@link #resolveHookMethodInternal}. + * + * @param hookName hook method name (or enum function name) + * @param args runtime arguments to use for overload resolution + * @return the resolved {@link Method} or {@code null} if none matched + */ + public Method resolveHookMethod(String hookName, Object[] args) { + if (hookName == null || hookName.isEmpty()) + return null; + + Method method = enumHookLookup.get(hookName); + if (method != null) + return method; + + Object[] safeArgs = args == null ? new Object[0] : args; + + String signatureKey = buildSignatureKey(hookName, safeArgs); + method = signatureCache.get(signatureKey); + if (method != null) + return method; + + method = resolveHookMethodInternal(interfaceType, hookName, safeArgs); + if (method != null) { + signatureCache.put(signatureKey, method); + return method; + } + + String noArgSignatureKey = buildSignatureKey(hookName, new Object[0]); + method = signatureCache.get(noArgSignatureKey); + if (method != null) + return method; + + method = resolveHookMethodInternal(interfaceType, hookName, new Object[0]); + if (method != null) { + signatureCache.put(noArgSignatureKey, method); + return method; + } + + return null; + } + + /** + * Returns an immutable copy of the available hook display names. + */ + public List getDisplayHookList() { + ensureHookListCreated(); + return new ArrayList<>(displayHookList); + } + + public Method getMethodForDisplayName(String displayName) { + if (displayName == null || displayName.isEmpty()) + return null; + + ensureHookListCreated(); + return displayHookMethods.get(displayName); + } + + /** + * Builds the human-readable hook list and display-name -> Method map. + * The list is computed lazily on first access and cached. + */ + private void ensureHookListCreated() { + if (hookListInitialized) + return; + + Map> grouped = Arrays.stream(interfaceType.getDeclaredMethods()) + .collect(Collectors.groupingBy(Method::getName)); + + for (Map.Entry> entry : grouped.entrySet()) { + List methods = entry.getValue(); + + methods.sort(Comparator.comparingInt(Method::getParameterCount)); + + boolean first = true; + for (Method m : methods) { + if (Modifier.isFinal(m.getModifiers())) + continue; + + String displayName = first ? m.getName() : + m.getName() + "(" + Arrays.stream(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")) + ")"; + + displayHookList.add(displayName); + displayHookMethods.put(displayName, m); + first = false; + } + } + + hookListInitialized = true; + } + + /** + * Maps EnumScriptType common names to their respective methods using + * the {@code @ScriptHook} annotation. + */ + private void initializeEnumHookMethods() { + for (Method method : interfaceType.getDeclaredMethods()) { + ScriptHook annotation = method.getAnnotation(ScriptHook.class); + if (annotation == null) + continue; + + for (EnumScriptType hookType : annotation.value()) { + enumHookLookup.put(hookType.function, method); + } + } + } + + /** + * Build a cache key for a hook name and runtime argument list. This key + * encodes the runtime classes of the provided arguments and is used for + * quick cache lookups. Note: keys are based on concrete runtime classes + * which may cause fragmentation when many subclasses are used. + * + * @param hookName hook name + * @param args runtime arguments + * @return a string key representing the hook signature + */ + public static String buildSignatureKey(String hookName, Object[] args) { + StringBuilder sb = new StringBuilder(); + sb.append(hookName).append('#'); + + if (args == null || args.length == 0) + return sb.append('0').toString(); + + sb.append(args.length).append(':'); + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (i > 0) + sb.append(','); + + sb.append(arg == null ? "null" : arg.getClass().getName()); + } + return sb.toString(); + } + + /** + * Internal resolution routine. Attempts to find the best matching method + * on the provided interface by comparing parameter counts and a simple + * match scoring function (see {@link #scoreMethodMatch}). + */ + private static Method resolveHookMethodInternal(Class interfaceType, String hookName, Object[] args) { + Object[] safeArgs = args == null ? new Object[0] : args; + + Method best = null; + int bestScore = Integer.MAX_VALUE; + + for (Method method : interfaceType.getDeclaredMethods()) { + if (!method.getName().equals(hookName) || Modifier.isFinal(method.getModifiers())) + continue; + + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length != safeArgs.length) + continue; + + int score = scoreMethodMatch(paramTypes, safeArgs); + if (score == Integer.MAX_VALUE) + continue; + + if (score < bestScore) { + bestScore = score; + best = method; + if (score == 0) + return best; + } + } + + return best; + } + + /** + * Score how well a method's parameter types match the provided runtime + * arguments. Lower scores are better; {@link Integer#MAX_VALUE} indicates + * an incompatible parameter. + * + * @param paramTypes method parameter types + * @param args runtime argument values + * @return score (lower is better) or Integer.MAX_VALUE for no match + */ + private static int scoreMethodMatch(Class[] paramTypes, Object[] args) { + int score = 0; + for (int i = 0; i < paramTypes.length; i++) { + Class paramType = paramTypes[i]; + Object arg = args[i]; + + if (arg == null) { + if (paramType.isPrimitive()) + return Integer.MAX_VALUE; + + score += 1; + continue; + } + + Class argType = arg.getClass(); + Class boxedParam = boxType(paramType); + + if (boxedParam.equals(argType)) + continue; + + if (boxedParam.isAssignableFrom(argType)) { + score += inheritanceDistance(argType, boxedParam); + continue; + } + + return Integer.MAX_VALUE; + } + + return score; + } + + private static Class boxType(Class type) { + if (!type.isPrimitive()) + return type; + if (type == boolean.class) + return Boolean.class; + if (type == byte.class) + return Byte.class; + if (type == short.class) + return Short.class; + if (type == int.class) + return Integer.class; + if (type == long.class) + return Long.class; + if (type == float.class) + return Float.class; + if (type == double.class) + return Double.class; + if (type == char.class) + return Character.class; + return type; + } + + /** + * Compute a simple inheritance distance between a child and parent class + * by walking superclasses. Returns a large value if {@code parent} is + * not assignable from {@code child}. + * + * Note: the matching algorithm uses superclass traversal to compute a + * distance metric; interface inheritance is not explored which can make + * matches against interfaces less optimal. Consider improving + * `inheritanceDistance` if interface-based scoring is required. + */ + private static int inheritanceDistance(Class child, Class parent) { + if (child.equals(parent)) + return 0; + + if (!parent.isAssignableFrom(child)) + return 1000; + + int distance = 0; + Class current = child; + while (current != null && !current.equals(parent)) { + current = current.getSuperclass(); + distance++; + } + return distance; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index bc3d8e915..8a217b8d0 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -1,6 +1,5 @@ package noppes.npcs.janino; -import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import io.github.somehussar.janinoloader.api.script.IScriptBodyBuilder; import io.github.somehussar.janinoloader.api.script.IScriptClassBody; @@ -12,7 +11,6 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.janino.annotations.ParamName; -import noppes.npcs.janino.annotations.ScriptHook; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; @@ -50,15 +48,8 @@ public abstract class JaninoScript implements IScriptUnit { public final IScriptBodyBuilder builder; protected IScriptClassBody scriptBody; - private boolean hooksInitialized = false; - // Hook list for GUI display - private List hookList = new ArrayList<>(); - // Map from display name (with parameters if overloaded) to Method - private Map hookMap = new HashMap<>(); - // Map from EnumScriptType to Method for OLD tab-based script system - private Map hookTypeMap = new HashMap<>(); + private final JaninoHookResolver hookResolver; - // Default imports for this script type (e.g., noppes.npcs.api.*) private final String[] defaultImports; protected JaninoScript(Class type, String[] defaultImports) { @@ -73,8 +64,7 @@ public abstract class JaninoScript implements IScriptUnit { this.scriptBody = builder.build(); - // Build hook type map from @ScriptHook annotations - initializeHookTypeMap(); + this.hookResolver = new JaninoHookResolver(type); } protected T getUnsafe() { @@ -147,8 +137,10 @@ public abstract class JaninoScript implements IScriptUnit { appendConsole(parentCause.getMessage()); appendConsole(rootCause.getMessage()); } + hookResolver.clearResolutionCaches(); } catch (Exception e) { appendConsole("Unknown error: " + e.getMessage()); + hookResolver.clearResolutionCaches(); } } @@ -197,54 +189,43 @@ public abstract class JaninoScript implements IScriptUnit { } } - /** - * Scans the Functions interface for @ScriptHook annotations and builds the hookTypeMap. - * This allows direct method invocation by EnumScriptType without switch statements. - */ - private void initializeHookTypeMap() { - for (Method method : type.getDeclaredMethods()) { - ScriptHook annotation = method.getAnnotation(ScriptHook.class); - if (annotation != null) { - for (EnumScriptType hookType : annotation.value()) { - hookTypeMap.put(hookType, method); - } - } - } + public void callByHookName(String hookName, Object arg) { + callFunction(hookName, arg); } - /** - * Call a hook method by its EnumScriptType. - * Used by the OLD tab-based script system to execute hooks directly. - * - * @param hookType The script type/hook to execute - * @param event The event object to pass to the hook method - */ - public void callByHookType(EnumScriptType hookType, Object event) { - Method method = hookTypeMap.get(hookType); - if (method == null) { - // Hook not implemented or not annotated - return; - } + @Override + public Object callFunction(String hookName, Object... args) { + if (hookName == null || hookName.isEmpty()) + return null; + + Method method = hookResolver.resolveHookMethod(hookName, args); + if (method == null) + return null; ensureCompiled(); T t = getUnsafe(); if (t == null) - return; + return null; + + Method invokeMethod = method; + Object[] invokeArgs = args == null ? new Object[0] : args; try { - sandbox.confine((PrivilegedAction) () -> { + return sandbox.confine((PrivilegedAction) () -> { try { - method.invoke(t, event); + return invokeMethod.invoke(t, invokeArgs); } catch (Exception e) { - appendConsole("Error calling hook " + hookType.name() + ": " + e.getMessage()); + appendConsole("Error calling hook " + hookName + ": " + e.getMessage()); + return null; } - return null; }); } catch (Exception e) { - appendConsole("Runtime Error in hook " + hookType.name() + ": " + e.getMessage()); + appendConsole("Runtime Error in hook " + hookName + ": " + e.getMessage()); + return null; } } + // ==================== IScriptUnit IMPLEMENTATION ==================== /** @@ -256,7 +237,10 @@ public abstract class JaninoScript implements IScriptUnit { */ @Override public void run(EnumScriptType type, Event event) { - callByHookType(type, event); + if (type == null) + return; + + callFunction(type.function, event); } /** @@ -268,13 +252,9 @@ public abstract class JaninoScript implements IScriptUnit { */ @Override public void run(String hookName, Object event) { - try { - EnumScriptType enumType = EnumScriptType.valueOfIgnoreCase(hookName); - callByHookType(enumType, event); - } catch (IllegalArgumentException ignored) { - // Hook name doesn't map to an EnumScriptType - } + callFunction(hookName, event); } + @Override public String getScript() { return this.script; @@ -284,6 +264,7 @@ public abstract class JaninoScript implements IScriptUnit { public void setScript(String script) { this.script = script; this.evaluated = false; + hookResolver.clearResolutionCaches(); } @Override @@ -295,8 +276,7 @@ public abstract class JaninoScript implements IScriptUnit { public void setExternalScripts(List externalScripts) { this.externalScripts = externalScripts; this.evaluated = false; - this.hookNameMap.clear(); - this.missingHookNames.clear(); + hookResolver.clearResolutionCaches(); } @Override @@ -343,19 +323,14 @@ public abstract class JaninoScript implements IScriptUnit { return script != null && !script.isEmpty(); } - @Override - public String generateHookStub(String hookName, Object hookData) { - // Build hook list and map - if (!hooksInitialized) { - createHookList(); - hooksInitialized = true; - } - - // Look up in hookMap (handles both simple names and overloaded display names) - Method method = hookMap.get(hookName); - if (method != null) - return generateMethodStub(method); + @Override + public String generateHookStub(String hookName, Object hookData) { + hookResolver.clearResolutionCaches(); + Method method = hookResolver.getMethodForDisplayName(hookName); + if (method != null) + return generateMethodStub(method); + // Fallback for unknown hook names - generate basic override return String.format("public void %s() {\n \n}\n", hookName); } @@ -416,45 +391,8 @@ public abstract class JaninoScript implements IScriptUnit { } - /** - * Create the hook list and hook map for GUI display and generation. - * Groups methods by name and creates display names with parameters for overloaded methods. - */ - private void createHookList() { - // Group all declared methods by their name - Map> grouped = Arrays.stream(type.getDeclaredMethods()) - .collect(Collectors.groupingBy(Method::getName)); - - for (Map.Entry> entry : grouped.entrySet()) { - List methods = entry.getValue(); - - // Sort overloads by parameter count (ascending) - methods.sort(Comparator.comparingInt(Method::getParameterCount)); - boolean first = true; - - for (Method m : methods) { - if (Modifier.isFinal(m.getModifiers())) - continue; // Skip final methods - - // For first (simplest) overload, use just the method name - // For additional overloads, include parameter types in display name - String displayName = first ? m.getName() : - m.getName() + "(" + Arrays.stream(m.getParameterTypes()) - .map(Class::getSimpleName) - .collect(Collectors.joining(", ")) + ")"; - - hookList.add(displayName); - hookMap.put(displayName, m); - first = false; - } - } - } - - /** - * Get the list of hook names for GUI display. - */ public List getHookList() { - return new ArrayList<>(hookList); + return hookResolver.getDisplayHookList(); } From f8c9faac0b394db22a33806f8581d554af1b921a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 04:06:11 +0200 Subject: [PATCH 34/64] - Implemented `getLanguageOptions()` with server-to-client fallback logic to ensure "Java/Janino" is correctly listed and synchronized in the script GUI. - Renamed `getScriptIndex` to `getLanguageIndex` in `GuiScriptInterface` and its subclasses to resolve naming ambiguity between tabs and engine selection. --- .../noppes/npcs/client/gui/GuiScript.java | 5 +- .../client/gui/script/GuiJaninoScript.java | 4 +- .../client/gui/script/GuiScriptInterface.java | 77 ++++++++++++++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 916a0ecd3..2b6f6d7c8 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -204,9 +204,10 @@ public class GuiScript extends GuiScriptInterface { consoleOptions.toArray(new String[0]), activeConsole)); addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); + List languageOptions = getLanguageOptions(); addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, - languages.keySet().toArray(new String[0]), getScriptIndex())); - getButton(103).enabled = languages.size() > 0; + languageOptions.toArray(new String[0]), getLanguageIndex(languageOptions))); + getButton(103).enabled = languageOptions.size() > 0; addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index 04d078fda..acfac310a 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -163,7 +163,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); String containerLang = container.getLanguage(); this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, new String[]{containerLang.equals("Java") ? "Janino (JVM 8)" : containerLang}, - this.getScriptIndex())); + this.getLanguageIndex())); // this.getButton(103).enabled = !this.languages.isEmpty(); this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); // Note: GuiJaninoScript is deprecated; enabled button disabled @@ -361,7 +361,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback return builder.toString(); } - private int getScriptIndex() { + private int getLanguageIndex() { return 0; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 76819ab56..f4d9b2c13 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -304,10 +304,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); + List languageOptions = getLanguageOptions(); this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, - (String[]) this.languages.keySet().toArray(new String[this.languages.keySet().size()]), - this.getScriptIndex())); - this.getButton(103).enabled = this.languages.size() > 0; + languageOptions.toArray(new String[0]), + this.getLanguageIndex(languageOptions))); + this.getButton(103).enabled = languageOptions.size() > 0; this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); @@ -405,19 +406,66 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return builder.toString(); } - public int getScriptIndex() { - int i = 0; - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.handler.getLanguage())) { + /** + * Finds the index of the current handler's language within the available options. + * + * @return The 0-based index, or 0 if not found. + */ + public int getLanguageIndex() { + return getLanguageIndex(getLanguageOptions()); + } + + /** + * Finds the index of the current handler's language within the provided list. + * + * @param languageOptions The list of available languages to search. + * @return The 0-based index, or 0 if not found. + */ + protected int getLanguageIndex(List languageOptions) { + if (languageOptions == null || languageOptions.isEmpty()) + return 0; + + int i = 0; + String current = this.handler != null ? this.handler.getLanguage() : null; + for (String language : languageOptions) { + if (current != null && language.equalsIgnoreCase(current)) return i; - } + i++; } return 0; } + /** + * Retrieves the list of available scripting languages for this handler. + *

+ * This method prioritizes languages provided by the server through the {@link #languages} map. + * If "Java" is not present but the handler supports Janino, it injects "Java" into the options + * and ensures the client-side Janino script list is synchronized for the {@link EventGuiScriptList}. + * + * @return A list of language names available for selection. + */ + protected List getLanguageOptions() { + List options = new ArrayList<>(); + if (this.languages != null && !this.languages.isEmpty()) + options.addAll(this.languages.keySet()); + + if ((options.isEmpty() || !options.contains("Java")) && this.handler != null && this.handler.supportsJanino()) { + List scripts = new ArrayList<>(); + if (ScriptController.Instance != null && ScriptController.Instance.scripts != null) { + scripts.addAll(ScriptController.Instance.scripts.keySet()); + } + if (this.languages == null) + this.languages = new HashMap(); + if (!this.languages.containsKey("Java")) + this.languages.put("Java", scripts); + options.add("Java"); + } + + return options; + } + public void confirmClicked(boolean flag, int i) { if (flag) { if (i == 0) { @@ -681,6 +729,17 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb languages.put(comp.getString("Language"), scripts); } + if (handler != null && handler.supportsJanino()) { + List javaScripts = (List) languages.get("Java"); + if (javaScripts == null || javaScripts.isEmpty()) { + javaScripts = new ArrayList<>(); + if (ScriptController.Instance != null && ScriptController.Instance.scripts != null) { + javaScripts.addAll(ScriptController.Instance.scripts.keySet()); + } + languages.put("Java", javaScripts); + } + } + this.languages = languages; this.initGui(); } From 4cb1eee0174ac50fd4e639e54ccf1f642df5c439 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 04:46:26 +0200 Subject: [PATCH 35/64] Only showed language button in script tab when script supports multiple languages --- .../noppes/npcs/client/gui/script/GuiScriptInterface.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index f4d9b2c13..f3173a2a2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -271,10 +271,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - // Language toggle button (only if handler supports Janino) - if (handler.supportsJanino() && container != null) { - String langLabel = container.isJanino() ? "Java" : "ECMAScript"; - this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, langLabel)); + // Language toggle button (only if handler supports Janino and there is more than one option) + if (handler.supportsJanino() && getLanguageOptions().size() > 1 && container != null) { + this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, container.getLanguage())); } this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); From 384b61832d2d0d4b5f5281c13f46d1fa202f849f Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 19 Jan 2026 16:29:24 -0500 Subject: [PATCH 36/64] Hitbox Fixes --- src/main/java/noppes/npcs/entity/EntityNPCInterface.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 702d27cd0..c30f15f57 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -1303,6 +1303,11 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.setRevengeTarget(null); this.deathTime = 0; //fleeingTick = 0; + + // Restore hitbox before moving entity to prevent bounding box validation issues + currentAnimation = EnumAnimation.NONE; + updateHitbox(); + if (ais.returnToStart && !hasOwner() && !this.isRemote()) setLocationAndAngles(getStartXPos(), getStartYPos(), getStartZPos(), rotationYaw, rotationPitch); killedtime = 0; @@ -1316,8 +1321,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } moveEntityWithHeading(0, 0); distanceWalkedModified = 0; - currentAnimation = EnumAnimation.NONE; - updateHitbox(); updateAI = true; ais.movingPos = 0; if (getOwner() != null) { From 692b9852371441e08fcdf9f01512ef701103e3a8 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 20 Jan 2026 23:50:42 -0500 Subject: [PATCH 37/64] Cleanup for Langs + Abilities --- .../gui/advanced/SubGuiAbilityConfig.java | 13 ++++---- .../advanced/ability/SubGuiAbilityBeam.java | 2 +- .../advanced/ability/SubGuiAbilityCharge.java | 4 +-- .../advanced/ability/SubGuiAbilityCutter.java | 4 +-- .../advanced/ability/SubGuiAbilityDash.java | 2 +- .../advanced/ability/SubGuiAbilityHeal.java | 2 +- .../ability/SubGuiAbilityHeavyHit.java | 2 +- .../advanced/ability/SubGuiAbilityOrb.java | 6 ++-- .../ability/SubGuiAbilityProjectile.java | 4 +-- .../ability/SubGuiAbilityShockwave.java | 2 +- .../advanced/ability/SubGuiAbilitySlam.java | 2 +- .../ability/SubGuiAbilityTeleport.java | 2 +- .../advanced/ability/SubGuiAbilityTrap.java | 2 +- .../advanced/ability/SubGuiAbilityVortex.java | 2 +- .../npcs/client/gui/player/GuiAuction.java | 2 +- .../gui/player/SubGuiAuctionCreate.java | 2 +- .../gui/select/GuiAnimationSelection.java | 2 +- .../assets/customnpcs/lang/en_US.lang | 30 ++++--------------- 18 files changed, 33 insertions(+), 52 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 6d12693bb..c88c18095 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -144,20 +144,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ═══════════════════════════════════════════════════════════════════════ // TOP TABS using GuiMenuTopButton // ═══════════════════════════════════════════════════════════════════════ - GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "ability.tab.general"); + GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); GuiMenuTopButton lastTab = generalTab; if (hasTypeSettings) { - GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "ability.tab.type"); + GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); typeTab.active = (activeTab == TAB_TYPE); addTopButton(typeTab); lastTab = typeTab; } - GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "ability.tab.target"); + GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "script.target"); targetTab.active = (activeTab == TAB_TARGET); addTopButton(targetTab); lastTab = targetTab; @@ -435,7 +435,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ───────────────────────────────────────────────────────────────────── // SOUNDS SECTION // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(30, "ability.sounds", col1LabelX, y)); + addLabel(new GuiNpcLabel(30, "advanced.sounds", col1LabelX, y)); y += 14; // Wind Up Sound @@ -455,7 +455,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ───────────────────────────────────────────────────────────────────── // ANIMATIONS SECTION // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(33, "ability.animations", col1LabelX, y)); + addLabel(new GuiNpcLabel(33, "menu.animations", col1LabelX, y)); y += 14; // Wind Up Animation @@ -486,7 +486,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Telegraph Type (separate column) String typeKey = ability.getTelegraphType().name().toLowerCase(); addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5)); - addLabel(new GuiNpcLabel(43, "ability.telegraph." + typeKey, guiLeft + 280, y + 5)); + String telegraphLangKey = typeKey.equals("none") ? "gui.none" : "telegraph." + typeKey; + addLabel(new GuiNpcLabel(43, telegraphLangKey, guiLeft + 280, y + 5)); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java index e951e5fbd..fb3e6b074 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java @@ -28,7 +28,7 @@ public class SubGuiAbilityBeam extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Damage Interval - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java index a7b19b05c..d3ff5fd81 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java @@ -27,10 +27,10 @@ public class SubGuiAbilityCharge extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, charge.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, charge.getChargeSpeed())); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index b825789a8..d917282f2 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -28,10 +28,10 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Range - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, cutter.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.range", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "gui.range", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, cutter.getRange())); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java index 2d0d5c33a..7ae0ea1f5 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java @@ -28,7 +28,7 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Dash Mode - addLabel(new GuiNpcLabel(100, "ability.dashMode", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); String[] dashModes = {"Aggressive", "Defensive"}; addButton(new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java index 365460672..30ccb739e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -40,7 +40,7 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius())); - addLabel(new GuiNpcLabel(103, "ability.instant", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5)); addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java index c045833b9..5c0d99c48 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java @@ -27,7 +27,7 @@ public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Potion Time - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage())); addLabel(new GuiNpcLabel(101, "ability.potionTime", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 5c82c42f8..fd75700c0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -28,16 +28,16 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed())); y += 24; // Row 2: Orb Size + Max Distance - addLabel(new GuiNpcLabel(102, "ability.orbSize", labelX, y + 5)); + addLabel(new GuiNpcLabel(102, "stats.size", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize())); addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java index cbde41b7f..88d7bbf00 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java @@ -28,10 +28,10 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, projectile.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); addTextField(createFloatField(101, col2FieldX, y, 50, projectile.getSpeed())); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java index 9230e242c..38ab22158 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java @@ -27,7 +27,7 @@ public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Push Radius - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, shockwave.getDamage())); addLabel(new GuiNpcLabel(101, "ability.pushRadius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java index 2fdb0d7fb..7f8c7194e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java @@ -27,7 +27,7 @@ public class SubGuiAbilitySlam extends SubGuiAbilityConfig { int col2FieldX = guiLeft + 205; // Row 1: Damage + Radius - addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, slam.getDamage())); addLabel(new GuiNpcLabel(101, "ability.radius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java index c4e9154e5..5570d944f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -71,7 +71,7 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { } // Damage + Damage Radius - addLabel(new GuiNpcLabel(106, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(106, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(106, fieldX, y, 50, teleport.getDamage())); addLabel(new GuiNpcLabel(107, "ability.dmgRadius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index 8c8d92bb4..88529fe68 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -38,7 +38,7 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { y += 24; // Row 2: Damage + Damage Radius - addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, trap.getDamage())); addLabel(new GuiNpcLabel(103, "ability.dmgRadius", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java index 6978abce7..a1abbd661 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java @@ -37,7 +37,7 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { y += 24; // Row 2: Damage + Knockback - addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5)); + addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, vortex.getDamage())); addLabel(new GuiNpcLabel(103, "ability.knockback", col2LabelX, y + 5)); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java index 8dcbcfcbe..30e857e7a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java @@ -527,7 +527,7 @@ public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, addLabel(new GuiNpcLabel(140, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, leftX, guiTop + 104, fee > 0 ? 0xAA0000 : 0x008800)); - addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.create"))); addButton(new GuiNpcButton(72, leftX + 100, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.cancel"))); if (getButton(71) != null) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java index 6d7f0a756..01c650bbd 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java @@ -73,7 +73,7 @@ public class SubGuiAuctionCreate extends SubGuiInterface { guiLeft + 20, guiTop + 142, fee > 0 ? 0xAA0000 : 0x008800)); // Create button - addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("auction.create"))); + addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.create"))); getButton(50).setEnabled(item != null); // Cancel button diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java index c0fbb0295..882f5817b 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -50,7 +50,7 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr public void initGui() { super.initGui(); - this.addLabel(new GuiNpcLabel(0, "gui.animations", guiLeft + 8, guiTop + 4)); + this.addLabel(new GuiNpcLabel(0, "menu.animations", guiLeft + 8, guiTop + 4)); this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear")); this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done")); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 66837c543..7f7314d1c 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1477,14 +1477,12 @@ ability.target.self=Self ability.target.aoe_self=AOE Self ability.target.aoe_target=AOE Target -ability.telegraph.none=None -ability.telegraph.circle=Circle -ability.telegraph.ring=Ring -ability.telegraph.line=Line -ability.telegraph.cone=Cone -ability.telegraph.point=Point +telegraph.circle=Circle +telegraph.ring=Ring +telegraph.line=Line +telegraph.cone=Cone +telegraph.point=Point -ability.tab.general=General ability.tab.timing=Timing ability.tab.effects=Effects ability.tab.telegraph=Telegraph @@ -1497,7 +1495,6 @@ ability.windUpSound=Wind Up Sound ability.activeSound=Active Sound ability.windUpAnim=Wind Up Anim ability.activeAnim=Active Anim -gui.animations=Animations ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit @@ -1516,15 +1513,11 @@ ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap # Ability GUI - Type Tab -ability.tab.type=Type # Common Type Settings -ability.damage=Damage ability.radius=Radius ability.knockback=Knockback ability.knockbackUp=KB Up -ability.speed=Speed -ability.range=Range ability.maxDist=Max Dist ability.minDist=Min Dist ability.distance=Distance @@ -1551,13 +1544,10 @@ ability.hitRadius=Hit Radius # Cutter Settings ability.arcAngle=Arc Angle ability.sweepWaves=Waves -ability.waveInterval=Interval ability.rotSpeed=Rot Speed ability.piercing=Piercing # Orb Settings -ability.orbSpeed=Speed -ability.orbSize=Size ability.homing=Homing ability.explosive=Explosive @@ -1569,7 +1559,6 @@ ability.aoe=AOE ability.maxTargets=Max Targets # Dash Settings -ability.dashMode=Mode ability.dash.aggressive=Aggressive ability.dash.defensive=Defensive ability.mode=Mode @@ -1609,7 +1598,6 @@ ability.counterAnim=Counter Anim ability.healAmount=Amount ability.healPercent=Percent ability.healRadius=Radius -ability.instant=Instant ability.healSelf=Heal Self ability.healAllies=Heal Allies @@ -1620,9 +1608,6 @@ ability.shape=Shape ability.placement=Placement ability.affectsCaster=Hit Caster ability.ignoreInvuln=Ign Invuln -ability.shape.circle=Circle -ability.shape.ring=Ring -ability.shape.cone=Cone ability.place.caster=At Caster ability.place.target=At Target ability.place.followCaster=Follow Caster @@ -1633,7 +1618,6 @@ ability.place.ahead=Ahead ability.triggerRadius=Trig Rad ability.armTime=Arm Time ability.maxTriggers=Max Trig -ability.visible=Visible # Ability Save/Load ability.save.confirm=Save ability preset @@ -1663,11 +1647,8 @@ ability.shockwaveFalloff=Damage/knockback drop off with distance # Ability Config GUI ability.typeId=Type ID -ability.tab.target=Target ability.conditions=Conditions ability.noTypeSettings=No type-specific settings -ability.sounds=Sounds -ability.animations=Animations # Conditions condition.edit=Edit Condition @@ -1726,7 +1707,6 @@ auction.claim=Claim auction.createListing=Create Listing auction.duration=Duration: auction.listingFee=Listing Fee: -auction.create=Create auction.sort.newest=Newest First auction.sort.oldest=Oldest First auction.sort.priceHigh=Most Expensive From d0dc8a4578c25c5d0792981b8a0496d3c4521b6f Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 12:38:56 -0500 Subject: [PATCH 38/64] Trader System + Stocks Implemented - Removed Auction System Temporarily for Full Rebase --- 1.11_Market_Update.md | 834 +++++++++++++++++ docs/CNPC_1.11_MARKET_UPDATE_PLAN.md | 324 ------- src/api | 2 +- src/main/java/foxz/utils/Market.java | 3 - .../data/attribute/ItemTradeAttribute.java | 308 ------- .../kamkeel/npcs/network/PacketHandler.java | 10 +- .../npcs/network/enums/EnumDataPacket.java | 3 - .../npcs/network/enums/EnumPlayerPacket.java | 1 + .../packets/data/large/AuctionDataPacket.java | 218 ----- .../packets/player/AuctionActionPacket.java | 579 ------------ .../network/packets/player/GetTraderData.java | 77 ++ src/main/java/noppes/npcs/CommonProxy.java | 4 - src/main/java/noppes/npcs/CustomNpcs.java | 8 - .../noppes/npcs/CustomNpcsPermissions.java | 7 - src/main/java/noppes/npcs/DataAdvanced.java | 3 - .../java/noppes/npcs/NoppesUtilServer.java | 1 + .../java/noppes/npcs/ServerTickHandler.java | 6 - .../java/noppes/npcs/client/ClientProxy.java | 5 - .../java/noppes/npcs/client/NoppesUtil.java | 61 -- .../client/gui/mainmenu/GuiNpcAdvanced.java | 3 - .../npcs/client/gui/player/GuiAuction.java | 841 ------------------ .../npcs/client/gui/player/GuiNPCTrader.java | 309 +++++-- .../client/gui/player/SubGuiAuctionBid.java | 221 ----- .../gui/player/SubGuiAuctionCreate.java | 178 ---- .../gui/player/SubGuiAuctionDetails.java | 200 ----- .../client/gui/roles/GuiNpcAuctioneer.java | 144 --- .../client/gui/roles/GuiNpcTraderSetup.java | 86 +- .../roles/SubGuiNpcTraderCooldownReset.java | 52 ++ .../gui/roles/SubGuiNpcTraderSettings.java | 66 ++ .../gui/roles/SubGuiNpcTraderStock.java | 159 ++++ .../gui/roles/SubGuiNpcTraderStockReset.java | 55 ++ .../gui/roles/SubGuiNpcTraderStockSlots.java | 76 ++ .../gui/util/GuiContainerNPCInterface.java | 9 + .../gui/util/GuiContainerNPCInterface2.java | 4 +- .../npcs/client/gui/util/GuiNPCInterface.java | 15 +- .../java/noppes/npcs/config/ConfigMarket.java | 173 +--- .../npcs/constants/EnumAuctionDuration.java | 82 -- .../npcs/constants/EnumAuctionSort.java | 39 - .../npcs/constants/EnumAuctionStatus.java | 54 -- .../noppes/npcs/constants/EnumGuiType.java | 4 +- .../noppes/npcs/constants/EnumRoleType.java | 2 +- .../noppes/npcs/constants/EnumStockReset.java | 45 +- .../npcs/containers/ContainerAuction.java | 193 ---- .../npcs/containers/ContainerNPCTrader.java | 108 ++- .../containers/ContainerNPCTraderSetup.java | 14 +- .../npcs/controllers/AuctionBlacklist.java | 419 --------- .../npcs/controllers/AuctionController.java | 834 ----------------- .../npcs/controllers/CurrencyController.java | 400 --------- .../npcs/controllers/MarketRegistry.java | 214 +++++ .../noppes/npcs/controllers/VaultHelper.java | 272 ------ .../npcs/controllers/data/AuctionFilter.java | 226 ----- .../npcs/controllers/data/AuctionListing.java | 397 --------- .../controllers/data/PlayerCurrencyData.java | 105 ++- .../npcs/controllers/data/PlayerData.java | 16 +- .../npcs/controllers/data/TraderStock.java | 50 +- .../noppes/npcs/roles/RoleAuctioneer.java | 135 --- .../java/noppes/npcs/roles/RoleTrader.java | 282 +++++- .../npcs/scripted/entity/ScriptPlayer.java | 194 +--- .../npcs/scripted/item/ScriptItemStack.java | 114 --- .../npcs/scripted/roles/ScriptRoleTrader.java | 130 ++- .../assets/customnpcs/lang/en_US.lang | 118 +-- 61 files changed, 2595 insertions(+), 6897 deletions(-) create mode 100644 1.11_Market_Update.md delete mode 100644 docs/CNPC_1.11_MARKET_UPDATE_PLAN.md delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java delete mode 100644 src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java delete mode 100644 src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuction.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java delete mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java delete mode 100644 src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java create mode 100644 src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionDuration.java delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionSort.java delete mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionStatus.java delete mode 100644 src/main/java/noppes/npcs/containers/ContainerAuction.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionBlacklist.java delete mode 100644 src/main/java/noppes/npcs/controllers/AuctionController.java delete mode 100644 src/main/java/noppes/npcs/controllers/CurrencyController.java create mode 100644 src/main/java/noppes/npcs/controllers/MarketRegistry.java delete mode 100644 src/main/java/noppes/npcs/controllers/VaultHelper.java delete mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionFilter.java delete mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionListing.java delete mode 100644 src/main/java/noppes/npcs/roles/RoleAuctioneer.java diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md new file mode 100644 index 000000000..304965950 --- /dev/null +++ b/1.11_Market_Update.md @@ -0,0 +1,834 @@ +# CustomNPC+ 1.11 Market Update - Implementation Plan + +## Overview + +The 1.11 Market Update introduces a comprehensive economy system for CustomNPC+: + +1. **Currency System** - Built-in CNPC+ currency with optional Vault integration +2. **Trader Enhancements** - Stock system, currency-based trades, reset timers +3. **Auction House** - Global auction system with bidding, buyout, and claims + +--- + +## 1. Currency System + +### Status: IMPLEMENTED + +The currency system provides a unified economy for CNPC+ features. + +### Features +- **Built-in CNPC+ Currency** - Stored in PlayerData, persists across profile slots +- **Vault Integration** - Optional, when enabled all operations use Vault instead +- **Scripting API** - Full access via `IPlayer` methods + +### Key Files +| File | Purpose | +|------|---------| +| `PlayerCurrencyData.java` | Per-player currency storage, Vault delegation | +| `IPlayerCurrencyData.java` | API interface for currency operations | +| `ConfigMarket.java` | Currency configuration (UseVault, CurrencyName, StartingBalance, MaxBalance) | +| `VaultUtil.java` | Vault API integration via reflection | + +### Configuration (ConfigMarket.java) +``` +Market.Currency: + - Use Vault: false + - Currency Name: "Coins" + - Starting Balance: 0 + - Max Balance: 2147483647 +``` + +### Scripting API (IPlayer) +```javascript +player.getCurrencyBalance() // Get balance +player.setCurrencyBalance(amount) // Set balance +player.depositCurrency(amount) // Add currency (returns success) +player.withdrawCurrency(amount) // Remove currency (returns success) +player.canAffordCurrency(amount) // Check if can afford +player.isUsingVaultCurrency() // Check if Vault is active +player.getFormattedCurrencyBalance() // Get formatted string +``` + +--- + +## 2. Trader Enhancements + +### Status: COMPLETE + +--- + +### 2.1 Stock System - IMPLEMENTED + +Traders can have limited stock that resets on configurable schedules. + +#### Stock Modes +| Mode | Description | Status | +|------|-------------|--------| +| **Global** | All players share the same stock pool | ✅ Working | +| **Per-Player** | Each player has their own stock counts | ✅ Working | + +#### Stock Reset Types (EnumStockReset) +| Type | Description | Status | +|------|-------------|--------| +| `NONE` | Stock never resets | ✅ Working | +| `MCDAILY` | Resets every MC day (24000 ticks) | ✅ Working | +| `MCWEEKLY` | Resets every 7 MC days (168000 ticks) | ✅ Working | +| `MCCUSTOM` | Custom MC tick interval | ✅ Working | +| `RLDAILY` | Resets every 24 real-life hours | ✅ Working | +| `RLWEEKLY` | Resets every 7 real-life days | ✅ Working | +| `RLCUSTOM` | Custom real-time interval | ✅ Working | + +#### Stock UI Features - IMPLEMENTED +| Feature | Status | Notes | +|---------|--------|-------| +| Stock count per item | ✅ | Shows to right of output slot | +| Out of stock indicator | ✅ | Dark overlay + red count | +| Reset timer countdown | ✅ | Real-time client-side countdown in top-right | +| "Reopen Trader" message | ✅ | Shows when countdown reaches 0 | +| Admin stock configuration | ✅ | SubGuiNpcTraderStock | +| Per-slot stock limits | ✅ | SubGuiNpcTraderStockSlots | +| Reset stock button | ✅ | With confirmation dialog | +| Reset cooldown button | ✅ | With confirmation dialog | + +#### Key Files +| File | Purpose | +|------|---------| +| `TraderStock.java` | Core stock data class with per-player/global modes | +| `TraderStock.PlayerTraderStock` | Inner class for per-player tracking | +| `EnumStockReset.java` | Reset type enum with intervals | +| `SubGuiNpcTraderStock.java` | Main stock configuration GUI | +| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limit editor | +| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | +| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | + +--- + +### 2.2 Trader Currency Support - IMPLEMENTED + +Trades can require BOTH items AND currency (additive costs). + +#### Trade Entry Structure +Each trade slot can have: +- **Sold Item** - What the player receives ✅ +- **Currency 1** - First item cost (existing) ✅ +- **Currency 2** - Second item cost (existing) ✅ +- **Currency Cost** - CNPC+ Currency / Vault cost ✅ NEW + +#### Example Trades +| Sold Item | Item Cost | Currency Cost | +|-----------|-----------|---------------| +| Diamond | 2 Emeralds | $0 | +| Diamond Sword | 5 Diamonds | $100 | +| Special Armor | None | $500 | +| Enchanted Book | 1 Book + 10 Lapis | $50 | + +#### Implementation Details +- Currency cost stored as `long[] currencyCost[18]` in RoleTrader +- Default to 0 (no currency cost) for legacy compatibility ✅ +- Check both item AND currency requirements before purchase ✅ +- Withdraw currency on successful purchase ✅ +- NBT serialization for persistence ✅ + +--- + +### 2.3 Player Trader GUI - IMPLEMENTED + +#### Layout (256px wide) +- 3 columns × 6 rows = 18 trade slots +- Column width: 80px +- Per slot: [Currency1][Currency2] = [Output] +- Player inventory positioned below + +#### Display Features +| Element | Position | Status | +|---------|----------|--------| +| "Trader" label | Top-left | ✅ | +| Reset countdown | Top-right | ✅ Real-time ticking | +| Stock count | Right of each trade | ✅ Green/Red | +| Player balance | Bottom-right | ✅ | +| Status message | Bottom-left | ✅ On hover | + +#### Status Messages (on hover) +| Condition | Message | Color | +|-----------|---------|-------| +| Out of stock | "Out of Stock ($COST)" | Red | +| Can't afford balance | "Insufficient Balance ($COST)" | Red | +| Missing item currency | "Unavailable ($COST)" | Red | +| Slot disabled | "Slot Disabled ($COST)" | Orange | +| Can purchase | "Available ($COST)" | Green | + +Cost suffix only appears if trade has currency cost > 0. + +--- + +### 2.4 Multi-Player Sync - IMPLEMENTED + +#### MarketRegistry System +Central registry for linked market synchronization across all NPCs sharing the same market. + +| Component | Purpose | +|-----------|---------| +| `MarketRegistry.java` | Central registry tracking traders and viewers per market | +| `marketTraders` | `Map>` - All traders using each market | +| `marketViewers` | `Map>` - All players viewing each market | + +#### Viewer Tracking System +| Feature | Status | Notes | +|---------|--------|-------| +| Register viewer on GUI open | ✅ | For shared stock mode (linked or local) | +| Unregister on GUI close | ✅ | Via onContainerClosed | +| Sync after purchase | ✅ | All viewers updated market-wide | +| Per-player mode optimization | ✅ | Only sync to purchasing player | +| Disconnected player cleanup | ✅ | Removed before sync | +| NPC unload cleanup | ✅ | Via delete() → onUnload() | + +#### Sync Behavior by Mode +| Mode | Linked Market | Behavior | +|------|---------------|----------| +| Shared Stock | Yes | All viewers across ALL NPCs with same market see update immediately | +| Shared Stock | No | All viewers of THIS NPC see update immediately | +| Per-Player Stock | Yes/No | Only purchasing player receives update | + +#### Data Flow +``` +Purchase on NPC1 (market="shop1") + ↓ +consumeStock() → Market.save() → MarketRegistry.syncMarket("shop1") + ↓ +┌──────────────────────────────────────────────┐ +│ 1. Update ALL traders' stock data │ +│ (NPC1, NPC2, NPC3... all with "shop1") │ +│ 2. Sync ALL viewers across ALL those NPCs │ +└──────────────────────────────────────────────┘ +``` + +--- + +### 2.5 Key Files Modified/Created + +| File | Changes | +|------|---------| +| `MarketRegistry.java` | NEW - Central registry for linked market sync | +| `RoleTrader.java` | currencyCost[], viewer tracking (local + registry), syncAllViewers(), resetCooldown(), delete() cleanup | +| `ContainerNPCTrader.java` | Layout constants, currency/stock validation, viewer registration | +| `GuiNPCTrader.java` | Client-side countdown, stock display, status messages | +| `GetTraderData.java` | Network packet for requesting trader data (IGuiData pattern) | +| `TraderStock.java` | Stock management with per-player/global modes, thread-safe methods | +| `EnumStockReset.java` | Reset type definitions with lang key support | +| `SubGuiNpcTraderStock.java` | Stock configuration dialog, custom time validation | +| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limits | +| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | +| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | +| `GuiNPCInterface.java` | Fixed SubGui background tiling for smaller widths | +| `Market.java` | Fixed save logic for linked markets | +| `IRoleTrader.java` | Extended API with stock manipulation methods | +| `ScriptRoleTrader.java` | Implemented new API methods | + +--- + +### 2.6 Resolved Issues + +All previously identified issues have been fixed: + +#### Critical Issues - FIXED +| Issue | Fix | +|-------|-----| +| Market load overwrites reset | Reordered `interact()` to load market BEFORE stock reset check | +| recordHistory flag inverted | Removed inverted early return in `Market.save()` | +| API returns wrong item | Fixed `getSellOption()` to return `inventorySold` instead of `inventoryCurrency` | + +#### Medium Priority - FIXED +| Issue | Fix | +|-------|-----| +| Thread safety | Added `synchronized` to `getAvailableStock()`, `consumeStock()`, `resetStock()` | +| Per-player cleanup | `playerStock.clear()` called on stock reset | +| Linked market sync | Implemented `MarketRegistry` for market-wide sync | +| Custom time validation | Added min validation (1 tick MC, 1000ms RL) in `SubGuiNpcTraderStock` | + +#### Low Priority - FIXED +| Issue | Fix | +|-------|-----| +| lastPurchaseTime unused | Removed from `PlayerTraderStock` | +| Currency format | Verified working correctly for all ranges | +| Hardcoded reset types | Now uses `EnumStockReset.getDisplayNames()` with lang keys | + +--- + +### 2.7 Scripting API - IMPLEMENTED + +Full API available via `IRoleTrader` interface: + +```javascript +// Stock Configuration +trader.isStockEnabled() +trader.setStockEnabled(boolean) +trader.isPerPlayerStock() +trader.setPerPlayerStock(boolean) +trader.getStockResetType() // Returns int (0-6 for EnumStockReset) +trader.setStockResetType(int) +trader.getCustomResetTime() +trader.setCustomResetTime(long) + +// Stock Limits +trader.getMaxStock(slot) // Max stock for slot (-1 = unlimited) +trader.setMaxStock(slot, amount) + +// Stock Queries +trader.getAvailableStock(slot) // Global mode +trader.getAvailableStock(slot, player) // Per-player mode +trader.getCurrentStock(slot) // Current stock (global) +trader.getPlayerPurchased(slot, player) // Amount purchased by player + +// Stock Manipulation +trader.setCurrentStock(slot, amount) // Set current stock (global) +trader.setPlayerPurchased(slot, player, amount) // Set player's purchased amount +trader.resetStock() // Reset all stock to max +trader.resetCooldown() // Reset cooldown timer + +// Time Queries +trader.getLastResetTime() // Timestamp of last reset +trader.getTimeUntilReset() // Time remaining until next reset + +// Currency Cost System +trader.getCurrencyCost(slot) +trader.setCurrencyCost(slot, amount) +trader.hasCurrencyCost(slot) +``` + +--- + +## 3. Auction House System + +### Status: NOT STARTED (Fresh implementation) + +### 3.1 Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ AuctionController │ +│ - Manages all auction data │ +│ - Handles listing/bidding/buyout logic │ +│ - Processes claims │ +│ - Saves/loads auction data │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ AuctionNotification │ +│ - Tracks pending notifications per player │ +│ - Sends notifications on login │ +│ - Types: Won, Outbid, Sold, Expired, Claimed │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 GUI Structure + +``` +GuiAuctionHouse (Base GUI - opened via Auctioneer NPC) + │ + ├── GuiAuction (Browse/Search auctions) + │ - Paginated item display (ContainerAuction) + │ - Next/Back page buttons + │ - Filter button → opens GuiAuctionFilter + │ - My Auctions button → opens GuiAuctionClaims + │ - Sell button → opens GuiAuctionSell + │ - Click item → SubGuiAuctionDetails + │ + ├── GuiAuctionSell (Create new listing) + │ - Item slot for item to sell + │ - Starting price input + │ - Buyout price input (0 = no buyout) + │ - Listing fee display + │ - Create button + │ + ├── GuiAuctionClaims (My Auctions / Claims) + │ - Active listings tab + │ - Pending claims tab (items + currency to collect) + │ - Cancel listing button + │ - Claim all button + │ + └── GuiAuctionFilter (Filter settings) + - Search text + - Sort options (EnumAuctionSort) + - Price range (min/max) + - Show my listings only toggle +``` + +### 3.3 Admin GUI + +``` +GuiManageAuction (Admin tool - like ManageMagic, ManageQuests) + │ + ├── Active Auctions Tab + │ - View all auctions + │ - Cancel any auction (with reason) + │ - View auction details + │ + ├── Blacklist Tab (Stub for future) + │ - Add/remove blacklisted items + │ - Add/remove blacklisted mods + │ + └── Settings Tab + - View current config values + - Quick reference for config file +``` + +### 3.4 Data Structures + +#### AuctionListing +```java +public class AuctionListing { + int id; // Unique listing ID + UUID sellerUUID; // Seller's UUID + String sellerName; // Seller's display name + ItemStack item; // Item being sold + long startingPrice; // Minimum starting bid + long buyoutPrice; // Instant buy price (0 = no buyout) + long currentBid; // Current highest bid + UUID highBidderUUID; // Current high bidder (null if no bids) + String highBidderName; // High bidder display name + int bidCount; // Number of bids placed + long createdTime; // When listing was created + long endTime; // When auction ends + EnumAuctionStatus status; // ACTIVE, ENDED, CANCELLED, CLAIMED +} +``` + +#### AuctionClaim +```java +public class AuctionClaim { + int id; // Unique claim ID + UUID playerUUID; // Player who can claim + int listingId; // Related auction listing + EnumClaimType type; // ITEM, CURRENCY, REFUND + ItemStack item; // Item to claim (if type = ITEM) + long currency; // Currency to claim (if type = CURRENCY/REFUND) + long createdTime; // When claim was created + boolean claimed; // Whether claimed +} +``` + +#### AuctionFilter +```java +public class AuctionFilter { + String searchText; // Item name search + EnumAuctionSort sortBy; // Sort order + long minPrice; // Minimum price filter + long maxPrice; // Maximum price filter (0 = no max) + boolean myListingsOnly; // Show only my listings +} +``` + +#### Enums +```java +public enum EnumAuctionStatus { + ACTIVE, // Auction is live + ENDED, // Auction ended (winner determined) + CANCELLED, // Cancelled by seller or admin + CLAIMED // All claims processed +} + +public enum EnumAuctionSort { + NEWEST, // Most recently listed + ENDING_SOON, // Ending soonest + PRICE_LOW, // Lowest current price + PRICE_HIGH, // Highest current price + MOST_BIDS // Most bid activity +} + +public enum EnumClaimType { + ITEM, // Winner claims item + CURRENCY, // Seller claims sale currency + REFUND // Outbid player claims refund +} +``` + +### 3.5 AuctionController + +Main controller handling all auction logic. + +#### Core Methods +```java +// Listing Management +int createListing(EntityPlayer seller, ItemStack item, long startingPrice, long buyoutPrice) +void cancelListing(int listingId, EntityPlayer player, boolean isAdmin) +List getActiveListings(AuctionFilter filter, int page, int pageSize) +AuctionListing getListing(int listingId) +int getPlayerListingCount(UUID playerUUID) + +// Bidding +boolean placeBid(int listingId, EntityPlayer bidder, long bidAmount) +boolean buyout(int listingId, EntityPlayer buyer) + +// Claims +List getPlayerClaims(UUID playerUUID) +boolean claimItem(int claimId, EntityPlayer player) +boolean claimCurrency(int claimId, EntityPlayer player) +void claimAll(EntityPlayer player) + +// Lifecycle +void processEndedAuctions() // Called on tick, creates claims for ended auctions +void onPlayerLogin(EntityPlayer player) // Send pending notifications + +// Persistence +void save() +void load() +``` + +#### Auction Flow + +**Creating a Listing:** +1. Validate item not blacklisted +2. Check player has listing slots available +3. Deduct listing fee from player +4. Remove item from player inventory +5. Create AuctionListing with ACTIVE status +6. Save + +**Placing a Bid:** +1. Validate auction is ACTIVE +2. Validate bid > current bid + minimum increment +3. Validate bidder has enough currency +4. Deduct bid amount from bidder +5. If previous bidder exists, create REFUND claim for them +6. Update listing with new bid/bidder +7. Apply snipe protection if needed +8. Save + +**Buyout:** +1. Validate auction is ACTIVE and has buyout price +2. Validate buyer has enough currency +3. Deduct buyout amount from buyer +4. If previous bidder exists, create REFUND claim +5. Create ITEM claim for buyer +6. Create CURRENCY claim for seller (minus sales tax) +7. Set status to ENDED +8. Save + +**Auction Ends (no buyout):** +1. If bids exist: + - Create ITEM claim for high bidder + - Create CURRENCY claim for seller (minus sales tax) +2. If no bids: + - Create ITEM claim for seller (item returned) +3. Set status to ENDED +4. Save + +### 3.6 Snipe Protection + +When a bid is placed with less than `SnipeProtectionMinutes` remaining: +- Extend the auction end time TO `SnipeProtectionMinutes` from now +- Never extend beyond `SnipeProtectionMinutes` +- Default: 2 minutes + +Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 + +### 3.7 Notifications + +#### AuctionNotification Class +```java +public class AuctionNotification { + UUID playerUUID; + EnumNotificationType type; + int listingId; + String message; + long timestamp; +} + +public enum EnumNotificationType { + AUCTION_WON, // You won an auction + AUCTION_OUTBID, // You were outbid + AUCTION_SOLD, // Your item sold + AUCTION_EXPIRED, // Your auction expired (no bids) + CLAIM_READY // You have items/currency to claim +} +``` + +Notifications are queued and sent when player logs in. + +### 3.8 Permissions + +| Permission | Default | Description | +|------------|---------|-------------| +| `customnpcs.auction.use` | true | Can use auction house | +| `customnpcs.auction.sell` | true | Can create listings | +| `customnpcs.auction.slots.X` | 5 | Max listing slots (X = number) | +| `customnpcs.auction.admin` | op | Admin auction management | + +--- + +## 4. Configuration + +### ConfigMarket.java Structure + +```java +// ========================================= +// Market General +// ========================================= +public static boolean MarketEnabled = true; + +// ========================================= +// Currency (Market.Currency) +// ========================================= +public static boolean UseVault = false; +public static String CurrencyName = "Coins"; +public static long StartingBalance = 0; +public static long MaxBalance = Long.MAX_VALUE; + +// ========================================= +// Trader (Market.Trader) +// ========================================= +public static boolean EnableStockByDefault = false; +public static String DefaultResetType = "NONE"; + +// ========================================= +// Auction (Market.Auction) +// ========================================= +public static boolean AuctionEnabled = true; +public static int AuctionDuration = 24; // Hours +public static long ListingFee = 10; // Flat fee to create listing +public static double SalesTaxPercent = 0.05; // 5% tax on sales +public static int DefaultMaxListings = 5; // Per player +public static int SnipeProtectionMinutes = 2; // Snipe protection timer +public static int ClaimExpirationDays = 30; // Days before unclaimed items deleted +public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase +``` + +--- + +## 5. File Structure + +### New Files to Create + +``` +constants/ + EnumAuctionStatus.java + EnumAuctionSort.java + EnumClaimType.java + EnumNotificationType.java + +controllers/ + AuctionController.java + AuctionNotification.java + +controllers/data/ + AuctionListing.java + AuctionClaim.java + AuctionFilter.java + +roles/ + RoleAuctioneer.java + +containers/ + ContainerAuction.java + +client/gui/player/ + GuiAuctionHouse.java (Base GUI) + GuiAuction.java (Browse auctions) + GuiAuctionSell.java (Create listing) + GuiAuctionClaims.java (My auctions/claims) + GuiAuctionFilter.java (Filter settings) + SubGuiAuctionDetails.java (View listing details) + +client/gui/global/ + GuiManageAuction.java (Admin management) + +network/packets/ + AuctionDataPacket.java (Server → Client data sync) + AuctionActionPacket.java (Client → Server actions) +``` + +### Files to Modify + +``` +CustomNpcs.java - Initialize AuctionController +ServerTickHandler.java - Process ended auctions, login notifications +EnumRoleType.java - Add Auctioneer +EnumGuiType.java - Add auction GUI types +DataAdvanced.java - Handle Auctioneer role +PacketHandler.java - Register auction packets +CustomNpcsPermissions.java - Auction permissions +ConfigMarket.java - Add Auction section +RoleTrader.java - Add currency cost per trade +ContainerNPCTrader.java - Currency + stock integration +en_US.lang - All auction lang keys +``` + +--- + +## 6. Data Storage + +### Auction Data File +Location: `customnpcs/auction.dat` + +Structure: +``` +NBTTagCompound root + ├── NextListingId: int + ├── NextClaimId: int + ├── Listings: NBTTagList + │ └── [AuctionListing NBT...] + ├── Claims: NBTTagList + │ └── [AuctionClaim NBT...] + └── PendingNotifications: NBTTagList + └── [AuctionNotification NBT...] +``` + +--- + +## 7. Lang Keys + +```properties +# Auction House +auction.title=Auction House +auction.browse=Browse +auction.sell=Sell +auction.claims=My Auctions +auction.filter=Filter + +# Browse +auction.nextPage=Next > +auction.prevPage=< Prev +auction.page=Page %d of %d +auction.noResults=No auctions found +auction.refresh=Refresh + +# Listing Display +auction.seller=Seller: %s +auction.currentBid=Current Bid: %s +auction.buyout=Buyout: %s +auction.timeLeft=Time Left: %s +auction.bids=%d bids +auction.noBids=No bids + +# Create Listing +auction.sell.title=Create Listing +auction.sell.startingPrice=Starting Price +auction.sell.buyoutPrice=Buyout Price +auction.sell.noBuyout=(0 = no buyout) +auction.sell.fee=Listing Fee: %s +auction.sell.create=Create Listing +auction.sell.noItem=Place an item to sell + +# Claims +auction.claims.title=My Auctions +auction.claims.active=Active Listings +auction.claims.pending=Pending Claims +auction.claims.claimAll=Claim All +auction.claims.cancel=Cancel Listing +auction.claims.empty=No pending claims + +# Filter +auction.filter.title=Filter Auctions +auction.filter.search=Search +auction.filter.minPrice=Min Price +auction.filter.maxPrice=Max Price +auction.filter.myOnly=My Listings Only +auction.filter.apply=Apply + +# Sort Options +auction.sort.newest=Newest +auction.sort.endingSoon=Ending Soon +auction.sort.priceLow=Price: Low to High +auction.sort.priceHigh=Price: High to Low +auction.sort.mostBids=Most Bids + +# Notifications +auction.notify.won=You won the auction for %s! +auction.notify.outbid=You were outbid on %s! +auction.notify.sold=Your %s sold for %s! +auction.notify.expired=Your auction for %s expired with no bids. +auction.notify.claim=You have items to claim at the Auction House. + +# Errors +auction.error.notEnoughCurrency=Not enough currency! +auction.error.auctionEnded=This auction has ended. +auction.error.bidTooLow=Bid must be at least %s. +auction.error.maxListings=You have reached your maximum listings. +auction.error.blacklisted=This item cannot be auctioned. +auction.error.noAuctioneer=You must visit an Auctioneer NPC. + +# Trader Currency +trader.currencyCost=Currency Cost +trader.outOfStock=Out of Stock +trader.stock=Stock: %d +``` + +--- + +## 8. Implementation Order + +### Phase 1: Trader Currency Integration - COMPLETE ✅ +1. ~~Add `currencyCost` field to RoleTrader trades~~ +2. ~~Update ContainerNPCTrader to check/deduct currency~~ +3. ~~Update trader setup GUI with currency input~~ +4. ~~Test with existing item trades (backward compatibility)~~ + +### Phase 2: Trader Stock UI - COMPLETE ✅ +1. ~~Add stock display to player trader GUI~~ +2. ~~Show "Out of Stock" indicator~~ +3. ~~Verify reset timers working~~ +4. ~~Real-time client-side countdown~~ +5. ~~Status messages with costs~~ +6. ~~Reset cooldown button with confirmation~~ + +### Phase 2.5: Trader Sync System - COMPLETE ✅ +1. ~~Implement MarketRegistry for linked market sync~~ +2. ~~Viewer tracking (market-wide and local)~~ +3. ~~NPC unload cleanup~~ +4. ~~Thread safety for stock operations~~ + +### Phase 3: Auction Core +1. Create enums (EnumAuctionStatus, EnumAuctionSort, etc.) +2. Create data classes (AuctionListing, AuctionClaim, AuctionFilter) +3. Create AuctionController with core logic +4. Create RoleAuctioneer +5. Add to CustomNpcs initialization + +### Phase 4: Auction GUI +1. Create GuiAuctionHouse base +2. Create GuiAuction (browse) +3. Create GuiAuctionSell +4. Create GuiAuctionClaims +5. Create GuiAuctionFilter +6. Create SubGuiAuctionDetails +7. Create ContainerAuction + +### Phase 5: Auction Network +1. Create AuctionDataPacket +2. Create AuctionActionPacket +3. Register packets + +### Phase 6: Admin & Polish +1. Create GuiManageAuction +2. Add permissions +3. Add notifications system +4. Add lang keys +5. Testing + +--- + +## 9. Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Claims System | Everything to claims | Prevents offline handling issues | +| Auction Duration | Single config value | Simplicity over flexibility | +| Snipe Protection | Reset to X minutes | Fair bidding, prevents last-second sniping | +| Listing Fees | Flat fee only | Simple, predictable | +| Sales Tax | Percentage of sale | Scales with item value | +| Data Storage | Single .dat file | Consistent with quests/dialogs | +| Blacklist | Stub for now | Complex feature for later | +| Currency per Trade | Additive with items | Backward compatible, flexible | + +--- + +## 10. Future Considerations (Not in 1.11) + +- Category-based filtering +- Advanced blacklist system (items, mods, NBT patterns) +- Auction history/statistics +- Command-based auction access +- Auction house tax sink +- Reserve prices +- Private auctions diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md deleted file mode 100644 index 4d62a7772..000000000 --- a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md +++ /dev/null @@ -1,324 +0,0 @@ -# CustomNPC+ 1.11 Market Update - Implementation Status - -## Overview - -This document tracks the implementation status of the CustomNPC+ 1.11 Market Update, which includes: - -1. **Auction House** - Global auction system with bidding, buyout, and claims -2. **Currency System** - Built-in currency with VaultAPI integration for Bukkit hybrid servers -3. **Trader Enhancements** - Stock/inventory system with reset timers - ---- - -## Implementation Status Summary - -| Phase | Feature | Status | -|-------|---------|--------| -| Phase 1 | Currency System Foundation | **NOT STARTED** | -| Phase 2 | Trader Enhancements | **COMPLETED** | -| Phase 3 | Item Attributes & Restrictions | **COMPLETED** | -| Phase 4 | Auction House Core | **COMPLETED** | -| Phase 5 | Network Packets & Permissions | **COMPLETED** | -| Phase 6 | Auction GUI & Notifications | **COMPLETED** | -| Phase 7 | Admin Management | **NOT STARTED** | - ---- - -## Completed Features - -### Phase 2: Trader Enhancements (COMPLETED) -- `EnumStockReset.java` - Stock reset timing options -- `TraderStock.java` - Stock tracking with per-player and per-server modes -- Modified `RoleTrader.java` with stock and currency support -- Modified `ContainerNPCTrader.java` for stock/currency transactions -- Stock persistence via NBT - -### Phase 3: Item Attributes & Restrictions (COMPLETED) -- `ItemTradeAttribute.java` - Untradeable, ProfileSlot Bound, Soulbound checks -- `AuctionBlacklist.java` - Item/mod/NBT blacklist system -- Integration with AuctionListing validation - -### Phase 4: Auction House Core (COMPLETED) -- `EnumAuctionDuration.java` - Configurable durations with lang keys -- `EnumAuctionStatus.java` - Auction state management -- `EnumAuctionSort.java` - Sort options with lang keys -- `AuctionListing.java` - Complete listing data structure with bidding logic -- `AuctionFilter.java` - Search/filter criteria (categories removed) -- `AuctionController.java` - Consolidated controller with: - - Listing creation/management - - Bidding and buyout logic - - Claim system for items and currency refunds - - Inline logging (via LogWriter) - - Inline player notifications (via chat messages) - - Pending notification queue for offline players - - Snipe protection - - Sales tax and listing fees -- `AuctionClaim.java` - Claim data for items/money -- `RoleAuctioneer.java` - Auctioneer NPC role -- `ContainerAuction.java` - Auction container -- `ConfigMarket.java` - All market configuration options - -### Phase 5: Network Packets & Permissions (COMPLETED) -- `AuctionActionPacket.java` - Client-to-server auction actions -- `AuctionDataPacket.java` - Server-to-client data sync -- Packet registration in `PacketHandler.java` -- Permission nodes added to `CustomNpcsPermissions.java` -- API methods added to `ICustomNpc.java` - -### Phase 6: Auction GUI & Notifications (COMPLETED) -- `GuiAuction.java` - Main auction browser with tabs -- `SubGuiAuctionCreate.java` - Create listing sub-GUI -- `SubGuiAuctionBid.java` - Place bid sub-GUI -- `SubGuiAuctionDetails.java` - View listing details sub-GUI -- All GUI strings use lang keys via `StatCollector.translateToLocal()` -- Localization keys added to `en_US.lang` - ---- - -## Removed/Changed Features - -### Removed: Category System -Originally planned to have item categories (Weapons, Armor, Tools, etc.) for filtering. This was **removed** to simplify the initial implementation: -- Deleted `EnumAuctionCategory.java` -- Removed category field from `AuctionListing.java` -- Removed category filtering from `AuctionFilter.java` -- Removed category buttons from `GuiAuction.java` - -**Rationale:** Categories can be added in a future update. Current implementation uses search-based filtering. - -### Consolidated: Controller Architecture -Originally planned separate controllers: -- ~~`AuctionLogger.java`~~ - **CONSOLIDATED** into `AuctionController.logEvent()` -- ~~`AuctionNotificationController.java`~~ - **CONSOLIDATED** into `AuctionController.notifyPlayer()` - -**Rationale:** Reduces complexity, all auction logic in one place. - -### Changed: Localization Approach -- All client GUI strings now use `StatCollector.translateToLocal()` with lang keys -- `EnumAuctionSort` uses lang keys for display names -- `EnumAuctionDuration` uses lang keys for display names - ---- - -## Not Yet Implemented - -### Phase 1: Currency System (NOT STARTED) -Requires implementation: -- [ ] `VaultHelper.java` - VaultAPI integration via reflection -- [ ] `CurrencyController.java` - Currency management -- [ ] `PlayerCurrencyData.java` - Per-player currency (shared across profile slots) -- [ ] Vault dependency in build files -- [ ] Currency configuration GUI - -**Note:** Currently auctions use `long` for prices but the actual currency deduction/deposit is not connected to any economy system. - -### Phase 7: Admin Management (NOT STARTED) -Requires implementation: -- [ ] `GuiAuctionAdmin.java` - Admin auction management GUI -- [ ] `GuiAuctionBlacklist.java` - Blacklist management GUI -- [ ] Admin methods in `AuctionController.java` (adminCancelListing, adminEditListing, etc.) -- [ ] Audit logging for admin actions -- [ ] `/cnpc auction admin` command - -### Additional Missing Features -- [ ] Auctioneer NPC setup GUI (`GuiNpcAuctioneerSetup.java`) -- [ ] Trader stock reset timer banner display -- [ ] Comprehensive testing -- [ ] Player balance display (requires Currency System) - ---- - -## Configuration Reference - -All settings are in `ConfigMarket.java`: - -```java -// Currency -public static String CurrencyName = "Gold"; - -// Auction Durations (in hours) -public static int AuctionDurationShort = 12; -public static int AuctionDurationMedium = 24; -public static int AuctionDurationLong = 48; -public static int AuctionDurationVeryLong = 72; - -// Auction Fees (per duration) -public static long AuctionFeeShort = 10; -public static long AuctionFeeMedium = 25; -public static long AuctionFeeLong = 50; -public static long AuctionFeeVeryLong = 100; - -// Listing Configuration -public static int DefaultMaxListings = 5; -public static double ListingFeePercent = 0.0; -public static double SalesTaxPercent = 0.05; -public static double MinBidIncrementPercent = 0.05; -public static int ClaimExpirationDays = 30; -public static boolean RequireAuctioneerNPC = true; - -// Snipe Protection -public static int SnipeProtectionMinutes = 5; -public static int SnipeProtectionThreshold = 5; -``` - ---- - -## File Inventory - -### New Files Created - -| File | Purpose | -|------|---------| -| `constants/EnumAuctionDuration.java` | Auction duration options | -| `constants/EnumAuctionSort.java` | Sort options with lang keys | -| `constants/EnumAuctionStatus.java` | Auction state enum | -| `constants/EnumStockReset.java` | Trader stock reset types | -| `controllers/AuctionController.java` | Main auction controller | -| `controllers/AuctionBlacklist.java` | Item blacklist system | -| `controllers/data/AuctionListing.java` | Auction listing data | -| `controllers/data/AuctionFilter.java` | Search/filter criteria | -| `controllers/data/AuctionClaim.java` | Claim data structure | -| `controllers/data/TraderStock.java` | Trader stock system | -| `controllers/data/attribute/ItemTradeAttribute.java` | Trade restriction attributes | -| `roles/RoleAuctioneer.java` | Auctioneer NPC role | -| `containers/ContainerAuction.java` | Auction GUI container | -| `client/gui/player/GuiAuction.java` | Main auction GUI | -| `client/gui/player/SubGuiAuctionCreate.java` | Create listing GUI | -| `client/gui/player/SubGuiAuctionBid.java` | Place bid GUI | -| `client/gui/player/SubGuiAuctionDetails.java` | Listing details GUI | -| `config/ConfigMarket.java` | Market configuration | -| `network/packets/player/AuctionActionPacket.java` | Auction action packet | -| `network/packets/data/AuctionDataPacket.java` | Auction data sync packet | - -### Files Deleted -| File | Reason | -|------|--------| -| ~~`constants/EnumAuctionCategory.java`~~ | Categories removed | -| ~~`controllers/AuctionLogger.java`~~ | Consolidated into AuctionController | -| ~~`controllers/AuctionNotificationController.java`~~ | Consolidated into AuctionController | - -### Files Modified - -| File | Changes | -|------|---------| -| `CustomNpcs.java` | Initialize AuctionController, AuctionBlacklist | -| `ServerTickHandler.java` | Call AuctionController.onPlayerLogin() | -| `constants/EnumRoleType.java` | Added Auctioneer | -| `constants/EnumGuiType.java` | Added auction GUI types | -| `DataAdvanced.java` | Handle Auctioneer role creation | -| `roles/RoleTrader.java` | Added stock system | -| `containers/ContainerNPCTrader.java` | Stock handling | -| `CustomNpcsPermissions.java` | Auction permission nodes | -| `api/ICustomNpc.java` | Auction API methods | -| `PacketHandler.java` | Register auction packets | -| `en_US.lang` | All auction lang keys | - ---- - -## Localization Keys Added - -```properties -# Main GUI -auction.title=Auction House -auction.browse=Browse -auction.myListings=My Listings -auction.myBids=My Bids -auction.claims=Claims -auction.sell=Sell -auction.search=Search -auction.refresh=Refresh -auction.viewDetails=View Details -auction.placeBid=Place Bid -auction.buyout=Buyout -auction.cancel=Cancel -auction.prevPage=< Prev -auction.nextPage=Next > -auction.noResults=No results -auction.page=Page %d of %d -auction.balance=Balance: %s -auction.listings=%d listings - -# Tab Titles -auction.browseTitle=Browse Auctions -auction.myListingsTitle=My Listings -auction.myBidsTitle=My Active Bids -auction.claimsTitle=Claim Items -auction.sellTitle=Create Listing - -# Listing Details -auction.item=Item: -auction.seller=Seller: -auction.startingPrice=Starting Price: -auction.buyoutPrice=Buyout Price: -auction.currentPrice=Current Price: -auction.currentBid=Current Bid: -auction.minimumBid=Minimum Bid: -auction.highBidder=High Bidder: -auction.totalBids=Total Bids: -auction.quantity=Quantity: -auction.timeLeft=Time Left: -auction.yourBid=Your Bid: -auction.yourBalance=Your Balance: -auction.ended=Ended -auction.noBidsYet=No bids yet - -# Create Listing -auction.createListing=Create Listing -auction.placeItemBelow=Place item in slot below: -auction.noItemSelected=(No item selected) -auction.noBuyoutHint=(0 = no buyout) -auction.duration=Duration: -auction.listingFee=Listing Fee: -auction.create=Create - -# Bid GUI -auction.minBid=Min Bid -auction.bidPlus10=+10% -auction.bidPlus25=+25% -auction.insufficientFunds=Insufficient funds! -auction.auctionEnded=Auction has ended! -auction.auctionDetails=Auction Details - -# Sort Options -auction.sort.newest=Newest First -auction.sort.oldest=Oldest First -auction.sort.priceHigh=Most Expensive -auction.sort.priceLow=Least Expensive -auction.sort.endingSoon=Ending Soon -auction.sort.mostTime=Most Time Left -auction.sort.nameAZ=Name: A to Z -auction.sort.nameZA=Name: Z to A -auction.sort.mostBids=Most Bids -auction.sort.leastBids=Least Bids - -# Duration Options -auction.duration.short=Short (12h) -auction.duration.medium=Medium (24h) -auction.duration.long=Long (48h) -auction.duration.veryLong=Very Long (72h) -``` - ---- - -## Design Decisions (Confirmed) - -1. **Outbid Refunds** → Go to claims (not direct to balance) -2. **Currency** → Shared across all profile slots per player (NOT slot-bound) -3. **Permissions** → Uses existing CustomNPC+ permission pattern (default: 5 slots) -4. **Categories** → Removed for initial implementation (can add later) -5. **Controller Architecture** → Single consolidated AuctionController -6. **Localization** → All client strings use StatCollector with lang keys -7. **StatCollector Usage** → Safe - only called from client-side GUI code -8. **Claim Expiration** → 30 days default (configurable) -9. **Snipe Protection** → Extend TO 5 minutes when bid placed near end -10. **Logging** → Inline in AuctionController using LogWriter - ---- - -## Next Steps - -1. **Currency System** - Implement VaultHelper and CurrencyController to connect actual economy -2. **Admin Management** - Implement admin GUI and commands -3. **Testing** - Comprehensive testing of all auction flows -4. **Auctioneer Setup GUI** - Create NPC configuration interface -5. **Trader Banner** - Add stock reset timer display above trader GUI diff --git a/src/api b/src/api index 866be1fef..339e0f164 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 866be1fef5bf37a002379140771096cebe29f286 +Subproject commit 339e0f164e8c7b08c3469f61027fa25aa9e5c118 diff --git a/src/main/java/foxz/utils/Market.java b/src/main/java/foxz/utils/Market.java index ca260b90b..54905423c 100644 --- a/src/main/java/foxz/utils/Market.java +++ b/src/main/java/foxz/utils/Market.java @@ -31,9 +31,6 @@ public class Market { NBTTagCompound roleCompound = r.writeNBT(new NBTTagCompound()); roleCompound.setString("SaveName", name); putMarketCache(name, roleCompound); - if (r.recordHistory) - return; - saveFile(roleCompound); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java b/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java deleted file mode 100644 index 180f41d02..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/attribute/ItemTradeAttribute.java +++ /dev/null @@ -1,308 +0,0 @@ -package kamkeel.npcs.controllers.data.attribute; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.controllers.data.PlayerData; - -import java.util.UUID; - -/** - * Utility class for managing item trade restrictions. - * Checks and sets attributes that control whether items can be traded/auctioned. - */ -public class ItemTradeAttribute { - - // New untradeable attribute - prevents all trading - public static final String TAG_UNTRADEABLE = "CNPC_Untradeable"; - - // Existing requirement keys (from ProfileSlotRequirement and SoulbindRequirement) - public static final String TAG_PROFILE_SLOT = "cnpc_profile_slot"; - public static final String TAG_SOULBIND = "cnpc_soulbind"; - - // ==================== Trade Check Methods ==================== - - /** - * Check if item can be traded/auctioned by the given player. - * An item cannot be traded if: - * - It has the Untradeable flag - * - It is bound to a specific profile slot (ProfileSlot Bound) - * - It is soulbound to a different player - * - * @param item The item to check - * @param player The player trying to trade (null to check without player context) - * @return true if the item can be traded - */ - public static boolean canTrade(ItemStack item, EntityPlayer player) { - if (item == null) { - return true; - } - if (!item.hasTagCompound()) { - return true; - } - - NBTTagCompound tag = item.getTagCompound(); - - // Check untradeable flag - always blocks trading - if (tag.getBoolean(TAG_UNTRADEABLE)) { - return false; - } - - // Check profile slot bound - blocks trading as it's tied to a slot - if (tag.hasKey(TAG_PROFILE_SLOT)) { - return false; - } - - // Check soulbound - blocks trading as it's tied to a player - if (tag.hasKey(TAG_SOULBIND)) { - // If we have a player context, check if they're the owner - if (player != null) { - String soulbindUUID = tag.getString(TAG_SOULBIND); - if (soulbindUUID != null && !soulbindUUID.isEmpty()) { - // Even if bound to this player, still can't trade - return false; - } - } - return false; - } - - return true; - } - - /** - * Check if item can be traded (without player context) - */ - public static boolean canTrade(ItemStack item) { - return canTrade(item, null); - } - - /** - * Get the reason why an item cannot be traded. - * Returns a translation key for localization. - * - * @param item The item to check - * @return Translation key for the block reason, or null if tradeable - */ - public static String getTradeBlockReason(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return null; - } - - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) { - return "gui.auction.untradeable"; - } - if (tag.hasKey(TAG_PROFILE_SLOT)) { - return "gui.auction.profileslotbound"; - } - if (tag.hasKey(TAG_SOULBIND)) { - return "gui.auction.soulbound"; - } - - return null; - } - - // ==================== Untradeable Methods ==================== - - /** - * Check if item is marked as untradeable - */ - public static boolean isUntradeable(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().getBoolean(TAG_UNTRADEABLE); - } - - /** - * Set the untradeable flag on an item - */ - public static void setUntradeable(ItemStack item, boolean untradeable) { - if (item == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (untradeable) { - item.getTagCompound().setBoolean(TAG_UNTRADEABLE, true); - } else { - item.getTagCompound().removeTag(TAG_UNTRADEABLE); - } - } - - // ==================== Profile Slot Bound Methods ==================== - - /** - * Check if item is bound to a profile slot - */ - public static boolean isProfileSlotBound(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().hasKey(TAG_PROFILE_SLOT); - } - - /** - * Get the profile slot an item is bound to - * @return The profile slot ID, or -1 if not bound - */ - public static int getProfileSlot(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return -1; - } - if (item.getTagCompound().hasKey(TAG_PROFILE_SLOT)) { - return item.getTagCompound().getInteger(TAG_PROFILE_SLOT); - } - return -1; - } - - /** - * Bind an item to a specific profile slot - */ - public static void setProfileSlotBound(ItemStack item, int slot) { - if (item == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - if (slot >= 0) { - item.getTagCompound().setInteger(TAG_PROFILE_SLOT, slot); - } else { - item.getTagCompound().removeTag(TAG_PROFILE_SLOT); - } - } - - /** - * Bind an item to the player's current profile slot - */ - public static void bindToCurrentSlot(ItemStack item, EntityPlayer player) { - if (item == null || player == null) { - return; - } - PlayerData data = PlayerData.get(player); - if (data != null) { - setProfileSlotBound(item, data.profileSlot); - } - } - - // ==================== Soulbind Methods ==================== - - /** - * Check if item is soulbound to any player - */ - public static boolean isSoulbound(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return false; - } - return item.getTagCompound().hasKey(TAG_SOULBIND); - } - - /** - * Check if item is soulbound to a specific player - */ - public static boolean isSoulboundTo(ItemStack item, EntityPlayer player) { - if (item == null || player == null || !item.hasTagCompound()) { - return false; - } - if (!item.getTagCompound().hasKey(TAG_SOULBIND)) { - return false; - } - String soulbindUUID = item.getTagCompound().getString(TAG_SOULBIND); - return player.getUniqueID().toString().equals(soulbindUUID); - } - - /** - * Get the UUID of the player the item is soulbound to - * @return The UUID string, or null if not soulbound - */ - public static String getSoulbindOwner(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return null; - } - if (item.getTagCompound().hasKey(TAG_SOULBIND)) { - return item.getTagCompound().getString(TAG_SOULBIND); - } - return null; - } - - /** - * Soulbind an item to a player - */ - public static void setSoulbound(ItemStack item, EntityPlayer player) { - if (item == null || player == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - item.getTagCompound().setString(TAG_SOULBIND, player.getUniqueID().toString()); - } - - /** - * Soulbind an item to a player by UUID - */ - public static void setSoulbound(ItemStack item, UUID playerUUID) { - if (item == null || playerUUID == null) { - return; - } - if (!item.hasTagCompound()) { - item.setTagCompound(new NBTTagCompound()); - } - item.getTagCompound().setString(TAG_SOULBIND, playerUUID.toString()); - } - - /** - * Remove soulbind from an item - */ - public static void removeSoulbind(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return; - } - item.getTagCompound().removeTag(TAG_SOULBIND); - } - - // ==================== Utility Methods ==================== - - /** - * Clear all trade restriction attributes from an item - */ - public static void clearAllRestrictions(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return; - } - NBTTagCompound tag = item.getTagCompound(); - tag.removeTag(TAG_UNTRADEABLE); - tag.removeTag(TAG_PROFILE_SLOT); - tag.removeTag(TAG_SOULBIND); - } - - /** - * Get a display string describing all trade restrictions on an item - * @return Human-readable string of restrictions, or empty if none - */ - public static String getRestrictionSummary(ItemStack item) { - if (item == null || !item.hasTagCompound()) { - return ""; - } - - StringBuilder summary = new StringBuilder(); - NBTTagCompound tag = item.getTagCompound(); - - if (tag.getBoolean(TAG_UNTRADEABLE)) { - summary.append("Untradeable"); - } - if (tag.hasKey(TAG_PROFILE_SLOT)) { - if (summary.length() > 0) summary.append(", "); - summary.append("Profile Slot Bound (Slot ").append(tag.getInteger(TAG_PROFILE_SLOT)).append(")"); - } - if (tag.hasKey(TAG_SOULBIND)) { - if (summary.length() > 0) summary.append(", "); - summary.append("Soulbound"); - } - - return summary.toString(); - } -} diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index b6d30d62d..3a4c35d05 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -36,7 +36,6 @@ import kamkeel.npcs.network.packets.data.gui.GuiWaypointPacket; import kamkeel.npcs.network.packets.data.gui.IsGuiOpenPacket; import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.network.packets.data.large.PartyDataPacket; -import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; import kamkeel.npcs.network.packets.data.large.ScrollListPacket; import kamkeel.npcs.network.packets.data.large.SyncEffectPacket; @@ -52,13 +51,13 @@ import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; import kamkeel.npcs.network.packets.player.DialogSelectPacket; import kamkeel.npcs.network.packets.player.FollowerPacket; import kamkeel.npcs.network.packets.player.GetNPCRole; +import kamkeel.npcs.network.packets.player.GetTraderData; import kamkeel.npcs.network.packets.player.InputDevicePacket; import kamkeel.npcs.network.packets.player.MailActionPacket; import kamkeel.npcs.network.packets.player.SaveBookPacket; @@ -566,9 +565,6 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); - - // Auction Data - DATA_PACKET.registerPacket(new AuctionDataPacket()); } public void registerPlayerPackets() { @@ -591,6 +587,7 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new CompanionActionPacket()); PLAYER_PACKET.registerPacket(new GetNPCRole()); + PLAYER_PACKET.registerPacket(new GetTraderData()); PLAYER_PACKET.registerPacket(new InputDevicePacket()); @@ -630,9 +627,6 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new PartyAcceptInvitePacket()); PLAYER_PACKET.registerPacket(new PartyIgnoreInvitePacket()); PLAYER_PACKET.registerPacket(new PartyLogToServerPacket()); - - // Auction Packets - PLAYER_PACKET.registerPacket(new AuctionActionPacket()); } public void registerChannels() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index ec8eabf41..ebcbdfc59 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,7 +62,4 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, - - // Auction - AUCTION_DATA, } diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 15c90fce5..48f400fb2 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -13,6 +13,7 @@ public enum EnumPlayerPacket { SaveBook, CompanionAction, GetRole, + GetTraderData, InputDevice, SpecialKeyState, ScreenSize, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java deleted file mode 100644 index f7ca16dd7..000000000 --- a/src/main/java/kamkeel/npcs/network/packets/data/large/AuctionDataPacket.java +++ /dev/null @@ -1,218 +0,0 @@ -package kamkeel.npcs.network.packets.data.large; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import kamkeel.npcs.network.LargeAbstractPacket; -import kamkeel.npcs.network.PacketChannel; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumDataPacket; -import kamkeel.npcs.util.ByteBufUtils; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; - -import java.io.IOException; -import java.util.List; - -/** - * Large packet for sending auction listing data from server to client. - * Used for initial load and search/filter queries. - */ -public final class AuctionDataPacket extends LargeAbstractPacket { - public static final String packetName = "Large|AuctionData"; - - private DataType dataType; - private List listings; - private int totalResults; - private int currentPage; - private int totalPages; - private long playerBalance; - - public AuctionDataPacket() { - } - - private AuctionDataPacket(DataType type, List listings, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - this.dataType = type; - this.listings = listings; - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - this.playerBalance = playerBalance; - } - - /** - * Send filtered listing results to client - */ - public static void sendFilteredListings(EntityPlayerMP player, AuctionFilter filter) { - if (AuctionController.Instance == null) { - return; - } - - AuctionController.FilterResult result = AuctionController.Instance.getFilteredListings(filter); - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.FILTERED_LISTINGS, - result.listings, - result.totalResults, - result.currentPage, - result.totalPages, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's own listings - */ - public static void sendMyListings(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List myListings = AuctionController.Instance - .getListingsBySeller(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.MY_LISTINGS, - myListings, - myListings.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's active bids - */ - public static void sendMyBids(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List myBids = AuctionController.Instance - .getListingsAsBidder(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.MY_BIDS, - myBids, - myBids.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - /** - * Send player's claimable items - */ - public static void sendClaimable(EntityPlayerMP player) { - if (AuctionController.Instance == null) { - return; - } - - List claimable = AuctionController.Instance - .getClaimableListings(player.getUniqueID()); - - long balance = 0; - if (noppes.npcs.controllers.CurrencyController.Instance != null) { - balance = noppes.npcs.controllers.CurrencyController.Instance.getBalance(player); - } - - AuctionDataPacket packet = new AuctionDataPacket( - DataType.CLAIMABLE, - claimable, - claimable.size(), - 0, - 1, - balance - ); - PacketHandler.Instance.sendToPlayer(packet, player); - } - - @Override - public Enum getType() { - return EnumDataPacket.AUCTION_DATA; - } - - @Override - public PacketChannel getChannel() { - return PacketHandler.DATA_PACKET; - } - - @Override - protected byte[] getData() throws IOException { - ByteBuf buffer = Unpooled.buffer(); - - buffer.writeInt(dataType.ordinal()); - buffer.writeInt(totalResults); - buffer.writeInt(currentPage); - buffer.writeInt(totalPages); - buffer.writeLong(playerBalance); - - // Write listings as NBT - NBTTagList listingList = new NBTTagList(); - if (listings != null) { - for (AuctionListing listing : listings) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - } - - NBTTagCompound compound = new NBTTagCompound(); - compound.setTag("Listings", listingList); - ByteBufUtils.writeNBT(buffer, compound); - - byte[] bytes = new byte[buffer.readableBytes()]; - buffer.readBytes(bytes); - return bytes; - } - - @Override - protected void handleCompleteData(ByteBuf data, EntityPlayer player) throws IOException { - // Client-side handling will be done in a client util class - // Similar to NoppesUtil.setScrollData() - int typeOrdinal = data.readInt(); - int totalResults = data.readInt(); - int currentPage = data.readInt(); - int totalPages = data.readInt(); - long playerBalance = data.readLong(); - - NBTTagCompound compound = ByteBufUtils.readNBT(data); - - DataType type = DataType.values()[typeOrdinal]; - - // Store data for client GUI to access - // This will be implemented in the client-side GUI handler - noppes.npcs.client.NoppesUtil.setAuctionData(type.ordinal(), compound, - totalResults, currentPage, totalPages, playerBalance); - } - - public enum DataType { - FILTERED_LISTINGS, - MY_LISTINGS, - MY_BIDS, - CLAIMABLE - } -} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java deleted file mode 100644 index a63312d80..000000000 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ /dev/null @@ -1,579 +0,0 @@ -package kamkeel.npcs.network.packets.player; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.network.AbstractPacket; -import kamkeel.npcs.network.PacketChannel; -import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.PacketHandler; -import kamkeel.npcs.network.enums.EnumPlayerPacket; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.ChatComponentText; -import noppes.npcs.CustomNpcsPermissions; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumRoleType; -import noppes.npcs.containers.ContainerAuction; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.CurrencyController; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.util.NBTJsonUtil; -import kamkeel.npcs.network.packets.data.large.AuctionDataPacket; - -import java.io.IOException; - -/** - * Packet for all auction-related player actions. - * Client -> Server for actions, Server -> Client for responses. - */ -public class AuctionActionPacket extends AbstractPacket { - public static final String packetName = "Player|Auction"; - - private Action action; - private int listingId; - private long amount; - private long buyoutPrice; - private int durationOrdinal; - private NBTTagCompound itemData; - private NBTTagCompound filterData; // For query operations - private String responseMessage; - private boolean success; - - public AuctionActionPacket() { - } - - private AuctionActionPacket(Action action) { - this.action = action; - } - - @Override - public boolean needsNPC() { - return true; - } - - // ==================== Client -> Server Actions ==================== - - /** - * Create a new auction listing - */ - @SideOnly(Side.CLIENT) - public static void CreateListing(ItemStack item, long startingPrice, long buyoutPrice, EnumAuctionDuration duration) { - AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); - packet.amount = startingPrice; - packet.buyoutPrice = buyoutPrice; - packet.durationOrdinal = duration.ordinal(); - packet.itemData = item.writeToNBT(new NBTTagCompound()); - PacketClient.sendClient(packet); - } - - /** - * Place a bid on an auction - */ - @SideOnly(Side.CLIENT) - public static void PlaceBid(int listingId, long bidAmount) { - AuctionActionPacket packet = new AuctionActionPacket(Action.PlaceBid); - packet.listingId = listingId; - packet.amount = bidAmount; - PacketClient.sendClient(packet); - } - - /** - * Execute buyout on an auction - */ - @SideOnly(Side.CLIENT) - public static void Buyout(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Cancel own listing - */ - @SideOnly(Side.CLIENT) - public static void CancelListing(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.CancelListing); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Claim item from won/expired/cancelled auction - */ - @SideOnly(Side.CLIENT) - public static void ClaimItem(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Claim proceeds from sold auction - */ - @SideOnly(Side.CLIENT) - public static void ClaimProceeds(int listingId) { - AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimProceeds); - packet.listingId = listingId; - PacketClient.sendClient(packet); - } - - /** - * Query filtered listings - */ - @SideOnly(Side.CLIENT) - public static void QueryListings(AuctionFilter filter) { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryListings); - packet.filterData = filter.writeToNBT(new NBTTagCompound()); - PacketClient.sendClient(packet); - } - - /** - * Request player's own listings - */ - @SideOnly(Side.CLIENT) - public static void QueryMyListings() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyListings); - PacketClient.sendClient(packet); - } - - /** - * Request player's active bids - */ - @SideOnly(Side.CLIENT) - public static void QueryMyBids() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryMyBids); - PacketClient.sendClient(packet); - } - - /** - * Request player's claimable items - */ - @SideOnly(Side.CLIENT) - public static void QueryClaimable() { - AuctionActionPacket packet = new AuctionActionPacket(Action.QueryClaimable); - PacketClient.sendClient(packet); - } - - @Override - public Enum getType() { - return EnumPlayerPacket.AuctionAction; - } - - @Override - public PacketChannel getChannel() { - return PacketHandler.PLAYER_PACKET; - } - - @Override - @SideOnly(Side.CLIENT) - public void sendData(ByteBuf out) throws IOException { - out.writeInt(action.ordinal()); - - switch (action) { - case CreateListing: - out.writeLong(amount); - out.writeLong(buyoutPrice); - out.writeInt(durationOrdinal); - String itemJson = NBTJsonUtil.Convert(itemData); - byte[] itemBytes = itemJson.getBytes("UTF-8"); - out.writeInt(itemBytes.length); - out.writeBytes(itemBytes); - break; - - case PlaceBid: - out.writeInt(listingId); - out.writeLong(amount); - break; - - case Buyout: - case CancelListing: - case ClaimItem: - case ClaimProceeds: - out.writeInt(listingId); - break; - - case QueryListings: - String filterJson = NBTJsonUtil.Convert(filterData); - byte[] filterBytes = filterJson.getBytes("UTF-8"); - out.writeInt(filterBytes.length); - out.writeBytes(filterBytes); - break; - - case QueryMyListings: - case QueryMyBids: - case QueryClaimable: - // No additional data needed - break; - } - } - - @Override - public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { - Action requestedAction = Action.values()[in.readInt()]; - - if (!(player instanceof EntityPlayerMP)) { - return; - } - - EntityPlayerMP playerMP = (EntityPlayerMP) player; - - // Verify NPC is an Auctioneer - if (npc == null || npc.advanced.role != EnumRoleType.Auctioneer) { - return; - } - - // Check if auction system is enabled - if (!ConfigMarket.AuctionEnabled) { - sendResponse(playerMP, false, "Auction system is disabled"); - return; - } - - // Check controller exists - if (AuctionController.Instance == null) { - sendResponse(playerMP, false, "Auction controller not initialized"); - return; - } - - switch (requestedAction) { - case CreateListing: - handleCreateListing(in, playerMP); - break; - case PlaceBid: - handlePlaceBid(in, playerMP); - break; - case Buyout: - handleBuyout(in, playerMP); - break; - case CancelListing: - handleCancelListing(in, playerMP); - break; - case ClaimItem: - handleClaimItem(in, playerMP); - break; - case ClaimProceeds: - handleClaimProceeds(in, playerMP); - break; - case QueryListings: - handleQueryListings(in, playerMP); - break; - case QueryMyListings: - handleQueryMyListings(playerMP); - break; - case QueryMyBids: - handleQueryMyBids(playerMP); - break; - case QueryClaimable: - handleQueryClaimable(playerMP); - break; - } - } - - private void handleCreateListing(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_LIST)) { - sendResponse(player, false, "You don't have permission to create listings"); - return; - } - - long startingPrice = in.readLong(); - long buyoutPrice = in.readLong(); - int durationOrdinal = in.readInt(); - - int itemBytesLength = in.readInt(); - byte[] itemBytes = new byte[itemBytesLength]; - in.readBytes(itemBytes); - String itemJson = new String(itemBytes, "UTF-8"); - - try { - NBTTagCompound itemNBT = NBTJsonUtil.Convert(itemJson); - ItemStack requestedItem = ItemStack.loadItemStackFromNBT(itemNBT); - - if (!(player.openContainer instanceof ContainerAuction)) { - sendResponse(player, false, "Open the auction listing screen to create a listing"); - return; - } - - ContainerAuction container = (ContainerAuction) player.openContainer; - ItemStack slotItem = container.getListingItem(); - - if (requestedItem == null || slotItem == null) { - sendResponse(player, false, "Place an item in the listing slot"); - return; - } - - if (!ItemStack.areItemStacksEqual(slotItem, requestedItem) || slotItem.stackSize != requestedItem.stackSize) { - sendResponse(player, false, "Listing item mismatch"); - return; - } - - // Validate duration - EnumAuctionDuration duration; - if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { - duration = EnumAuctionDuration.values()[durationOrdinal]; - } else { - duration = EnumAuctionDuration.MEDIUM; - } - - // Validate prices - if (startingPrice < 1) { - sendResponse(player, false, "Starting price must be at least 1"); - return; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - sendResponse(player, false, "Buyout price must be higher than starting price"); - return; - } - - // Create the listing - AuctionListing listing = AuctionController.Instance.createListing( - player, slotItem, startingPrice, buyoutPrice, duration); - - if (listing != null) { - container.clearListingSlot(); - sendResponse(player, true, "Listing created successfully"); - } else { - sendResponse(player, false, "Failed to create listing. Check your balance and listing limit."); - } - } catch (Exception e) { - sendResponse(player, false, "Error creating listing: " + e.getMessage()); - } - } - - private void handlePlaceBid(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BID)) { - sendResponse(player, false, "You don't have permission to bid"); - return; - } - - int listingId = in.readInt(); - long bidAmount = in.readLong(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (!listing.isActive()) { - sendResponse(player, false, "This auction has ended"); - return; - } - - if (bidAmount < listing.getMinimumBid()) { - sendResponse(player, false, "Bid must be at least " + listing.getMinimumBid()); - return; - } - - if (!CurrencyController.Instance.canAfford(player, bidAmount)) { - sendResponse(player, false, "You cannot afford this bid"); - return; - } - - if (AuctionController.Instance.placeBid(listingId, player, bidAmount)) { - sendResponse(player, true, "Bid placed successfully"); - } else { - sendResponse(player, false, "Failed to place bid"); - } - } - - private void handleBuyout(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_BUYOUT)) { - sendResponse(player, false, "You don't have permission to use buyout"); - return; - } - - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (!listing.isActive()) { - sendResponse(player, false, "This auction has ended"); - return; - } - - if (listing.buyoutPrice <= 0) { - sendResponse(player, false, "This listing has no buyout option"); - return; - } - - if (!CurrencyController.Instance.canAfford(player, listing.buyoutPrice)) { - sendResponse(player, false, "You cannot afford the buyout price"); - return; - } - - if (AuctionController.Instance.buyout(listingId, player)) { - sendResponse(player, true, "Buyout successful! Claim your item."); - } else { - sendResponse(player, false, "Failed to execute buyout"); - } - } - - private void handleCancelListing(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - // Admin can cancel any listing - boolean isAdmin = CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_ADMIN); - - if (!listing.sellerUUID.equals(player.getUniqueID()) && !isAdmin) { - sendResponse(player, false, "You can only cancel your own listings"); - return; - } - - if (listing.hasBids() && !isAdmin) { - sendResponse(player, false, "Cannot cancel listing with active bids"); - return; - } - - if (AuctionController.Instance.cancelListing(listingId, player, isAdmin)) { - sendResponse(player, true, "Listing cancelled. Claim your item."); - } else { - sendResponse(player, false, "Failed to cancel listing"); - } - } - - private void handleClaimItem(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - ItemStack claimed = null; - - // Try buyer claim first - if (listing.highBidderUUID != null && listing.highBidderUUID.equals(player.getUniqueID())) { - claimed = AuctionController.Instance.claimBuyerItem(listingId, player); - } - // Then try seller reclaim (for expired/cancelled) - else if (listing.sellerUUID.equals(player.getUniqueID())) { - claimed = AuctionController.Instance.claimExpiredItem(listingId, player); - } - - if (claimed != null) { - // Give item to player - if (!player.inventory.addItemStackToInventory(claimed)) { - // Drop if inventory full - player.entityDropItem(claimed, 0.5f); - } - sendResponse(player, true, "Item claimed"); - } else { - sendResponse(player, false, "Cannot claim item"); - } - } - - private void handleClaimProceeds(ByteBuf in, EntityPlayerMP player) throws IOException { - int listingId = in.readInt(); - - AuctionListing listing = AuctionController.Instance.getListing(listingId); - if (listing == null) { - sendResponse(player, false, "Listing not found"); - return; - } - - if (AuctionController.Instance.claimSellerProceeds(listingId, player)) { - long proceeds = listing.getSellerProceeds(); - sendResponse(player, true, "Claimed " + proceeds + " " + ConfigMarket.CurrencyName); - } else { - sendResponse(player, false, "Cannot claim proceeds"); - } - } - - // ==================== Query Handlers ==================== - - private void handleQueryListings(ByteBuf in, EntityPlayerMP player) throws IOException { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - int filterBytesLength = in.readInt(); - byte[] filterBytes = new byte[filterBytesLength]; - in.readBytes(filterBytes); - String filterJson = new String(filterBytes, "UTF-8"); - - try { - NBTTagCompound filterNBT = NBTJsonUtil.Convert(filterJson); - AuctionFilter filter = new AuctionFilter(); - filter.readFromNBT(filterNBT); - - // Send filtered listings to client - AuctionDataPacket.sendFilteredListings(player, filter); - } catch (Exception e) { - sendResponse(player, false, "Error parsing filter"); - } - } - - private void handleQueryMyListings(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendMyListings(player); - } - - private void handleQueryMyBids(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendMyBids(player); - } - - private void handleQueryClaimable(EntityPlayerMP player) { - // Check permission - if (!CustomNpcsPermissions.hasPermission(player, CustomNpcsPermissions.AUCTION_USE)) { - sendResponse(player, false, "You don't have permission to view auctions"); - return; - } - - AuctionDataPacket.sendClaimable(player); - } - - /** - * Send a response message to the player - */ - private void sendResponse(EntityPlayerMP player, boolean success, String message) { - // Use chat message for now; can be extended to use GUI updates - if (message != null && !message.isEmpty()) { - player.addChatMessage(new ChatComponentText("[Auction] " + message)); - } - } - - private enum Action { - CreateListing, - PlaceBid, - Buyout, - CancelListing, - ClaimItem, - ClaimProceeds, - QueryListings, - QueryMyListings, - QueryMyBids, - QueryClaimable - } -} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java new file mode 100644 index 000000000..5b7cb7dfe --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java @@ -0,0 +1,77 @@ +package kamkeel.npcs.network.packets.player; + +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.roles.RoleTrader; + +import java.io.IOException; + +/** + * Client requests trader-specific GUI data from server. + * Server responds with player balance, stock info, currency costs via GuiDataPacket. + */ +public class GetTraderData extends AbstractPacket { + public static final String packetName = "Player|GetTraderData"; + + @Override + public Enum getType() { + return EnumPlayerPacket.GetTraderData; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + public boolean needsNPC() { + return true; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + // No data needed for request + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!(npc.roleInterface instanceof RoleTrader)) + return; + + RoleTrader role = (RoleTrader) npc.roleInterface; + NBTTagCompound compound = new NBTTagCompound(); + + // Player balance + PlayerData data = PlayerData.get(player); + compound.setLong("Balance", data.currencyData.getBalance()); + + // Stock info + compound.setBoolean("StockEnabled", role.stock.enableStock); + compound.setLong("ResetTimeMillis", role.getResetTimeRemainingMillis()); + + // Available stock per slot (player-specific if perPlayer mode) + String playerName = player.getCommandSenderName(); + int[] stock = new int[18]; + for (int i = 0; i < 18; i++) { + stock[i] = role.getAvailableStock(i, playerName); + } + compound.setIntArray("Stock", stock); + + // Currency cost per slot + for (int i = 0; i < 18; i++) { + compound.setLong("Cost" + i, role.getCurrencyCost(i)); + } + + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } +} diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index a73c179e4..58f931ab8 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -15,7 +15,6 @@ import noppes.npcs.api.IWorld; import noppes.npcs.blocks.tiles.TileNpcContainer; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; -import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -132,9 +131,6 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.CompanionInv) return new ContainerNPCCompanion(npc, player); - if (gui == EnumGuiType.PlayerAuction) - return new ContainerAuction(npc, player); - return null; } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index b29a6cd0b..41cd8770e 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -59,10 +59,6 @@ import noppes.npcs.controllers.GlobalDataController; import noppes.npcs.controllers.LinkedItemController; import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; -import noppes.npcs.controllers.VaultHelper; -import noppes.npcs.controllers.CurrencyController; -import noppes.npcs.controllers.AuctionBlacklist; -import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -314,10 +310,6 @@ public class CustomNpcs { //Loading items in the about to start event was corrupting items with a damage value @EventHandler public void started(FMLServerStartedEvent event) { - new VaultHelper(); - new CurrencyController(); - new AuctionBlacklist(); - new AuctionController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 34a5fbf50..c83e92c22 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -92,13 +92,6 @@ public class CustomNpcsPermissions { public static final Permission SOULSTONE_ALL = new Permission("customnpcs.soulstone.all"); - // Auction System - public static final Permission AUCTION_USE = new Permission("customnpcs.auction.use"); - public static final Permission AUCTION_LIST = new Permission("customnpcs.auction.list"); - public static final Permission AUCTION_BID = new Permission("customnpcs.auction.bid"); - public static final Permission AUCTION_BUYOUT = new Permission("customnpcs.auction.buyout"); - public static final Permission AUCTION_ADMIN = new Permission("customnpcs.auction.admin"); - // Modify / Save Scripts public static final Permission SCRIPT = new Permission("customnpcs.script"); public static final Permission SCRIPT_NPC = new Permission("customnpcs.script.npc"); diff --git a/src/main/java/noppes/npcs/DataAdvanced.java b/src/main/java/noppes/npcs/DataAdvanced.java index f03714506..45ecfe4a8 100644 --- a/src/main/java/noppes/npcs/DataAdvanced.java +++ b/src/main/java/noppes/npcs/DataAdvanced.java @@ -18,7 +18,6 @@ import noppes.npcs.roles.JobGuard; import noppes.npcs.roles.JobHealer; import noppes.npcs.roles.JobItemGiver; import noppes.npcs.roles.JobSpawner; -import noppes.npcs.roles.RoleAuctioneer; import noppes.npcs.roles.RoleBank; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; @@ -219,8 +218,6 @@ public class DataAdvanced { npc.roleInterface = new RoleCompanion(npc); else if (role == EnumRoleType.Mount && !(npc.roleInterface instanceof RoleMount)) npc.roleInterface = new RoleMount(npc); - else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) - npc.roleInterface = new RoleAuctioneer(npc); } public void setJob(int i) { diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 89bdf58c8..08c04f6f4 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -384,6 +384,7 @@ public class NoppesUtilServer { if (gui == EnumGuiType.PlayerFollower || gui == EnumGuiType.PlayerFollowerHire || gui == EnumGuiType.PlayerTrader || gui == EnumGuiType.PlayerTransporter) { sendRoleData(player, npc); } + // Note: Trader-specific data (balance, stock, costs) is requested by client via GetTraderData packet } private static ArrayList getScrollData(EntityPlayer player, EnumGuiType gui, EntityNPCInterface npc) { diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index fe846df19..3648f00d7 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,7 +10,6 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; import noppes.npcs.client.AnalyticsTracking; -import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; @@ -66,11 +65,6 @@ public class ServerTickHandler { ProfileController.Instance.login(player); SyncController.beginLogin(player); SyncController.syncEffects(player); - - // Send pending auction notifications - if (AuctionController.Instance != null) { - AuctionController.Instance.onPlayerLogin(player); - } } @SubscribeEvent diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index a31b57a7f..4e64f8351 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -104,7 +104,6 @@ import noppes.npcs.client.gui.player.GuiCrate; import noppes.npcs.client.gui.player.GuiMailbox; import noppes.npcs.client.gui.player.GuiMailmanWrite; import noppes.npcs.client.gui.player.GuiNPCBankChest; -import noppes.npcs.client.gui.player.GuiAuction; import noppes.npcs.client.gui.player.GuiNPCTrader; import noppes.npcs.client.gui.player.GuiNpcAnvil; import noppes.npcs.client.gui.player.GuiNpcCarpentryBench; @@ -180,7 +179,6 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.config.StringCache; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.containers.ContainerAnvilRepair; -import noppes.npcs.containers.ContainerAuction; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; import noppes.npcs.containers.ContainerCustomGui; @@ -576,9 +574,6 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.MagicBook) return new GuiNpcMagicBook(); - else if (gui == EnumGuiType.PlayerAuction) - return new GuiAuction(npc, (ContainerAuction) container); - else if (gui == EnumGuiType.GlobalRemote) return new GuiNPCGlobalMainMenu(null); diff --git a/src/main/java/noppes/npcs/client/NoppesUtil.java b/src/main/java/noppes/npcs/client/NoppesUtil.java index 64b26fa5d..620142564 100644 --- a/src/main/java/noppes/npcs/client/NoppesUtil.java +++ b/src/main/java/noppes/npcs/client/NoppesUtil.java @@ -182,67 +182,6 @@ public class NoppesUtil { data = new HashMap(); } - // ==================== Auction Data ==================== - - // Static storage for auction data that the GUI can access - private static NBTTagCompound auctionListingData; - private static int auctionDataType; - private static int auctionTotalResults; - private static int auctionCurrentPage; - private static int auctionTotalPages; - private static long auctionPlayerBalance; - - public static void setAuctionData(int dataType, NBTTagCompound compound, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - auctionDataType = dataType; - auctionListingData = compound; - auctionTotalResults = totalResults; - auctionCurrentPage = currentPage; - auctionTotalPages = totalPages; - auctionPlayerBalance = playerBalance; - - // Notify the current GUI if it's an auction GUI - GuiScreen gui = Minecraft.getMinecraft().currentScreen; - if (gui instanceof IAuctionData) { - ((IAuctionData) gui).onAuctionDataReceived(dataType, compound, - totalResults, currentPage, totalPages, playerBalance); - } - } - - public static NBTTagCompound getAuctionListingData() { - return auctionListingData; - } - - public static int getAuctionDataType() { - return auctionDataType; - } - - public static int getAuctionTotalResults() { - return auctionTotalResults; - } - - public static int getAuctionCurrentPage() { - return auctionCurrentPage; - } - - public static int getAuctionTotalPages() { - return auctionTotalPages; - } - - public static long getAuctionPlayerBalance() { - return auctionPlayerBalance; - } - - /** - * Interface for GUIs that receive auction data - */ - public interface IAuctionData { - void onAuctionDataReceived(int dataType, NBTTagCompound data, - int totalResults, int currentPage, int totalPages, - long playerBalance); - } - public static void guiQuestCompletion(EntityPlayer player, NBTTagCompound read) { Quest quest = new Quest(); quest.readNBT(read); diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 7ee5e8881..885c61d94 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -26,7 +26,6 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; import noppes.npcs.client.gui.roles.GuiNpcSpawner; -import noppes.npcs.client.gui.roles.GuiNpcAuctioneer; import noppes.npcs.client.gui.roles.GuiNpcMount; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; @@ -158,8 +157,6 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { displayGuiScreen(new GuiNpcCompanion(npc)); else if (npc.advanced.role == EnumRoleType.Mount) displayGuiScreen(new GuiNpcMount(npc)); - else if (npc.advanced.role == EnumRoleType.Auctioneer) - displayGuiScreen(new GuiNpcAuctioneer(npc)); } else if (compound.hasKey("JobData")) { if (npc.jobInterface != null) npc.jobInterface.readFromNBT(compound); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java deleted file mode 100644 index 30e857e7a..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuction.java +++ /dev/null @@ -1,841 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.inventory.Slot; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.CustomNpcResourceListener; -import noppes.npcs.client.NoppesUtil; -import noppes.npcs.client.gui.util.*; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.containers.ContainerAuction; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.entity.EntityNPCInterface; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Main auction house browse GUI. - * Displays listings with search, sort, and pagination. - */ -public class GuiAuction extends GuiContainerNPCInterface implements IGuiData, - ICustomScrollListener, NoppesUtil.IAuctionData { - - private static final ResourceLocation BG_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/standardbg.png"); - private static final int LEFT_PANEL_X = 8; - private static final int LEFT_PANEL_WIDTH = 190; - private static final int RIGHT_PANEL_X = 210; - private static final int ACTION_BUTTON_WIDTH = 98; - private static final int LISTING_SCROLL_HEIGHT = 100; - private static final int LISTING_SCROLL_WIDTH = 190; - private static final int PAGE_BUTTON_WIDTH = 60; - private static final int SEARCH_FIELD_WIDTH = 140; - private static final int SEARCH_BUTTON_WIDTH = 46; - private static final int HIDDEN_SLOT_POSITION = -1000; - private static final int INVENTORY_START_X = 8; - private static final int INVENTORY_START_Y = 166; - private static final int HOTBAR_Y_OFFSET = 58; - - // GUI Components - private GuiNpcTextField searchField; - private GuiNpcTextField startingPriceField; - private GuiNpcTextField buyoutPriceField; - private GuiCustomScroll listingScroll; - - // Current state - private AuctionFilter currentFilter = new AuctionFilter(); - private List listings = new ArrayList<>(); - private int totalResults = 0; - private int currentPage = 0; - private int totalPages = 0; - private long playerBalance = 0; - - // Tab state (0=Browse, 1=My Listings, 2=My Bids, 3=Claims, 4=Create) - private int currentTab = 0; - private int selectedDuration = 1; - - // Selected listing for details - private int selectedListingIndex = -1; - - // Countdown update timer - private long lastCountdownUpdate = 0; - - private enum ActionMode { - BID, - CANCEL, - CLAIM - } - - public GuiAuction(EntityNPCInterface npc, ContainerAuction container) { - super(npc, container); - this.title = ""; - this.xSize = 319; - this.ySize = 238; - this.closeOnEsc = true; - this.drawDefaultBackground = true; - - // Request initial data - AuctionActionPacket.QueryListings(currentFilter); - } - - @Override - public void initGui() { - super.initGui(); - - int leftX = guiLeft + LEFT_PANEL_X; - int rightX = guiLeft + RIGHT_PANEL_X; - int actionY = guiTop + 44; - boolean isCreateTab = currentTab == 4; - boolean isBrowseTab = currentTab == 0; - - updateListingSlotPosition(isCreateTab); - - // Tab buttons at top - GuiMenuTopButton browseButton = new GuiMenuTopButton(0, guiLeft + 4, guiTop - 17, StatCollector.translateToLocal("auction.browse")); - addTopButton(browseButton); - GuiMenuTopButton myListingsButton = new GuiMenuTopButton(1, browseButton, StatCollector.translateToLocal("auction.myListings")); - addTopButton(myListingsButton); - GuiMenuTopButton myBidsButton = new GuiMenuTopButton(2, myListingsButton, StatCollector.translateToLocal("auction.myBids")); - addTopButton(myBidsButton); - GuiMenuTopButton claimsButton = new GuiMenuTopButton(3, myBidsButton, StatCollector.translateToLocal("auction.claims")); - addTopButton(claimsButton); - GuiMenuTopButton sellButton = new GuiMenuTopButton(4, claimsButton, StatCollector.translateToLocal("auction.sell")); - addTopButton(sellButton); - getTopButton(currentTab).setEnabled(false); - - if (isCreateTab) { - addCreateControls(leftX, rightX); - } else { - if (isBrowseTab) { - // Search field - searchField = new GuiNpcTextField(10, this, leftX, guiTop + 6, SEARCH_FIELD_WIDTH, 14, currentFilter.searchText); - searchField.setMaxStringLength(50); - addTextField(searchField); - - // Search button - addButton(new GuiNpcButton(11, leftX + SEARCH_FIELD_WIDTH + 4, guiTop + 5, SEARCH_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.search"))); - - // Sort button - addButton(new GuiNpcButton(30, leftX, guiTop + 24, LEFT_PANEL_WIDTH, 16, currentFilter.sortOrder.getDisplayName())); - } - - // Listing scroll area - listingScroll = new GuiCustomScroll(this, 0); - listingScroll.guiLeft = leftX; - listingScroll.guiTop = guiTop + 44; - listingScroll.setSize(LISTING_SCROLL_WIDTH, LISTING_SCROLL_HEIGHT); - addScroll(listingScroll); - updateListingDisplay(); - - if (isBrowseTab) { - // Pagination buttons - addButton(new GuiNpcButton(40, leftX, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.prevPage"))); - addLabel(new GuiNpcLabel(41, getPageLabel(), leftX + 70, guiTop + 152, CustomNpcResourceListener.DefaultTextColor)); - addButton(new GuiNpcButton(42, leftX + LEFT_PANEL_WIDTH - PAGE_BUTTON_WIDTH, guiTop + 148, PAGE_BUTTON_WIDTH, 16, - StatCollector.translateToLocal("auction.nextPage"))); - } - - // Action buttons (right side) - addActionButtons(rightX, actionY); - - // Balance display - addLabel(new GuiNpcLabel(60, String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)), - rightX, guiTop + 170, 0x00AA00)); - } - - updateButtonStates(); - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update countdowns every second - long now = System.currentTimeMillis(); - if (now - lastCountdownUpdate >= 1000) { - lastCountdownUpdate = now; - updateListingDisplay(); - } - - if (currentTab == 4 && inventorySlots instanceof ContainerAuction) { - ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); - GuiNpcLabel itemLabel = getLabel(102); - GuiNpcButton createButton = getButton(71); - if (item != null) { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); - itemLabel.color = 0x008800; - } - if (createButton != null) { - createButton.setEnabled(true); - } - } else { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); - itemLabel.color = 0x888888; - } - if (createButton != null) { - createButton.setEnabled(false); - } - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - super.actionPerformed(button); - int id = button.id; - - // Tab buttons - if (id >= 0 && id <= 4) { - switchTab(id); - } - // Search button - else if (id == 11) { - if (currentTab == 0) { - performSearch(); - } - } - // Sort button - else if (id == 30) { - if (currentTab == 0) { - cycleSortOrder(); - } - } - // Pagination - else if (id == 40) { - if (currentTab == 0) { - previousPage(); - } - } else if (id == 42) { - if (currentTab == 0) { - nextPage(); - } - } - // Actions - else if (id == 50) { - viewDetails(); - } else if (id == 51) { - ActionMode mode = getActionMode(); - if (mode == ActionMode.BID) { - placeBid(); - } else if (mode == ActionMode.CANCEL) { - cancelListing(); - } else if (mode == ActionMode.CLAIM) { - claimListing(); - } - } else if (id == 52) { - if (getActionMode() == ActionMode.BID) { - buyout(); - } - } else if (id == 53) { - refresh(); - } else if (id == 70) { - selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; - initGui(); - } else if (id == 71) { - createListing(); - } else if (id == 72) { - switchTab(0); - } - } - - private void switchTab(int tab) { - currentTab = tab; - currentPage = 0; - selectedListingIndex = -1; - - switch (tab) { - case 0: // Browse - AuctionActionPacket.QueryListings(currentFilter); - break; - case 1: // My Listings - AuctionActionPacket.QueryMyListings(); - break; - case 2: // My Bids - AuctionActionPacket.QueryMyBids(); - break; - case 3: // Claims - AuctionActionPacket.QueryClaimable(); - break; - case 4: - break; - } - - initGui(); - } - - private void performSearch() { - currentFilter.searchText = searchField.getText(); - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - } - - private void cycleSortOrder() { - currentFilter.sortOrder = currentFilter.sortOrder.next(); - currentFilter.page = 0; - currentPage = 0; - AuctionActionPacket.QueryListings(currentFilter); - initGui(); - } - - private void previousPage() { - if (currentPage > 0) { - currentPage--; - currentFilter.page = currentPage; - AuctionActionPacket.QueryListings(currentFilter); - } - } - - private void nextPage() { - if (currentPage < totalPages - 1) { - currentPage++; - currentFilter.page = currentPage; - AuctionActionPacket.QueryListings(currentFilter); - } - } - - private void viewDetails() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.item == null) { - return; - } - setSubGui(new SubGuiAuctionDetails(this, listing)); - } - } - - private void placeBid() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.item == null) { - return; - } - setSubGui(new SubGuiAuctionBid(this, listing, playerBalance)); - } - } - - private void buyout() { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - if (listing.buyoutPrice > 0 && playerBalance >= listing.buyoutPrice) { - AuctionActionPacket.Buyout(listing.id); - refresh(); - } - } - } - - private void refresh() { - switch (currentTab) { - case 0: - AuctionActionPacket.QueryListings(currentFilter); - break; - case 1: - AuctionActionPacket.QueryMyListings(); - break; - case 2: - AuctionActionPacket.QueryMyBids(); - break; - case 3: - AuctionActionPacket.QueryClaimable(); - break; - } - } - - private void updateListingDisplay() { - if (listingScroll == null) return; - - List displayList = new ArrayList<>(); - for (AuctionListing listing : listings) { - String line = formatListingLine(listing); - displayList.add(line); - } - listingScroll.setList(displayList, true, false); - - if (selectedListingIndex >= 0 && selectedListingIndex < displayList.size()) { - listingScroll.selected = selectedListingIndex; - } else { - selectedListingIndex = -1; - listingScroll.selected = -1; - } - } - - private String formatListingLine(AuctionListing listing) { - String itemName = listing.item == null - ? "Unknown" - : listing.item.getDisplayName(); - if (itemName.length() > 15) { - itemName = itemName.substring(0, 12) + "..."; - } - - long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - String priceStr = formatCurrency(price); - - String timeStr = formatTimeRemaining(listing.getTimeRemaining()); - - return String.format("%s - %s (%s)", itemName, priceStr, timeStr); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return days + "d " + (hours % 24) + "h"; - } else if (hours > 0) { - return hours + "h " + (minutes % 60) + "m"; - } else if (minutes > 0) { - return minutes + "m " + (seconds % 60) + "s"; - } else { - return seconds + "s"; - } - } - - private String formatCurrency(long amount) { - if (amount >= 1000000) { - return String.format("%.1fM", amount / 1000000.0); - } else if (amount >= 1000) { - return String.format("%.1fK", amount / 1000.0); - } - return String.valueOf(amount); - } - - private String getPageLabel() { - if (totalPages == 0) { - return StatCollector.translateToLocal("auction.noResults"); - } - return String.format(StatCollector.translateToLocal("auction.page"), currentPage + 1, totalPages); - } - - private void updateButtonStates() { - ActionMode mode = getActionMode(); - AuctionListing listing = getSelectedListing(); - boolean hasSelection = listing != null; - - // Pagination buttons - if (getButton(40) != null) getButton(40).setEnabled(currentPage > 0); - if (getButton(42) != null) getButton(42).setEnabled(currentPage < totalPages - 1); - - // Action buttons based on selection - if (getButton(50) != null) getButton(50).setEnabled(hasSelection); - if (getButton(51) != null) { - if (!hasSelection) { - getButton(51).setEnabled(false); - } else if (mode == ActionMode.BID) { - getButton(51).setEnabled(canPlaceBid(listing)); - } else if (mode == ActionMode.CANCEL) { - getButton(51).setEnabled(canCancelListing(listing)); - } else { - getButton(51).setEnabled(getClaimAction(listing) != ClaimAction.NONE); - } - } - - if (getButton(52) != null) { - if (!hasSelection || mode != ActionMode.BID) { - getButton(52).setEnabled(false); - } else { - getButton(52).setEnabled(canBuyout(listing)); - } - } - } - - private void updateListingSlotPosition(boolean showInventory) { - if (!(inventorySlots instanceof ContainerAuction)) { - return; - } - - ContainerAuction container = (ContainerAuction) inventorySlots; - if (showInventory) { - container.listingSlot.xDisplayPosition = RIGHT_PANEL_X + 41; - container.listingSlot.yDisplayPosition = 64; - - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 9; col++) { - int slotIndex = 1 + col + row * 9; - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = INVENTORY_START_X + col * 18; - slot.yDisplayPosition = INVENTORY_START_Y + row * 18; - } - } - - for (int col = 0; col < 9; col++) { - int slotIndex = 28 + col; - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = INVENTORY_START_X + col * 18; - slot.yDisplayPosition = INVENTORY_START_Y + HOTBAR_Y_OFFSET; - } - } else { - container.listingSlot.xDisplayPosition = HIDDEN_SLOT_POSITION; - container.listingSlot.yDisplayPosition = HIDDEN_SLOT_POSITION; - - for (int slotIndex = 1; slotIndex < container.inventorySlots.size(); slotIndex++) { - Slot slot = (Slot) container.inventorySlots.get(slotIndex); - slot.xDisplayPosition = HIDDEN_SLOT_POSITION; - slot.yDisplayPosition = HIDDEN_SLOT_POSITION; - } - } - } - - private void addCreateControls(int leftX, int rightX) { - addLabel(new GuiNpcLabel(100, StatCollector.translateToLocal("auction.createListing"), leftX, guiTop + 8, 0x404040)); - addLabel(new GuiNpcLabel(101, StatCollector.translateToLocal("auction.placeItemBelow"), rightX, guiTop + 32, 0x404040)); - - ItemStack item = ((ContainerAuction) inventorySlots).getListingItem(); - if (item != null) { - addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), - rightX, guiTop + 86, 0x008800)); - } else { - addLabel(new GuiNpcLabel(102, StatCollector.translateToLocal("auction.noItemSelected"), rightX, guiTop + 86, 0x888888)); - } - - addLabel(new GuiNpcLabel(110, StatCollector.translateToLocal("auction.startingPrice"), leftX, guiTop + 36, 0x404040)); - startingPriceField = new GuiNpcTextField(111, this, leftX + 120, guiTop + 34, 90, 14, "100"); - startingPriceField.setIntegersOnly(); - startingPriceField.setMinMaxDefault(1, 999999999, 100); - addTextField(startingPriceField); - - addLabel(new GuiNpcLabel(120, StatCollector.translateToLocal("auction.buyoutPrice"), leftX, guiTop + 58, 0x404040)); - buyoutPriceField = new GuiNpcTextField(121, this, leftX + 120, guiTop + 56, 90, 14, "0"); - buyoutPriceField.setIntegersOnly(); - buyoutPriceField.setMinMaxDefault(0, 999999999, 0); - addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(122, StatCollector.translateToLocal("auction.noBuyoutHint"), leftX + 214, guiTop + 58, 0x888888)); - - addLabel(new GuiNpcLabel(130, StatCollector.translateToLocal("auction.duration"), leftX, guiTop + 80, 0x404040)); - addButton(new GuiNpcButton(70, leftX + 120, guiTop + 76, 110, 20, getDurationDisplay())); - - long fee = getListingFee(); - addLabel(new GuiNpcLabel(140, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - leftX, guiTop + 104, fee > 0 ? 0xAA0000 : 0x008800)); - - addButton(new GuiNpcButton(71, leftX, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.create"))); - addButton(new GuiNpcButton(72, leftX + 100, guiTop + 126, 90, 20, StatCollector.translateToLocal("gui.cancel"))); - - if (getButton(71) != null) { - getButton(71).setEnabled(item != null); - } - } - - private String getDurationDisplay() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getDisplayName(); - } - - private long getListingFee() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getListingFee(); - } - - private void createListing() { - ContainerAuction container = (ContainerAuction) inventorySlots; - ItemStack item = container.getListingItem(); - if (item == null) { - return; - } - - long startingPrice = parsePrice(startingPriceField, 1); - long buyoutPrice = parsePrice(buyoutPriceField, 0); - - if (startingPrice < 1) { - startingPrice = 1; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - return; - } - - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); - container.clearListingSlot(); - switchTab(0); - } - - private void cancelListing() { - AuctionListing listing = getSelectedListing(); - if (!canCancelListing(listing)) { - return; - } - - AuctionActionPacket.CancelListing(listing.id); - refresh(); - } - - private void claimListing() { - AuctionListing listing = getSelectedListing(); - if (listing == null) { - return; - } - - ClaimAction action = getClaimAction(listing); - if (action == ClaimAction.ITEM) { - AuctionActionPacket.ClaimItem(listing.id); - } else if (action == ClaimAction.PROCEEDS) { - AuctionActionPacket.ClaimProceeds(listing.id); - } else { - return; - } - - refresh(); - } - - private ActionMode getActionMode() { - if (currentTab == 1) { - return ActionMode.CANCEL; - } - if (currentTab == 3) { - return ActionMode.CLAIM; - } - return ActionMode.BID; - } - - private String getPrimaryActionLabel(ActionMode mode) { - if (mode == ActionMode.CANCEL) { - return StatCollector.translateToLocal("auction.cancel"); - } - if (mode == ActionMode.CLAIM) { - return StatCollector.translateToLocal("auction.claim"); - } - return StatCollector.translateToLocal("auction.placeBid"); - } - - private void addActionButtons(int rightX, int actionY) { - ActionMode mode = getActionMode(); - addButton(new GuiNpcButton(50, rightX, actionY, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.viewDetails"))); - addButton(new GuiNpcButton(51, rightX, actionY + 24, ACTION_BUTTON_WIDTH, 20, - getPrimaryActionLabel(mode))); - if (mode == ActionMode.BID) { - addButton(new GuiNpcButton(52, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.buyout"))); - addButton(new GuiNpcButton(53, rightX, actionY + 72, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.refresh"))); - } else { - addButton(new GuiNpcButton(53, rightX, actionY + 48, ACTION_BUTTON_WIDTH, 20, - StatCollector.translateToLocal("auction.refresh"))); - } - } - - private AuctionListing getSelectedListing() { - if (selectedListingIndex < 0 || selectedListingIndex >= listings.size()) { - return null; - } - AuctionListing listing = listings.get(selectedListingIndex); - if (listing == null || listing.item == null) { - return null; - } - return listing; - } - - private boolean canPlaceBid(AuctionListing listing) { - if (listing == null || !listing.isActive()) { - return false; - } - if (isPlayerSeller(listing)) { - return false; - } - return playerBalance >= listing.getMinimumBid(); - } - - private boolean canBuyout(AuctionListing listing) { - if (listing == null || !listing.isActive()) { - return false; - } - if (listing.buyoutPrice <= 0) { - return false; - } - if (isPlayerSeller(listing)) { - return false; - } - return playerBalance >= listing.buyoutPrice; - } - - private boolean canCancelListing(AuctionListing listing) { - if (listing == null || listing.item == null) { - return false; - } - return listing.status == EnumAuctionStatus.ACTIVE && !listing.hasBids(); - } - - private boolean isPlayerSeller(AuctionListing listing) { - if (mc == null || mc.thePlayer == null || listing == null || listing.sellerUUID == null) { - return false; - } - UUID playerId = mc.thePlayer.getUniqueID(); - return listing.sellerUUID.equals(playerId); - } - - private enum ClaimAction { - ITEM, - PROCEEDS, - NONE - } - - private ClaimAction getClaimAction(AuctionListing listing) { - if (listing == null || mc == null || mc.thePlayer == null) { - return ClaimAction.NONE; - } - - UUID playerId = mc.thePlayer.getUniqueID(); - if (listing.sellerUUID != null && listing.sellerUUID.equals(playerId) && !listing.sellerClaimed) { - if (listing.status == EnumAuctionStatus.SOLD) { - return ClaimAction.PROCEEDS; - } - if (listing.status == EnumAuctionStatus.CANCELLED || listing.status == EnumAuctionStatus.EXPIRED) { - return ClaimAction.ITEM; - } - } - - if (listing.highBidderUUID != null && listing.highBidderUUID.equals(playerId) - && listing.status == EnumAuctionStatus.SOLD && !listing.buyerClaimed) { - return ClaimAction.ITEM; - } - - return ClaimAction.NONE; - } - - private long parsePrice(GuiNpcTextField field, long defaultValue) { - if (field == null) { - return defaultValue; - } - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - @Override - protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { - drawDefaultBackground(); - GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); - - // Draw background - mc.renderEngine.bindTexture(BG_TEXTURE); - drawTexturedModalRect(guiLeft, guiTop, 0, 0, 252, ySize); - drawTexturedModalRect(guiLeft + 252, guiTop, 188, 0, xSize - 252, ySize); - - // Draw selected item preview - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - AuctionListing listing = listings.get(selectedListingIndex); - drawItemPreview(listing, guiLeft + RIGHT_PANEL_X + 41, guiTop + 192); - } - - super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); - } - - private void drawItemPreview(AuctionListing listing, int x, int y) { - if (listing == null || listing.item == null) return; - - RenderHelper.enableGUIStandardItemLighting(); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_COLOR_MATERIAL); - GL11.glEnable(GL11.GL_LIGHTING); - - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, listing.item, x, y); - - RenderHelper.disableStandardItemLighting(); - } - - @Override - protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { - // Title - String titleText = getTabTitle(); - fontRendererObj.drawString(titleText, (xSize - fontRendererObj.getStringWidth(titleText)) / 2, -8, - CustomNpcResourceListener.DefaultTextColor); - - if (currentTab != 4) { - String listingsLabel = String.format(StatCollector.translateToLocal("auction.listings"), totalResults); - fontRendererObj.drawString(listingsLabel, LEFT_PANEL_X, 36, 0x666666); - } - } - - private String getTabTitle() { - switch (currentTab) { - case 0: return StatCollector.translateToLocal("auction.browseTitle"); - case 1: return StatCollector.translateToLocal("auction.myListingsTitle"); - case 2: return StatCollector.translateToLocal("auction.myBidsTitle"); - case 3: return StatCollector.translateToLocal("auction.claimsTitle"); - case 4: return StatCollector.translateToLocal("auction.sellTitle"); - default: return StatCollector.translateToLocal("auction.title"); - } - } - - // ICustomScrollListener - @Override - public void customScrollClicked(int mouseX, int mouseY, int mouseButton, GuiCustomScroll scroll) { - int index = scroll.selected; - selectedListingIndex = index; - updateButtonStates(); - } - - @Override - public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (selectedListingIndex >= 0 && selectedListingIndex < listings.size()) { - viewDetails(); - } - } - - // IGuiData - @Override - public void setGuiData(NBTTagCompound compound) { - // This can be used for initial setup data - } - - // IAuctionData - @Override - public void onAuctionDataReceived(int dataType, NBTTagCompound data, - int totalResults, int currentPage, int totalPages, - long playerBalance) { - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - this.playerBalance = playerBalance; - - // Parse listings from NBT - listings.clear(); - NBTTagList listingList = data.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - NBTTagCompound listingNBT = listingList.getCompoundTagAt(i); - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingNBT); - listings.add(listing); - } - - selectedListingIndex = -1; - updateListingDisplay(); - updateButtonStates(); - - // Update labels - if (getLabel(41) != null) { - getLabel(41).label = getPageLabel(); - } - if (getLabel(60) != null) { - getLabel(60).label = String.format(StatCollector.translateToLocal("auction.balance"), formatCurrency(playerBalance)); - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index d23b15398..f11bbb2e6 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -1,136 +1,335 @@ package noppes.npcs.client.gui.player; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.player.GetTraderData; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.util.GuiContainerNPCInterface; +import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.containers.ContainerNPCTrader; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; -public class GuiNPCTrader extends GuiContainerNPCInterface { - private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/trader.png"); +public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { + private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/tradersetup.png"); private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; private ContainerNPCTrader container; + // Layout constants matching ContainerNPCTrader + private static final int COLUMN_WIDTH = ContainerNPCTrader.COLUMN_WIDTH; + private static final int COLUMN_START_X = ContainerNPCTrader.COLUMN_START_X; + private static final int ROW_HEIGHT = ContainerNPCTrader.ROW_HEIGHT; + private static final int ROW_START_Y = ContainerNPCTrader.ROW_START_Y; + private static final int CURRENCY1_OFFSET = ContainerNPCTrader.CURRENCY1_OFFSET; + private static final int CURRENCY2_OFFSET = ContainerNPCTrader.CURRENCY2_OFFSET; + private static final int OUTPUT_OFFSET = ContainerNPCTrader.OUTPUT_OFFSET; + + // Data received from server via IGuiData + private long playerBalance = 0; + private int[] availableStock = new int[18]; + private long[] currencyCost = new long[18]; + private boolean stockEnabled = false; + + // Client-side countdown: target time when stock resets (System.currentTimeMillis()) + private long resetTargetTime = -1; + + // Current hover status for bottom display + private String statusMessage = ""; + private int statusColor = 0xFFFFFF; + public GuiNPCTrader(EntityNPCInterface npc, ContainerNPCTrader container) { super(npc, container); this.container = container; role = (RoleTrader) npc.roleInterface; closeOnEsc = true; - ySize = 224; - xSize = 223; - this.title = "role.trader"; + ySize = 216; + xSize = 256; + this.title = ""; + + // Initialize stock to unlimited + for (int i = 0; i < 18; i++) { + availableStock[i] = Integer.MAX_VALUE; + currencyCost[i] = 0; + } + + // Request trader data from server (balance, stock, currency costs) + PacketClient.sendClient(new GetTraderData()); } @Override - protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { + public void setGuiData(NBTTagCompound compound) { + playerBalance = compound.getLong("Balance"); + stockEnabled = compound.getBoolean("StockEnabled"); + + // Calculate target time for client-side countdown + long remainingMillis = compound.getLong("ResetTimeMillis"); + if (remainingMillis > 0) { + resetTargetTime = System.currentTimeMillis() + remainingMillis; + } else { + resetTargetTime = -1; + } + + int[] stock = compound.getIntArray("Stock"); + if (stock != null && stock.length == 18) { + availableStock = stock; + } + + for (int i = 0; i < 18; i++) { + currencyCost[i] = compound.getLong("Cost" + i); + } + } + + @Override + protected void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) { + // Reset status each frame + statusMessage = ""; + statusColor = 0xFFFFFF; + this.drawWorldBackground(0); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(resource); drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + + // Draw "Trader" label in TOP LEFT (above the GUI) + String traderLabel = StatCollector.translateToLocal("role.trader"); + fontRendererObj.drawString(traderLabel, guiLeft + 4, guiTop - 8, 0xFFFFFF); + + // Draw reset timer in TOP RIGHT (above the GUI) + if (stockEnabled && resetTargetTime > 0) { + String timerText = getResetCountdownText(); + int timerWidth = fontRendererObj.getStringWidth(timerText); + int timerColor = timerText.equals(StatCollector.translateToLocal("trader.reopentrader")) ? 0xFFAA00 : 0xFFFFFF; + fontRendererObj.drawString(timerText, guiLeft + xSize - timerWidth - 4, guiTop - 8, timerColor); + } + RenderHelper.enableGUIStandardItemLighting(); GL11.glEnable(GL12.GL_RESCALE_NORMAL); GL11.glEnable(GL11.GL_COLOR_MATERIAL); GL11.glEnable(GL11.GL_LIGHTING); - mc.renderEngine.bindTexture(slot); - for (int slot = 0; slot < 18; slot++) { - int x = guiLeft + slot % 3 * 72 + 10; - int y = guiTop + slot / 3 * 21 + 6; + // Draw trade slots (3 columns x 6 rows, 80px column width) + for (int slotIdx = 0; slotIdx < 18; slotIdx++) { + int col = slotIdx % 3; + int row = slotIdx / 3; + int x = guiLeft + COLUMN_START_X + col * COLUMN_WIDTH; + int y = guiTop + ROW_START_Y + row * ROW_HEIGHT; - ItemStack item = role.inventoryCurrency.items.get(slot); - ItemStack item2 = role.inventoryCurrency.items.get(slot + 18); + // Get item currencies + ItemStack item = role.inventoryCurrency.items.get(slotIdx); + ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18); if (item == null) { item = item2; item2 = null; } - if (NoppesUtilPlayer.compareItems(item, item2, false, false)) { + if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { item = item.copy(); item.stackSize += item2.stackSize; item2 = null; } - ItemStack sold = role.inventorySold.items.get(slot); + ItemStack sold = role.inventorySold.items.get(slotIdx); + + // Draw slot backgrounds (18x18 each) GL11.glColor4f(1, 1, 1, 1); mc.renderEngine.bindTexture(this.slot); - drawTexturedModalRect(x + 42, y, 0, 0, 18, 18); - if (item != null && sold != null) { + + // Currency slot 1 + drawTexturedModalRect(x + CURRENCY1_OFFSET, y, 0, 0, 18, 18); + // Currency slot 2 + drawTexturedModalRect(x + CURRENCY2_OFFSET, y, 0, 0, 18, 18); + // Output slot + drawTexturedModalRect(x + OUTPUT_OFFSET, y, 0, 0, 18, 18); + + // "=" sign between currency and output + fontRendererObj.drawString("=", x + 40, y + 5, CustomNpcResourceListener.DefaultTextColor); + + // Draw currency items (centered: +1 offset for 16x16 item in 18x18 slot) + if (sold != null) { RenderHelper.enableGUIStandardItemLighting(); + // Draw secondary currency (slot 1) if (item2 != null) { - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1); + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1); } - GL11.glColor4f(1, 1, 1, 1); - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1); + + // Draw primary currency (slot 2) + if (item != null) { + GL11.glColor4f(1, 1, 1, 1); + itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1); + itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1); + } + RenderHelper.disableStandardItemLighting(); - fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor); + // Draw stock count to the RIGHT of the trade (after output slot) + if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) { + String stockText = String.valueOf(availableStock[slotIdx]); + int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000; + fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor); + } + // Draw dark overlay for out of stock items + if (stockEnabled && availableStock[slotIdx] <= 0) { + drawGradientRect(x + OUTPUT_OFFSET + 1, y + 1, x + OUTPUT_OFFSET + 17, y + 17, 0xC0101010, 0xC0101010); + } } } + GL11.glEnable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_DEPTH_TEST); RenderHelper.enableStandardItemLighting(); - super.drawGuiContainerBackgroundLayer(f, i, j); + + super.drawGuiContainerBackgroundLayer(f, mouseX, mouseY); + + // Draw player balance (BOTTOM RIGHT, below GUI) + int infoY = guiTop + ySize + 2; + String balanceText = StatCollector.translateToLocal("trader.balance") + ": $" + formatCurrency(playerBalance); + int balanceWidth = fontRendererObj.getStringWidth(balanceText); + fontRendererObj.drawString(balanceText, guiLeft + xSize - balanceWidth - 4, infoY, 0x36e82a); } @Override - protected void drawGuiContainerForegroundLayer(int par1, int par2) { - for (int slot = 0; slot < 18; slot++) { - int x = slot % 3 * 72 + 10; - int y = slot / 3 * 21 + 6; + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + for (int slotIdx = 0; slotIdx < 18; slotIdx++) { + int col = slotIdx % 3; + int row = slotIdx / 3; + int x = COLUMN_START_X + col * COLUMN_WIDTH; + int y = ROW_START_Y + row * ROW_HEIGHT; - ItemStack item = role.inventoryCurrency.items.get(slot); - ItemStack item2 = role.inventoryCurrency.items.get(slot + 18); + ItemStack item = role.inventoryCurrency.items.get(slotIdx); + ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18); if (item == null) { item = item2; item2 = null; } - if (NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { + if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) { item = item.copy(); item.stackSize += item2.stackSize; item2 = null; } - ItemStack sold = role.inventorySold.items.get(slot); - if (item == null || sold == null) + + ItemStack sold = role.inventorySold.items.get(slotIdx); + if (sold == null) { continue; - - if (this.func_146978_c(x + 43, y + 1, 16, 16, par1, par2)) { - if (!container.canBuy(slot, player)) { - GL11.glTranslatef(0, 0, 300); - if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT)) - this.drawGradientRect(x + 17, y, x + 35, y + 18, 0x70771010, 0x70771010); - if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT)) - this.drawGradientRect(x - 1, y, x + 17, y + 18, 0x70771010, 0x70771010); - - String title = StatCollector.translateToLocal("trader.insufficient"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xDD0000); - GL11.glTranslatef(0, 0, -300); - } else if (!container.isSlotEnabled(slot, player)) { - String title = StatCollector.translateToLocal("trader.slotdisabled"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xFF4000); - } else { - String title = StatCollector.translateToLocal("trader.sufficient"); - this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0x00DD00); - } } - if (this.func_146978_c(x, y, 16, 16, par1, par2) && item2 != null) { - this.renderToolTip(item2, par1 - guiLeft, par2 - guiTop); + // Check if hovering over output slot (18x18 area) + if (this.func_146978_c(x + OUTPUT_OFFSET, y, 18, 18, mouseX, mouseY)) { + updateSlotStatus(slotIdx, item, item2, x, y); } - if (this.func_146978_c(x + 18, y, 16, 16, par1, par2)) { - this.renderToolTip(item, par1 - guiLeft, par2 - guiTop); + + // Tooltips for currency items + if (this.func_146978_c(x + CURRENCY1_OFFSET, y, 18, 18, mouseX, mouseY) && item2 != null) { + this.renderToolTip(item2, mouseX - guiLeft, mouseY - guiTop); + } + if (this.func_146978_c(x + CURRENCY2_OFFSET, y, 18, 18, mouseX, mouseY) && item != null) { + this.renderToolTip(item, mouseX - guiLeft, mouseY - guiTop); } } + + // Draw status message in BOTTOM LEFT (in foreground coordinates) + if (!statusMessage.isEmpty()) { + fontRendererObj.drawString(statusMessage, 4, ySize + 4, statusColor); + } + } + + /** + * Update status message when hovering over a trade slot + */ + private void updateSlotStatus(int slotIdx, ItemStack item, ItemStack item2, int x, int y) { + long cost = currencyCost[slotIdx]; + String costSuffix = cost > 0 ? " ($" + formatCurrency(cost) + ")" : ""; + + // Check conditions in order of priority + if (stockEnabled && availableStock[slotIdx] <= 0) { + // Out of stock + statusMessage = StatCollector.translateToLocal("trader.outofstock") + costSuffix; + statusColor = 0xDD0000; + } else if (cost > 0 && playerBalance < cost) { + // Can't afford currency cost + statusMessage = StatCollector.translateToLocal("trader.insufficient.currency") + costSuffix; + statusColor = 0xDD0000; + } else if (!container.canBuy(slotIdx, player)) { + // Missing item currency - highlight which ones + GL11.glTranslatef(0, 0, 300); + if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT)) { + this.drawGradientRect(x + CURRENCY2_OFFSET, y, x + CURRENCY2_OFFSET + 18, y + 18, 0x70771010, 0x70771010); + } + if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT)) { + this.drawGradientRect(x + CURRENCY1_OFFSET, y, x + CURRENCY1_OFFSET + 18, y + 18, 0x70771010, 0x70771010); + } + GL11.glTranslatef(0, 0, -300); + statusMessage = StatCollector.translateToLocal("trader.unavailable") + costSuffix; + statusColor = 0xDD0000; + } else if (!container.isSlotEnabled(slotIdx, player)) { + // Slot disabled + statusMessage = StatCollector.translateToLocal("trader.slotdisabled") + costSuffix; + statusColor = 0xFF4000; + } else { + // Can buy! + statusMessage = StatCollector.translateToLocal("trader.available") + costSuffix; + statusColor = 0x00DD00; + } + } + + /** + * Get the countdown text for stock reset timer. + * Calculated client-side each frame for smooth countdown. + */ + private String getResetCountdownText() { + if (resetTargetTime <= 0) { + return ""; + } + + long remaining = resetTargetTime - System.currentTimeMillis(); + if (remaining <= 0) { + return StatCollector.translateToLocal("trader.reopentrader"); + } + + // Format as HH:MM:SS or D days HH:MM:SS + long seconds = (remaining / 1000) % 60; + long minutes = (remaining / 60000) % 60; + long hours = (remaining / 3600000) % 24; + long days = remaining / 86400000; + + StringBuilder sb = new StringBuilder(); + sb.append(StatCollector.translateToLocal("trader.restock")).append(": "); + + if (days > 0) { + sb.append(days).append("d "); + } + sb.append(String.format("%02d:%02d:%02d", hours, minutes, seconds)); + + return sb.toString(); + } + + /** + * Format currency for display (e.g., 1000 -> "1,000") + */ + private String formatCurrency(long amount) { + if (amount < 1000) { + return String.valueOf(amount); + } + StringBuilder sb = new StringBuilder(); + String str = String.valueOf(amount); + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) { + sb.insert(0, ','); + } + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java deleted file mode 100644 index 99f1e00f1..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ /dev/null @@ -1,221 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; - -/** - * Sub GUI for placing a bid on an auction. - */ -public class SubGuiAuctionBid extends SubGuiInterface { - private AuctionListing listing; - private long playerBalance; - private GuiNpcTextField bidField; - private long lastUpdate = 0; - - public SubGuiAuctionBid(GuiAuction parent, AuctionListing listing, long playerBalance) { - this.listing = listing; - this.playerBalance = playerBalance; - setBackground("menubg.png"); - xSize = 240; - ySize = 165; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - int y = guiTop + 10; - int labelColor = 0x404040; - - if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 60, y, 0xAA0000)); - addButton(new GuiNpcButton(31, guiLeft + 80, guiTop + 125, 80, 20, StatCollector.translateToLocal("gui.cancel"))); - return; - } - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.placeBid"), guiLeft + 90, y, labelColor)); - y += 18; - - // Item name - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item") + " " + listing.item.getDisplayName(), guiLeft + 15, y, 0x000000)); - y += 14; - - // Current price - long currentPrice = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.currentPrice"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(3, formatCurrency(currentPrice), guiLeft + 120, y, 0x008800)); - y += 14; - - // Minimum bid - long minBid = listing.getMinimumBid(); - addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(5, formatCurrency(minBid), guiLeft + 120, y, 0x0066AA)); - y += 14; - - // Your balance - addLabel(new GuiNpcLabel(6, StatCollector.translateToLocal("auction.yourBalance"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(7, formatCurrency(playerBalance), guiLeft + 120, y, - playerBalance >= minBid ? 0x008800 : 0xAA0000)); - y += 14; - - // Time remaining - addLabel(new GuiNpcLabel(8, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 15, y, labelColor)); - addLabel(new GuiNpcLabel(9, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, - getTimeColor(listing.getTimeRemaining()))); - y += 18; - - // Bid input - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.yourBid"), guiLeft + 15, y + 3, labelColor)); - bidField = new GuiNpcTextField(11, this, guiLeft + 100, y, 110, 16, String.valueOf(minBid)); - bidField.setIntegersOnly(); - int minBidInt = (int) Math.min(Integer.MAX_VALUE, Math.max(1L, minBid)); - bidField.setMinMaxDefault(minBidInt, Integer.MAX_VALUE, minBidInt); - addTextField(bidField); - y += 24; - - // Quick bid buttons - addButton(new GuiNpcButton(20, guiLeft + 15, y, 60, 16, StatCollector.translateToLocal("auction.minBid"))); - addButton(new GuiNpcButton(21, guiLeft + 79, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus10"))); - addButton(new GuiNpcButton(22, guiLeft + 143, y, 60, 16, StatCollector.translateToLocal("auction.bidPlus25"))); - y += 22; - - // Place bid button - GuiNpcButton bidButton = new GuiNpcButton(30, guiLeft + 35, y, 80, 20, StatCollector.translateToLocal("auction.placeBid")); - bidButton.setEnabled(playerBalance >= minBid && listing.isActive()); - addButton(bidButton); - - // Cancel button - addButton(new GuiNpcButton(31, guiLeft + 125, y, 80, 20, StatCollector.translateToLocal("gui.cancel"))); - - // Error message area - if (playerBalance < minBid) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.insufficientFunds"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); - } else if (!listing.isActive()) { - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.auctionEnded"), guiLeft + 70, guiTop + ySize - 15, 0xAA0000)); - } - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - - if (hours > 0) { - return String.format("%dh %dm", hours, minutes % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - private int getTimeColor(long millis) { - if (millis <= 0) return 0xAA0000; - if (millis < 5 * 60 * 1000) return 0xFF0000; - if (millis < 30 * 60 * 1000) return 0xFF6600; - return 0x008800; - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update time remaining every second - long now = System.currentTimeMillis(); - if (now - lastUpdate >= 1000) { - lastUpdate = now; - - GuiNpcLabel timeLabel = getLabel(9); - if (timeLabel != null) { - long remaining = listing.getTimeRemaining(); - timeLabel.label = formatTimeRemaining(remaining); - timeLabel.color = getTimeColor(remaining); - } - - // Disable bid button if auction ended - GuiNpcButton bidButton = getButton(30); - if (bidButton != null && !listing.isActive()) { - bidButton.setEnabled(false); - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - int id = button.id; - - long minBid = listing.getMinimumBid(); - - if (id == 20) { - // Min bid - bidField.setText(String.valueOf(minBid)); - } else if (id == 21) { - // +10% - long newBid = (long) (minBid * 1.10); - if (newBid <= playerBalance) { - bidField.setText(String.valueOf(newBid)); - } - } else if (id == 22) { - // +25% - long newBid = (long) (minBid * 1.25); - if (newBid <= playerBalance) { - bidField.setText(String.valueOf(newBid)); - } - } else if (id == 30) { - // Place bid - placeBid(); - } else if (id == 31) { - // Cancel - close(); - } - } - - private void placeBid() { - long bidAmount = parseBid(bidField); - long minBid = listing.getMinimumBid(); - - if (bidAmount < minBid) { - return; - } - - if (bidAmount > playerBalance) { - return; - } - - if (!listing.isActive()) { - return; - } - - // Send bid to server - AuctionActionPacket.PlaceBid(listing.id, bidAmount); - close(); - } - - private long parseBid(GuiNpcTextField field) { - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return 0L; - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java deleted file mode 100644 index 01c650bbd..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionCreate.java +++ /dev/null @@ -1,178 +0,0 @@ -package noppes.npcs.client.gui.player; - -import kamkeel.npcs.network.packets.player.AuctionActionPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.item.ItemStack; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.containers.ContainerAuction; - -/** - * Sub GUI for creating a new auction listing. - */ -public class SubGuiAuctionCreate extends SubGuiInterface { - private ContainerAuction container; - - private GuiNpcTextField startingPriceField; - private GuiNpcTextField buyoutPriceField; - private int selectedDuration = 1; // Default to MEDIUM - - public SubGuiAuctionCreate(GuiAuction parent, ContainerAuction container) { - this.container = container; - setBackground("menubg.png"); - xSize = 260; - ySize = 190; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.createListing"), guiLeft + 80, guiTop + 8, 0x404040)); - - // Item slot instructions - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.placeItemBelow"), guiLeft + 20, guiTop + 28, 0x404040)); - - // Item info (shows when item is in slot) - ItemStack item = container.getListingItem(); - if (item != null) { - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(), guiLeft + 20, guiTop + 50, 0x008800)); - } else { - addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 20, guiTop + 50, 0x888888)); - } - - // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, guiTop + 74, 0x404040)); - startingPriceField = new GuiNpcTextField(11, this, guiLeft + 120, guiTop + 72, 90, 14, "100"); - startingPriceField.setIntegersOnly(); - startingPriceField.setMinMaxDefault(1, 999999999, 100); - addTextField(startingPriceField); - - // Buyout price (optional) - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, guiTop + 96, 0x404040)); - buyoutPriceField = new GuiNpcTextField(21, this, guiLeft + 120, guiTop + 94, 90, 14, "0"); - buyoutPriceField.setIntegersOnly(); - buyoutPriceField.setMinMaxDefault(0, 999999999, 0); - addTextField(buyoutPriceField); - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.noBuyoutHint"), guiLeft + 215, guiTop + 96, 0x888888)); - - // Duration selection - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.duration"), guiLeft + 20, guiTop + 118, 0x404040)); - addButton(new GuiNpcButton(31, guiLeft + 120, guiTop + 114, 110, 20, getDurationDisplay())); - - // Fee display - long fee = getListingFee(); - addLabel(new GuiNpcLabel(40, StatCollector.translateToLocal("auction.listingFee") + " " + fee + " " + ConfigMarket.CurrencyName, - guiLeft + 20, guiTop + 142, fee > 0 ? 0xAA0000 : 0x008800)); - - // Create button - addButton(new GuiNpcButton(50, guiLeft + 30, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.create"))); - getButton(50).setEnabled(item != null); - - // Cancel button - addButton(new GuiNpcButton(51, guiLeft + 140, guiTop + 162, 90, 20, StatCollector.translateToLocal("gui.cancel"))); - } - - private String getDurationDisplay() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getDisplayName(); - } - - private long getListingFee() { - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - return duration.getListingFee(); - } - - @Override - public void actionPerformed(GuiButton button) { - int id = button.id; - - if (id == 31) { - // Cycle duration - selectedDuration = (selectedDuration + 1) % EnumAuctionDuration.values().length; - initGui(); - } else if (id == 50) { - // Create listing - createListing(); - } else if (id == 51) { - // Cancel - close(); - } - } - - private void createListing() { - ItemStack item = container.getListingItem(); - if (item == null) { - return; - } - - long startingPrice = parsePrice(startingPriceField, 1); - long buyoutPrice = parsePrice(buyoutPriceField, 0); - - if (startingPrice < 1) { - startingPrice = 1; - } - - if (buyoutPrice > 0 && buyoutPrice < startingPrice) { - // Show error - buyout must be higher than starting - return; - } - - EnumAuctionDuration duration = EnumAuctionDuration.values()[selectedDuration]; - - // Send to server - AuctionActionPacket.CreateListing(item, startingPrice, buyoutPrice, duration); - - // Clear slot and close - container.clearListingSlot(); - close(); - } - - private long parsePrice(GuiNpcTextField field, long defaultValue) { - try { - return Long.parseLong(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update item display if it changed - ItemStack item = container.getListingItem(); - GuiNpcLabel itemLabel = getLabel(2); - GuiNpcButton createButton = getButton(50); - - if (item != null) { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.item") + " " + item.getDisplayName(); - itemLabel.color = 0x008800; - } - if (createButton != null) { - createButton.setEnabled(true); - } - } else { - if (itemLabel != null) { - itemLabel.label = StatCollector.translateToLocal("auction.noItemSelected"); - itemLabel.color = 0x888888; - } - if (createButton != null) { - createButton.setEnabled(false); - } - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java deleted file mode 100644 index e4131fe94..000000000 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionDetails.java +++ /dev/null @@ -1,200 +0,0 @@ -package noppes.npcs.client.gui.player; - -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.AuctionListing; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; - -/** - * Sub GUI for viewing auction listing details. - */ -public class SubGuiAuctionDetails extends SubGuiInterface { - private AuctionListing listing; - private long lastUpdate = 0; - - public SubGuiAuctionDetails(GuiAuction parent, AuctionListing listing) { - this.listing = listing; - setBackground("menubg.png"); - xSize = 280; - ySize = 210; - closeOnEsc = true; - } - - @Override - public void initGui() { - super.initGui(); - - int y = guiTop + 8; - int labelColor = 0x404040; - int valueColor = 0x000000; - - if (listing.item == null) { - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.noItemSelected"), guiLeft + 80, y, 0xAA0000)); - addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); - return; - } - - // Title - addLabel(new GuiNpcLabel(0, StatCollector.translateToLocal("auction.auctionDetails"), guiLeft + 100, y, labelColor)); - y += 16; - - // Item name - addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("auction.item"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(2, listing.item.getDisplayName(), guiLeft + 100, y, valueColor)); - y += 14; - - // Quantity - addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("auction.quantity"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(4, String.valueOf(listing.item.stackSize), guiLeft + 100, y, valueColor)); - y += 14; - - // Seller - addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("auction.seller"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(6, listing.sellerName, guiLeft + 100, y, valueColor)); - y += 14; - - // Starting price - addLabel(new GuiNpcLabel(10, StatCollector.translateToLocal("auction.startingPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(11, formatCurrency(listing.startingPrice), guiLeft + 120, y, valueColor)); - y += 14; - - // Current bid - if (listing.currentBid > 0) { - addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, formatCurrency(listing.currentBid), guiLeft + 120, y, 0x008800)); - y += 14; - - // High bidder - addLabel(new GuiNpcLabel(14, StatCollector.translateToLocal("auction.highBidder"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(15, listing.highBidderName, guiLeft + 120, y, valueColor)); - y += 14; - - // Bid count - addLabel(new GuiNpcLabel(16, StatCollector.translateToLocal("auction.totalBids"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(17, String.valueOf(listing.bidCount), guiLeft + 120, y, valueColor)); - y += 14; - } else { - addLabel(new GuiNpcLabel(12, StatCollector.translateToLocal("auction.currentBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(13, StatCollector.translateToLocal("auction.noBidsYet"), guiLeft + 120, y, 0x888888)); - y += 14; - } - - // Buyout price - if (listing.buyoutPrice > 0) { - addLabel(new GuiNpcLabel(20, StatCollector.translateToLocal("auction.buyoutPrice"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(21, formatCurrency(listing.buyoutPrice), guiLeft + 120, y, 0xAA6600)); - y += 14; - } - - // Minimum bid - addLabel(new GuiNpcLabel(22, StatCollector.translateToLocal("auction.minimumBid"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(23, formatCurrency(listing.getMinimumBid()), guiLeft + 120, y, 0x0066AA)); - y += 14; - - // Time remaining (will be updated) - addLabel(new GuiNpcLabel(30, StatCollector.translateToLocal("auction.timeLeft"), guiLeft + 20, y, labelColor)); - addLabel(new GuiNpcLabel(31, formatTimeRemaining(listing.getTimeRemaining()), guiLeft + 120, y, - getTimeColor(listing.getTimeRemaining()))); - y += 20; - - // Close button - addButton(new GuiNpcButton(50, guiLeft + 100, guiTop + 185, 80, 20, StatCollector.translateToLocal("gui.close"))); - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private String formatTimeRemaining(long millis) { - if (millis <= 0) return StatCollector.translateToLocal("auction.ended"); - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return String.format("%dd %dh %dm", days, hours % 24, minutes % 60); - } else if (hours > 0) { - return String.format("%dh %dm %ds", hours, minutes % 60, seconds % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - private int getTimeColor(long millis) { - if (millis <= 0) return 0xAA0000; - if (millis < 5 * 60 * 1000) return 0xFF0000; // < 5 minutes - red - if (millis < 30 * 60 * 1000) return 0xFF6600; // < 30 minutes - orange - if (millis < 60 * 60 * 1000) return 0xAAAA00; // < 1 hour - yellow - return 0x008800; // green - } - - @Override - public void updateScreen() { - super.updateScreen(); - - // Update time remaining every second - long now = System.currentTimeMillis(); - if (now - lastUpdate >= 1000) { - lastUpdate = now; - - GuiNpcLabel timeLabel = getLabel(31); - if (timeLabel != null) { - long remaining = listing.getTimeRemaining(); - timeLabel.label = formatTimeRemaining(remaining); - timeLabel.color = getTimeColor(remaining); - } - } - } - - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - super.drawScreen(mouseX, mouseY, partialTicks); - - // Draw item icon - if (listing.item != null) { - int itemX = guiLeft + 230; - int itemY = guiTop + 32; - - RenderHelper.enableGUIStandardItemLighting(); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_COLOR_MATERIAL); - GL11.glEnable(GL11.GL_LIGHTING); - GL11.glScalef(2.0f, 2.0f, 2.0f); - - itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, - listing.item, itemX / 2, itemY / 2); - itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, - listing.item, itemX / 2, itemY / 2); - - GL11.glScalef(0.5f, 0.5f, 0.5f); - RenderHelper.disableStandardItemLighting(); - - // Tooltip on hover - if (mouseX >= itemX && mouseX < itemX + 32 && mouseY >= itemY && mouseY < itemY + 32) { - renderToolTip(listing.item, mouseX, mouseY); - } - } - } - - @Override - public void actionPerformed(GuiButton button) { - if (button.id == 50) { - close(); - } - } - - @Override - public void save() { - // Nothing to save - } -} diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java deleted file mode 100644 index d5fb8e962..000000000 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcAuctioneer.java +++ /dev/null @@ -1,144 +0,0 @@ -package noppes.npcs.client.gui.roles; - -import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.role.RoleSavePacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.client.gui.util.GuiNPCInterface2; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.roles.RoleAuctioneer; - -public class GuiNpcAuctioneer extends GuiNPCInterface2 implements ITextfieldListener { - - private final RoleAuctioneer role; - - public GuiNpcAuctioneer(EntityNPCInterface npc) { - super(npc); - this.role = npc.roleInterface instanceof RoleAuctioneer ? (RoleAuctioneer) npc.roleInterface : null; - } - - @Override - public void initGui() { - super.initGui(); - if (role == null) { - close(); - return; - } - - int y = guiTop + 10; - - // Left column - Tab visibility - addLabel(new GuiNpcLabel(0, "role.auctioneer.tabs", guiLeft + 10, y, 0x404040)); - y += 18; - - addLabel(new GuiNpcLabel(1, "role.auctioneer.showBrowse", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(1, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showActiveListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(2, "role.auctioneer.showMyListings", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(2, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(3, "role.auctioneer.showMyBids", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(3, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showMyBids ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(4, "role.auctioneer.showClaims", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(4, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.showClaims ? 1 : 0)); - y += 30; - - // Actions - addLabel(new GuiNpcLabel(10, "role.auctioneer.actions", guiLeft + 10, y, 0x404040)); - y += 18; - - addLabel(new GuiNpcLabel(11, "role.auctioneer.allowCreate", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(11, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowCreatingListings ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(12, "role.auctioneer.allowBid", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(12, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBidding ? 1 : 0)); - y += 22; - - addLabel(new GuiNpcLabel(13, "role.auctioneer.allowBuyout", guiLeft + 10, y + 5, 0x404040)); - addButton(new GuiNpcButton(13, guiLeft + 140, y, 50, 20, new String[]{"gui.no", "gui.yes"}, role.allowBuyout ? 1 : 0)); - - // Right column - Messages - int rightX = guiLeft + 210; - int rightY = guiTop + 10; - - addLabel(new GuiNpcLabel(20, "role.auctioneer.messages", rightX, rightY, 0x404040)); - rightY += 18; - - addLabel(new GuiNpcLabel(21, "role.auctioneer.welcome", rightX, rightY + 5, 0x404040)); - rightY += 18; - GuiNpcTextField welcomeField = new GuiNpcTextField(21, this, rightX, rightY, 180, 20, role.welcomeMessage); - welcomeField.setMaxStringLength(200); - addTextField(welcomeField); - rightY += 28; - - addLabel(new GuiNpcLabel(22, "role.auctioneer.noAuction", rightX, rightY + 5, 0x404040)); - rightY += 18; - GuiNpcTextField noAuctionField = new GuiNpcTextField(22, this, rightX, rightY, 180, 20, role.noAuctionMessage); - noAuctionField.setMaxStringLength(200); - addTextField(noAuctionField); - } - - @Override - public void buttonEvent(GuiButton guibutton) { - if (role == null) return; - - if (guibutton instanceof GuiNpcButton) { - GuiNpcButton button = (GuiNpcButton) guibutton; - boolean value = button.getValue() == 1; - - switch (button.id) { - case 1: - role.showActiveListings = value; - break; - case 2: - role.showMyListings = value; - break; - case 3: - role.showMyBids = value; - break; - case 4: - role.showClaims = value; - break; - case 11: - role.allowCreatingListings = value; - break; - case 12: - role.allowBidding = value; - break; - case 13: - role.allowBuyout = value; - break; - } - } - } - - @Override - public void unFocused(GuiNpcTextField textfield) { - if (role == null) return; - - switch (textfield.id) { - case 21: - role.welcomeMessage = textfield.getText(); - break; - case 22: - role.noAuctionMessage = textfield.getText(); - break; - } - } - - @Override - public void save() { - if (role != null) { - PacketClient.sendClient(new RoleSavePacket(role.writeToNBT(new NBTTagCompound()))); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 43c259da7..6c4745d42 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -8,7 +8,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; -import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.ITextfieldListener; @@ -22,6 +22,9 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png"); private RoleTrader role; + // Currency cost textfield IDs start at 100 (100-117 for slots 0-17) + private static final int CURRENCY_FIELD_ID_START = 100; + public GuiNpcTraderSetup(EntityNPCInterface npc, ContainerNPCTraderSetup container) { super(npc, container); ySize = 220; @@ -34,17 +37,30 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex super.initGui(); buttonList.clear(); setBackground("tradersetup.png"); + + // Linked Market name addLabel(new GuiNpcLabel(0, "role.marketname", guiLeft + 214, guiTop + 150)); addTextField(new GuiNpcTextField(0, this, guiLeft + 214, guiTop + 160, 180, 20, role.marketName)); - addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 260, guiTop + 29)); - addButton(new GuiNpcButtonYesNo(1, guiLeft + 340, guiTop + 24, role.ignoreDamage)); + // Settings button (opens SubGuiNpcTraderSettings) + addButton(new GuiNpcButton(1, guiLeft + 214, guiTop + 184, 88, 20, "gui.settings")); + getButton(1).setHoverText("gui.settings.hover"); - addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 260, guiTop + 51)); - addButton(new GuiNpcButtonYesNo(2, guiLeft + 340, guiTop + 46, role.ignoreNBT)); + // Stock Options button (opens SubGuiNpcTraderStock) + addButton(new GuiNpcButton(2, guiLeft + 306, guiTop + 184, 88, 20, "stock.options")); + getButton(2).setHoverText("stock.options.hover"); - addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 260, guiTop + 73)); - addButton(new GuiNpcButtonYesNo(3, guiLeft + 340, guiTop + 68, role.recordHistory)); + // Currency cost textfields for each slot (FIRST - before item slots) + // Column width: 130px for better spacing + for (int i = 0; i < 18; i++) { + int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label + int y = guiTop + i / 3 * 22 + 14; + long cost = role.getCurrencyCost(i); + GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : ""); + field.setMaxStringLength(8); + field.setIntegersOnly(); + addTextField(field); + } } @Override @@ -57,13 +73,12 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void actionPerformed(GuiButton guibutton) { if (guibutton.id == 1) { - role.ignoreDamage = ((GuiNpcButtonYesNo) guibutton).getBoolean(); + // Open Settings SubGui + setSubGui(new SubGuiNpcTraderSettings(role)); } if (guibutton.id == 2) { - role.ignoreNBT = ((GuiNpcButtonYesNo) guibutton).getBoolean(); - } - if (guibutton.id == 3) { - role.recordHistory = ((GuiNpcButtonYesNo) guibutton).getBoolean(); + // Open Stock Options SubGui + setSubGui(new SubGuiNpcTraderStock(role)); } } @@ -71,19 +86,31 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex protected void drawGuiContainerBackgroundLayer(float f, int i, int j) { super.drawGuiContainerBackgroundLayer(f, i, j); GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + // Column width: 130px for better spacing for (int slot = 0; slot < 18; slot++) { - int x = guiLeft + slot % 3 * 94 + 7; + int x = guiLeft + slot % 3 * 130 + 7; int y = guiTop + slot / 3 * 22 + 4; - mc.renderEngine.bindTexture(this.slot); - GL11.glColor4f(1, 1, 1, 1); - drawTexturedModalRect(x - 1, y, 0, 0, 18, 18); - drawTexturedModalRect(x + 17, y, 0, 0, 18, 18); + // "$" label for currency cost + fontRendererObj.drawString("$", x, y + 5, CustomNpcResourceListener.DefaultTextColor); - fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor); + // Currency textfield is at x + 8 (drawn by textfield system) + + // Item Cost 1 slot mc.renderEngine.bindTexture(this.slot); GL11.glColor4f(1, 1, 1, 1); - drawTexturedModalRect(x + 42, y, 0, 0, 18, 18); + drawTexturedModalRect(x + 50, y, 0, 0, 18, 18); + + // Item Cost 2 slot + drawTexturedModalRect(x + 68, y, 0, 0, 18, 18); + + // "=" sign + fontRendererObj.drawString("=", x + 88, y + 5, CustomNpcResourceListener.DefaultTextColor); + + // Output slot + mc.renderEngine.bindTexture(this.slot); + GL11.glColor4f(1, 1, 1, 1); + drawTexturedModalRect(x + 104, y, 0, 0, 18, 18); } } @@ -95,10 +122,23 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex @Override public void unFocused(GuiNpcTextField guiNpcTextField) { - String name = guiNpcTextField.getText(); - if (!name.equalsIgnoreCase(role.marketName)) { - role.marketName = name; - PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); + int id = guiNpcTextField.id; + + // Handle market name field + if (id == 0) { + String name = guiNpcTextField.getText(); + if (!name.equalsIgnoreCase(role.marketName)) { + role.marketName = name; + PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true)); + } + return; + } + + // Handle currency cost fields (IDs 100-117) + if (id >= CURRENCY_FIELD_ID_START && id < CURRENCY_FIELD_ID_START + 18) { + int slot = id - CURRENCY_FIELD_ID_START; + long cost = guiNpcTextField.getInteger(); + role.setCurrencyCost(slot, cost); } } } diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java new file mode 100644 index 000000000..4c72e2059 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderCooldownReset.java @@ -0,0 +1,52 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Confirmation dialog for resetting trader stock cooldown. + */ +public class SubGuiNpcTraderCooldownReset extends SubGuiInterface { + + private final RoleTrader role; + public boolean confirmed = false; + + public SubGuiNpcTraderCooldownReset(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 200; + ySize = 100; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 15; + + addLabel(new GuiNpcLabel(0, "stock.cooldown.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "stock.cooldown.warning", guiLeft + 10, y)); + + y += 35; + + addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes")); + addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + // Reset the cooldown + role.resetCooldown(); + confirmed = true; + close(); + } else if (guibutton.id == 1) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java new file mode 100644 index 000000000..d9e2a0b40 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderSettings.java @@ -0,0 +1,66 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +public class SubGuiNpcTraderSettings extends SubGuiInterface { + + private final RoleTrader role; + + public SubGuiNpcTraderSettings(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 176; + ySize = 136; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 14; + + addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(1, guiLeft + 120, y, role.ignoreDamage)); + getButton(1).setHoverText("gui.ignoreDamage.hover"); + + y += 28; + + addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(2, guiLeft + 120, y, role.ignoreNBT)); + getButton(2).setHoverText("gui.ignoreNBT.hover"); + + y += 28; + + addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(3, guiLeft + 120, y, role.recordHistory)); + getButton(3).setHoverText("gui.recordHistory.hover"); + + y += 34; + + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, y, 60, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case 0: + close(); + break; + case 1: + role.ignoreDamage = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 2: + role.ignoreNBT = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 3: + role.recordHistory = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java new file mode 100644 index 000000000..2e369b7e6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java @@ -0,0 +1,159 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.client.gui.util.SubGuiNpcCooldownPicker; +import noppes.npcs.constants.EnumStockReset; +import noppes.npcs.roles.RoleTrader; + +public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiListener, ITextfieldListener { + + private final RoleTrader role; + + public SubGuiNpcTraderStock(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 220; + ySize = 220; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + + // Enable Stock + addLabel(new GuiNpcLabel(1, "stock.enable", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(1, guiLeft + 140, y, role.stock.enableStock)); + getButton(1).setHoverText("stock.enable.hover"); + + y += 24; + + // Per-Player Stock + addLabel(new GuiNpcLabel(2, "stock.perplayer", guiLeft + 10, y + 5)); + addButton(new GuiNpcButtonYesNo(2, guiLeft + 140, y, role.stock.perPlayer)); + getButton(2).setHoverText("stock.perplayer.hover"); + + y += 24; + + // Reset Type + addLabel(new GuiNpcLabel(3, "stock.resettype", guiLeft + 10, y + 5)); + addButton(new GuiNpcButton(3, guiLeft + 90, y, 120, 20, EnumStockReset.getDisplayNames(), role.stock.resetType.ordinal())); + getButton(3).setHoverText("stock.resettype.hover"); + + y += 24; + + // Custom Time button (only visible for custom types) + EnumStockReset resetType = role.stock.resetType; + if (resetType == EnumStockReset.MCCUSTOM || resetType == EnumStockReset.RLCUSTOM) { + addLabel(new GuiNpcLabel(4, "stock.customtime", guiLeft + 10, y + 5)); + addButton(new GuiNpcButton(4, guiLeft + 90, y, 120, 20, "gui.edit")); + getButton(4).setHoverText("stock.customtime.hover"); + y += 24; + } + + // Default Max Stock (applies to all slots as default) + addLabel(new GuiNpcLabel(5, "stock.defaultmax", guiLeft + 10, y + 5)); + GuiNpcTextField defaultStock = new GuiNpcTextField(5, this, fontRendererObj, guiLeft + 140, y, 60, 20, getDefaultStockString()); + defaultStock.setIntegersOnly(); + defaultStock.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); + addTextField(defaultStock); + + y += 26; + + // Per-Slot Stock button + addButton(new GuiNpcButton(5, guiLeft + 10, y, 95, 20, "stock.perslot")); + getButton(5).setHoverText("stock.perslot.hover"); + + // Reset Stock button + addButton(new GuiNpcButton(6, guiLeft + 115, y, 95, 20, "stock.reset")); + getButton(6).setHoverText("stock.reset.hover"); + + y += 24; + + // Reset Cooldown button + addButton(new GuiNpcButton(7, guiLeft + 10, y, 200, 20, "stock.resetcooldown")); + getButton(7).setHoverText("stock.resetcooldown.hover"); + + // Done button + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 40, 60, 20, "gui.done")); + } + + private String getDefaultStockString() { + // Check if all stocks are the same, if so return that value + int first = role.stock.maxStock[0]; + for (int i = 1; i < 18; i++) { + if (role.stock.maxStock[i] != first) { + return "-1"; // Mixed values, return unlimited + } + } + return String.valueOf(first); + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case 0: // Done + close(); + break; + case 1: // Enable Stock + role.stock.enableStock = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 2: // Per-Player + role.stock.perPlayer = ((GuiNpcButtonYesNo) button).getBoolean(); + break; + case 3: // Reset Type + int value = ((GuiNpcButton) button).getValue(); + role.stock.resetType = EnumStockReset.values()[value]; + initGui(); // Refresh to show/hide custom time button + break; + case 4: // Custom Time + boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM; + setSubGui(new SubGuiNpcCooldownPicker(isMC, role.stock.customResetTime)); + break; + case 5: // Per-Slot Stock + setSubGui(new SubGuiNpcTraderStockSlots(role)); + break; + case 6: // Reset Stock + setSubGui(new SubGuiNpcTraderStockReset(role)); + break; + case 7: // Reset Cooldown + setSubGui(new SubGuiNpcTraderCooldownReset(role)); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subGui) { + if (subGui instanceof SubGuiNpcCooldownPicker) { + long value = ((SubGuiNpcCooldownPicker) subGui).cooldownValue; + // Validate minimum: 1 tick for MC time, 1000ms (1 second) for real time + boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM; + long minimum = isMC ? 1 : 1000; + role.stock.customResetTime = Math.max(minimum, value); + } + // Refresh after per-slot editing + if (subGui instanceof SubGuiNpcTraderStockSlots) { + initGui(); + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + if (textField.id == 5) { + int value = textField.getInteger(); + // Apply to all slots + for (int i = 0; i < 18; i++) { + role.stock.setMaxStock(i, value); + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java new file mode 100644 index 000000000..027108f76 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockReset.java @@ -0,0 +1,55 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Confirmation dialog for resetting trader stock. + */ +public class SubGuiNpcTraderStockReset extends SubGuiInterface { + + private final RoleTrader role; + public boolean confirmed = false; + + public SubGuiNpcTraderStockReset(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 200; + ySize = 100; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 15; + + addLabel(new GuiNpcLabel(0, "stock.reset.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "stock.reset.warning", guiLeft + 10, y)); + + y += 35; + + addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes")); + addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + // Reset the stock + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (role.npc != null && role.npc.worldObj != null ? role.npc.worldObj.getTotalWorldTime() : 0); + role.stock.resetStock(currentTime); + confirmed = true; + close(); + } else if (guibutton.id == 1) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java new file mode 100644 index 000000000..b4a76ba48 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java @@ -0,0 +1,76 @@ +package noppes.npcs.client.gui.roles; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.roles.RoleTrader; + +/** + * Per-slot stock management dialog. + * Shows all 18 slots with their max stock values. + */ +public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextfieldListener { + + private final RoleTrader role; + + public SubGuiNpcTraderStockSlots(RoleTrader role) { + this.role = role; + setBackground("menubg.png"); + xSize = 256; + ySize = 216; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + // Title + addLabel(new GuiNpcLabel(0, "stock.perslot.title", guiLeft + 10, guiTop + 6)); + + // 18 slots in 3 columns x 6 rows + int startY = guiTop + 22; + int colWidth = 80; + + for (int i = 0; i < 18; i++) { + int col = i % 3; + int row = i / 3; + + int x = guiLeft + 10 + col * colWidth; + int y = startY + row * 28; + + // Slot label + addLabel(new GuiNpcLabel(i + 1, "Slot " + (i + 1) + ":", x, y + 5)); + + // Max stock textfield + int maxStock = role.stock.maxStock[i]; + String value = maxStock < 0 ? "-1" : String.valueOf(maxStock); + GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value); + field.setIntegersOnly(); + field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); + addTextField(field); + } + + // Done button + addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 26, 60, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton button) { + if (button.id == 0) { + close(); + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + int slot = textField.id; + if (slot >= 0 && slot < 18) { + int value = textField.getInteger(); + role.stock.setMaxStock(slot, value); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java index b4049bc8a..54ca9da5d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface.java @@ -224,6 +224,15 @@ public abstract class GuiContainerNPCInterface extends GuiContainer { RenderHelper.disableStandardItemLighting(); subgui.drawScreen(i, j, f); } + + // Draw hover text for buttons + boolean subGui = hasSubGui(); + for (GuiCustomScroll scroll : new ArrayList(scrolls.values())) { + scroll.drawHover(i, j); + } + for (GuiNpcButton button : buttons.values()) { + button.drawHover(i, j, subGui); + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java index 3455e4986..5e97ec19a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiContainerNPCInterface2.java @@ -61,9 +61,9 @@ public abstract class GuiContainerNPCInterface2 extends GuiContainerNPCInterface GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); mc.renderEngine.bindTexture(defaultBackground2); if (this instanceof GuiNpcTraderSetup) - drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 256, 220); + drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 210, 220); else - drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 256, 220); + drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 230, 220); menu.drawElements(fontRendererObj, i, j, mc, f); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 1d453a395..7e7d2acd8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -401,10 +401,23 @@ public abstract class GuiNPCInterface extends GuiScreen { GL11.glScalef(bgScale * bgScaleX, bgScale * bgScaleY, bgScale * bgScaleZ); mc.renderEngine.bindTexture(background); if (xSize > 256) { + // GUI wider than texture: draw left portion, then right portion from texture edge drawTexturedModalRect(0, 0, 0, 0, 250, ySize); drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, ySize); - } else + } else if (xSize < 256) { + // GUI narrower than texture: stitch left and right edges together + // Left portion (first half of GUI) + int leftWidth = xSize / 2; + // Right portion width (remaining) + int rightWidth = xSize - leftWidth; + // Draw left side from texture start + drawTexturedModalRect(0, 0, 0, 0, leftWidth, ySize); + // Draw right side from texture end (256 - rightWidth) + drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, ySize); + } else { + // GUI exactly 256px: draw full texture drawTexturedModalRect(0, 0, 0, 0, xSize, ySize); + } GL11.glPopMatrix(); } diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index f0f019290..c84ec3604 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -2,7 +2,6 @@ package noppes.npcs.config; import cpw.mods.fml.common.FMLLog; import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.common.config.Property; import org.apache.logging.log4j.Level; import java.io.File; @@ -14,7 +13,6 @@ public class ConfigMarket { public final static String MARKET = "Market"; public final static String CURRENCY = "Market.Currency"; - public final static String AUCTION = "Market.Auction"; public final static String TRADER = "Market.Trader"; // ========================================= @@ -25,37 +23,10 @@ public class ConfigMarket { // ========================================= // Currency Settings // ========================================= - public static boolean UseVaultIfAvailable = true; - public static String CurrencyName = "Gold"; - public static String FallbackCurrencyItem = "customnpcs:npcCoinGold"; - public static long FallbackItemRatio = 1; - public static long StartingBalance = 1000; - public static long MaxBalance = 999999999999L; - - // ========================================= - // Auction Settings - // ========================================= - public static boolean AuctionEnabled = true; - public static int DefaultMaxListings = 5; - public static double ListingFeePercent = 0.0; - public static double SalesTaxPercent = 0.05; - public static boolean RequireAuctioneerNPC = true; - public static int ClaimExpirationDays = 30; - public static double MinBidIncrementPercent = 0.05; - public static int SnipeProtectionMinutes = 5; - public static int SnipeProtectionThreshold = 5; - - // Auction Duration Settings (in hours) - public static int AuctionDurationShort = 2; - public static int AuctionDurationMedium = 12; - public static int AuctionDurationLong = 24; - public static int AuctionDurationVeryLong = 48; - - // Auction Duration Fees (flat currency amount) - public static long AuctionFeeShort = 0; - public static long AuctionFeeMedium = 0; - public static long AuctionFeeLong = 0; - public static long AuctionFeeVeryLong = 0; + public static boolean UseVault = false; + public static String CurrencyName = "Coins"; + public static long StartingBalance = 0; + public static long MaxBalance = Long.MAX_VALUE; // ========================================= // Trader Settings @@ -78,91 +49,19 @@ public class ConfigMarket { // ========================================= // Currency Settings // ========================================= - config.setCategoryComment(CURRENCY, "Currency system settings for the virtual economy"); + config.setCategoryComment(CURRENCY, "CNPC+ Currency system settings. When UseVault is enabled, all currency operations use Vault instead of the built-in system."); - UseVaultIfAvailable = config.get(CURRENCY, "Use Vault If Available", true, - "Use VaultAPI if available on Bukkit hybrid servers").getBoolean(true); + UseVault = config.get(CURRENCY, "Use Vault", false, + "If true, use Vault API for all currency operations instead of CNPC+ built-in currency. The built-in currency data will be preserved but unused.").getBoolean(false); - CurrencyName = config.get(CURRENCY, "Currency Name", "Gold", - "Display name for the currency").getString(); + CurrencyName = config.get(CURRENCY, "Currency Name", "Coins", + "Display name for the currency (e.g. 'Coins', 'Gold', 'Credits')").getString(); - FallbackCurrencyItem = config.get(CURRENCY, "Fallback Currency Item", "customnpcs:npcCoinGold", - "Item registry name to use as currency when Vault is unavailable. Leave empty to disable.").getString(); + StartingBalance = config.get(CURRENCY, "Starting Balance", 0, + "Starting currency balance for new players").getInt(0); - FallbackItemRatio = config.get(CURRENCY, "Fallback Item Ratio", 1, - "Conversion ratio: 1 fallback item = X virtual currency").getInt(1); - - StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, - "Starting balance for new players (only used when Vault is not available)").getInt(1000); - - Property maxBalanceProp = config.get(CURRENCY, "Maximum Balance", String.valueOf(MaxBalance), - "Maximum balance a player can have (only used when Vault is not available)"); - try { - MaxBalance = Long.parseLong(maxBalanceProp.getString()); - } catch (NumberFormatException e) { - MaxBalance = 999999999999L; - } - if (MaxBalance < 0) { - MaxBalance = 0; - } - - // ========================================= - // Auction Settings - // ========================================= - config.setCategoryComment(AUCTION, "Auction house settings"); - - AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, - "Enable/disable the auction house system").getBoolean(true); - - DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, - "Default maximum auction listings per player (without permission override)").getInt(5); - - ListingFeePercent = config.get(AUCTION, "Listing Fee Percent", 0.0, - "Listing fee as percentage of starting price (0.0 = no fee)").getDouble(0.0); - - SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, - "Sales tax as percentage taken from final sale price (0.05 = 5%)").getDouble(0.05); - - RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, - "Require an Auctioneer NPC to access auction house").getBoolean(true); - - ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, - "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); - - MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, - "Minimum bid increment as percentage (0.05 = 5% minimum increase)").getDouble(0.05); - - SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 5, - "Extend auction TO this many minutes when bid placed near end. Set to 0 to disable.").getInt(5); - - SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, - "How many minutes before end triggers snipe protection").getInt(5); - - // Duration settings - AuctionDurationShort = config.get(AUCTION, "Duration Short Hours", 2, - "Short auction duration in hours").getInt(2); - - AuctionDurationMedium = config.get(AUCTION, "Duration Medium Hours", 12, - "Medium auction duration in hours").getInt(12); - - AuctionDurationLong = config.get(AUCTION, "Duration Long Hours", 24, - "Long auction duration in hours").getInt(24); - - AuctionDurationVeryLong = config.get(AUCTION, "Duration Very Long Hours", 48, - "Very long auction duration in hours").getInt(48); - - // Duration fees - AuctionFeeShort = config.get(AUCTION, "Fee Short Duration", 0, - "Flat fee for short duration auctions").getInt(0); - - AuctionFeeMedium = config.get(AUCTION, "Fee Medium Duration", 0, - "Flat fee for medium duration auctions").getInt(0); - - AuctionFeeLong = config.get(AUCTION, "Fee Long Duration", 0, - "Flat fee for long duration auctions").getInt(0); - - AuctionFeeVeryLong = config.get(AUCTION, "Fee Very Long Duration", 0, - "Flat fee for very long duration auctions").getInt(0); + MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE, + "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE); // ========================================= // Trader Settings @@ -181,32 +80,10 @@ public class ConfigMarket { ))); config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( - "Use Vault If Available", + "Use Vault", "Currency Name", - "Fallback Currency Item", - "Fallback Item Ratio", "Starting Balance", - "Maximum Balance" - ))); - - config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( - "Enable Auction House", - "Default Max Listings", - "Listing Fee Percent", - "Sales Tax Percent", - "Require Auctioneer NPC", - "Claim Expiration Days", - "Min Bid Increment Percent", - "Snipe Protection Minutes", - "Snipe Protection Threshold", - "Duration Short Hours", - "Duration Medium Hours", - "Duration Long Hours", - "Duration Very Long Hours", - "Fee Short Duration", - "Fee Medium Duration", - "Fee Long Duration", - "Fee Very Long Duration" + "Max Balance" ))); config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( @@ -214,26 +91,6 @@ public class ConfigMarket { "Default Reset Type" ))); - // Validation - if (DefaultMaxListings < 1) { - DefaultMaxListings = 1; - } - if (ClaimExpirationDays < 0) { - ClaimExpirationDays = 0; - } - if (SnipeProtectionMinutes < 0) { - SnipeProtectionMinutes = 0; - } - if (SnipeProtectionThreshold < 0) { - SnipeProtectionThreshold = 0; - } - if (StartingBalance < 0) { - StartingBalance = 0; - } - if (MaxBalance < 1) { - MaxBalance = 1; - } - } catch (Exception e) { FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration"); } finally { diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java b/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java deleted file mode 100644 index c901d21ce..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionDuration.java +++ /dev/null @@ -1,82 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.util.StatCollector; -import noppes.npcs.config.ConfigMarket; - -/** - * Duration options for auction listings. - * Values are configurable via ConfigMarket. - */ -public enum EnumAuctionDuration { - SHORT("auction.duration.short"), - MEDIUM("auction.duration.medium"), - LONG("auction.duration.long"), - VERY_LONG("auction.duration.veryLong"); - - private final String langKey; - - EnumAuctionDuration(String langKey) { - this.langKey = langKey; - } - - /** - * Get duration in milliseconds - */ - public long getMillis() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort * 60L * 60L * 1000L; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium * 60L * 60L * 1000L; - case LONG: - return ConfigMarket.AuctionDurationLong * 60L * 60L * 1000L; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong * 60L * 60L * 1000L; - default: - return 24 * 60L * 60L * 1000L; - } - } - - /** - * Get duration in hours - */ - public int getHours() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionDurationShort; - case MEDIUM: - return ConfigMarket.AuctionDurationMedium; - case LONG: - return ConfigMarket.AuctionDurationLong; - case VERY_LONG: - return ConfigMarket.AuctionDurationVeryLong; - default: - return 24; - } - } - - /** - * Get display name for GUI - */ - public String getDisplayName() { - return StatCollector.translateToLocal(langKey); - } - - /** - * Get listing fee for this duration - */ - public long getListingFee() { - switch (this) { - case SHORT: - return ConfigMarket.AuctionFeeShort; - case MEDIUM: - return ConfigMarket.AuctionFeeMedium; - case LONG: - return ConfigMarket.AuctionFeeLong; - case VERY_LONG: - return ConfigMarket.AuctionFeeVeryLong; - default: - return 0; - } - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java deleted file mode 100644 index 75220861c..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java +++ /dev/null @@ -1,39 +0,0 @@ -package noppes.npcs.constants; - -import net.minecraft.util.StatCollector; - -/** - * Sorting options for auction listings. - */ -public enum EnumAuctionSort { - NEWEST_FIRST("auction.sort.newest"), - OLDEST_FIRST("auction.sort.oldest"), - PRICE_HIGH_TO_LOW("auction.sort.priceHigh"), - PRICE_LOW_TO_HIGH("auction.sort.priceLow"), - ENDING_SOON("auction.sort.endingSoon"), - MOST_TIME_LEFT("auction.sort.mostTime"), - NAME_A_TO_Z("auction.sort.nameAZ"), - NAME_Z_TO_A("auction.sort.nameZA"), - MOST_BIDS("auction.sort.mostBids"), - LEAST_BIDS("auction.sort.leastBids"); - - private final String langKey; - - EnumAuctionSort(String langKey) { - this.langKey = langKey; - } - - public String getDisplayName() { - return StatCollector.translateToLocal(langKey); - } - - public EnumAuctionSort next() { - EnumAuctionSort[] values = values(); - return values[(this.ordinal() + 1) % values.length]; - } - - public EnumAuctionSort previous() { - EnumAuctionSort[] values = values(); - return values[(this.ordinal() - 1 + values.length) % values.length]; - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java deleted file mode 100644 index 9e614e496..000000000 --- a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java +++ /dev/null @@ -1,54 +0,0 @@ -package noppes.npcs.constants; - -/** - * Status of an auction listing. - */ -public enum EnumAuctionStatus { - ACTIVE, // Currently listed and accepting bids - SOLD, // Successfully sold (buyout or auction end with bids) - EXPIRED, // Ended without any bids - CANCELLED, // Cancelled by seller (only allowed before first bid) - CLAIMED; // Item/money has been claimed by respective parties - - /** - * Get display name for GUI - */ - public String getDisplayName() { - switch (this) { - case ACTIVE: return "Active"; - case SOLD: return "Sold"; - case EXPIRED: return "Expired"; - case CANCELLED: return "Cancelled"; - case CLAIMED: return "Claimed"; - default: return "Unknown"; - } - } - - /** - * Get display color for GUI (MC color codes) - */ - public char getColorCode() { - switch (this) { - case ACTIVE: return 'a'; // Green - case SOLD: return '6'; // Gold - case EXPIRED: return '7'; // Gray - case CANCELLED: return 'c'; // Red - case CLAIMED: return '8'; // Dark gray - default: return 'f'; // White - } - } - - /** - * Check if listing can still receive bids - */ - public boolean canBid() { - return this == ACTIVE; - } - - /** - * Check if listing needs claiming (item or money) - */ - public boolean needsClaiming() { - return this == SOLD || this == EXPIRED || this == CANCELLED; - } -} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index 6c85cb4f5..f2d911561 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -64,7 +64,5 @@ public enum EnumGuiType { ScriptBlock, Paintbrush, MagicBook, - ManageAbilities, - PlayerAuction, - SetupAuctioneer; + ManageAbilities; } diff --git a/src/main/java/noppes/npcs/constants/EnumRoleType.java b/src/main/java/noppes/npcs/constants/EnumRoleType.java index 33f36941a..624658fb3 100644 --- a/src/main/java/noppes/npcs/constants/EnumRoleType.java +++ b/src/main/java/noppes/npcs/constants/EnumRoleType.java @@ -2,5 +2,5 @@ package noppes.npcs.constants; public enum EnumRoleType { - None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount, Auctioneer + None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount } diff --git a/src/main/java/noppes/npcs/constants/EnumStockReset.java b/src/main/java/noppes/npcs/constants/EnumStockReset.java index cdc56cb95..485318dd5 100644 --- a/src/main/java/noppes/npcs/constants/EnumStockReset.java +++ b/src/main/java/noppes/npcs/constants/EnumStockReset.java @@ -1,17 +1,25 @@ package noppes.npcs.constants; +import net.minecraft.util.StatCollector; + /** * Stock reset types for Trader NPCs. * Mirrors EnumQuestRepeat pattern for consistency. */ public enum EnumStockReset { - NONE, // Never resets - MCDAILY, // Every 24000 MC ticks (1 MC day) - MCWEEKLY, // Every 168000 MC ticks (7 MC days) - MCCUSTOM, // Custom MC tick interval - RLDAILY, // Every 24 real hours - RLWEEKLY, // Every 7 real days - RLCUSTOM; // Custom real-time interval + NONE("stock.reset.never"), + MCDAILY("stock.reset.mcdaily"), + MCWEEKLY("stock.reset.mcweekly"), + MCCUSTOM("stock.reset.mccustom"), + RLDAILY("stock.reset.rldaily"), + RLWEEKLY("stock.reset.rlweekly"), + RLCUSTOM("stock.reset.rlcustom"); + + private final String langKey; + + EnumStockReset(String langKey) { + this.langKey = langKey; + } /** * Get reset interval in milliseconds for real-time types @@ -47,18 +55,21 @@ public enum EnumStockReset { } /** - * Get display name for GUI + * Get display name for GUI (translated) */ public String getDisplayName() { - switch (this) { - case NONE: return "Never"; - case MCDAILY: return "MC Daily"; - case MCWEEKLY: return "MC Weekly"; - case MCCUSTOM: return "MC Custom"; - case RLDAILY: return "Real Daily"; - case RLWEEKLY: return "Real Weekly"; - case RLCUSTOM: return "Real Custom"; - default: return "Unknown"; + return StatCollector.translateToLocal(langKey); + } + + /** + * Get all display names for dropdown menus + */ + public static String[] getDisplayNames() { + EnumStockReset[] values = values(); + String[] names = new String[values.length]; + for (int i = 0; i < values.length; i++) { + names[i] = values[i].getDisplayName(); } + return names; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java deleted file mode 100644 index 2efd2b1b5..000000000 --- a/src/main/java/noppes/npcs/containers/ContainerAuction.java +++ /dev/null @@ -1,193 +0,0 @@ -package noppes.npcs.containers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import noppes.npcs.entity.EntityNPCInterface; - -/** - * Container for auction house GUI. - * Handles player inventory and item slot for creating listings. - */ -public class ContainerAuction extends Container { - public EntityNPCInterface npc; - public EntityPlayer player; - - // Slot for item being listed - public SlotAuctionItem listingSlot; - private IInventory listingInventory; - - public ContainerAuction(EntityNPCInterface npc, EntityPlayer player) { - this.npc = npc; - this.player = player; - - // Create a single-slot inventory for the item being listed - listingInventory = new InventoryAuctionListing(); - listingSlot = new SlotAuctionItem(listingInventory, 0, 251, 146); - addSlotToContainer(listingSlot); - - // Player inventory slots (standard positioning) - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col + row * 9 + 9, - 8 + col * 18, 166 + row * 18)); - } - } - - // Player hotbar - for (int col = 0; col < 9; col++) { - addSlotToContainer(new Slot(player.inventory, col, 8 + col * 18, 224)); - } - } - - @Override - public boolean canInteractWith(EntityPlayer player) { - return npc != null && npc.isEntityAlive() && - player.getDistanceSqToEntity(npc) < 64.0D; - } - - @Override - public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { - ItemStack itemstack = null; - Slot slot = (Slot) inventorySlots.get(slotIndex); - - if (slot != null && slot.getHasStack()) { - ItemStack stackInSlot = slot.getStack(); - itemstack = stackInSlot.copy(); - - // If from listing slot, move to player inventory - if (slotIndex == 0) { - if (!mergeItemStack(stackInSlot, 1, 37, true)) { - return null; - } - } - // If from player inventory, move to listing slot - else { - if (!mergeItemStack(stackInSlot, 0, 1, false)) { - return null; - } - } - - if (stackInSlot.stackSize == 0) { - slot.putStack(null); - } else { - slot.onSlotChanged(); - } - } - - return itemstack; - } - - @Override - public void onContainerClosed(EntityPlayer player) { - super.onContainerClosed(player); - - // Return any item in the listing slot to player - ItemStack stack = listingInventory.getStackInSlot(0); - if (stack != null) { - if (!player.inventory.addItemStackToInventory(stack)) { - player.entityDropItem(stack, 0.5f); - } - } - } - - /** - * Get the item currently in the listing slot - */ - public ItemStack getListingItem() { - return listingInventory.getStackInSlot(0); - } - - /** - * Clear the listing slot (after successful listing creation) - */ - public void clearListingSlot() { - listingInventory.setInventorySlotContents(0, null); - } - - /** - * Simple inventory for the listing slot - */ - private class InventoryAuctionListing implements IInventory { - private ItemStack item; - - @Override - public int getSizeInventory() { return 1; } - - @Override - public ItemStack getStackInSlot(int slot) { return item; } - - @Override - public ItemStack decrStackSize(int slot, int amount) { - if (item != null) { - ItemStack result; - if (item.stackSize <= amount) { - result = item; - item = null; - return result; - } else { - result = item.splitStack(amount); - if (item.stackSize == 0) { - item = null; - } - return result; - } - } - return null; - } - - @Override - public ItemStack getStackInSlotOnClosing(int slot) { - ItemStack result = item; - item = null; - return result; - } - - @Override - public void setInventorySlotContents(int slot, ItemStack stack) { - item = stack; - } - - @Override - public String getInventoryName() { return "AuctionListing"; } - - @Override - public boolean hasCustomInventoryName() { return false; } - - @Override - public int getInventoryStackLimit() { return 64; } - - @Override - public void markDirty() {} - - @Override - public boolean isUseableByPlayer(EntityPlayer player) { return true; } - - @Override - public void openInventory() {} - - @Override - public void closeInventory() {} - - @Override - public boolean isItemValidForSlot(int slot, ItemStack stack) { return true; } - } - - /** - * Custom slot for auction listings - */ - public class SlotAuctionItem extends Slot { - public SlotAuctionItem(IInventory inv, int index, int x, int y) { - super(inv, index, x, y); - } - - @Override - public boolean isItemValid(ItemStack stack) { - // Check if item can be traded (not untradeable, soulbound, etc.) - return kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute.canTrade(stack); - } - } -} diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index 936868152..e6b4c86c8 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -1,45 +1,71 @@ package noppes.npcs.containers; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilPlayer; -import noppes.npcs.controllers.CurrencyController; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; public class ContainerNPCTrader extends ContainerNpcInterface { public RoleTrader role; + private final EntityNPCInterface npc; + + // Layout constants for 256px wide GUI + public static final int COLUMN_WIDTH = 80; + public static final int COLUMN_START_X = 8; + public static final int ROW_HEIGHT = 22; + public static final int ROW_START_Y = 6; + + // Slot offsets within each column (for 18x18 slots, items render centered) + public static final int CURRENCY1_OFFSET = 2; // Primary currency slot + public static final int CURRENCY2_OFFSET = 20; // Secondary currency slot + public static final int OUTPUT_OFFSET = 50; // Output slot public ContainerNPCTrader(EntityNPCInterface npc, EntityPlayer player) { super(player); + this.npc = npc; role = (RoleTrader) npc.roleInterface; - for (int i = 0; i < 18; i++) { - int x = 53; - x += i % 3 * 72; - int y = 7; - y += i / 3 * 21; + // Register this player as viewing the trader (server-side only) + // Needed for shared stock (not per-player) to sync updates to other viewers + // Works for both linked markets (MarketRegistry) and normal traders (local viewers) + if (player instanceof EntityPlayerMP && !role.stock.perPlayer) { + role.registerViewer((EntityPlayerMP) player); + } - ItemStack item = role.inventoryCurrency.items.get(i); - ItemStack item2 = role.inventoryCurrency.items.get(i + 18); - if (item == null) { - item = item2; - item2 = null; - } - addSlotToContainer(new Slot(role.inventorySold, i, x, y)); + // Trade slots: 3 columns x 6 rows, 80px column width + // Layout per column: [Slot1][Slot2] = [Output] + for (int i = 0; i < 18; i++) { + int col = i % 3; + int row = i / 3; + int x = COLUMN_START_X + col * COLUMN_WIDTH; + int y = ROW_START_Y + row * ROW_HEIGHT; + // Output slot position (slot texture is 18x18, item renders centered at +1) + addSlotToContainer(new Slot(role.inventorySold, i, x + OUTPUT_OFFSET + 1, y + 1)); } for (int i1 = 0; i1 < 3; i1++) { for (int l1 = 0; l1 < 9; l1++) { - addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 32 + l1 * 18, 140 + i1 * 18)); + addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 48 + l1 * 18, 137 + i1 * 18)); } - } + // Player hotbar for (int j1 = 0; j1 < 9; j1++) { - addSlotToContainer(new Slot(player.inventory, j1, 32 + j1 * 18, 198)); + addSlotToContainer(new Slot(player.inventory, j1, 48 + j1 * 18, 195)); + } + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + // Unregister this player from viewer tracking (only if registered for shared stock) + if (player instanceof EntityPlayerMP && !role.stock.perPlayer) { + role.unregisterViewer((EntityPlayerMP) player); } } @@ -71,25 +97,27 @@ public class ContainerNPCTrader extends ContainerNpcInterface { return null; // Out of stock } - // Handle currency-based purchase - if (role.hasSlotPrice(i)) { - long price = role.getSlotPrice(i); - if (CurrencyController.Instance == null) { - return null; // Currency system not available + // Item-based currency + if (!canBuy(i, entityplayer)) + return null; + + // Currency cost check (additive to item costs) + long currencyCost = role.getCurrencyCost(i); + if (currencyCost > 0) { + PlayerData data = PlayerData.get(entityplayer); + if (data.currencyData.getBalance() < currencyCost) { + return null; // Can't afford currency cost } - if (!CurrencyController.Instance.canAfford(entityplayer, price)) { - return null; // Can't afford - } - // Withdraw currency - if (!CurrencyController.Instance.withdraw(entityplayer, price)) { - return null; // Withdrawal failed - } - } else { - // Item-based currency - existing logic - if (!canBuy(i, entityplayer)) - return null; - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + } + + // Consume item currency + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + + // Withdraw currency cost (after item consumption to maintain order) + if (currencyCost > 0) { + PlayerData data = PlayerData.get(entityplayer); + data.currencyData.withdraw(currencyCost); } // Consume stock @@ -98,6 +126,18 @@ public class ContainerNPCTrader extends ContainerNpcInterface { ItemStack soldItem = item.copy(); givePlayer(soldItem, entityplayer); role.addPurchase(i, entityplayer.getDisplayName()); + + // Sync updated data after purchase + // For per-player stocks: sync only to the purchasing player + // For shared stocks: consumeStock() handles sync (market-wide or local) + if (entityplayer instanceof EntityPlayerMP) { + if (role.stock.perPlayer) { + // Per-player: only sync to purchasing player (their own stock/balance) + role.syncToPlayer((EntityPlayerMP) entityplayer); + } + // Shared stock sync is already handled in consumeStock() + } + return soldItem; } diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java b/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java index 8209f22a8..f5d347e16 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTraderSetup.java @@ -13,15 +13,19 @@ public class ContainerNPCTraderSetup extends Container { public ContainerNPCTraderSetup(EntityNPCInterface npc, EntityPlayer player) { role = (RoleTrader) npc.roleInterface; + // New layout: $[Currency TextField], Item Cost 1, Item Cost 2, =, Output + // Column width: 130px for better spacing for (int i = 0; i < 18; i++) { int x = 7; - x += i % 3 * 94; + x += i % 3 * 130; int y = 15; y += i / 3 * 22; - addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x, y)); - addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 18, y)); - addSlotToContainer(new Slot(role.inventorySold, i, x + 43, y)); - + // Item Cost 1 (primary currency) + addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 51, y)); + // Item Cost 2 (secondary currency) + addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x + 69, y)); + // Output (sold item) + addSlotToContainer(new Slot(role.inventorySold, i, x + 105, y)); } for (int i1 = 0; i1 < 3; i1++) { diff --git a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java deleted file mode 100644 index 020bb0bcb..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionBlacklist.java +++ /dev/null @@ -1,419 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.nbt.NBTTagString; -import noppes.npcs.CustomNpcs; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.HashSet; -import java.util.Set; - -/** - * Controller for managing auction house item blacklisting. - * Allows administrators to block specific items, entire mods, - * or items with certain NBT tags from being listed. - */ -public class AuctionBlacklist { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static AuctionBlacklist Instance; - - // Blacklist by item registry name (e.g., "minecraft:diamond_sword") - private Set blacklistedItems = new HashSet<>(); - - // Blacklist by mod ID (e.g., "minecraft" blocks all vanilla items) - private Set blacklistedMods = new HashSet<>(); - - // Blacklist by NBT tag presence (e.g., items with specific custom data) - private Set blacklistedNBTTags = new HashSet<>(); - - public AuctionBlacklist() { - Instance = this; - load(); - } - - // ==================== Blacklist Checking ==================== - - /** - * Check if an item is blacklisted from the auction house. - * - * @param item The item to check - * @return true if the item is blacklisted - */ - public boolean isBlacklisted(ItemStack item) { - if (item == null || item.getItem() == null) { - return false; - } - - // Check item registry name - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - // Direct item blacklist - if (blacklistedItems.contains(itemName)) { - return true; - } - - // Also check with metadata for specific variants - String itemNameWithMeta = itemName + ":" + item.getItemDamage(); - if (blacklistedItems.contains(itemNameWithMeta)) { - return true; - } - - // Check mod ID - String modId = getModIdFromName(itemName); - if (modId != null && blacklistedMods.contains(modId)) { - return true; - } - } - - // Check NBT tags - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (hasNestedTag(tag, nbtTag)) { - return true; - } - } - } - - return false; - } - - /** - * Get the reason why an item is blacklisted - * @return Description of why blacklisted, or null if not blacklisted - */ - public String getBlacklistReason(ItemStack item) { - if (item == null || item.getItem() == null) { - return null; - } - - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - if (blacklistedItems.contains(itemName)) { - return "Item is blacklisted: " + itemName; - } - - String itemNameWithMeta = itemName + ":" + item.getItemDamage(); - if (blacklistedItems.contains(itemNameWithMeta)) { - return "Item variant is blacklisted: " + itemNameWithMeta; - } - - String modId = getModIdFromName(itemName); - if (modId != null && blacklistedMods.contains(modId)) { - return "Mod is blacklisted: " + modId; - } - } - - if (item.hasTagCompound()) { - NBTTagCompound tag = item.getTagCompound(); - for (String nbtTag : blacklistedNBTTags) { - if (hasNestedTag(tag, nbtTag)) { - return "Item has blacklisted NBT tag: " + nbtTag; - } - } - } - - return null; - } - - /** - * Extract mod ID from registry name (e.g., "minecraft" from "minecraft:stone") - */ - private String getModIdFromName(String registryName) { - if (registryName == null) { - return null; - } - int colonIndex = registryName.indexOf(':'); - if (colonIndex > 0) { - return registryName.substring(0, colonIndex); - } - return null; - } - - /** - * Check for nested NBT tag (supports dot notation like "display.Name") - */ - private boolean hasNestedTag(NBTTagCompound tag, String path) { - if (tag == null || path == null || path.isEmpty()) { - return false; - } - - String[] parts = path.split("\\."); - NBTTagCompound current = tag; - - for (int i = 0; i < parts.length - 1; i++) { - if (!current.hasKey(parts[i])) { - return false; - } - if (current.hasKey(parts[i], 10)) { // 10 = Compound tag - current = current.getCompoundTag(parts[i]); - } else { - return false; - } - } - - return current.hasKey(parts[parts.length - 1]); - } - - // ==================== Item Blacklist Management ==================== - - /** - * Add an item to the blacklist by registry name - * @param itemName Registry name (e.g., "minecraft:diamond_sword" or "minecraft:wool:14") - */ - public void addItem(String itemName) { - if (itemName != null && !itemName.isEmpty()) { - blacklistedItems.add(itemName); - save(); - logger.info("Added item to auction blacklist: " + itemName); - } - } - - /** - * Add an item to the blacklist from an ItemStack - */ - public void addItem(ItemStack item, boolean includeMeta) { - if (item == null || item.getItem() == null) { - return; - } - String itemName = Item.itemRegistry.getNameForObject(item.getItem()); - if (itemName != null) { - if (includeMeta && item.getHasSubtypes()) { - addItem(itemName + ":" + item.getItemDamage()); - } else { - addItem(itemName); - } - } - } - - /** - * Remove an item from the blacklist - */ - public void removeItem(String itemName) { - if (blacklistedItems.remove(itemName)) { - save(); - logger.info("Removed item from auction blacklist: " + itemName); - } - } - - /** - * Check if a specific item is blacklisted - */ - public boolean isItemBlacklisted(String itemName) { - return blacklistedItems.contains(itemName); - } - - /** - * Get all blacklisted items - */ - public Set getBlacklistedItems() { - return new HashSet<>(blacklistedItems); - } - - // ==================== Mod Blacklist Management ==================== - - /** - * Add an entire mod to the blacklist - * @param modId The mod ID (e.g., "minecraft", "customnpcs") - */ - public void addMod(String modId) { - if (modId != null && !modId.isEmpty()) { - blacklistedMods.add(modId); - save(); - logger.info("Added mod to auction blacklist: " + modId); - } - } - - /** - * Remove a mod from the blacklist - */ - public void removeMod(String modId) { - if (blacklistedMods.remove(modId)) { - save(); - logger.info("Removed mod from auction blacklist: " + modId); - } - } - - /** - * Check if a mod is blacklisted - */ - public boolean isModBlacklisted(String modId) { - return blacklistedMods.contains(modId); - } - - /** - * Get all blacklisted mods - */ - public Set getBlacklistedMods() { - return new HashSet<>(blacklistedMods); - } - - // ==================== NBT Tag Blacklist Management ==================== - - /** - * Add an NBT tag to the blacklist - * Items containing this tag will be blocked - * @param tagPath The tag path (e.g., "display.Name" or "CustomData") - */ - public void addNBTTag(String tagPath) { - if (tagPath != null && !tagPath.isEmpty()) { - blacklistedNBTTags.add(tagPath); - save(); - logger.info("Added NBT tag to auction blacklist: " + tagPath); - } - } - - /** - * Remove an NBT tag from the blacklist - */ - public void removeNBTTag(String tagPath) { - if (blacklistedNBTTags.remove(tagPath)) { - save(); - logger.info("Removed NBT tag from auction blacklist: " + tagPath); - } - } - - /** - * Check if an NBT tag is blacklisted - */ - public boolean isNBTTagBlacklisted(String tagPath) { - return blacklistedNBTTags.contains(tagPath); - } - - /** - * Get all blacklisted NBT tags - */ - public Set getBlacklistedNBTTags() { - return new HashSet<>(blacklistedNBTTags); - } - - // ==================== Persistence ==================== - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - try { - NBTTagCompound compound = new NBTTagCompound(); - - // Save blacklisted items - NBTTagList itemList = new NBTTagList(); - for (String item : blacklistedItems) { - itemList.appendTag(new NBTTagString(item)); - } - compound.setTag("Items", itemList); - - // Save blacklisted mods - NBTTagList modList = new NBTTagList(); - for (String mod : blacklistedMods) { - modList.appendTag(new NBTTagString(mod)); - } - compound.setTag("Mods", modList); - - // Save blacklisted NBT tags - NBTTagList nbtList = new NBTTagList(); - for (String nbt : blacklistedNBTTags) { - nbtList.appendTag(new NBTTagString(nbt)); - } - compound.setTag("NBTTags", nbtList); - - // Write to file - File file = new File(saveDir, "auction_blacklist.dat"); - File backup = new File(saveDir, "auction_blacklist.dat_old"); - - if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } - file.renameTo(backup); - } - - file = new File(saveDir, "auction_blacklist.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - - } catch (Exception e) { - logger.error("Error saving auction blacklist", e); - } - } - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - blacklistedItems.clear(); - blacklistedMods.clear(); - blacklistedNBTTags.clear(); - - try { - File file = new File(saveDir, "auction_blacklist.dat"); - if (!file.exists()) { - // Try backup - file = new File(saveDir, "auction_blacklist.dat_old"); - if (!file.exists()) { - return; - } - } - - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - // Load blacklisted items - NBTTagList itemList = compound.getTagList("Items", 8); // 8 = String tag - for (int i = 0; i < itemList.tagCount(); i++) { - blacklistedItems.add(itemList.getStringTagAt(i)); - } - - // Load blacklisted mods - NBTTagList modList = compound.getTagList("Mods", 8); - for (int i = 0; i < modList.tagCount(); i++) { - blacklistedMods.add(modList.getStringTagAt(i)); - } - - // Load blacklisted NBT tags - NBTTagList nbtList = compound.getTagList("NBTTags", 8); - for (int i = 0; i < nbtList.tagCount(); i++) { - blacklistedNBTTags.add(nbtList.getStringTagAt(i)); - } - - logger.info("Loaded auction blacklist: {} items, {} mods, {} NBT tags", - blacklistedItems.size(), blacklistedMods.size(), blacklistedNBTTags.size()); - - } catch (Exception e) { - logger.error("Error loading auction blacklist", e); - } - } - - /** - * Clear all blacklist entries - */ - public void clearAll() { - blacklistedItems.clear(); - blacklistedMods.clear(); - blacklistedNBTTags.clear(); - save(); - logger.info("Cleared all auction blacklist entries"); - } - - /** - * Get total number of blacklist entries - */ - public int getTotalEntries() { - return blacklistedItems.size() + blacklistedMods.size() + blacklistedNBTTags.size(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java deleted file mode 100644 index 8333fd01a..000000000 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ /dev/null @@ -1,834 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.ChatComponentText; -import net.minecraft.util.EnumChatFormatting; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionSort; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.data.AuctionFilter; -import noppes.npcs.controllers.data.AuctionListing; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -/** - * Controller for the global auction house. - * Manages all auction listings, bidding, claims, notifications, and logging. - */ -public class AuctionController { - private static final String LOG_PREFIX = "[Auction] "; - - public static AuctionController Instance; - - // All auction listings by ID - private Map listings = new ConcurrentHashMap<>(); - - // Next available listing ID - private int nextId = 1; - - // Indices for fast lookup - private Map> listingsBySeller = new ConcurrentHashMap<>(); - private Map> listingsByBidder = new ConcurrentHashMap<>(); - - // Pending notifications for offline players - private Map> pendingNotifications = new ConcurrentHashMap<>(); - - public AuctionController() { - Instance = this; - load(); - } - - // ==================== Listing Management ==================== - - /** - * Create a new auction listing - * @return The new listing, or null if creation failed - */ - public AuctionListing createListing(EntityPlayer seller, ItemStack item, - long startingPrice, long buyoutPrice, - EnumAuctionDuration duration) { - // Validate item - String validation = AuctionListing.validateItem(item); - if (validation != null) { - LogWriter.info(LOG_PREFIX + "Listing validation failed: " + validation); - return null; - } - - // Check seller's listing limit - int currentListings = getActiveListingsCount(seller.getUniqueID()); - int maxListings = getMaxListings(seller); - if (currentListings >= maxListings) { - return null; - } - - // Calculate and charge listing fee - AuctionListing listing = new AuctionListing( - seller.getUniqueID(), - seller.getCommandSenderName(), - item, - startingPrice, - buyoutPrice, - duration - ); - - long listingFee = listing.getListingFee(); - if (listingFee > 0) { - if (!CurrencyController.Instance.canAfford(seller, listingFee)) { - return null; - } - CurrencyController.Instance.withdraw(seller, listingFee); - } - - // Assign ID and store - listing.id = nextId++; - listings.put(listing.id, listing); - - // Update indices - addToSellerIndex(seller.getUniqueID(), listing.id); - - save(); - - // Log to CNPC+ - logEvent("LISTING_CREATED", String.format("ID=%d, Seller=%s, Item=%s x%d, StartPrice=%d, Buyout=%s", - listing.id, listing.sellerName, item.getDisplayName(), item.stackSize, - listing.startingPrice, listing.buyoutPrice > 0 ? listing.buyoutPrice : "None")); - - return listing; - } - - /** - * Get a listing by ID - */ - public AuctionListing getListing(int id) { - return listings.get(id); - } - - /** - * Get all active listings - */ - public List getActiveListings() { - processExpiredAuctions(); - return listings.values().stream() - .filter(AuctionListing::isActive) - .collect(Collectors.toList()); - } - - /** - * Search listings by item name - */ - public List searchListings(String query) { - String lowerQuery = query.toLowerCase(); - return getActiveListings().stream() - .filter(l -> l.item != null) - .filter(l -> l.item.getDisplayName().toLowerCase().contains(lowerQuery)) - .collect(Collectors.toList()); - } - - // ==================== Filtering & Sorting ==================== - - /** - * Get filtered and sorted auction listings. - * @param filter The filter criteria - * @return FilterResult containing matching listings and metadata - */ - public FilterResult getFilteredListings(AuctionFilter filter) { - processExpiredAuctions(); - - // Get all active listings - List results = listings.values().stream() - .filter(AuctionListing::isActive) - .filter(filter::matches) - .collect(Collectors.toList()); - - // Sort the results - sortListings(results, filter.sortOrder); - - // Calculate pagination - int totalResults = results.size(); - int totalPages = totalResults == 0 - ? 0 - : Math.max(1, (int) Math.ceil((double) totalResults / filter.pageSize)); - int currentPage = Math.max(0, filter.page); - if (totalPages > 0 && currentPage >= totalPages) { - currentPage = totalPages - 1; - } - int startIndex = currentPage * filter.pageSize; - int endIndex = Math.min(startIndex + filter.pageSize, totalResults); - - // Get the page slice - List pageResults; - if (startIndex >= totalResults) { - pageResults = new ArrayList<>(); - } else { - pageResults = new ArrayList<>(results.subList(startIndex, endIndex)); - } - - return new FilterResult(pageResults, totalResults, currentPage, totalPages); - } - - /** - * Sort listings according to the specified sort order - */ - public void sortListings(List listings, EnumAuctionSort sortOrder) { - if (listings == null || listings.isEmpty()) { - return; - } - - Comparator comparator; - - switch (sortOrder) { - case NEWEST_FIRST: - comparator = Comparator.comparingLong(l -> -l.listingTime); - break; - case OLDEST_FIRST: - comparator = Comparator.comparingLong(l -> l.listingTime); - break; - case PRICE_HIGH_TO_LOW: - comparator = Comparator.comparingLong((AuctionListing l) -> - -(l.currentBid > 0 ? l.currentBid : l.startingPrice)); - break; - case PRICE_LOW_TO_HIGH: - comparator = Comparator.comparingLong(l -> - l.currentBid > 0 ? l.currentBid : l.startingPrice); - break; - case ENDING_SOON: - comparator = Comparator.comparingLong(l -> l.expirationTime); - break; - case MOST_TIME_LEFT: - comparator = Comparator.comparingLong(l -> -l.expirationTime); - break; - case NAME_A_TO_Z: - comparator = Comparator.comparing(l -> getListingName(l).toLowerCase()); - break; - case NAME_Z_TO_A: - comparator = Comparator.comparing((AuctionListing l) -> - getListingName(l).toLowerCase()).reversed(); - break; - case MOST_BIDS: - comparator = Comparator.comparingInt(l -> -l.bidCount); - break; - case LEAST_BIDS: - comparator = Comparator.comparingInt(l -> l.bidCount); - break; - default: - comparator = Comparator.comparingLong(l -> l.expirationTime); - } - - listings.sort(comparator); - } - - /** - * Result container for filtered queries - */ - public static class FilterResult { - public final List listings; - public final int totalResults; - public final int currentPage; - public final int totalPages; - - public FilterResult(List listings, int totalResults, - int currentPage, int totalPages) { - this.listings = listings; - this.totalResults = totalResults; - this.currentPage = currentPage; - this.totalPages = totalPages; - } - - public boolean hasNextPage() { - return currentPage < totalPages - 1; - } - - public boolean hasPreviousPage() { - return currentPage > 0; - } - - public boolean isEmpty() { - return listings == null || listings.isEmpty(); - } - } - - /** - * Get all listings by a seller - */ - public List getListingsBySeller(UUID sellerUUID) { - List ids = listingsBySeller.get(sellerUUID); - if (ids == null) { - return new ArrayList<>(); - } - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - /** - * Get active listing count for a seller - */ - public int getActiveListingsCount(UUID sellerUUID) { - return (int) getListingsBySeller(sellerUUID).stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .count(); - } - - /** - * Get listings where player is the high bidder - */ - public List getListingsAsBidder(UUID bidderUUID) { - List ids = listingsByBidder.get(bidderUUID); - if (ids == null) { - return new ArrayList<>(); - } - return ids.stream() - .map(listings::get) - .filter(Objects::nonNull) - .filter(AuctionListing::isActive) - .filter(l -> bidderUUID.equals(l.highBidderUUID)) - .collect(Collectors.toList()); - } - - /** - * Get listings that need claiming by a player - */ - public List getClaimableListings(UUID playerUUID) { - List claimable = new ArrayList<>(); - - for (AuctionListing listing : listings.values()) { - // Seller can claim proceeds from sold items - if (listing.sellerUUID.equals(playerUUID) && - listing.status == EnumAuctionStatus.SOLD && - !listing.sellerClaimed) { - claimable.add(listing); - } - - // Seller can claim back cancelled/expired items - if (listing.sellerUUID.equals(playerUUID) && - (listing.status == EnumAuctionStatus.CANCELLED || - listing.status == EnumAuctionStatus.EXPIRED) && - !listing.sellerClaimed) { - claimable.add(listing); - } - - // Buyer can claim won items - if (listing.highBidderUUID != null && - listing.highBidderUUID.equals(playerUUID) && - listing.status == EnumAuctionStatus.SOLD && - !listing.buyerClaimed) { - claimable.add(listing); - } - } - - return claimable; - } - - // ==================== Bidding ==================== - - /** - * Place a bid on an auction - */ - public boolean placeBid(int listingId, EntityPlayer bidder, long amount) { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return false; - } - - if (!CurrencyController.Instance.canAfford(bidder, amount)) { - return false; - } - - // Store previous bidder for refund - UUID previousBidder = listing.highBidderUUID; - String previousBidderName = listing.highBidderName; - long previousBidAmount = listing.currentBid; - - if (!listing.placeBid(bidder.getUniqueID(), bidder.getCommandSenderName(), amount)) { - return false; - } - - CurrencyController.Instance.withdraw(bidder, amount); - - // Refund previous bidder - if (previousBidder != null) { - CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, - "Auction Outbid", String.valueOf(listingId)); - - // Notify outbid player - notifyPlayer(previousBidder, EnumChatFormatting.RED + - "You have been outbid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + "! New bid: " + formatCurrency(amount)); - - logEvent("OUTBID", String.format("ID=%d, OutbidPlayer=%s, PrevBid=%d, NewBid=%d", - listingId, previousBidderName, previousBidAmount, amount)); - } - - // Notify seller - notifyPlayer(listing.sellerUUID, EnumChatFormatting.AQUA + - bidder.getCommandSenderName() + " placed a bid of " + formatCurrency(amount) + - " on your " + listing.item.getDisplayName() + "!"); - - addToBidderIndex(bidder.getUniqueID(), listingId); - save(); - - logEvent("BID_PLACED", String.format("ID=%d, Bidder=%s, Amount=%d", - listingId, bidder.getCommandSenderName(), amount)); - - return true; - } - - /** - * Execute buyout on an auction - */ - public boolean buyout(int listingId, EntityPlayer buyer) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.buyoutPrice <= 0) { - return false; - } - - if (!CurrencyController.Instance.canAfford(buyer, listing.buyoutPrice)) { - return false; - } - - UUID previousBidder = listing.highBidderUUID; - String previousBidderName = listing.highBidderName; - long previousBidAmount = listing.currentBid; - - if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { - return false; - } - - CurrencyController.Instance.withdraw(buyer, listing.buyoutPrice); - - // Refund previous bidder - if (previousBidder != null) { - CurrencyController.Instance.addClaim(previousBidder, previousBidAmount, - "Auction Outbid (Buyout)", String.valueOf(listingId)); - - notifyPlayer(previousBidder, EnumChatFormatting.RED + - "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was outbid by a buyout. Your " + - formatCurrency(previousBidAmount) + " has been refunded."); - } - - addToBidderIndex(buyer.getUniqueID(), listingId); - save(); - - // Notify buyer and seller - notifyPlayer(buyer.getUniqueID(), EnumChatFormatting.GREEN + - "Buyout successful! Claim your " + listing.item.getDisplayName() + " from an Auctioneer."); - - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + - "Your " + listing.item.getDisplayName() + " was bought out for " + - formatCurrency(listing.buyoutPrice) + "! Claim your proceeds."); - - logEvent("BUYOUT", String.format("ID=%d, Buyer=%s, Price=%d, Seller=%s", - listingId, buyer.getCommandSenderName(), listing.buyoutPrice, listing.sellerName)); - - return true; - } - - /** - * Cancel an auction listing - */ - public boolean cancelListing(int listingId, EntityPlayer requester, boolean forceCancel) { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return false; - } - - boolean byAdmin = !listing.sellerUUID.equals(requester.getUniqueID()); - if (byAdmin && !forceCancel) { - return false; - } - if (!forceCancel && listing.hasBids()) { - return false; - } - if (listing.status != EnumAuctionStatus.ACTIVE) { - return false; - } - - if (forceCancel && listing.hasBids() && listing.highBidderUUID != null) { - CurrencyController.Instance.addClaim(listing.highBidderUUID, listing.currentBid, - "Auction Cancelled", String.valueOf(listingId)); - notifyPlayer(listing.highBidderUUID, EnumChatFormatting.RED + - "Your bid on " + EnumChatFormatting.WHITE + listing.item.getDisplayName() + - EnumChatFormatting.RED + " was cancelled. Your " + - formatCurrency(listing.currentBid) + " has been refunded."); - } - - listing.status = EnumAuctionStatus.CANCELLED; - - save(); - - if (byAdmin) { - notifyPlayer(listing.sellerUUID, EnumChatFormatting.RED + - "Your auction for " + listing.item.getDisplayName() + - " was cancelled by an administrator."); - } - - logEvent("CANCELLED", String.format("ID=%d, CancelledBy=%s, ByAdmin=%b", - listingId, requester.getCommandSenderName(), byAdmin)); - - return true; - } - - // ==================== Claiming ==================== - - /** - * Claim seller proceeds from a sold auction - */ - public boolean claimSellerProceeds(int listingId, EntityPlayer seller) { - AuctionListing listing = listings.get(listingId); - if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || - listing.status != EnumAuctionStatus.SOLD || listing.sellerClaimed) { - return false; - } - - long proceeds = listing.getSellerProceeds(); - CurrencyController.Instance.deposit(seller, proceeds); - - listing.sellerClaimed = true; - checkAndMarkClaimed(listing); - save(); - - logEvent("PROCEEDS_CLAIMED", String.format("ID=%d, Seller=%s, Amount=%d", - listingId, seller.getCommandSenderName(), proceeds)); - - return true; - } - - /** - * Claim item from won auction - */ - public ItemStack claimBuyerItem(int listingId, EntityPlayer buyer) { - AuctionListing listing = listings.get(listingId); - if (listing == null || listing.highBidderUUID == null || - !listing.highBidderUUID.equals(buyer.getUniqueID()) || - listing.status != EnumAuctionStatus.SOLD || listing.buyerClaimed) { - return null; - } - - listing.buyerClaimed = true; - checkAndMarkClaimed(listing); - save(); - - logEvent("ITEM_CLAIMED", String.format("ID=%d, Buyer=%s, Item=%s", - listingId, buyer.getCommandSenderName(), listing.item.getDisplayName())); - - return listing.item.copy(); - } - - /** - * Claim item from cancelled or expired auction - */ - public ItemStack claimExpiredItem(int listingId, EntityPlayer seller) { - AuctionListing listing = listings.get(listingId); - if (listing == null || !listing.sellerUUID.equals(seller.getUniqueID()) || - (listing.status != EnumAuctionStatus.CANCELLED && listing.status != EnumAuctionStatus.EXPIRED) || - listing.sellerClaimed) { - return null; - } - - listing.sellerClaimed = true; - listing.status = EnumAuctionStatus.CLAIMED; - save(); - - logEvent("ITEM_RECLAIMED", String.format("ID=%d, Seller=%s, Item=%s", - listingId, seller.getCommandSenderName(), listing.item.getDisplayName())); - - return listing.item.copy(); - } - - private void checkAndMarkClaimed(AuctionListing listing) { - if (listing.status == EnumAuctionStatus.SOLD && - listing.sellerClaimed && listing.buyerClaimed) { - listing.status = EnumAuctionStatus.CLAIMED; - } - } - - // ==================== Expiration Processing ==================== - - /** - * Process all expired auctions - */ - public void processExpiredAuctions() { - boolean changed = false; - - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { - listing.finalize(); - changed = true; - - if (listing.status == EnumAuctionStatus.SOLD) { - // Notify winner and seller - notifyPlayer(listing.highBidderUUID, EnumChatFormatting.GREEN + - "You won the auction for " + listing.item.getDisplayName() + - " for " + formatCurrency(listing.currentBid) + "! Claim your item."); - - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GREEN + - "Your " + listing.item.getDisplayName() + " sold for " + - formatCurrency(listing.currentBid) + "! Claim your proceeds."); - - logEvent("AUCTION_SOLD", String.format("ID=%d, Winner=%s, Price=%d, Seller=%s", - listing.id, listing.highBidderName, listing.currentBid, listing.sellerName)); - } else { - notifyPlayer(listing.sellerUUID, EnumChatFormatting.GRAY + - "Your auction for " + listing.item.getDisplayName() + - " has expired with no bids. Claim your item."); - - logEvent("AUCTION_EXPIRED", String.format("ID=%d, Item=%s, Seller=%s", - listing.id, listing.item.getDisplayName(), listing.sellerName)); - } - } - } - - if (changed) { - save(); - } - } - - /** - * Clean up old claimed listings - */ - public void cleanupOldListings() { - long claimExpirationMs = ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L; - if (claimExpirationMs <= 0) return; - - long cutoffTime = System.currentTimeMillis() - claimExpirationMs; - List toRemove = new ArrayList<>(); - - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.CLAIMED && - listing.expirationTime < cutoffTime) { - toRemove.add(listing.id); - } - } - - for (int id : toRemove) { - AuctionListing removed = listings.remove(id); - if (removed != null) { - removeFromSellerIndex(removed.sellerUUID, id); - if (removed.highBidderUUID != null) { - removeFromBidderIndex(removed.highBidderUUID, id); - } - } - } - - if (!toRemove.isEmpty()) { - save(); - logEvent("CLEANUP", "Removed " + toRemove.size() + " old listings"); - } - } - - // ==================== Notifications ==================== - - /** - * Send notification to player (or queue if offline) - */ - private void notifyPlayer(UUID playerUUID, String message) { - if (playerUUID == null) return; - - EntityPlayerMP player = getOnlinePlayer(playerUUID); - String fullMessage = EnumChatFormatting.YELLOW + "[Auction] " + message; - - if (player != null) { - player.addChatMessage(new ChatComponentText(fullMessage)); - } else { - pendingNotifications.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(fullMessage); - // Limit queue size - List queue = pendingNotifications.get(playerUUID); - while (queue.size() > 50) { - queue.remove(0); - } - } - } - - /** - * Send pending notifications on player login - */ - public void onPlayerLogin(EntityPlayerMP player) { - UUID uuid = player.getUniqueID(); - - List notifications = pendingNotifications.remove(uuid); - if (notifications != null && !notifications.isEmpty()) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.WHITE + - "You have " + notifications.size() + " auction notification(s):")); - - for (String msg : notifications) { - player.addChatMessage(new ChatComponentText(msg)); - } - } - - // Check claimable items - int claimableCount = getClaimableListings(uuid).size(); - if (claimableCount > 0) { - player.addChatMessage(new ChatComponentText( - EnumChatFormatting.YELLOW + "[Auction] " + EnumChatFormatting.GREEN + - "You have " + claimableCount + " item(s) waiting to be claimed!")); - } - } - - private EntityPlayerMP getOnlinePlayer(UUID uuid) { - if (CustomNpcs.getServer() == null) return null; - - for (Object obj : CustomNpcs.getServer().getConfigurationManager().playerEntityList) { - EntityPlayerMP player = (EntityPlayerMP) obj; - if (player.getUniqueID().equals(uuid)) { - return player; - } - } - return null; - } - - // ==================== Index Management ==================== - - private void addToSellerIndex(UUID seller, int listingId) { - listingsBySeller.computeIfAbsent(seller, k -> new ArrayList<>()).add(listingId); - } - - private void removeFromSellerIndex(UUID seller, int listingId) { - List ids = listingsBySeller.get(seller); - if (ids != null) { - ids.remove(Integer.valueOf(listingId)); - } - } - - private void addToBidderIndex(UUID bidder, int listingId) { - listingsByBidder.computeIfAbsent(bidder, k -> new ArrayList<>()).add(listingId); - } - - private void removeFromBidderIndex(UUID bidder, int listingId) { - List ids = listingsByBidder.get(bidder); - if (ids != null) { - ids.remove(Integer.valueOf(listingId)); - } - } - - private void rebuildIndices() { - listingsBySeller.clear(); - listingsByBidder.clear(); - - for (AuctionListing listing : listings.values()) { - addToSellerIndex(listing.sellerUUID, listing.id); - if (listing.highBidderUUID != null) { - addToBidderIndex(listing.highBidderUUID, listing.id); - } - } - } - - // ==================== Helpers ==================== - - public int getMaxListings(EntityPlayer player) { - return ConfigMarket.DefaultMaxListings; - } - - private String formatCurrency(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - private void logEvent(String event, String details) { - LogWriter.info(LOG_PREFIX + event + ": " + details); - } - - private String getListingName(AuctionListing listing) { - if (listing == null || listing.item == null) { - return ""; - } - return listing.item.getDisplayName(); - } - - // ==================== Persistence ==================== - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) return; - - try { - NBTTagCompound compound = new NBTTagCompound(); - compound.setInteger("NextId", nextId); - - NBTTagList listingList = new NBTTagList(); - for (AuctionListing listing : listings.values()) { - listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); - } - compound.setTag("Listings", listingList); - - File file = new File(saveDir, "auctions.dat"); - File backup = new File(saveDir, "auctions.dat_old"); - - if (file.exists()) { - if (backup.exists()) backup.delete(); - file.renameTo(backup); - } - - file = new File(saveDir, "auctions.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - - } catch (Exception e) { - LogWriter.error(LOG_PREFIX + "Error saving auctions", e); - } - } - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) return; - - listings.clear(); - - try { - File file = new File(saveDir, "auctions.dat"); - if (!file.exists()) { - file = new File(saveDir, "auctions.dat_old"); - if (!file.exists()) return; - } - - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - nextId = compound.getInteger("NextId"); - if (nextId < 1) nextId = 1; - - NBTTagList listingList = compound.getTagList("Listings", 10); - for (int i = 0; i < listingList.tagCount(); i++) { - AuctionListing listing = new AuctionListing(); - listing.readFromNBT(listingList.getCompoundTagAt(i)); - listings.put(listing.id, listing); - } - - rebuildIndices(); - - logEvent("SYSTEM_STARTUP", String.format("Loaded %d listings, %d active", - listings.size(), getActiveListingCount())); - - } catch (Exception e) { - LogWriter.error(LOG_PREFIX + "Error loading auctions", e); - } - } - - // ==================== Statistics ==================== - - public int getActiveListingCount() { - return (int) listings.values().stream() - .filter(l -> l.status == EnumAuctionStatus.ACTIVE) - .count(); - } - - public int getTotalListingCount() { - return listings.size(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/CurrencyController.java b/src/main/java/noppes/npcs/controllers/CurrencyController.java deleted file mode 100644 index 16d6c99f4..000000000 --- a/src/main/java/noppes/npcs/controllers/CurrencyController.java +++ /dev/null @@ -1,400 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.CompressedStreamTools; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import noppes.npcs.CustomNpcs; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.PlayerData; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.*; - -/** - * Controller for managing currency operations including: - * - Claims system (for auction refunds, NPC payments, etc.) - * - Transaction history - * - Currency administration - * - * Delegates actual balance operations to VaultHelper. - */ -public class CurrencyController { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static CurrencyController Instance; - - // Pending claims per player UUID - claims that can be collected - private Map> pendingClaims = new HashMap<>(); - - // File path for persistence - private String filePath = ""; - - public CurrencyController() { - Instance = this; - load(); - } - - /** - * Get player's current balance - */ - public long getBalance(EntityPlayer player) { - return VaultHelper.Instance.getBalance(player); - } - - /** - * Deposit currency directly to player's balance - */ - public boolean deposit(EntityPlayer player, long amount) { - return VaultHelper.Instance.deposit(player, amount); - } - - /** - * Withdraw currency from player's balance - */ - public boolean withdraw(EntityPlayer player, long amount) { - return VaultHelper.Instance.withdraw(player, amount); - } - - /** - * Check if player can afford an amount - */ - public boolean canAfford(EntityPlayer player, long amount) { - return VaultHelper.Instance.canAfford(player, amount); - } - - /** - * Transfer currency between players - */ - public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { - return VaultHelper.Instance.transfer(from, to, amount); - } - - /** - * Format amount for display - */ - public String formatAmount(long amount) { - return VaultHelper.Instance.formatAmount(amount); - } - - // ==================== CLAIMS SYSTEM ==================== - - /** - * Add a claim for a player (e.g., auction refund, NPC payment) - * Claims can be collected later by the player. - * - * @param playerUUID The player's UUID - * @param amount The amount to claim - * @param source Description of where this claim came from - * @param sourceId Optional ID of the source (e.g., auction ID) - */ - public void addClaim(UUID playerUUID, long amount, String source, String sourceId) { - if (amount <= 0) { - return; - } - - CurrencyClaim claim = new CurrencyClaim(); - claim.amount = amount; - claim.source = source; - claim.sourceId = sourceId; - claim.timestamp = System.currentTimeMillis(); - claim.expiresAt = claim.timestamp + (ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L); - - List claims = pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()); - claims.add(claim); - - save(); - logger.info("Added claim of {} for player {} from {}", amount, playerUUID, source); - } - - /** - * Get all pending claims for a player - */ - public List getPendingClaims(EntityPlayer player) { - return getPendingClaims(player.getUniqueID()); - } - - /** - * Get all pending claims for a player by UUID - */ - public List getPendingClaims(UUID playerUUID) { - List claims = pendingClaims.get(playerUUID); - if (claims == null) { - return new ArrayList<>(); - } - - // Filter out expired claims - long now = System.currentTimeMillis(); - claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); - - return new ArrayList<>(claims); - } - - /** - * Get total pending claim amount for a player - */ - public long getTotalPendingClaims(EntityPlayer player) { - return getTotalPendingClaims(player.getUniqueID()); - } - - /** - * Get total pending claim amount for a player by UUID - */ - public long getTotalPendingClaims(UUID playerUUID) { - List claims = getPendingClaims(playerUUID); - long total = 0; - for (CurrencyClaim claim : claims) { - total += claim.amount; - } - return total; - } - - /** - * Claim all pending claims for a player - * @return Amount successfully claimed - */ - public long claimAll(EntityPlayer player) { - List claims = getPendingClaims(player); - if (claims.isEmpty()) { - return 0; - } - - long totalClaimed = 0; - - for (CurrencyClaim claim : claims) { - if (deposit(player, claim.amount)) { - totalClaimed += claim.amount; - } - } - - // Clear claims after successful deposit - pendingClaims.remove(player.getUniqueID()); - save(); - - return totalClaimed; - } - - /** - * Claim a specific claim by index - * @return true if successful - */ - public boolean claimSingle(EntityPlayer player, int claimIndex) { - List claims = pendingClaims.get(player.getUniqueID()); - if (claims == null || claimIndex < 0 || claimIndex >= claims.size()) { - return false; - } - - CurrencyClaim claim = claims.get(claimIndex); - if (deposit(player, claim.amount)) { - claims.remove(claimIndex); - if (claims.isEmpty()) { - pendingClaims.remove(player.getUniqueID()); - } - save(); - return true; - } - - return false; - } - - /** - * Check if player has any pending claims - */ - public boolean hasPendingClaims(EntityPlayer player) { - return !getPendingClaims(player).isEmpty(); - } - - // ==================== PERSISTENCE ==================== - - public void load() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - filePath = saveDir.getAbsolutePath(); - pendingClaims.clear(); - - try { - File file = new File(saveDir, "currency_claims.dat"); - if (file.exists()) { - loadFromFile(file); - } - } catch (Exception e) { - logger.error("Error loading currency claims", e); - try { - File file = new File(saveDir, "currency_claims.dat_old"); - if (file.exists()) { - loadFromFile(file); - } - } catch (Exception ee) { - logger.error("Error loading backup currency claims", ee); - } - } - - // Clean expired claims - cleanExpiredClaims(); - } - - private void loadFromFile(File file) throws Exception { - FileInputStream fis = new FileInputStream(file); - NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); - fis.close(); - - NBTTagList claimsList = compound.getTagList("Claims", 10); - for (int i = 0; i < claimsList.tagCount(); i++) { - NBTTagCompound playerCompound = claimsList.getCompoundTagAt(i); - UUID playerUUID = UUID.fromString(playerCompound.getString("UUID")); - - List playerClaims = new ArrayList<>(); - NBTTagList playerClaimsList = playerCompound.getTagList("PlayerClaims", 10); - for (int j = 0; j < playerClaimsList.tagCount(); j++) { - NBTTagCompound claimCompound = playerClaimsList.getCompoundTagAt(j); - CurrencyClaim claim = new CurrencyClaim(); - claim.readFromNBT(claimCompound); - playerClaims.add(claim); - } - - if (!playerClaims.isEmpty()) { - pendingClaims.put(playerUUID, playerClaims); - } - } - - logger.info("Loaded {} player claim records", pendingClaims.size()); - } - - public void save() { - File saveDir = CustomNpcs.getWorldSaveDirectory(); - if (saveDir == null) { - return; - } - - try { - File file = new File(saveDir, "currency_claims.dat"); - File backup = new File(saveDir, "currency_claims.dat_old"); - - if (file.exists()) { - if (backup.exists()) { - backup.delete(); - } - file.renameTo(backup); - } - - NBTTagCompound compound = new NBTTagCompound(); - NBTTagList claimsList = new NBTTagList(); - - for (Map.Entry> entry : pendingClaims.entrySet()) { - if (entry.getValue().isEmpty()) { - continue; - } - - NBTTagCompound playerCompound = new NBTTagCompound(); - playerCompound.setString("UUID", entry.getKey().toString()); - - NBTTagList playerClaimsList = new NBTTagList(); - for (CurrencyClaim claim : entry.getValue()) { - NBTTagCompound claimCompound = new NBTTagCompound(); - claim.writeToNBT(claimCompound); - playerClaimsList.appendTag(claimCompound); - } - playerCompound.setTag("PlayerClaims", playerClaimsList); - - claimsList.appendTag(playerCompound); - } - - compound.setTag("Claims", claimsList); - - file = new File(saveDir, "currency_claims.dat"); - FileOutputStream fos = new FileOutputStream(file); - CompressedStreamTools.writeCompressed(compound, fos); - fos.close(); - } catch (Exception e) { - logger.error("Error saving currency claims", e); - } - } - - /** - * Clean up expired claims - */ - public void cleanExpiredClaims() { - long now = System.currentTimeMillis(); - boolean changed = false; - - Iterator>> it = pendingClaims.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry> entry = it.next(); - List claims = entry.getValue(); - - int before = claims.size(); - claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); - - if (claims.size() != before) { - changed = true; - } - - if (claims.isEmpty()) { - it.remove(); - } - } - - if (changed) { - save(); - } - } - - // ==================== CLAIM DATA CLASS ==================== - - public static class CurrencyClaim { - public long amount; - public String source; // e.g., "Auction Refund", "NPC Payment", "Quest Reward" - public String sourceId; // e.g., auction ID - public long timestamp; // When the claim was created - public long expiresAt; // When the claim expires (0 = never) - - public void writeToNBT(NBTTagCompound compound) { - compound.setLong("Amount", amount); - compound.setString("Source", source != null ? source : ""); - compound.setString("SourceId", sourceId != null ? sourceId : ""); - compound.setLong("Timestamp", timestamp); - compound.setLong("ExpiresAt", expiresAt); - } - - public void readFromNBT(NBTTagCompound compound) { - amount = compound.getLong("Amount"); - source = compound.getString("Source"); - sourceId = compound.getString("SourceId"); - timestamp = compound.getLong("Timestamp"); - expiresAt = compound.getLong("ExpiresAt"); - } - - /** - * Get formatted time remaining until expiration - */ - public String getTimeRemaining() { - if (expiresAt <= 0) { - return "Never"; - } - - long remaining = expiresAt - System.currentTimeMillis(); - if (remaining <= 0) { - return "Expired"; - } - - long days = remaining / (24L * 60L * 60L * 1000L); - long hours = (remaining % (24L * 60L * 60L * 1000L)) / (60L * 60L * 1000L); - - if (days > 0) { - return days + " days, " + hours + " hours"; - } else if (hours > 0) { - return hours + " hours"; - } else { - long minutes = remaining / (60L * 1000L); - return minutes + " minutes"; - } - } - } -} diff --git a/src/main/java/noppes/npcs/controllers/MarketRegistry.java b/src/main/java/noppes/npcs/controllers/MarketRegistry.java new file mode 100644 index 000000000..1110f9e5f --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/MarketRegistry.java @@ -0,0 +1,214 @@ +package noppes.npcs.controllers; + +import foxz.utils.Market; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.roles.RoleTrader; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Central registry for linked market synchronization. + * Tracks which RoleTrader instances use which markets, + * and which players are viewing each market. + * + * This enables immediate sync across all traders sharing + * the same market when a purchase occurs. + */ +public class MarketRegistry { + + // marketName -> Set of traders using this market (WeakHashMap for auto-cleanup) + private static final Map> marketTraders = new ConcurrentHashMap<>(); + + // marketName -> Set of players viewing this market (across all NPCs) + private static final Map> marketViewers = new ConcurrentHashMap<>(); + + // ==================== Trader Registration ==================== + + /** + * Register a trader as using a linked market. + * Should be called when NPC loads or marketName is set. + */ + public static void registerTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (traders) { + traders.add(trader); + } + } + + /** + * Unregister a trader from a linked market. + * Should be called when NPC unloads or marketName changes. + */ + public static void unregisterTrader(String marketName, RoleTrader trader) { + if (marketName == null || marketName.isEmpty() || trader == null) return; + + Set traders = marketTraders.get(marketName); + if (traders != null) { + synchronized (traders) { + traders.remove(trader); + if (traders.isEmpty()) { + marketTraders.remove(marketName); + } + } + } + } + + /** + * Change a trader's market registration. + * Handles unregistering from old market and registering to new. + */ + public static void updateTraderMarket(String oldMarket, String newMarket, RoleTrader trader) { + if (trader == null) return; + + if (oldMarket != null && !oldMarket.isEmpty()) { + unregisterTrader(oldMarket, trader); + } + if (newMarket != null && !newMarket.isEmpty()) { + registerTrader(newMarket, trader); + } + } + + // ==================== Viewer Registration ==================== + + /** + * Register a player as viewing a market (via any NPC). + * Should be called when player opens trader GUI with linked market. + */ + public static void registerViewer(String marketName, EntityPlayerMP player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.computeIfAbsent(marketName, + k -> Collections.newSetFromMap(new WeakHashMap<>())); + + synchronized (viewers) { + viewers.add(player); + } + } + + /** + * Unregister a player from viewing a market. + * Should be called when player closes trader GUI. + */ + public static void unregisterViewer(String marketName, EntityPlayerMP player) { + if (marketName == null || marketName.isEmpty() || player == null) return; + + Set viewers = marketViewers.get(marketName); + if (viewers != null) { + synchronized (viewers) { + viewers.remove(player); + if (viewers.isEmpty()) { + marketViewers.remove(marketName); + } + } + } + } + + // ==================== Synchronization ==================== + + /** + * Sync stock data to all traders and viewers of a market. + * Called after a purchase to update all linked traders and their viewers. + * + * @param marketName The market to sync + */ + public static void syncMarket(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + // Get latest data from cache + NBTTagCompound marketData = Market.getMarketCache(marketName); + if (marketData == null) return; + + // Update all traders' internal stock data + Set traders = marketTraders.get(marketName); + if (traders != null) { + NBTTagCompound stockData = marketData.getCompoundTag("Stock"); + + synchronized (traders) { + // Clean up any null references (WeakHashMap cleanup) + traders.removeIf(t -> t == null || t.npc == null); + + for (RoleTrader trader : traders) { + // Update stock data from cache + if (stockData != null && stockData.hasKey("EnableStock")) { + trader.stock.readFromNBT(stockData); + } + } + } + } + + // Sync all viewers (across all NPCs for this market) + syncMarketViewers(marketName); + } + + /** + * Sync stock data to all players viewing a specific market. + * Each viewer gets data relative to their own player (for per-player stock display). + */ + public static void syncMarketViewers(String marketName) { + if (marketName == null || marketName.isEmpty()) return; + + Set viewers = marketViewers.get(marketName); + if (viewers == null || viewers.isEmpty()) return; + + // Find any trader with this market to use for sync (they all have same data now) + Set traders = marketTraders.get(marketName); + if (traders == null || traders.isEmpty()) return; + + RoleTrader syncSource = null; + synchronized (traders) { + for (RoleTrader trader : traders) { + if (trader != null && trader.npc != null) { + syncSource = trader; + break; + } + } + } + + if (syncSource == null) return; + + // Sync to all viewers + synchronized (viewers) { + // Remove disconnected players + viewers.removeIf(p -> p == null || p.playerNetServerHandler == null); + + for (EntityPlayerMP viewer : viewers) { + syncSource.syncToPlayer(viewer); + } + } + } + + // ==================== Utility ==================== + + /** + * Get count of traders registered to a market (for debugging/info). + */ + public static int getTraderCount(String marketName) { + Set traders = marketTraders.get(marketName); + return traders != null ? traders.size() : 0; + } + + /** + * Get count of viewers for a market (for debugging/info). + */ + public static int getViewerCount(String marketName) { + Set viewers = marketViewers.get(marketName); + return viewers != null ? viewers.size() : 0; + } + + /** + * Clear all registrations (for server shutdown/reload). + */ + public static void clear() { + marketTraders.clear(); + marketViewers.clear(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java deleted file mode 100644 index 54b9235ef..000000000 --- a/src/main/java/noppes/npcs/controllers/VaultHelper.java +++ /dev/null @@ -1,272 +0,0 @@ -package noppes.npcs.controllers; - -import net.minecraft.entity.player.EntityPlayer; -import noppes.npcs.CustomNpcs; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.controllers.PlayerDataController; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.lang.reflect.Method; - -/** - * Helper class for VaultAPI integration on Bukkit hybrid servers. - * Uses reflection to integrate with Vault when available. - * Falls back to internal currency system when Vault is not present. - */ -public class VaultHelper { - private static final Logger logger = LogManager.getLogger(CustomNpcs.class); - - public static VaultHelper Instance; - - // Vault reflection references - private boolean vaultAvailable = false; - private Object economy = null; - private Method getBalance; - private Method depositPlayer; - private Method withdrawPlayer; - private Method hasAccount; - private Method createPlayerAccount; - private Method responseSuccess; - - // Bukkit references for getting offline player - private Class bukkitClass; - private Method getOfflinePlayer; - - public VaultHelper() { - Instance = this; - - if (!ConfigMarket.UseVaultIfAvailable) { - logger.info("Vault integration disabled by config"); - return; - } - - try { - // Check for Bukkit first - bukkitClass = Class.forName("org.bukkit.Bukkit"); - getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); - - // Check for Vault - Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); - - // Get the Economy service from Bukkit's ServicesManager - Method getServicesManager = bukkitClass.getMethod("getServicesManager"); - Object servicesManager = getServicesManager.invoke(null); - - Method getRegistration = servicesManager.getClass().getMethod("getRegistration", Class.class); - Object registration = getRegistration.invoke(servicesManager, vaultClass); - - if (registration != null) { - Method getProvider = registration.getClass().getMethod("getProvider"); - economy = getProvider.invoke(registration); - - if (economy != null) { - // Get Economy methods - using OfflinePlayer versions for broader compatibility - Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); - getBalance = economy.getClass().getMethod("getBalance", offlinePlayerClass); - depositPlayer = economy.getClass().getMethod("depositPlayer", offlinePlayerClass, double.class); - withdrawPlayer = economy.getClass().getMethod("withdrawPlayer", offlinePlayerClass, double.class); - hasAccount = economy.getClass().getMethod("hasAccount", offlinePlayerClass); - createPlayerAccount = economy.getClass().getMethod("createPlayerAccount", offlinePlayerClass); - - // EconomyResponse.transactionSuccess() method - Class economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); - responseSuccess = economyResponseClass.getField("transactionSuccess").getClass().getMethod("booleanValue"); - - vaultAvailable = true; - logger.info("Vault Economy integration enabled - using external economy"); - } - } - - if (!vaultAvailable) { - logger.info("Vault found but no Economy provider registered - using internal currency"); - } - } catch (ClassNotFoundException e) { - // Vault not present - use internal currency - logger.info("Vault not found - using internal currency system"); - } catch (Exception e) { - logger.warn("Error initializing Vault integration - using internal currency: " + e.getMessage()); - } - } - - /** - * Check if Vault economy is available and enabled - */ - public boolean isVaultEnabled() { - return vaultAvailable && ConfigMarket.UseVaultIfAvailable; - } - - /** - * Get player's balance - * Uses Vault if available, otherwise internal currency - */ - public long getBalance(EntityPlayer player) { - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - Double balance = (Double) getBalance.invoke(economy, offlinePlayer); - return balance.longValue(); - } catch (Exception e) { - logger.warn("Vault getBalance failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - return data.currencyData.getBalance(); - } - - /** - * Deposit currency to player's balance - * Uses Vault if available, otherwise internal currency - * @return true if successful - */ - public boolean deposit(EntityPlayer player, long amount) { - if (amount <= 0) { - return false; - } - - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - - // Ensure account exists - Boolean hasAcc = (Boolean) hasAccount.invoke(economy, offlinePlayer); - if (!hasAcc) { - createPlayerAccount.invoke(economy, offlinePlayer); - } - - Object response = depositPlayer.invoke(economy, offlinePlayer, (double) amount); - // Check if transaction succeeded by checking the transactionSuccess field - Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); - - if (success) { - // Also update internal tracking for statistics - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(getBalance(player)); - return true; - } - return false; - } catch (Exception e) { - logger.warn("Vault deposit failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - boolean success = data.currencyData.deposit(amount); - if (success) { - data.save(); - } - return success; - } - - /** - * Withdraw currency from player's balance - * Uses Vault if available, otherwise internal currency - * @return true if successful - */ - public boolean withdraw(EntityPlayer player, long amount) { - if (amount <= 0) { - return false; - } - - if (isVaultEnabled()) { - try { - Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); - - // Check if player has enough - Double currentBalance = (Double) getBalance.invoke(economy, offlinePlayer); - if (currentBalance < amount) { - return false; - } - - Object response = withdrawPlayer.invoke(economy, offlinePlayer, (double) amount); - Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); - - if (success) { - // Also update internal tracking for statistics - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(getBalance(player)); - return true; - } - return false; - } catch (Exception e) { - logger.warn("Vault withdraw failed, falling back to internal: " + e.getMessage()); - } - } - - // Fall back to internal currency - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - boolean success = data.currencyData.withdraw(amount); - if (success) { - data.save(); - } - return success; - } - - /** - * Check if player can afford an amount - */ - public boolean canAfford(EntityPlayer player, long amount) { - return getBalance(player) >= amount; - } - - /** - * Transfer currency from one player to another - * @return true if successful - */ - public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { - if (amount <= 0) { - return false; - } - - if (!canAfford(from, amount)) { - return false; - } - - // Withdraw from sender first - if (!withdraw(from, amount)) { - return false; - } - - // Deposit to receiver - if (!deposit(to, amount)) { - // Rollback - deposit back to sender - deposit(from, amount); - return false; - } - - return true; - } - - /** - * Set player's balance directly (admin operation) - * Only works with internal currency - Vault manages its own balances - */ - public void setBalance(EntityPlayer player, long amount) { - if (isVaultEnabled()) { - logger.warn("Cannot set balance directly when using Vault - use Vault's economy commands"); - return; - } - - PlayerData data = PlayerDataController.Instance.getPlayerData(player); - data.currencyData.setBalance(amount); - data.save(); - } - - /** - * Format an amount for display with currency name - */ - public String formatAmount(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - /** - * Get the currency name - */ - public String getCurrencyName() { - return ConfigMarket.CurrencyName; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java deleted file mode 100644 index 176896fae..000000000 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ /dev/null @@ -1,226 +0,0 @@ -package noppes.npcs.controllers.data; - -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.constants.EnumAuctionSort; - -/** - * Filter/search criteria for auction listings. - * Used to query the auction controller with specific criteria. - */ -public class AuctionFilter { - - /** Search text to match against item names */ - public String searchText = ""; - - /** How to sort results */ - public EnumAuctionSort sortOrder = EnumAuctionSort.ENDING_SOON; - - /** Minimum current bid/price filter */ - public long minPrice = 0; - - /** Maximum current bid/price filter (0 = no max) */ - public long maxPrice = 0; - - /** Only show listings with buyout option */ - public boolean buyoutOnly = false; - - /** Only show listings without any bids yet */ - public boolean noBidsOnly = false; - - /** Page number for pagination (0-indexed) */ - public int page = 0; - - /** Items per page */ - public int pageSize = 20; - - /** Filter by specific seller UUID string (empty = all sellers) */ - public String sellerUUID = ""; - - /** Filter by specific seller name (partial match, empty = all) */ - public String sellerName = ""; - - public AuctionFilter() { - } - - /** - * Create a filter with search text - */ - public static AuctionFilter search(String text) { - AuctionFilter filter = new AuctionFilter(); - filter.searchText = text != null ? text : ""; - return filter; - } - - /** - * Create a filter for a specific seller - */ - public static AuctionFilter seller(String sellerUUID) { - AuctionFilter filter = new AuctionFilter(); - filter.sellerUUID = sellerUUID != null ? sellerUUID : ""; - return filter; - } - - // Builder-style methods for chaining - - public AuctionFilter withSearch(String text) { - this.searchText = text != null ? text : ""; - return this; - } - - public AuctionFilter withSort(EnumAuctionSort sort) { - this.sortOrder = sort; - return this; - } - - public AuctionFilter withPriceRange(long min, long max) { - this.minPrice = min; - this.maxPrice = max; - return this; - } - - public AuctionFilter withBuyoutOnly(boolean buyoutOnly) { - this.buyoutOnly = buyoutOnly; - return this; - } - - public AuctionFilter withNoBidsOnly(boolean noBidsOnly) { - this.noBidsOnly = noBidsOnly; - return this; - } - - public AuctionFilter withPage(int page, int pageSize) { - this.page = Math.max(0, page); - this.pageSize = Math.max(1, Math.min(100, pageSize)); - return this; - } - - public AuctionFilter withSeller(String sellerUUID, String sellerName) { - this.sellerUUID = sellerUUID != null ? sellerUUID : ""; - this.sellerName = sellerName != null ? sellerName : ""; - return this; - } - - /** - * Check if a listing matches this filter's criteria - */ - public boolean matches(AuctionListing listing) { - if (listing == null) { - return false; - } - if (listing.item == null) { - return false; - } - - // Search text filter (case-insensitive) - if (!searchText.isEmpty()) { - String lowerSearch = searchText.toLowerCase(); - String itemName = listing.item.getDisplayName().toLowerCase(); - if (!itemName.contains(lowerSearch)) { - return false; - } - } - - // Price range filter (uses current bid or starting price) - long price = listing.currentBid > 0 ? listing.currentBid : listing.startingPrice; - if (minPrice > 0 && price < minPrice) { - return false; - } - if (maxPrice > 0 && price > maxPrice) { - return false; - } - - // Buyout only filter - if (buyoutOnly && listing.buyoutPrice <= 0) { - return false; - } - - // No bids only filter - if (noBidsOnly && listing.hasBids()) { - return false; - } - - // Seller UUID filter - if (!sellerUUID.isEmpty() && !listing.sellerUUID.toString().equals(sellerUUID)) { - return false; - } - - // Seller name filter (partial match) - if (!sellerName.isEmpty()) { - String lowerName = sellerName.toLowerCase(); - if (!listing.sellerName.toLowerCase().contains(lowerName)) { - return false; - } - } - - return true; - } - - /** - * Reset all filters to defaults - */ - public void reset() { - searchText = ""; - sortOrder = EnumAuctionSort.ENDING_SOON; - minPrice = 0; - maxPrice = 0; - buyoutOnly = false; - noBidsOnly = false; - page = 0; - sellerUUID = ""; - sellerName = ""; - } - - /** - * Write filter to NBT for network transmission - */ - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("SearchText", searchText); - compound.setInteger("SortOrder", sortOrder.ordinal()); - compound.setLong("MinPrice", minPrice); - compound.setLong("MaxPrice", maxPrice); - compound.setBoolean("BuyoutOnly", buyoutOnly); - compound.setBoolean("NoBidsOnly", noBidsOnly); - compound.setInteger("Page", page); - compound.setInteger("PageSize", pageSize); - compound.setString("SellerUUID", sellerUUID); - compound.setString("SellerName", sellerName); - return compound; - } - - /** - * Read filter from NBT - */ - public void readFromNBT(NBTTagCompound compound) { - searchText = compound.getString("SearchText"); - - int sortOrdinal = compound.getInteger("SortOrder"); - if (sortOrdinal >= 0 && sortOrdinal < EnumAuctionSort.values().length) { - sortOrder = EnumAuctionSort.values()[sortOrdinal]; - } - - minPrice = compound.getLong("MinPrice"); - maxPrice = compound.getLong("MaxPrice"); - buyoutOnly = compound.getBoolean("BuyoutOnly"); - noBidsOnly = compound.getBoolean("NoBidsOnly"); - page = Math.max(0, compound.getInteger("Page")); - pageSize = compound.getInteger("PageSize"); - if (pageSize <= 0) { - pageSize = 20; - } else { - pageSize = Math.min(100, pageSize); - } - sellerUUID = compound.getString("SellerUUID"); - sellerName = compound.getString("SellerName"); - } - - @Override - public String toString() { - return "AuctionFilter{" + - "search='" + searchText + '\'' + - ", sort=" + sortOrder + - ", price=" + minPrice + "-" + maxPrice + - ", buyoutOnly=" + buyoutOnly + - ", page=" + page + - '}'; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java deleted file mode 100644 index 5366e367b..000000000 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ /dev/null @@ -1,397 +0,0 @@ -package noppes.npcs.controllers.data; - -import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumAuctionDuration; -import noppes.npcs.constants.EnumAuctionStatus; -import noppes.npcs.controllers.AuctionBlacklist; - -import java.util.UUID; - -/** - * Represents a single auction listing. - * Contains all data about the item, seller, bidding, and timing. - */ -public class AuctionListing { - // Unique identifier - public int id; - - // Seller info - public UUID sellerUUID; - public String sellerName; - - // Item being sold - public ItemStack item; - - // Pricing - public long startingPrice; - public long buyoutPrice; // 0 = no buyout option - public long currentBid; - - // Current high bidder - public UUID highBidderUUID; - public String highBidderName; - - // Previous bidder (for refund notifications) - public UUID previousBidderUUID; - public String previousBidderName; - public long previousBidAmount; - - // Timing - public long listingTime; // When the auction was created - public long expirationTime; // When the auction ends - public EnumAuctionDuration duration; - - // Status - public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; - - // Claim tracking - public boolean sellerClaimed = false; // Has seller claimed their proceeds? - public boolean buyerClaimed = false; // Has buyer claimed their item? - - // Bid tracking - public int bidCount = 0; // Number of bids placed - - public AuctionListing() { - } - - /** - * Alias for listingTime (when the auction was created) - */ - public long getCreationTime() { - return listingTime; - } - - // For sorting compatibility - public long creationTime() { - return listingTime; - } - - /** - * Create a new auction listing - */ - public AuctionListing(UUID seller, String sellerName, ItemStack item, - long startPrice, long buyout, EnumAuctionDuration duration) { - this.sellerUUID = seller; - this.sellerName = sellerName; - this.item = item.copy(); - this.startingPrice = startPrice; - this.buyoutPrice = buyout; - this.currentBid = 0; // No bids yet - this.duration = duration; - this.listingTime = System.currentTimeMillis(); - this.expirationTime = listingTime + duration.getMillis(); - this.status = EnumAuctionStatus.ACTIVE; - } - - // ==================== Status Checks ==================== - - /** - * Check if auction has expired - */ - public boolean isExpired() { - return System.currentTimeMillis() >= expirationTime; - } - - /** - * Check if auction is still active - */ - public boolean isActive() { - return status == EnumAuctionStatus.ACTIVE && !isExpired(); - } - - /** - * Get time remaining in milliseconds - */ - public long getTimeRemaining() { - return Math.max(0, expirationTime - System.currentTimeMillis()); - } - - /** - * Get formatted time remaining - */ - public String getTimeRemainingFormatted() { - long remaining = getTimeRemaining(); - - if (remaining <= 0) { - return "Ended"; - } - - long hours = remaining / (60L * 60L * 1000L); - long minutes = (remaining % (60L * 60L * 1000L)) / (60L * 1000L); - - if (hours > 24) { - long days = hours / 24; - hours = hours % 24; - return days + "d " + hours + "h"; - } else if (hours > 0) { - return hours + "h " + minutes + "m"; - } else { - long seconds = (remaining % (60L * 1000L)) / 1000L; - return minutes + "m " + seconds + "s"; - } - } - - /** - * Check if auction has any bids - */ - public boolean hasBids() { - return highBidderUUID != null; - } - - // ==================== Bidding Methods ==================== - - /** - * Calculate minimum valid bid amount - */ - public long getMinimumBid() { - if (!hasBids()) { - return startingPrice; - } - // Minimum increment is configured percent or 1, whichever is greater - long minIncrement = Math.max(1, (long) (currentBid * ConfigMarket.MinBidIncrementPercent)); - return currentBid + minIncrement; - } - - /** - * Place a bid - * @return true if bid was successful - */ - public boolean placeBid(UUID bidder, String bidderName, long amount) { - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - if (isExpired()) { - return false; - } - if (amount < getMinimumBid()) { - return false; - } - if (bidder.equals(sellerUUID)) { - return false; // Can't bid on own item - } - - // Store previous bidder for refund notification - if (highBidderUUID != null) { - previousBidderUUID = highBidderUUID; - previousBidderName = highBidderName; - previousBidAmount = currentBid; - } - - // Update high bidder - highBidderUUID = bidder; - highBidderName = bidderName; - currentBid = amount; - bidCount++; - - // Snipe protection - extend if bid placed in final minutes - if (ConfigMarket.SnipeProtectionMinutes > 0) { - long timeRemaining = getTimeRemaining(); - long thresholdMs = ConfigMarket.SnipeProtectionThreshold * 60L * 1000L; - - if (timeRemaining < thresholdMs) { - // Extend TO snipe protection time, not by it - long newExpiration = System.currentTimeMillis() + - (ConfigMarket.SnipeProtectionMinutes * 60L * 1000L); - // Only extend if it would actually extend the auction - if (newExpiration > expirationTime) { - expirationTime = newExpiration; - } - } - } - - return true; - } - - /** - * Execute buyout (instant purchase at buyout price) - * @return true if buyout was successful - */ - public boolean buyout(UUID buyer, String buyerName) { - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - if (isExpired()) { - return false; - } - if (buyoutPrice <= 0) { - return false; // No buyout available - } - if (buyer.equals(sellerUUID)) { - return false; // Can't buy own item - } - - // Store previous bidder for refund - if (highBidderUUID != null) { - previousBidderUUID = highBidderUUID; - previousBidderName = highBidderName; - previousBidAmount = currentBid; - } - - highBidderUUID = buyer; - highBidderName = buyerName; - currentBid = buyoutPrice; - status = EnumAuctionStatus.SOLD; - - return true; - } - - /** - * Cancel listing (seller only, before any bids) - * @return true if cancellation was successful - */ - public boolean cancel(UUID requester) { - if (!requester.equals(sellerUUID)) { - return false; // Only seller can cancel - } - if (hasBids()) { - return false; // Can't cancel with active bids - } - if (status != EnumAuctionStatus.ACTIVE) { - return false; - } - - status = EnumAuctionStatus.CANCELLED; - return true; - } - - /** - * Finalize auction when it expires - * Call this when auction timer runs out - */ - public void finalize() { - if (status != EnumAuctionStatus.ACTIVE) { - return; - } - - if (hasBids()) { - status = EnumAuctionStatus.SOLD; - } else { - status = EnumAuctionStatus.EXPIRED; - } - } - - // ==================== Validation ==================== - - /** - * Validate that the item can be listed - * @return null if valid, error message if invalid - */ - public static String validateItem(ItemStack item) { - if (item == null || item.getItem() == null) { - return "Invalid item"; - } - - // Check trade restrictions (Untradeable, ProfileSlot Bound, Soulbound) - if (!ItemTradeAttribute.canTrade(item)) { - String reason = ItemTradeAttribute.getTradeBlockReason(item); - return reason != null ? reason : "Item cannot be traded"; - } - - // Check blacklist - if (AuctionBlacklist.Instance != null && AuctionBlacklist.Instance.isBlacklisted(item)) { - return "Item is blacklisted from auction"; - } - - return null; // Valid - } - - // ==================== Financial Calculations ==================== - - /** - * Get listing fee for this auction - */ - public long getListingFee() { - long flatFee = duration.getListingFee(); - long percentFee = (long) (startingPrice * ConfigMarket.ListingFeePercent); - return flatFee + percentFee; - } - - /** - * Get seller's proceeds after sales tax - */ - public long getSellerProceeds() { - if (status != EnumAuctionStatus.SOLD) { - return 0; - } - long tax = (long) (currentBid * ConfigMarket.SalesTaxPercent); - return currentBid - tax; - } - - // ==================== NBT Serialization ==================== - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("Id", id); - compound.setString("SellerUUID", sellerUUID.toString()); - compound.setString("SellerName", sellerName); - compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); - compound.setLong("StartPrice", startingPrice); - compound.setLong("BuyoutPrice", buyoutPrice); - compound.setLong("CurrentBid", currentBid); - - if (highBidderUUID != null) { - compound.setString("BidderUUID", highBidderUUID.toString()); - compound.setString("BidderName", highBidderName); - } - - if (previousBidderUUID != null) { - compound.setString("PrevBidderUUID", previousBidderUUID.toString()); - compound.setString("PrevBidderName", previousBidderName); - compound.setLong("PrevBidAmount", previousBidAmount); - } - - compound.setLong("ListingTime", listingTime); - compound.setLong("ExpirationTime", expirationTime); - compound.setInteger("Duration", duration.ordinal()); - compound.setInteger("Status", status.ordinal()); - compound.setBoolean("SellerClaimed", sellerClaimed); - compound.setBoolean("BuyerClaimed", buyerClaimed); - compound.setInteger("BidCount", bidCount); - - return compound; - } - - public void readFromNBT(NBTTagCompound compound) { - id = compound.getInteger("Id"); - sellerUUID = UUID.fromString(compound.getString("SellerUUID")); - sellerName = compound.getString("SellerName"); - item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); - startingPrice = compound.getLong("StartPrice"); - buyoutPrice = compound.getLong("BuyoutPrice"); - currentBid = compound.getLong("CurrentBid"); - - if (compound.hasKey("BidderUUID")) { - highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); - highBidderName = compound.getString("BidderName"); - } - - if (compound.hasKey("PrevBidderUUID")) { - previousBidderUUID = UUID.fromString(compound.getString("PrevBidderUUID")); - previousBidderName = compound.getString("PrevBidderName"); - previousBidAmount = compound.getLong("PrevBidAmount"); - } - - listingTime = compound.getLong("ListingTime"); - expirationTime = compound.getLong("ExpirationTime"); - - int durationOrdinal = compound.getInteger("Duration"); - if (durationOrdinal >= 0 && durationOrdinal < EnumAuctionDuration.values().length) { - duration = EnumAuctionDuration.values()[durationOrdinal]; - } else { - duration = EnumAuctionDuration.LONG; - } - - int statusOrdinal = compound.getInteger("Status"); - if (statusOrdinal >= 0 && statusOrdinal < EnumAuctionStatus.values().length) { - status = EnumAuctionStatus.values()[statusOrdinal]; - } else { - status = EnumAuctionStatus.ACTIVE; - } - - sellerClaimed = compound.getBoolean("SellerClaimed"); - buyerClaimed = compound.getBoolean("BuyerClaimed"); - bidCount = compound.getInteger("BidCount"); - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java index 511321dbb..1271594ba 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -1,24 +1,49 @@ package noppes.npcs.controllers.data; +import kamkeel.npcs.util.VaultUtil; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IPlayerCurrencyData; import noppes.npcs.config.ConfigMarket; /** * Stores player currency balance. * This data is NOT profile slot bound - all profile slots share the same currency pool. - * When Vault API is available, this serves as a cache/fallback. + * When Vault API is available and UseVault config is enabled, operations delegate to Vault. + * The built-in CNPC+ currency data is always preserved in NBT, but unused when Vault is active. */ -public class PlayerCurrencyData { +public class PlayerCurrencyData implements IPlayerCurrencyData { + private final PlayerData playerData; + + // Built-in CNPC+ currency (always saved, used as fallback) private long balance; private long lifetimeEarned; private long lifetimeSpent; - public PlayerCurrencyData() { + public PlayerCurrencyData(PlayerData playerData) { + this.playerData = playerData; this.balance = ConfigMarket.StartingBalance; this.lifetimeEarned = 0; this.lifetimeSpent = 0; } + /** + * Check if Vault should be used for currency operations. + * Requires both config enabled AND Vault actually available. + */ + private boolean shouldUseVault() { + return ConfigMarket.UseVault && VaultUtil.isEnabled(); + } + + /** + * Get player name for Vault operations + */ + private String getPlayerName() { + if (playerData != null && playerData.player != null) { + return playerData.player.getCommandSenderName(); + } + return playerData != null ? playerData.playername : null; + } + public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("CurrencyBalance")) { this.balance = compound.getLong("CurrencyBalance"); @@ -30,6 +55,7 @@ public class PlayerCurrencyData { } public void writeToNBT(NBTTagCompound compound) { + // Always save built-in currency data (preserved even when using Vault) compound.setLong("CurrencyBalance", this.balance); compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); @@ -38,13 +64,21 @@ public class PlayerCurrencyData { /** * Get the current balance */ + @Override public long getBalance() { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return (long) VaultUtil.getBalance(playerName); + } + } return balance; } /** * Set the balance directly (use with caution) */ + @Override public void setBalance(long balance) { if (balance < 0) { balance = 0; @@ -52,6 +86,14 @@ public class PlayerCurrencyData { if (balance > ConfigMarket.MaxBalance) { balance = ConfigMarket.MaxBalance; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + VaultUtil.setBalance(playerName, balance); + return; + } + } this.balance = balance; } @@ -60,10 +102,24 @@ public class PlayerCurrencyData { * @param amount Amount to add * @return true if successful, false if would exceed max balance */ + @Override public boolean deposit(long amount) { if (amount <= 0) { return false; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.addMoney(playerName, amount); + if (success) { + this.lifetimeEarned += amount; + } + return success; + } + } + + // Built-in currency long newBalance = this.balance + amount; if (newBalance > ConfigMarket.MaxBalance) { return false; @@ -82,10 +138,24 @@ public class PlayerCurrencyData { * @param amount Amount to remove * @return true if successful, false if insufficient funds */ + @Override public boolean withdraw(long amount) { if (amount <= 0) { return false; } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.withdrawMoney(playerName, amount); + if (success) { + this.lifetimeSpent += amount; + } + return success; + } + } + + // Built-in currency if (this.balance < amount) { return false; } @@ -97,13 +167,21 @@ public class PlayerCurrencyData { /** * Check if player can afford an amount */ + @Override public boolean canAfford(long amount) { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return VaultUtil.has(playerName, amount); + } + } return this.balance >= amount; } /** * Get lifetime earned currency */ + @Override public long getLifetimeEarned() { return lifetimeEarned; } @@ -111,6 +189,7 @@ public class PlayerCurrencyData { /** * Get lifetime spent currency */ + @Override public long getLifetimeSpent() { return lifetimeSpent; } @@ -118,7 +197,11 @@ public class PlayerCurrencyData { /** * Format the balance for display */ + @Override public String formatBalance() { + if (shouldUseVault()) { + return VaultUtil.format(getBalance()); + } return formatAmount(this.balance); } @@ -128,4 +211,20 @@ public class PlayerCurrencyData { public static String formatAmount(long amount) { return String.format("%,d %s", amount, ConfigMarket.CurrencyName); } + + /** + * Check if Vault is being used for currency operations. + * Returns true only if config is enabled AND Vault is actually available. + */ + @Override + public boolean isUsingVault() { + return shouldUseVault(); + } + + /** + * Check if Vault is configured (regardless of availability) + */ + public boolean isVaultConfigured() { + return ConfigMarket.UseVault; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 08fdebdd0..d2a949074 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -17,6 +17,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; +import noppes.npcs.api.handler.IPlayerCurrencyData; import noppes.npcs.api.handler.IPlayerData; import noppes.npcs.api.handler.IPlayerDialogData; import noppes.npcs.api.handler.IPlayerFactionData; @@ -60,7 +61,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public MagicData magicData = new MagicData(); // Currency data - NOT slot-bound, shared across all profile slots - public PlayerCurrencyData currencyData = new PlayerCurrencyData(); + public PlayerCurrencyData currencyData = new PlayerCurrencyData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -400,6 +401,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return mailData; } + public IPlayerCurrencyData getCurrencyData() { + return currencyData; + } + public synchronized void save() { // Don't Save this is a Modification of a Profile's PlayerData Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid)); @@ -409,7 +414,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); - // Add currency data to main save (NOT slot-bound, preserved across profile changes) + // Currency data is saved at player level, NOT per profile slot currencyData.writeToNBT(compound); final String filename; @@ -449,11 +454,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { data = getNBT(); } - // Load currency BEFORE setNBT so we can preserve it - // Currency is stored in main file but NOT in slot data - if (data.hasKey("CurrencyBalance")) { - currencyData.readFromNBT(data); - } + // Load currency BEFORE setNBT - currency is player-level, NOT per profile slot + currencyData.readFromNBT(data); setNBT(data); } diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index f3c5f4db9..68bc96c53 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -45,7 +45,7 @@ public class TraderStock { * @param playerName The player name (for per-player mode) * @return Available stock count, or Integer.MAX_VALUE if unlimited */ - public int getAvailableStock(int slot, String playerName) { + public synchronized int getAvailableStock(int slot, String playerName) { if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { return Integer.MAX_VALUE; // Unlimited } @@ -75,7 +75,7 @@ public class TraderStock { * Consume stock when purchase is made * @return true if successful, false if insufficient stock */ - public boolean consumeStock(int slot, String playerName, int amount) { + public synchronized boolean consumeStock(int slot, String playerName, int amount) { if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { return true; // Unlimited, always succeeds } @@ -115,7 +115,7 @@ public class TraderStock { * Reset all stock to max values * @param currentTime Current time to record as reset time */ - public void resetStock(long currentTime) { + public synchronized void resetStock(long currentTime) { lastResetTime = currentTime; // Reset per-server stock @@ -123,7 +123,7 @@ public class TraderStock { currentStock[i] = maxStock[i]; } - // Clear per-player stock + // Clear per-player stock (cleanup per-player data on reset) playerStock.clear(); } @@ -217,6 +217,44 @@ public class TraderStock { } } + /** + * Validate and clamp all current stock values to not exceed max stock. + * Should be called when trader GUI opens to ensure consistency. + * @return true if any stock was clamped + */ + public boolean validateStock() { + boolean changed = false; + for (int i = 0; i < 18; i++) { + if (maxStock[i] >= 0) { + // Has a max limit + if (currentStock[i] < 0) { + // Not initialized, set to max + currentStock[i] = maxStock[i]; + changed = true; + } else if (currentStock[i] > maxStock[i]) { + // Exceeds max, clamp down + currentStock[i] = maxStock[i]; + changed = true; + } + } + } + + // Also validate per-player stock + for (PlayerTraderStock pStock : playerStock.values()) { + for (int i = 0; i < 18; i++) { + if (maxStock[i] >= 0) { + int available = pStock.getStock(i, maxStock[i]); + if (available < 0) { + // Over-purchased somehow, reset + pStock.purchasedAmounts[i] = maxStock[i]; + changed = true; + } + } + } + } + return changed; + } + // ==================== NBT Serialization ==================== public NBTTagCompound writeToNBT(NBTTagCompound compound) { @@ -280,7 +318,6 @@ public class TraderStock { public static class PlayerTraderStock { public int[] purchasedAmounts = new int[18]; - public long lastPurchaseTime = 0; public PlayerTraderStock() { for (int i = 0; i < 18; i++) { @@ -297,7 +334,6 @@ public class TraderStock { if (slot < 0 || slot >= 18) return false; if (getStock(slot, maxStock) >= amount) { purchasedAmounts[slot] += amount; - lastPurchaseTime = System.currentTimeMillis(); return true; } return false; @@ -305,7 +341,6 @@ public class TraderStock { public void writeToNBT(NBTTagCompound compound) { compound.setIntArray("Purchased", purchasedAmounts); - compound.setLong("LastPurchase", lastPurchaseTime); } public void readFromNBT(NBTTagCompound compound) { @@ -313,7 +348,6 @@ public class TraderStock { if (loaded != null && loaded.length == 18) { purchasedAmounts = loaded; } - lastPurchaseTime = compound.getLong("LastPurchase"); } } } diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java deleted file mode 100644 index 4d8febaf1..000000000 --- a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java +++ /dev/null @@ -1,135 +0,0 @@ -package noppes.npcs.roles; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NoppesUtilServer; -import noppes.npcs.config.ConfigMarket; -import noppes.npcs.constants.EnumGuiType; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.data.Line; -import noppes.npcs.entity.EntityNPCInterface; - -/** - * NPC role for Auctioneer - provides access to the global auction house. - */ -public class RoleAuctioneer extends RoleInterface { - - // Configuration - public boolean showActiveListings = true; // Show active listings tab - public boolean showMyListings = true; // Show player's listings tab - public boolean showMyBids = true; // Show player's bids tab - public boolean showClaims = true; // Show claimable items tab - public boolean allowCreatingListings = true; // Allow creating new listings - public boolean allowBidding = true; // Allow bidding on items - public boolean allowBuyout = true; // Allow buyout purchases - - // Custom greeting messages - public String welcomeMessage = ""; - public String noAuctionMessage = ""; - - public RoleAuctioneer(EntityNPCInterface npc) { - super(npc); - } - - @Override - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setBoolean("ShowActiveListings", showActiveListings); - compound.setBoolean("ShowMyListings", showMyListings); - compound.setBoolean("ShowMyBids", showMyBids); - compound.setBoolean("ShowClaims", showClaims); - compound.setBoolean("AllowCreatingListings", allowCreatingListings); - compound.setBoolean("AllowBidding", allowBidding); - compound.setBoolean("AllowBuyout", allowBuyout); - compound.setString("WelcomeMessage", welcomeMessage); - compound.setString("NoAuctionMessage", noAuctionMessage); - return compound; - } - - @Override - public void readFromNBT(NBTTagCompound compound) { - showActiveListings = compound.hasKey("ShowActiveListings") ? - compound.getBoolean("ShowActiveListings") : true; - showMyListings = compound.hasKey("ShowMyListings") ? - compound.getBoolean("ShowMyListings") : true; - showMyBids = compound.hasKey("ShowMyBids") ? - compound.getBoolean("ShowMyBids") : true; - showClaims = compound.hasKey("ShowClaims") ? - compound.getBoolean("ShowClaims") : true; - allowCreatingListings = compound.hasKey("AllowCreatingListings") ? - compound.getBoolean("AllowCreatingListings") : true; - allowBidding = compound.hasKey("AllowBidding") ? - compound.getBoolean("AllowBidding") : true; - allowBuyout = compound.hasKey("AllowBuyout") ? - compound.getBoolean("AllowBuyout") : true; - welcomeMessage = compound.getString("WelcomeMessage"); - noAuctionMessage = compound.getString("NoAuctionMessage"); - } - - @Override - public void interact(EntityPlayer player) { - // Check if auction system is enabled - if (!ConfigMarket.AuctionEnabled) { - if (!noAuctionMessage.isEmpty()) { - npc.say(player, new Line(noAuctionMessage)); - } else { - npc.say(player, new Line("The auction house is currently closed.")); - } - return; - } - - // Process expired auctions before opening GUI - if (AuctionController.Instance != null) { - AuctionController.Instance.processExpiredAuctions(); - } - - // Say welcome message - if (!welcomeMessage.isEmpty()) { - npc.say(player, new Line(welcomeMessage)); - } else { - npc.say(player, npc.advanced.getInteractLine()); - } - - // Open auction GUI - NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); - } - - /** - * Check if player has claims waiting - */ - public boolean hasClaimsWaiting(EntityPlayer player) { - if (AuctionController.Instance == null) { - return false; - } - return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); - } - - /** - * Get number of active listings in the auction house - */ - public int getActiveListingCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingCount(); - } - - /** - * Get player's current listing count - */ - public int getPlayerListingCount(EntityPlayer player) { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); - } - - /** - * Get player's maximum allowed listings - */ - public int getPlayerMaxListings(EntityPlayer player) { - if (AuctionController.Instance == null) { - return ConfigMarket.DefaultMaxListings; - } - return AuctionController.Instance.getMaxListings(player); - } -} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 6215eef2f..107076a44 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -1,7 +1,9 @@ package noppes.npcs.roles; import foxz.utils.Market; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -10,10 +12,14 @@ import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.NoppesUtilServer; import noppes.npcs.NpcMiscInventory; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.MarketRegistry; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.TraderStock; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -35,9 +41,11 @@ public class RoleTrader extends RoleInterface { // Stock system public TraderStock stock = new TraderStock(); - // Currency system - public boolean useCurrency = false; // Use virtual currency instead of items - public long[] slotPrices = new long[18]; // Currency prices per slot + // Currency cost per trade slot (ADDITIVE to item costs, 0 = no currency required) + public long[] currencyCost = new long[18]; + + // Local viewer tracking for non-linked traders (per-NPC sync) + private final Set localViewers = new HashSet<>(); public RoleTrader(EntityNPCInterface npc) { super(npc); @@ -72,24 +80,27 @@ public class RoleTrader extends RoleInterface { // Stock system nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); - // Currency system - nbttagcompound.setBoolean("UseCurrency", useCurrency); - NBTTagList priceList = new NBTTagList(); - for (long price : slotPrices) { - NBTTagCompound priceTag = new NBTTagCompound(); - priceTag.setLong("Price", price); - priceList.appendTag(priceTag); + // Currency cost per slot (additive to item costs) + NBTTagList currencyList = new NBTTagList(); + for (long cost : currencyCost) { + NBTTagCompound costTag = new NBTTagCompound(); + costTag.setLong("Cost", cost); + currencyList.appendTag(costTag); } - nbttagcompound.setTag("SlotPrices", priceList); + nbttagcompound.setTag("CurrencyCost", currencyList); return nbttagcompound; } @Override public void readFromNBT(NBTTagCompound nbttagcompound) { + String oldMarket = marketName; marketName = nbttagcompound.getString("TraderMarket"); readNBT(nbttagcompound); + // Register with MarketRegistry for linked market sync + MarketRegistry.updateTraderMarket(oldMarket, marketName, this); + try { Market.getMarket(this, marketName); } catch (Exception ex) { @@ -123,39 +134,50 @@ public class RoleTrader extends RoleInterface { stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); } - // Currency system - useCurrency = nbttagcompound.getBoolean("UseCurrency"); - if (nbttagcompound.hasKey("SlotPrices")) { - NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); - for (int i = 0; i < priceList.tagCount() && i < 18; i++) { - slotPrices[i] = priceList.getCompoundTagAt(i).getLong("Price"); + // Currency cost per slot (additive to item costs) + if (nbttagcompound.hasKey("CurrencyCost")) { + NBTTagList currencyList = nbttagcompound.getTagList("CurrencyCost", 10); + for (int i = 0; i < currencyList.tagCount() && i < 18; i++) { + currencyCost[i] = currencyList.getCompoundTagAt(i).getLong("Cost"); } } } @Override public void interact(EntityPlayer player) { - // Check for stock reset before opening GUI - if (stock.enableStock) { - long currentTime = stock.resetType.isRealTime() - ? System.currentTimeMillis() - : player.worldObj.getTotalWorldTime(); - - if (stock.shouldReset(currentTime)) { - stock.resetStock(currentTime); - // Save to market if using shared market - if (!marketName.isEmpty()) { - Market.save(this, marketName); - } - } - } - npc.say(player, npc.advanced.getInteractLine()); + + // Load from market FIRST to get latest shared data try { Market.getMarket(this, marketName); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); } + + // THEN check for stock reset (after loading latest market data) + if (stock.enableStock) { + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + boolean needsSave = false; + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + needsSave = true; + } + + // Validate stock levels (clamp current to max if exceeded) + if (stock.validateStock()) { + needsSave = true; + } + + // Save to market if using shared market and changes were made + if (needsSave && !marketName.isEmpty()) { + Market.save(this, marketName); + } + } + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); } @@ -213,15 +235,35 @@ public class RoleTrader extends RoleInterface { } /** - * Consume stock after purchase + * Consume stock after purchase. + * For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise). + * + * @param slot The slot purchased from + * @param playerName The player making the purchase + * @param amount Amount to consume + * @return true if stock was consumed successfully */ public boolean consumeStock(int slot, String playerName, int amount) { if (!stock.enableStock) { return true; // Stock system disabled } boolean consumed = stock.consumeStock(slot, playerName, amount); - if (consumed && !marketName.isEmpty()) { - Market.save(this, marketName); + if (consumed) { + // Save to market if linked + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + + // Sync viewers for shared stock (not per-player) + if (!stock.perPlayer) { + if (!marketName.isEmpty()) { + // Linked market: sync all traders and viewers market-wide + MarketRegistry.syncMarket(marketName); + } else { + // No linked market: sync only this NPC's viewers + syncLocalViewers(); + } + } } return consumed; } @@ -239,29 +281,175 @@ public class RoleTrader extends RoleInterface { return stock.getTimeUntilResetFormatted(currentTime); } - // ==================== Currency System Methods ==================== - /** - * Get the currency price for a slot + * Get time until stock reset in real-time milliseconds. + * Converts MC ticks to approximate real-time for client countdown display. + * @return Remaining time in milliseconds, or -1 if no reset scheduled */ - public long getSlotPrice(int slot) { - if (slot < 0 || slot >= 18) return 0; - return slotPrices[slot]; + public long getResetTimeRemainingMillis() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return -1; + } + + long currentTime; + long remaining; + + if (stock.resetType.isRealTime()) { + // Real time - already in milliseconds + currentTime = System.currentTimeMillis(); + remaining = stock.getTimeUntilReset(currentTime); + } else { + // MC time - convert ticks to real milliseconds (1 tick = 50ms) + currentTime = (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + long remainingTicks = stock.getTimeUntilReset(currentTime); + remaining = remainingTicks * 50L; + } + + return remaining; } /** - * Set the currency price for a slot + * Reset the stock cooldown timer (for admin use) */ - public void setSlotPrice(int slot, long price) { + public void resetCooldown() { + stock.lastResetTime = 0; + } + + // ==================== Currency Cost Methods ==================== + + /** + * Get the currency cost for a slot (additive to item costs) + */ + public long getCurrencyCost(int slot) { + if (slot < 0 || slot >= 18) return 0; + return currencyCost[slot]; + } + + /** + * Set the currency cost for a slot (additive to item costs) + */ + public void setCurrencyCost(int slot, long cost) { if (slot >= 0 && slot < 18) { - slotPrices[slot] = Math.max(0, price); + currencyCost[slot] = Math.max(0, cost); } } /** - * Check if a slot has a currency price set + * Check if a slot has a currency cost (greater than 0) */ - public boolean hasSlotPrice(int slot) { - return useCurrency && slot >= 0 && slot < 18 && slotPrices[slot] > 0; + public boolean hasCurrencyCost(int slot) { + return slot >= 0 && slot < 18 && currencyCost[slot] > 0; + } + + // ==================== Viewer Tracking Methods ==================== + + /** + * Register a player as viewing this trader's GUI. + * - For linked markets: uses MarketRegistry (market-wide sync) + * - For normal traders: uses local viewer set (per-NPC sync) + */ + public void registerViewer(EntityPlayerMP player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + // Linked market: use MarketRegistry for market-wide tracking + MarketRegistry.registerViewer(marketName, player); + } else { + // No linked market: use local viewer set + localViewers.add(player); + } + } + + /** + * Unregister a player from viewing this trader's GUI. + * - For linked markets: uses MarketRegistry + * - For normal traders: uses local viewer set + */ + public void unregisterViewer(EntityPlayerMP player) { + if (player == null) return; + + if (!marketName.isEmpty()) { + MarketRegistry.unregisterViewer(marketName, player); + } else { + localViewers.remove(player); + } + } + + /** + * Sync trader data to all players viewing this trader. + * - For linked markets: syncs ALL viewers across ALL NPCs sharing the market + * - For normal traders: syncs only viewers of THIS specific NPC + */ + public void syncAllViewers() { + if (!marketName.isEmpty()) { + // Linked market: sync all viewers across all NPCs with this market + MarketRegistry.syncMarketViewers(marketName); + } else { + // No linked market: sync only local viewers + syncLocalViewers(); + } + } + + /** + * Sync to all players viewing this specific NPC (for non-linked traders). + */ + private void syncLocalViewers() { + // Remove disconnected players + localViewers.removeIf(player -> player.playerNetServerHandler == null); + + for (EntityPlayerMP viewer : localViewers) { + syncToPlayer(viewer); + } + } + + /** + * Called when this trader is unloaded (NPC death, chunk unload, etc.) + * Cleans up MarketRegistry registration. + */ + public void onUnload() { + if (!marketName.isEmpty()) { + MarketRegistry.unregisterTrader(marketName, this); + } + localViewers.clear(); + } + + /** + * Send trader data to a specific player + */ + public void syncToPlayer(EntityPlayerMP player) { + if (player == null || player.playerNetServerHandler == null) { + return; + } + + NBTTagCompound compound = new NBTTagCompound(); + + // Player balance + PlayerData data = PlayerData.get(player); + compound.setLong("Balance", data.currencyData.getBalance()); + + // Stock info + compound.setBoolean("StockEnabled", stock.enableStock); + compound.setString("ResetTime", getTimeUntilResetFormatted()); + + // Available stock per slot (player-specific if perPlayer mode) + String playerName = player.getCommandSenderName(); + int[] stockArr = new int[18]; + for (int i = 0; i < 18; i++) { + stockArr[i] = getAvailableStock(i, playerName); + } + compound.setIntArray("Stock", stockArr); + + // Currency cost per slot + for (int i = 0; i < 18; i++) { + compound.setLong("Cost" + i, getCurrencyCost(i)); + } + + GuiDataPacket.sendGuiData(player, compound); + } + + @Override + public void delete() { + onUnload(); + super.delete(); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 9fc547260..85a15041e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,14 +49,11 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.PlayerQuestController; import noppes.npcs.controllers.QuestController; -import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.data.Dialog; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -899,185 +896,42 @@ public class ScriptPlayer extends ScriptLivingBase return (IPlayer[]) list.toArray(new IPlayer[list.size()]); } - // ==================== CURRENCY METHODS ==================== + // ========================================= + // Currency Methods + // ========================================= - /** - * Get the player's current currency balance. - * Uses Vault if available, otherwise uses internal currency system. - * @return The player's currency balance - * @since 1.11 - */ + @Override public long getCurrencyBalance() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.getBalance(player); + return getData().currencyData.getBalance(); } - /** - * Add currency to the player's balance. - * @param amount The amount to add (must be positive) - * @return true if successful, false if failed (e.g., would exceed max balance) - * @since 1.11 - */ - public boolean addCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.deposit(player, amount); + @Override + public void setCurrencyBalance(long amount) { + getData().currencyData.setBalance(amount); } - /** - * Remove currency from the player's balance. - * @param amount The amount to remove (must be positive) - * @return true if successful, false if insufficient funds - * @since 1.11 - */ - public boolean removeCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.withdraw(player, amount); + @Override + public boolean depositCurrency(long amount) { + return getData().currencyData.deposit(amount); } - /** - * Check if the player can afford a specific amount. - * @param amount The amount to check - * @return true if player has sufficient balance - * @since 1.11 - */ + @Override + public boolean withdrawCurrency(long amount) { + return getData().currencyData.withdraw(amount); + } + + @Override public boolean canAffordCurrency(long amount) { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.canAfford(player, amount); + return getData().currencyData.canAfford(amount); } - /** - * Get the configured currency name (e.g., "Gold"). - * @return The currency name - * @since 1.11 - */ - public String getCurrencyName() { - if (VaultHelper.Instance == null) { - return "Currency"; - } - return VaultHelper.Instance.getCurrencyName(); + @Override + public boolean isUsingVaultCurrency() { + return getData().currencyData.isUsingVault(); } - /** - * Format a currency amount for display (e.g., "1,000 Gold"). - * @param amount The amount to format - * @return Formatted string - * @since 1.11 - */ - public String formatCurrency(long amount) { - if (CurrencyController.Instance == null) { - return String.valueOf(amount); - } - return CurrencyController.Instance.formatAmount(amount); - } - - /** - * Check if the player has pending currency claims. - * Claims are created by auction refunds, NPC payments, etc. - * @return true if there are pending claims - * @since 1.11 - */ - public boolean hasPendingClaims() { - if (CurrencyController.Instance == null) { - return false; - } - return CurrencyController.Instance.hasPendingClaims(player); - } - - /** - * Get the total amount of pending currency claims. - * @return Total pending claim amount - * @since 1.11 - */ - public long getPendingClaimsTotal() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.getTotalPendingClaims(player); - } - - /** - * Claim all pending currency claims and add them to balance. - * @return The total amount claimed - * @since 1.11 - */ - public long claimAllCurrency() { - if (CurrencyController.Instance == null) { - return 0; - } - return CurrencyController.Instance.claimAll(player); - } - - // ==================== AUCTION METHODS ==================== - - /** - * Get the number of active auction listings the player has. - * @return Number of active listings - * @since 1.11 - */ - public int getAuctionListingCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getActiveListingsCount(player.getUniqueID()); - } - - /** - * Get the maximum number of auction listings the player is allowed. - * @return Maximum listing count - * @since 1.11 - */ - public int getMaxAuctionListings() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getMaxListings(player); - } - - /** - * Check if the player has auction items waiting to be claimed. - * This includes won items, expired listings, and sale proceeds. - * @return true if there are claimable items - * @since 1.11 - */ - public boolean hasAuctionClaims() { - if (AuctionController.Instance == null) { - return false; - } - return !AuctionController.Instance.getClaimableListings(player.getUniqueID()).isEmpty(); - } - - /** - * Get the number of auction items waiting to be claimed. - * @return Number of claimable items/listings - * @since 1.11 - */ - public int getAuctionClaimCount() { - if (AuctionController.Instance == null) { - return 0; - } - return AuctionController.Instance.getClaimableListings(player.getUniqueID()).size(); - } - - /** - * Get the number of auctions where the player is the current high bidder. - * @return Number of active bids - * @since 1.11 - */ - public int getActiveAuctionBidCount() { - if (AuctionController.Instance == null) { - return 0; - } - return (int) AuctionController.Instance.getListingsAsBidder(player.getUniqueID()) - .stream() - .filter(l -> l.isActive()) - .count(); + @Override + public String getFormattedCurrencyBalance() { + return getData().currencyData.formatBalance(); } } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java index a2588059f..6736369b9 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptItemStack.java @@ -1,7 +1,6 @@ package noppes.npcs.scripted.item; import com.google.common.collect.Multimap; -import kamkeel.npcs.controllers.data.attribute.ItemTradeAttribute; import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker; import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry; import kamkeel.npcs.util.AttributeItemUtil; @@ -593,117 +592,4 @@ public class ScriptItemStack implements IItemStack { Set keys = reqTag.func_150296_c(); return keys.toArray(new String[keys.size()]); } - - // ==================== Trade Restriction Methods ==================== - - /** - * Check if this item can be traded or auctioned. - * An item cannot be traded if it is: - * - Marked as untradeable - * - Bound to a profile slot - * - Soulbound to a player - * @return true if the item can be traded - * @since 1.11 - */ - public boolean canTrade() { - return ItemTradeAttribute.canTrade(item); - } - - /** - * Get the reason why this item cannot be traded. - * @return Localization key for the block reason, or null if tradeable - * @since 1.11 - */ - public String getTradeBlockReason() { - return ItemTradeAttribute.getTradeBlockReason(item); - } - - /** - * Check if this item is marked as untradeable - * @return true if untradeable - * @since 1.11 - */ - public boolean isUntradeable() { - return ItemTradeAttribute.isUntradeable(item); - } - - /** - * Set whether this item is untradeable - * @param untradeable true to mark as untradeable - * @since 1.11 - */ - public void setUntradeable(boolean untradeable) { - ItemTradeAttribute.setUntradeable(item, untradeable); - } - - /** - * Check if this item is bound to a profile slot - * @return true if profile slot bound - * @since 1.11 - */ - public boolean isProfileSlotBound() { - return ItemTradeAttribute.isProfileSlotBound(item); - } - - /** - * Get the profile slot this item is bound to - * @return Profile slot ID, or -1 if not bound - * @since 1.11 - */ - public int getProfileSlot() { - return ItemTradeAttribute.getProfileSlot(item); - } - - /** - * Bind this item to a specific profile slot - * @param slot The profile slot ID, or -1 to unbind - * @since 1.11 - */ - public void setProfileSlotBound(int slot) { - ItemTradeAttribute.setProfileSlotBound(item, slot); - } - - /** - * Check if this item is soulbound to any player - * @return true if soulbound - * @since 1.11 - */ - public boolean isSoulbound() { - return ItemTradeAttribute.isSoulbound(item); - } - - /** - * Get the UUID of the player this item is soulbound to - * @return UUID string, or null if not soulbound - * @since 1.11 - */ - public String getSoulbindOwner() { - return ItemTradeAttribute.getSoulbindOwner(item); - } - - /** - * Remove the soulbind from this item - * @since 1.11 - */ - public void removeSoulbind() { - ItemTradeAttribute.removeSoulbind(item); - } - - /** - * Clear all trade restrictions from this item - * (untradeable, profile slot bound, soulbound) - * @since 1.11 - */ - public void clearTradeRestrictions() { - ItemTradeAttribute.clearAllRestrictions(item); - } - - /** - * Get a summary of all trade restrictions on this item - * @return Human-readable string of restrictions, or empty if none - * @since 1.11 - */ - public String getRestrictionSummary() { - return ItemTradeAttribute.getRestrictionSummary(item); - } } diff --git a/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java b/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java index 825c2e46b..3574e529e 100644 --- a/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java +++ b/src/main/java/noppes/npcs/scripted/roles/ScriptRoleTrader.java @@ -42,7 +42,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader public IItemStack getSellOption(int slot) { if (slot >= 18 || slot < 0) return null; if (role.inventorySold.items.get(slot) == null) return null; - return NpcAPI.Instance().getIItemStack(role.inventoryCurrency.items.get(slot)); + return NpcAPI.Instance().getIItemStack(role.inventorySold.items.get(slot)); } public IItemStack[] getCurrency(int slot) { @@ -100,7 +100,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader public boolean isSlotEnabled(int slot) { if (slot >= 18 || slot < 0) return false; - return role.disableSlot[slot] > 0; + return role.disableSlot[slot] <= 0; // enabled when NOT disabled } public boolean isSlotEnabled(int slot, IPlayer player) { @@ -128,6 +128,132 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader role.getArrayByName(player.getDisplayName(), role.playerDisableSlot)[slot] = 0; } + // ==================== Stock System ==================== + + public boolean isStockEnabled() { + return role.stock.enableStock; + } + + public void setStockEnabled(boolean enabled) { + role.stock.enableStock = enabled; + } + + public boolean isPerPlayerStock() { + return role.stock.perPlayer; + } + + public void setPerPlayerStock(boolean perPlayer) { + role.stock.perPlayer = perPlayer; + } + + public int getStockResetType() { + return role.stock.resetType.ordinal(); + } + + public void setStockResetType(int type) { + noppes.npcs.constants.EnumStockReset[] values = noppes.npcs.constants.EnumStockReset.values(); + if (type >= 0 && type < values.length) { + role.stock.resetType = values[type]; + } + } + + public long getCustomResetTime() { + return role.stock.customResetTime; + } + + public void setCustomResetTime(long time) { + role.stock.customResetTime = Math.max(0, time); + } + + public int getMaxStock(int slot) { + if (slot < 0 || slot >= 18) return -1; + return role.stock.maxStock[slot]; + } + + public void setMaxStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + role.stock.setMaxStock(slot, amount); + } + } + + public int getAvailableStock(int slot) { + if (slot < 0 || slot >= 18) return 0; + return role.stock.getAvailableStock(slot, ""); + } + + public int getAvailableStock(int slot, IPlayer player) { + if (slot < 0 || slot >= 18) return 0; + return role.stock.getAvailableStock(slot, player.getDisplayName()); + } + + public void resetStock() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + role.stock.resetStock(currentTime); + } + + public void resetCooldown() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + role.stock.lastResetTime = currentTime; + } + + public int getCurrentStock(int slot) { + if (slot < 0 || slot >= 18) return -1; + return role.stock.currentStock[slot]; + } + + public void setCurrentStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + role.stock.currentStock[slot] = amount; + } + } + + public int getPlayerPurchased(int slot, IPlayer player) { + if (slot < 0 || slot >= 18) return 0; + noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock = + role.stock.playerStock.get(player.getDisplayName()); + if (pStock == null) return 0; + return pStock.purchasedAmounts[slot]; + } + + public void setPlayerPurchased(int slot, IPlayer player, int amount) { + if (slot < 0 || slot >= 18) return; + noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock = + role.stock.playerStock.computeIfAbsent( + player.getDisplayName(), + k -> new noppes.npcs.controllers.data.TraderStock.PlayerTraderStock() + ); + pStock.purchasedAmounts[slot] = Math.max(0, amount); + } + + public long getLastResetTime() { + return role.stock.lastResetTime; + } + + public long getTimeUntilReset() { + long currentTime = role.stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + return role.stock.getTimeUntilReset(currentTime); + } + + // ==================== Currency Cost System ==================== + + public long getCurrencyCost(int slot) { + return role.getCurrencyCost(slot); + } + + public void setCurrencyCost(int slot, long cost) { + role.setCurrencyCost(slot, cost); + } + + public boolean hasCurrencyCost(int slot) { + return role.hasCurrencyCost(slot); + } + @Override public int getType() { return RoleType.TRADER; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 7f7314d1c..715ca4884 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -443,9 +443,54 @@ role.auctioneer.messages=Messages role.auctioneer.welcome=Welcome Message role.auctioneer.noAuction=Auction Disabled Message -trader.insufficient=Insufficient Currency -trader.sufficient=Sufficient Currency +trader.unavailable=Unavailable +trader.insufficient.currency=Insufficient Balance +trader.available=Available trader.slotdisabled=Slot Disabled +trader.currencycost=Cost +trader.outofstock=Out of Stock +trader.instock=in stock +trader.restock=Restock +trader.balance=Balance +trader.reopentrader=Reopen Trader + +stock.options=Stock Options +stock.enable=Enable Stock +stock.perplayer=Per-Player +stock.resettype=Reset Type +stock.customtime=Custom Time +stock.defaultmax=Default Max +stock.perslot=Per-Slot Stock +stock.reset=Reset Stock +stock.resetcooldown=Reset Cooldown +stock.perslot.title=Per-Slot Stock Limits +stock.reset.confirm=Reset all stock? +stock.reset.warning=This cannot be undone. +stock.cooldown.confirm=Reset cooldown timer? +stock.cooldown.warning=Stock will reset on next open. + +stock.reset.never=Never +stock.reset.mcdaily=MC Daily +stock.reset.mcweekly=MC Weekly +stock.reset.mccustom=MC Custom +stock.reset.rldaily=Real Daily +stock.reset.rlweekly=Real Weekly +stock.reset.rlcustom=Real Custom + +stock.options.hover=Configure stock limits and reset timers +stock.enable.hover=Enable limited stock for trade slots +stock.perplayer.hover=Track stock separately for each player +stock.resettype.hover=When stock automatically replenishes +stock.customtime.hover=Set a custom reset interval +stock.defaultmax.hover=Default max stock for all slots (-1 = unlimited) +stock.perslot.hover=Set max stock for individual slots +stock.reset.hover=Reset all stock to max values +stock.resetcooldown.hover=Force stock to reset on next trader open + +gui.settings.hover=Configure trading options +gui.ignoreDamage.hover=Ignore item damage when comparing currency +gui.ignoreNBT.hover=Ignore NBT data when comparing currency +gui.recordHistory.hover=Record purchase history for analytics job.name=Job job.none=No Job @@ -1660,72 +1705,3 @@ condition.hit_count=Hit Count condition.percent=Threshold condition.hits=Required Hits condition.within=Within (ticks) - -auction.title=Auction House -auction.browse=Browse -auction.myListings=My Listings -auction.myBids=My Bids -auction.claims=Claims -auction.sell=Sell -auction.search=Search -auction.refresh=Refresh -auction.viewDetails=View Details -auction.placeBid=Place Bid -auction.buyout=Buyout -auction.cancel=Cancel -auction.prevPage=< Prev -auction.nextPage=Next > -auction.noResults=No results -auction.page=Page %d of %d -auction.balance=Balance: %s -auction.listings=%d listings -auction.browseTitle=Browse Auctions -auction.myListingsTitle=My Listings -auction.myBidsTitle=My Active Bids -auction.claimsTitle=Claim Items -auction.sellTitle=Create Listing -auction.currentPrice=Current Price: -auction.minimumBid=Minimum Bid: -auction.yourBalance=Your Balance: -auction.timeLeft=Time Left: -auction.yourBid=Your Bid: -auction.minBid=Min Bid -auction.bidPlus10=+10% -auction.bidPlus25=+25% -auction.insufficientFunds=Insufficient funds! -auction.auctionEnded=Auction has ended! -auction.ended=Ended -auction.item=Item: -auction.seller=Seller: -auction.startingPrice=Starting Price: -auction.buyoutPrice=Buyout Price: -auction.noBuyout=None -auction.bidCount=Bid Count: -auction.highBidder=High Bidder: -auction.noBids=No bids -auction.claim=Claim -auction.createListing=Create Listing -auction.duration=Duration: -auction.listingFee=Listing Fee: -auction.sort.newest=Newest First -auction.sort.oldest=Oldest First -auction.sort.priceHigh=Most Expensive -auction.sort.priceLow=Least Expensive -auction.sort.endingSoon=Ending Soon -auction.sort.mostTime=Most Time Left -auction.sort.nameAZ=Name: A to Z -auction.sort.nameZA=Name: Z to A -auction.sort.mostBids=Most Bids -auction.sort.leastBids=Least Bids -auction.duration.short=Short (12h) -auction.duration.medium=Medium (24h) -auction.duration.long=Long (48h) -auction.duration.veryLong=Very Long (72h) -auction.placeItemBelow=Place item in slot below: -auction.noItemSelected=(No item selected) -auction.noBuyoutHint=(0 = no buyout) -auction.quantity=Quantity: -auction.totalBids=Total Bids: -auction.noBidsYet=No bids yet -auction.auctionDetails=Auction Details -auction.currentBid=Current Bid: From 50e9f1b3e5a54f5d273c5ee4a06dfb7b61e258de Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 20:05:07 +0200 Subject: [PATCH 39/64] HUGE IScriptHandlerPacket merge. All ScriptHandlers that send packets now directly define their data through the ScriptHandler class itself, NOT through their own GuiScript extended class. --- .../request/script/BlockScriptPacket.java | 4 + .../request/script/item/ItemScriptPacket.java | 4 + .../npcs/blocks/tiles/TileScripted.java | 40 +++++- .../java/noppes/npcs/client/ClientProxy.java | 25 +++- .../noppes/npcs/client/gui/GuiScript.java | 3 +- .../client/gui/script/GuiNPCEventScripts.java | 60 +-------- .../client/gui/script/GuiScriptAllNPCs.java | 63 ---------- .../client/gui/script/GuiScriptBlock.java | 39 ------ .../client/gui/script/GuiScriptForge.java | 118 ------------------ .../client/gui/script/GuiScriptGlobal.java | 9 +- .../client/gui/script/GuiScriptInterface.java | 15 ++- .../npcs/client/gui/script/GuiScriptItem.java | 74 ----------- .../client/gui/script/GuiScriptPlayers.java | 71 ----------- .../npcs/controllers/data/DataScript.java | 25 +++- .../controllers/data/ForgeDataScript.java | 107 +++++++++++++++- .../controllers/data/GlobalNPCDataScript.java | 25 +++- .../controllers/data/PlayerDataScript.java | 29 ++++- .../npcs/scripted/item/ScriptCustomItem.java | 55 +++++++- 18 files changed, 323 insertions(+), 443 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java index 0fd7de927..ce94c98f9 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java @@ -84,6 +84,10 @@ public final class BlockScriptPacket extends AbstractPacket { NBTTagCompound compound = ((TileScripted) tile).getNBT(new NBTTagCompound()); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + + NBTTagCompound loadComplete = new NBTTagCompound(); + loadComplete.setInteger("LoadComplete", 1); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, loadComplete); } else { if (!player.capabilities.isCreativeMode) { return; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java index 478340a63..7d21242bd 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java @@ -75,6 +75,10 @@ public final class ItemScriptPacket extends AbstractPacket { NBTTagCompound compound = iw.getMCNbt(); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + + NBTTagCompound loadComplete = new NBTTagCompound(); + loadComplete.setInteger("LoadComplete", 1); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, loadComplete); } else { if (!player.capabilities.isCreativeMode) { return; diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 969ea0bac..c26a2f20a 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -3,6 +3,7 @@ package noppes.npcs.blocks.tiles; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; import net.minecraft.block.Block; import net.minecraft.client.renderer.texture.ITickable; import net.minecraft.item.Item; @@ -25,7 +26,9 @@ import noppes.npcs.api.IBlock; import noppes.npcs.api.block.ITextPlane; import noppes.npcs.client.renderer.blocks.BlockScriptedRenderer; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.IScriptBlockHandler; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.data.DataTimers; @@ -39,7 +42,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; -public class TileScripted extends TileEntity implements IScriptBlockHandler { +public class TileScripted extends TileEntity implements IScriptBlockHandler, IScriptHandlerPacket { public List scripts = new ArrayList<>(); public Map tempData = new HashMap<>(); @@ -161,6 +164,41 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { text6.setNBT(compound.getCompoundTag("Text6")); } + @Override + public ScriptContext getContext() { + return ScriptContext.BLOCK; + } + + @Override + public String getHookContext() { + return noppes.npcs.api.handler.IScriptHookHandler.CONTEXT_BLOCK; + } + + @Override + public void requestData() { + BlockScriptPacket.Get(this.xCoord, this.yCoord, this.zCoord); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + BlockScriptPacket.Save(this.xCoord, this.yCoord, this.zCoord, nbt); + } + + @Override + public GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + setNBT(compound); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + @Override + public void sync() { + sendSavePacket(-1, getScripts().size(), getNBT(new NBTTagCompound())); + } + @Override public void writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index d6169cbbd..249836b80 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -21,7 +21,9 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Container; import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; import net.minecraft.stats.Achievement; +import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ChatComponentText; import net.minecraft.util.ReportedException; import net.minecraft.util.ResourceLocation; @@ -120,9 +122,8 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerSetup; import noppes.npcs.client.gui.roles.GuiNpcItemGiver; import noppes.npcs.client.gui.roles.GuiNpcTraderSetup; import noppes.npcs.client.gui.roles.GuiNpcTransporter; -import noppes.npcs.client.gui.script.GuiScriptBlock; import noppes.npcs.client.gui.script.GuiScriptGlobal; -import noppes.npcs.client.gui.script.GuiScriptItem; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.script.PackageFinder; import noppes.npcs.client.model.ModelNPCGolem; import noppes.npcs.client.model.ModelNpcCrystal; @@ -138,6 +139,7 @@ import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; import kamkeel.npcs.client.renderer.TelegraphRenderer; +import noppes.npcs.scripted.item.ScriptCustomItem; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; @@ -492,7 +494,7 @@ public class ClientProxy extends CommonProxy { return new GuiScript(npc); else if (gui == EnumGuiType.ScriptItem) - return new GuiScriptItem(); + return GuiScriptInterface.create(null, new ScriptCustomItem(new ItemStack(CustomItems.scripted_item))); else if (gui == EnumGuiType.PlayerCarpentryBench) return new GuiNpcCarpentryBench((ContainerCarpentryBench) container); @@ -567,7 +569,7 @@ public class ClientProxy extends CommonProxy { return new GuiCustom((ContainerCustomGui) container); else if (gui == EnumGuiType.ScriptBlock) - return new GuiScriptBlock(x, y, z); + return getScriptBlockGui(x, y, z); else if (gui == EnumGuiType.Paintbrush) return new GuiNpcPaintbrush(); @@ -619,9 +621,22 @@ public class ClientProxy extends CommonProxy { if (guiscreen != null) { minecraft.displayGuiScreen((GuiScreen) guiscreen); } - } + + private GuiScreen getScriptBlockGui(int x, int y, int z) { + World world = Minecraft.getMinecraft().theWorld; + if (world == null) + return null; + + TileEntity tile = world.getTileEntity(x, y, z); + if (!(tile instanceof TileScripted)) + return null; + + return GuiScriptInterface.create(null, (TileScripted) tile); + } + + @Override public void spawnParticle(EntityLivingBase player, String string, Object... ob) { if (string.equals("Spell")) { diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 2b6f6d7c8..b98e94ac4 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -10,7 +10,6 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; -import noppes.npcs.client.gui.script.GuiNPCEventScripts; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.*; import noppes.npcs.constants.ScriptContext; @@ -270,7 +269,7 @@ public class GuiScript extends GuiScriptInterface { } if (guibutton.id == 16) { close(); - mc.displayGuiScreen(new GuiNPCEventScripts(npc)); + GuiScriptInterface.open(this, new DataScript(npc)); } if (guibutton.id == 17) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index e0ad70596..cff2c111e 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -1,65 +1,13 @@ package noppes.npcs.client.gui.script; -import kamkeel.npcs.network.packets.request.script.EventScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.DataScript; -import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.EntityNPCInterface; -import java.util.ArrayList; -import java.util.List; - public class GuiNPCEventScripts extends GuiScriptInterface { - private final DataScript script; - public GuiNPCEventScripts(EntityNPCInterface npc) { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); - this.script = new DataScript(npc); - this.handler = this.script; - EventScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - EventScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - EventScriptPacket.Save(-1, containers.size(), scriptData); - } + this.handler = new DataScript(npc); + if (this.handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) this.handler).requestData(); } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java deleted file mode 100644 index 71bc73d7d..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java +++ /dev/null @@ -1,63 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.GlobalNPCDataScript; -import noppes.npcs.controllers.data.IScriptUnit; - -import java.util.ArrayList; -import java.util.List; - -public class GuiScriptAllNPCs extends GuiScriptInterface { - private final GlobalNPCDataScript script = new GlobalNPCDataScript(null); - - public GuiScriptAllNPCs() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); - this.handler = this.script; - GlobalNPCScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - GlobalNPCScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - GlobalNPCScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java deleted file mode 100644 index b11947ee7..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ /dev/null @@ -1,39 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.blocks.tiles.TileScripted; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptHookController; - -import java.util.ArrayList; - -public class GuiScriptBlock extends GuiScriptInterface { - private final TileScripted tileScripted; - - public GuiScriptBlock(int x, int y, int z) { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_BLOCK)); - - this.handler = this.tileScripted = (TileScripted) player.worldObj.getTileEntity(x, y, z); - BlockScriptPacket.Get(tileScripted.xCoord, tileScripted.yCoord, tileScripted.zCoord); - } - - public void setGuiData(NBTTagCompound compound) { - this.tileScripted.setNBT(compound); - super.setGuiData(compound); - loaded = true; - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.BLOCK; - } - - public void save() { - if (loaded) { - super.save(); - BlockScriptPacket.Save(tileScripted.xCoord, tileScripted.yCoord, tileScripted.zCoord, this.tileScripted.getNBT(new NBTTagCompound())); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java deleted file mode 100644 index 59333eee4..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java +++ /dev/null @@ -1,118 +0,0 @@ -package noppes.npcs.client.gui.script; - -import com.google.common.reflect.ClassPath; -import cpw.mods.fml.common.eventhandler.Event; -import cpw.mods.fml.common.gameevent.TickEvent; -import cpw.mods.fml.common.network.FMLNetworkEvent; -import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.event.entity.EntityEvent; -import net.minecraftforge.event.entity.player.ItemTooltipEvent; -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.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.ForgeDataScript; -import noppes.npcs.controllers.data.IScriptUnit; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -public class GuiScriptForge extends GuiScriptInterface { - private final ForgeDataScript script = new ForgeDataScript(); - - public GuiScriptForge() { - // Add built-in "init" hook from controller - hookList.addAll(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE)); - - // Dynamically discover Forge event hooks - ArrayList list = new ArrayList(); - try { - list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); - list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); - list.removeAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event.terraingen")); - } catch (IOException e) { - e.printStackTrace(); - } - - for (ClassPath.ClassInfo e1 : list) { - ClassPath.ClassInfo classLoader = e1; - Class infoClass = classLoader.load(); - ArrayList c = new ArrayList(Arrays.asList(infoClass.getDeclaredClasses())); - if (c.isEmpty()) { - c.add(infoClass); - } - Iterator var10 = c.iterator(); - while (var10.hasNext()) { - Class c1 = (Class) var10.next(); - if (!EntityEvent.EntityConstructing.class.isAssignableFrom(c1) && !WorldEvent.PotentialSpawns.class.isAssignableFrom(c1) && !TickEvent.RenderTickEvent.class.isAssignableFrom(c1) && !TickEvent.ClientTickEvent.class.isAssignableFrom(c1) && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(c1) && !ItemTooltipEvent.class.isAssignableFrom(c1) && Event.class.isAssignableFrom(c1) && !Modifier.isAbstract(c1.getModifiers()) && Modifier.isPublic(c1.getModifiers()) && !ChunkEvent.class.isAssignableFrom(c1) && !ChunkWatchEvent.class.isAssignableFrom(c1) && !ChunkDataEvent.class.isAssignableFrom(c1)) { - String eventName = c1.getName(); - int i = eventName.lastIndexOf("."); - eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", "")); - - hookList.add(eventName); - } - } - } - hookList.add("onCNPCNaturalSpawn"); - - // Add any addon-registered hooks for the forge context - for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) { - if (!hookList.contains(addonHook)) { - hookList.add(addonHook); - } - } - - this.handler = this.script; - ForgeScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - ForgeScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - ForgeScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java index 15aec96dd..52ab86b24 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java @@ -8,6 +8,9 @@ import net.minecraft.util.ResourceLocation; import noppes.npcs.client.gui.util.GuiNPCInterface; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.controllers.data.ForgeDataScript; +import noppes.npcs.controllers.data.GlobalNPCDataScript; +import noppes.npcs.controllers.data.PlayerDataScript; import org.lwjgl.opengl.GL11; import java.util.HashMap; @@ -65,13 +68,13 @@ public class GuiScriptGlobal extends GuiNPCInterface implements IGuiData { protected void actionPerformed(GuiButton guibutton) { if (guibutton.id == 0) { - this.displayGuiScreen(new GuiScriptPlayers()); + GuiScriptInterface.open(this, new PlayerDataScript(null)); } if (guibutton.id == 1) { - this.displayGuiScreen(new GuiScriptForge()); + GuiScriptInterface.open(this, new ForgeDataScript()); } if (guibutton.id == 2) { - this.displayGuiScreen(new GuiScriptAllNPCs()); + GuiScriptInterface.open(this, new GlobalNPCDataScript(null)); } if (guibutton.id > 2) { try { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index f3173a2a2..5c3ee2596 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -88,10 +88,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } /** - * Create a GuiScriptInterface for a single script without a handler. - * Wraps the script in a SingleScriptHandler automatically. + * Create a GuiScriptInterface for a handler without displaying it. + * Useful when a caller needs to return a GuiScreen (e.g. ClientProxy). */ - public static GuiScriptInterface open(GuiScreen parent, IScriptHandler handler) { + public static GuiScriptInterface create(GuiScreen parent, IScriptHandler handler) { GuiScriptInterface gui = new GuiScriptInterface(); gui.handler = handler; gui.parent = parent; @@ -103,6 +103,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (handler instanceof IScriptHandlerPacket) ((IScriptHandlerPacket) handler).requestData(); + return gui; + } + + /** + * Open a GuiScriptInterface for a handler. + * Wraps the script in a SingleScriptHandler automatically. + */ + public static GuiScriptInterface open(GuiScreen parent, IScriptHandler handler) { + GuiScriptInterface gui = create(parent, handler); Minecraft.getMinecraft().displayGuiScreen(gui); return gui; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java deleted file mode 100644 index fceaeb6ed..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ /dev/null @@ -1,74 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; -import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.CustomItems; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.scripted.item.ScriptCustomItem; - -import java.util.*; - -public class GuiScriptItem extends GuiScriptInterface { - private ScriptCustomItem item; - public static Map consoleText = new HashMap<>(); - - public GuiScriptItem() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_ITEM)); - - this.handler = this.item = new ScriptCustomItem(new ItemStack(CustomItems.scripted_item)); - ItemScriptErrorPacket.Get(); - ItemScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("ItemScriptConsole")) { - consoleText = NBTTags.GetLongStringMap(compound.getTagList("ItemScriptConsole", 10)); - initGui(); - } else { - this.item.setMCNbt(compound); - this.item.loadScriptData(); - super.setGuiData(compound); - loaded = true; - } - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.ITEM; - } - - public void save() { - if (loaded) { - super.save(); - ItemScriptPacket.Save(this.item.getMCNbt()); - } - } - - protected String getConsoleText() { - Map map = consoleText; - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - public void confirmClicked(boolean flag, int i) { - if (i == 102) { - if (activeTab <= 0) { - consoleText = new HashMap<>(); - ItemScriptErrorPacket.Clear(); - } - } - super.confirmClicked(flag, i); - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java deleted file mode 100644 index de884b356..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ /dev/null @@ -1,71 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.PlayerDataScript; - -import java.util.ArrayList; -import java.util.List; - -public class GuiScriptPlayers extends GuiScriptInterface { - private PlayerDataScript script = new PlayerDataScript((EntityPlayer) null); - - public GuiScriptPlayers() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_PLAYER)); - - this.handler = this.script; - PlayerScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.PLAYER; - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - PlayerScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - PlayerScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index cd6f4bb54..37f58453d 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -3,6 +3,7 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.relauncher.Side; +import kamkeel.npcs.network.packets.request.script.EventScriptPacket; import net.minecraft.entity.Entity; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -12,10 +13,12 @@ import noppes.npcs.EventHooks; import noppes.npcs.LogWriter; import noppes.npcs.NBTTags; import noppes.npcs.api.IWorld; +import noppes.npcs.api.handler.IScriptHookHandler; 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; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; @@ -36,7 +39,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class DataScript implements IScriptHandler { +public class DataScript implements IScriptHandlerPacket { public List eventScripts = new ArrayList<>(); private HashMap scripts = new HashMap<>(); @@ -210,6 +213,26 @@ public class DataScript implements IScriptHandler { return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && ConfigScript.ScriptingEnabled; } + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_NPC; + } + + @Override + public void requestData() { + EventScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + EventScriptPacket.Save(index, totalCount, nbt); + } + public Map getOldConsoleText() { Map map = new TreeMap<>(); diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index c7f345d73..0ecdf27c0 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -1,17 +1,37 @@ package noppes.npcs.controllers.data; +import com.google.common.reflect.ClassPath; import cpw.mods.fml.common.eventhandler.Event; +import cpw.mods.fml.common.gameevent.TickEvent; +import cpw.mods.fml.common.network.FMLNetworkEvent; +import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.event.entity.EntityEvent; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +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.EventHooks; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.config.ConfigScript; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; +import org.apache.commons.lang3.StringUtils; +import java.io.IOException; +import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; -public class ForgeDataScript implements IScriptHandler { +public class ForgeDataScript implements IScriptHandlerPacket { + private static final Object HOOK_LOCK = new Object(); + private static List cachedHooks; private List scripts = new ArrayList<>(); private String scriptLanguage = "ECMAScript"; public long lastInited = -1L; @@ -81,6 +101,91 @@ public class ForgeDataScript implements IScriptHandler { return this.enabled && ConfigScript.GlobalForgeScripts && ScriptController.HasStart && this.scripts.size() > 0; } + @Override + public ScriptContext getContext() { + return ScriptContext.FORGE; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_FORGE; + } + + @Override + public List getHooks() { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + synchronized (HOOK_LOCK) { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + List hookList = new ArrayList<>(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE)); + + ArrayList list = new ArrayList(); + try { + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); + list.removeAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event.terraingen")); + } catch (IOException e) { + e.printStackTrace(); + } + + for (ClassPath.ClassInfo e1 : list) { + ClassPath.ClassInfo classLoader = e1; + Class infoClass = classLoader.load(); + ArrayList c = new ArrayList(Arrays.asList(infoClass.getDeclaredClasses())); + if (c.isEmpty()) { + c.add(infoClass); + } + Iterator var10 = c.iterator(); + while (var10.hasNext()) { + Class c1 = (Class) var10.next(); + if (!EntityEvent.EntityConstructing.class.isAssignableFrom(c1) + && !WorldEvent.PotentialSpawns.class.isAssignableFrom(c1) + && !TickEvent.RenderTickEvent.class.isAssignableFrom(c1) + && !TickEvent.ClientTickEvent.class.isAssignableFrom(c1) + && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(c1) + && !ItemTooltipEvent.class.isAssignableFrom(c1) + && Event.class.isAssignableFrom(c1) + && !Modifier.isAbstract(c1.getModifiers()) + && Modifier.isPublic(c1.getModifiers()) + && !ChunkEvent.class.isAssignableFrom(c1) + && !ChunkWatchEvent.class.isAssignableFrom(c1) + && !ChunkDataEvent.class.isAssignableFrom(c1)) { + String eventName = c1.getName(); + int i = eventName.lastIndexOf("."); + eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", "")); + + hookList.add(eventName); + } + } + } + + hookList.add("onCNPCNaturalSpawn"); + + for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) { + if (!hookList.contains(addonHook)) { + hookList.add(addonHook); + } + } + + cachedHooks = hookList; + } + + return new ArrayList<>(cachedHooks); + } + + @Override + public void requestData() { + ForgeScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + ForgeScriptPacket.Save(index, totalCount, nbt); + } + public boolean isClient() { return false; } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index f8580963b..5561c255c 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -2,11 +2,14 @@ package noppes.npcs.controllers.data; import com.google.common.base.Preconditions; import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigScript; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; @@ -25,7 +28,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.TreeMap; -public class GlobalNPCDataScript implements IScriptHandler { +public class GlobalNPCDataScript implements IScriptHandlerPacket { public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityNPCInterface npc; @@ -75,6 +78,26 @@ public class GlobalNPCDataScript implements IScriptHandler { return ConfigScript.GlobalNPCScripts && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; } + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_NPC; + } + + @Override + public void requestData() { + GlobalNPCScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + GlobalNPCScriptPacket.Save(index, totalCount, nbt); + } + @Override public void callScript(String hookName, Event event) { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 1fd06d927..3e794a37f 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -2,14 +2,17 @@ package noppes.npcs.controllers.data; import com.google.common.base.Preconditions; import cpw.mods.fml.common.eventhandler.Event; +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.EventHooks; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.scripted.NpcAPI; @@ -30,7 +33,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.TreeMap; -public class PlayerDataScript implements IScriptHandler { +public class PlayerDataScript implements IScriptHandlerPacket { public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityPlayer player; @@ -89,6 +92,26 @@ public class PlayerDataScript implements IScriptHandler { return ConfigScript.GlobalPlayerScripts && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); } + @Override + public ScriptContext getContext() { + return ScriptContext.PLAYER; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_PLAYER; + } + + @Override + public void requestData() { + PlayerScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + PlayerScriptPacket.Save(index, totalCount, nbt); + } + @Override public void callScript(String hookName, Event event) { @@ -200,7 +223,7 @@ public class PlayerDataScript implements IScriptHandler { if (ConfigScript.IndividualPlayerScripts) { return console; } - return IScriptHandler.super.getConsoleText(); + return IScriptHandlerPacket.super.getConsoleText(); } @Override @@ -209,7 +232,7 @@ public class PlayerDataScript implements IScriptHandler { console.clear(); return; } - IScriptHandler.super.clearConsole(); + IScriptHandlerPacket.super.clearConsole(); } public static final class ToStringHelper { diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 02f27b71c..2e17555d3 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -1,15 +1,20 @@ package noppes.npcs.scripted.item; import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; +import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.scripted.CustomNPCsException; import java.util.ArrayList; @@ -18,13 +23,15 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; -public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, IScriptHandler { +public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, IScriptHandlerPacket { public List scripts = new ArrayList(); public List errored = new ArrayList(); public String scriptLanguage = "ECMAScript"; public boolean enabled = false; public boolean loaded = false; + private Map clientConsoleText = new TreeMap<>(); + public double durabilityValue = 1.0D; public int stackSize = 64; @@ -60,6 +67,48 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } } + @Override + public ScriptContext getContext() { + return ScriptContext.ITEM; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_ITEM; + } + + @Override + public void requestData() { + ItemScriptErrorPacket.Get(); + ItemScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + ItemScriptPacket.Save(nbt); + } + + @Override + public GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + if (compound.hasKey("ItemScriptConsole")) { + clientConsoleText = NBTTags.GetLongStringMap(compound.getTagList("ItemScriptConsole", 10)); + return new GuiDataResult(GuiDataKind.TAB, -1); + } + + setMCNbt(compound); + loadScriptData(); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + @Override + public void sync() { + sendSavePacket(-1, getScripts().size(), getMCNbt()); + } + public int getType() { return 6; } @@ -142,10 +191,12 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } public Map getConsoleText() { - return new TreeMap<>(); + return clientConsoleText; } public void clearConsole() { + clientConsoleText.clear(); + ItemScriptErrorPacket.Clear(); } public int getMaxStackSize() { From d835d2acb04a2c18975147ffcfcdcb1fb8442513 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 20:48:17 +0200 Subject: [PATCH 40/64] Migrated all EffectScript.ScriptType hooks to EnumScriptType --- src/main/java/noppes/npcs/EventHooks.java | 6 +++--- .../noppes/npcs/constants/EnumScriptType.java | 4 ++++ .../npcs/controllers/ScriptHookController.java | 13 ++++++------- .../npcs/controllers/data/CustomEffect.java | 7 ++++--- .../npcs/controllers/data/EffectScript.java | 18 ------------------ 5 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index a0c3d2b57..f72bbdab7 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -491,21 +491,21 @@ public class EventHooks { public static boolean onEffectAdded(IPlayer player, IPlayerEffect effect) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Added event = new PlayerEvent.EffectEvent.Added(player, effect); - handler.callScript(EffectScript.ScriptType.OnEffectAdd.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_ADD.function, event); return NpcAPI.EVENT_BUS.post(event); } public static boolean onEffectTick(IPlayer player, IPlayerEffect effect) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Ticked event = new PlayerEvent.EffectEvent.Ticked(player, effect); - handler.callScript(EffectScript.ScriptType.OnEffectTick.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_TICK.function, event); return NpcAPI.EVENT_BUS.post(event); } public static boolean onEffectRemove(IPlayer player, IPlayerEffect effect, EffectEvent.ExpirationType type) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Removed event = new PlayerEvent.EffectEvent.Removed(player, effect, type); - handler.callScript(EffectScript.ScriptType.OnEffectRemove.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_REMOVE.function, event); return NpcAPI.EVENT_BUS.post(event); } diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index b1052c86c..99a8b4091 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -116,6 +116,10 @@ public enum EnumScriptType { PROFILE_REMOVE("profileRemove"), PROFILE_CREATE("profileCreate"), + ON_EFFECT_ADD("onEffectAdd"), + ON_EFFECT_TICK("onEffectTick"), + ON_EFFECT_REMOVE("onEffectRemove"), + ABILITY_START("abilityStart"), ABILITY_EXECUTE("abilityExecute"), ABILITY_INTERRUPT("abilityInterrupt"), diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index 0a64be83c..b599250a8 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -2,7 +2,6 @@ package noppes.npcs.controllers; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.RecipeScript; import java.util.ArrayList; @@ -145,9 +144,9 @@ public class ScriptHookController implements IScriptHookHandler { EnumScriptType.PROFILE_CHANGE.function, EnumScriptType.PROFILE_REMOVE.function, EnumScriptType.PROFILE_CREATE.function, - EffectScript.ScriptType.OnEffectAdd.function, - EffectScript.ScriptType.OnEffectTick.function, - EffectScript.ScriptType.OnEffectRemove.function + EnumScriptType.ON_EFFECT_ADD.function, + EnumScriptType.ON_EFFECT_TICK.function, + EnumScriptType.ON_EFFECT_REMOVE.function ); // Block hooks @@ -209,9 +208,9 @@ public class ScriptHookController implements IScriptHookHandler { // Effect hooks registerBuiltIn(CONTEXT_EFFECT, - EffectScript.ScriptType.OnEffectAdd.function, - EffectScript.ScriptType.OnEffectTick.function, - EffectScript.ScriptType.OnEffectRemove.function + EnumScriptType.ON_EFFECT_ADD.function, + EnumScriptType.ON_EFFECT_TICK.function, + EnumScriptType.ON_EFFECT_REMOVE.function ); // Forge hooks - just init, the rest are dynamically discovered diff --git a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java index 3834bef08..9b621234d 100644 --- a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java +++ b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java @@ -7,6 +7,7 @@ import noppes.npcs.EventHooks; import noppes.npcs.NoppesUtilServer; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.handler.data.ICustomEffect; +import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.CustomEffectController; import noppes.npcs.scripted.event.player.PlayerEvent; @@ -193,7 +194,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectAdd, event); + script.callScript(EnumScriptType.ON_EFFECT_ADD, event); } EventHooks.onEffectAdded(iPlayer, playerEffect); @@ -213,7 +214,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectTick, event); + script.callScript(EnumScriptType.ON_EFFECT_TICK, event); } EventHooks.onEffectTick(iPlayer, playerEffect); @@ -234,7 +235,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectRemove, event); + script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); } EventHooks.onEffectRemove(iPlayer, playerEffect, type); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index 8169dbbd7..b2ae04b98 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -3,7 +3,6 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.scripted.event.player.PlayerEvent; /** * Script handler for CustomEffect scripts. @@ -33,12 +32,6 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP return IScriptHookHandler.CONTEXT_EFFECT; } - /** - * Convenience method to call a script with a typed event. - */ - public void callScript(ScriptType type, PlayerEvent.EffectEvent event) { - callScript(type.function, event); - } @Override public void requestData() { @@ -52,15 +45,4 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP EffectScriptPacket.Save(effectId, index, totalCount, nbt); } - public enum ScriptType { - OnEffectAdd("onEffectAdd"), - OnEffectTick("onEffectTick"), - OnEffectRemove("onEffectRemove"); - - public final String function; - - ScriptType(String functionName) { - this.function = functionName; - } - } } From 7050e9b7d4dbb541fec634a41b46cdb14c940ba3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 20:51:46 +0200 Subject: [PATCH 41/64] Implemented Janino scripting for player events. via JaninoPlayerScript --- .../controllers/data/PlayerDataScript.java | 6 + .../npcs/janino/impl/JaninoPlayerScript.java | 269 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 3e794a37f..c329c4c6e 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -15,6 +15,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.janino.impl.JaninoPlayerScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerEvent; @@ -427,4 +428,9 @@ public class PlayerDataScript implements IScriptHandlerPacket { public static PlayerDataScript.ToStringHelper toStringHelper(Object self) { return new PlayerDataScript.ToStringHelper(self.getClass().getSimpleName()); } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new JaninoPlayerScript(); + } } diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java new file mode 100644 index 000000000..e5f61cdc9 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -0,0 +1,269 @@ +package noppes.npcs.janino.impl; + +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.ICustomNPCsEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IQuestEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.JaninoScript; +import noppes.npcs.janino.annotations.ParamName; +import noppes.npcs.janino.annotations.ScriptHook; + +/** + * Janino (Java) script implementation for player events. + */ +public class JaninoPlayerScript extends JaninoScript { + + /** Default imports available to player scripts without explicit import statements. */ + public static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.*", + "noppes.npcs.api.entity.*", + "noppes.npcs.api.event.*", + "noppes.npcs.api.handler.*", + "noppes.npcs.api.handler.data.*", + "noppes.npcs.api.item.*", + "noppes.npcs.api.gui.*" + }; + + public JaninoPlayerScript() { + super(Functions.class, DEFAULT_IMPORTS); + } + + /** + * Interface defining the hook methods available for player scripts. + * Each method corresponds to a player hook from ScriptHookController CONTEXT_PLAYER. + */ + public interface Functions { + // Core lifecycle + @ScriptHook(EnumScriptType.INIT) + default void init(@ParamName("event") IPlayerEvent.InitEvent event) {} + + @ScriptHook(EnumScriptType.TICK) + default void tick(@ParamName("event") IPlayerEvent.UpdateEvent event) {} + + // Interaction + @ScriptHook(EnumScriptType.INTERACT) + default void interact(@ParamName("event") IPlayerEvent.InteractEvent event) {} + + @ScriptHook(EnumScriptType.RIGHT_CLICK) + default void rightClick(@ParamName("event") IPlayerEvent.RightClickEvent event) {} + + // Combat + @ScriptHook(EnumScriptType.ATTACK) + default void attack(@ParamName("event") IPlayerEvent.AttackEvent event) {} + + @ScriptHook(EnumScriptType.ATTACKED) + default void attacked(@ParamName("event") IPlayerEvent.AttackedEvent event) {} + + @ScriptHook(EnumScriptType.DAMAGED_ENTITY) + default void damagedEntity(@ParamName("event") IPlayerEvent.DamagedEntityEvent event) {} + + @ScriptHook(EnumScriptType.DAMAGED) + default void damaged(@ParamName("event") IPlayerEvent.DamagedEvent event) {} + + @ScriptHook(EnumScriptType.KILLS) + default void kills(@ParamName("event") IPlayerEvent.KilledEntityEvent event) {} + + @ScriptHook(EnumScriptType.KILLED) + default void killed(@ParamName("event") IPlayerEvent.DiedEvent event) {} + + // Inventory and drops + @ScriptHook(EnumScriptType.DROP) + default void drop(@ParamName("event") IPlayerEvent.DropEvent event) {} + + @ScriptHook(EnumScriptType.RESPAWN) + default void respawn(@ParamName("event") IPlayerEvent.RespawnEvent event) {} + + @ScriptHook(EnumScriptType.BREAK_BLOCK) + default void breakBlock(@ParamName("event") IPlayerEvent.BreakEvent event) {} + + // Communication + @ScriptHook(EnumScriptType.CHAT) + default void chat(@ParamName("event") IPlayerEvent.ChatEvent event) {} + + @ScriptHook(EnumScriptType.LOGIN) + default void login(@ParamName("event") IPlayerEvent.LoginEvent event) {} + + @ScriptHook(EnumScriptType.LOGOUT) + default void logout(@ParamName("event") IPlayerEvent.LogoutEvent event) {} + + @ScriptHook(EnumScriptType.KEY_PRESSED) + default void keyPressed(@ParamName("event") IPlayerEvent.KeyPressedEvent event) {} + + @ScriptHook(EnumScriptType.MOUSE_CLICKED) + default void mouseClicked(@ParamName("event") IPlayerEvent.MouseClickedEvent event) {} + + // Item interactions + @ScriptHook(EnumScriptType.TOSS) + default void toss(@ParamName("event") IPlayerEvent.TossEvent event) {} + + @ScriptHook(EnumScriptType.PICKUP) + default void pickUp(@ParamName("event") IPlayerEvent.PickUpEvent event) {} + + @ScriptHook(EnumScriptType.PICKUP_XP) + default void pickupXP(@ParamName("event") IPlayerEvent.PickupXPEvent event) {} + + @ScriptHook(EnumScriptType.RANGED_CHARGE) + default void rangedCharge(@ParamName("event") IPlayerEvent.RangedChargeEvent event) {} + + @ScriptHook(EnumScriptType.RANGED_LAUNCHED) + default void rangedLaunched(@ParamName("event") IPlayerEvent.RangedLaunchedEvent event) {} + + @ScriptHook(EnumScriptType.TIMER) + default void timer(@ParamName("event") IPlayerEvent.TimerEvent event) {} + + // Item usage + @ScriptHook(EnumScriptType.START_USING_ITEM) + default void startItem(@ParamName("event") IPlayerEvent.StartUsingItem event) {} + + @ScriptHook(EnumScriptType.USING_ITEM) + default void usingItem(@ParamName("event") IPlayerEvent.UsingItem event) {} + + @ScriptHook(EnumScriptType.STOP_USING_ITEM) + default void stopItem(@ParamName("event") IPlayerEvent.StopUsingItem event) {} + + @ScriptHook(EnumScriptType.FINISH_USING_ITEM) + default void finishItem(@ParamName("event") IPlayerEvent.FinishUsingItem event) {} + + // Containers and world interaction + @ScriptHook(EnumScriptType.CONTAINER_OPEN) + default void containerOpen(@ParamName("event") IPlayerEvent.ContainerOpen event) {} + + @ScriptHook(EnumScriptType.USE_HOE) + default void useHoe(@ParamName("event") IPlayerEvent.UseHoeEvent event) {} + + @ScriptHook(EnumScriptType.BONEMEAL) + default void bonemeal(@ParamName("event") IPlayerEvent.BonemealEvent event) {} + + @ScriptHook(EnumScriptType.FILL_BUCKET) + default void fillBucket(@ParamName("event") IPlayerEvent.FillBucketEvent event) {} + + // Movement and status + @ScriptHook(EnumScriptType.JUMP) + default void jump(@ParamName("event") IPlayerEvent.JumpEvent event) {} + + @ScriptHook(EnumScriptType.FALL) + default void fall(@ParamName("event") IPlayerEvent.FallEvent event) {} + + @ScriptHook(EnumScriptType.WAKE_UP) + default void wakeUp(@ParamName("event") IPlayerEvent.WakeUpEvent event) {} + + @ScriptHook(EnumScriptType.SLEEP) + default void sleep(@ParamName("event") IPlayerEvent.SleepEvent event) {} + + @ScriptHook(EnumScriptType.PLAYSOUND) + default void playSound(@ParamName("event") IPlayerEvent.SoundEvent event) {} + + @ScriptHook(EnumScriptType.LIGHTNING) + default void lightning(@ParamName("event") IPlayerEvent.LightningEvent event) {} + + @ScriptHook(EnumScriptType.CHANGED_DIM) + default void changedDim(@ParamName("event") IPlayerEvent.ChangedDimension event) {} + + // Quest + @ScriptHook(EnumScriptType.QUEST_START) + default void questStart(@ParamName("event") IQuestEvent.QuestStartEvent event) {} + + @ScriptHook(EnumScriptType.QUEST_COMPLETED) + default void questCompleted(@ParamName("event") IQuestEvent.QuestCompletedEvent event) {} + + @ScriptHook(EnumScriptType.QUEST_TURNIN) + default void questTurnIn(@ParamName("event") IQuestEvent.QuestTurnedInEvent event) {} + + // Faction + @ScriptHook(EnumScriptType.FACTION_POINTS) + default void factionPoints(@ParamName("event") IFactionEvent.FactionPoints event) {} + + // Dialog + @ScriptHook(EnumScriptType.DIALOG_OPEN) + default void dialogOpen(@ParamName("event") IDialogEvent.DialogOpen event) {} + + @ScriptHook(EnumScriptType.DIALOG_OPTION) + default void dialogOption(@ParamName("event") IDialogEvent.DialogOption event) {} + + @ScriptHook(EnumScriptType.DIALOG_CLOSE) + default void dialogClose(@ParamName("event") IDialogEvent.DialogClosed event) {} + + // Commands + @ScriptHook(EnumScriptType.SCRIPT_COMMAND) + default void scriptCommand(@ParamName("event") ICustomNPCsEvent.ScriptedCommandEvent event) {} + + // Custom GUI + @ScriptHook(EnumScriptType.CUSTOM_GUI_CLOSED) + default void customGuiClosed(@ParamName("event") ICustomGuiEvent.CloseEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_BUTTON) + default void customGuiButton(@ParamName("event") ICustomGuiEvent.ButtonEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_SLOT) + default void customGuiSlot(@ParamName("event") ICustomGuiEvent.SlotEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_SLOT_CLICKED) + default void customGuiSlotClicked(@ParamName("event") ICustomGuiEvent.SlotClickEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_SCROLL) + default void customGuiScroll(@ParamName("event") ICustomGuiEvent.ScrollEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_TEXTFIELD) + default void customGuiTextfield(@ParamName("event") ICustomGuiEvent.UnfocusedEvent event) {} + + // Party + @ScriptHook(EnumScriptType.PARTY_QUEST_COMPLETED) + default void partyQuestCompleted(@ParamName("event") IPartyEvent.PartyQuestCompletedEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_QUEST_SET) + default void partyQuestSet(@ParamName("event") IPartyEvent.PartyQuestSetEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_QUEST_TURNED_IN) + default void partyQuestTurnedIn(@ParamName("event") IPartyEvent.PartyQuestTurnedInEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_INVITE) + default void partyInvite(@ParamName("event") IPartyEvent.PartyInviteEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_KICK) + default void partyKick(@ParamName("event") IPartyEvent.PartyKickEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_LEAVE) + default void partyLeave(@ParamName("event") IPartyEvent.PartyLeaveEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_DISBAND) + default void partyDisband(@ParamName("event") IPartyEvent.PartyDisbandEvent event) {} + + // Animation + @ScriptHook(EnumScriptType.ANIMATION_START) + default void animationStart(@ParamName("event") IAnimationEvent.Started event) {} + + @ScriptHook(EnumScriptType.ANIMATION_END) + default void animationEnd(@ParamName("event") IAnimationEvent.Ended event) {} + + @ScriptHook(EnumScriptType.ANIMATION_FRAME_ENTER) + default void frameEnter(@ParamName("event") IAnimationEvent.IFrameEvent.Entered event) {} + + @ScriptHook(EnumScriptType.ANIMATION_FRAME_EXIT) + default void frameExit(@ParamName("event") IAnimationEvent.IFrameEvent.Exited event) {} + + // Profile + @ScriptHook(EnumScriptType.PROFILE_CHANGE) + default void profileChange(@ParamName("event") IPlayerEvent.ProfileEvent.Changed event) {} + + @ScriptHook(EnumScriptType.PROFILE_REMOVE) + default void profileRemove(@ParamName("event") IPlayerEvent.ProfileEvent.Removed event) {} + + @ScriptHook(EnumScriptType.PROFILE_CREATE) + default void profileCreate(@ParamName("event") IPlayerEvent.ProfileEvent.Create event) {} + + // Effects + @ScriptHook(EnumScriptType.ON_EFFECT_ADD) + default void onEffectAdd(@ParamName("event") IPlayerEvent.EffectEvent.Added event) {} + + @ScriptHook(EnumScriptType.ON_EFFECT_TICK) + default void onEffectTick(@ParamName("event") IPlayerEvent.EffectEvent.Ticked event) {} + + @ScriptHook(EnumScriptType.ON_EFFECT_REMOVE) + default void onEffectRemove(@ParamName("event") IPlayerEvent.EffectEvent.Removed event) {} + } +} From e2ca76e626afc4437f8b31f7649a48f5242454e6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 21:02:24 +0200 Subject: [PATCH 42/64] Removed default imports for server event janino scripts --- .../noppes/npcs/janino/impl/JaninoNpcScript.java | 13 +------------ .../npcs/janino/impl/JaninoPlayerScript.java | 15 ++------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 4b051b37e..5b3ec4dbd 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -14,19 +14,8 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoNpcScript extends JaninoScript { - /** Default imports available to NPC scripts without explicit import statements. */ - public static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.*", - "noppes.npcs.api.entity.*", - "noppes.npcs.api.event.*", - "noppes.npcs.api.handler.*", - "noppes.npcs.api.handler.data.*", - "noppes.npcs.api.item.*", - "noppes.npcs.api.ability.*" - }; - public JaninoNpcScript() { - super(Functions.class, DEFAULT_IMPORTS); + super(Functions.class, null); } /** diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index e5f61cdc9..15d822615 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -17,20 +17,9 @@ import noppes.npcs.janino.annotations.ScriptHook; * Janino (Java) script implementation for player events. */ public class JaninoPlayerScript extends JaninoScript { - - /** Default imports available to player scripts without explicit import statements. */ - public static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.*", - "noppes.npcs.api.entity.*", - "noppes.npcs.api.event.*", - "noppes.npcs.api.handler.*", - "noppes.npcs.api.handler.data.*", - "noppes.npcs.api.item.*", - "noppes.npcs.api.gui.*" - }; - + public JaninoPlayerScript() { - super(Functions.class, DEFAULT_IMPORTS); + super(Functions.class, null); } /** From 19b01e20991cbd6c6a5449925d30d1a3793ab678 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 14:27:48 -0500 Subject: [PATCH 43/64] Compress Script Hooks and Context System --- src/api | 2 +- .../npcs/blocks/tiles/TileScripted.java | 5 - .../noppes/npcs/constants/ScriptContext.java | 65 +++- .../npcs/controllers/ScriptController.java | 8 +- .../controllers/ScriptHookController.java | 54 +-- .../npcs/controllers/data/DataScript.java | 6 - .../npcs/controllers/data/EffectScript.java | 29 +- .../controllers/data/ForgeDataScript.java | 204 +++++------- .../controllers/data/GlobalNPCDataScript.java | 315 +----------------- .../npcs/controllers/data/IScriptHandler.java | 8 +- .../controllers/data/LinkedItemScript.java | 8 +- .../controllers/data/MultiScriptHandler.java | 141 ++++++++ .../controllers/data/PlayerDataScript.java | 291 ++-------------- .../npcs/controllers/data/RecipeScript.java | 6 +- .../npcs/controllers/data/ScriptHandler.java | 7 + .../npcs/scripted/item/ScriptCustomItem.java | 6 - .../npcs/util/ScriptToStringHelper.java | 210 ++++++++++++ 17 files changed, 595 insertions(+), 770 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java create mode 100644 src/main/java/noppes/npcs/util/ScriptToStringHelper.java diff --git a/src/api b/src/api index 339e0f164..c962f678a 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 339e0f164e8c7b08c3469f61027fa25aa9e5c118 +Subproject commit c962f678a4f1bb83194d805cf3b5681fd5c70b1a diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index c26a2f20a..b9558dd93 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -169,11 +169,6 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler, ISc return ScriptContext.BLOCK; } - @Override - public String getHookContext() { - return noppes.npcs.api.handler.IScriptHookHandler.CONTEXT_BLOCK; - } - @Override public void requestData() { BlockScriptPacket.Get(this.xCoord, this.yCoord, this.zCoord); diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index 942c406e6..f2f614a1e 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -16,10 +16,13 @@ import java.util.concurrent.ConcurrentHashMap; * - ICustomGuiEvent (custom GUI hooks) * - etc. * + * Each context also has a hookContext string used by ScriptHookController for + * hook registration and lookup. + * * This is a registerable system - any mod can register their own contexts: * * // Register a new context with multiple namespaces: - * ScriptContext.register("DBC", "IDBCEvent", "IDBCPlayerEvent", "IDBCFormEvent"); + * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); * * // Or add namespaces to an existing context: * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); @@ -31,12 +34,12 @@ public class ScriptContext { // ==================== BUILT-IN CONTEXTS ==================== - public static final ScriptContext NPC = register("NPC", + public static final ScriptContext NPC = register("NPC", "npc", INpcEvent.class, IProjectileEvent.class ); - public static final ScriptContext PLAYER = register("PLAYER", + public static final ScriptContext PLAYER = register("PLAYER", "player", IPlayerEvent.class, IAnimationEvent.class, IPartyEvent.class, @@ -46,19 +49,31 @@ public class ScriptContext { ICustomGuiEvent.class ); - public static final ScriptContext BLOCK = register("BLOCK", + public static final ScriptContext BLOCK = register("BLOCK", "block", IBlockEvent.class ); - public static final ScriptContext ITEM = register("ITEM", + public static final ScriptContext ITEM = register("ITEM", "item", IItemEvent.class ); - public static final ScriptContext FORGE = register("FORGE", + public static final ScriptContext FORGE = register("FORGE", "forge", IForgeEvent.class ); - public static final ScriptContext GLOBAL = register("GLOBAL", + public static final ScriptContext LINKED_ITEM = register("LINKED_ITEM", "linked_item", + IItemEvent.class + ); + + public static final ScriptContext RECIPE = register("RECIPE", "recipe", + "Recipe" + ); + + public static final ScriptContext EFFECT = register("EFFECT", "effect", + "Effect" + ); + + public static final ScriptContext GLOBAL = register("GLOBAL", "", "Global" // Special case: Global namespace doesn't have a corresponding event class ); @@ -67,13 +82,17 @@ public class ScriptContext { /** Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") */ public final String id; + /** Hook context identifier used by ScriptHookController (e.g., "npc", "player") */ + public final String hookContext; + /** The event interface namespaces this context supports */ private final List namespaces; // ==================== CONSTRUCTOR ==================== - private ScriptContext(String id, String... namespaces) { + private ScriptContext(String id, String hookContext, String... namespaces) { this.id = id; + this.hookContext = hookContext != null ? hookContext : ""; this.namespaces = new ArrayList<>(Arrays.asList(namespaces)); } @@ -123,14 +142,15 @@ public class ScriptContext { // ==================== REGISTRATION API ==================== /** - * Register a new script context with one or more namespaces. + * Register a new script context with a hook context and one or more namespaces. * * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "dbc") * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") * @return The registered ScriptContext */ - public static ScriptContext register(String id, String... namespaces) { - ScriptContext context = new ScriptContext(id, namespaces); + public static ScriptContext register(String id, String hookContext, String... namespaces) { + ScriptContext context = new ScriptContext(id, hookContext, namespaces); REGISTRY.put(id, context); return context; } @@ -140,18 +160,19 @@ public class ScriptContext { * Automatically extracts the simple name from each class. * * Example: - * ScriptContext.register("NPC", INpcEvent.class, IProjectileEvent.class); + * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); * * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "npc") * @param eventClasses The event interface classes (simple names will be extracted) * @return The registered ScriptContext */ - public static ScriptContext register(String id, Class... eventClasses) { + public static ScriptContext register(String id, String hookContext, Class... eventClasses) { String[] namespaces = new String[eventClasses.length]; for (int i = 0; i < eventClasses.length; i++) { namespaces[i] = eventClasses[i].getSimpleName(); } - return register(id, namespaces); + return register(id, hookContext, namespaces); } /** @@ -180,6 +201,22 @@ public class ScriptContext { return GLOBAL; } + /** + * Find a script context by its hook context string. + * + * @param hookContext The hook context string (e.g., "npc", "player") + * @return The ScriptContext with that hook context, or GLOBAL if not found + */ + public static ScriptContext byHookContext(String hookContext) { + if (hookContext == null || hookContext.isEmpty()) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (hookContext.equals(ctx.hookContext)) { + return ctx; + } + } + return GLOBAL; + } + /** * Get all registered script contexts. * diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index b5d21563c..af2153cff 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -134,7 +134,7 @@ public class ScriptController { File file = this.forgeScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.forgeScripts.writeToNBT(new NBTTagCompound())); - this.forgeScripts.lastInited = -1L; + this.forgeScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -170,7 +170,7 @@ public class ScriptController { File file = this.playerScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new NBTTagCompound())); - this.playerScripts.lastInited = -1L; + this.playerScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -212,7 +212,7 @@ public class ScriptController { File file = this.npcScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.globalNpcScripts.writeToNBT(new NBTTagCompound())); - this.globalNpcScripts.lastInited = -1L; + this.globalNpcScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -227,7 +227,7 @@ public class ScriptController { File file = this.playerScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new NBTTagCompound())); - this.playerScripts.lastInited = -1L; + this.playerScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index b599250a8..bc20eccff 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -2,6 +2,8 @@ package noppes.npcs.controllers; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.RecipeScript; import java.util.ArrayList; @@ -24,32 +26,22 @@ public class ScriptHookController implements IScriptHookHandler { // Addon-registered hooks per context private final Map> addonHooks = new HashMap<>(); - // All available contexts - private static final String[] CONTEXTS = { - CONTEXT_NPC, - CONTEXT_PLAYER, - CONTEXT_BLOCK, - CONTEXT_ITEM, - CONTEXT_LINKED_ITEM, - CONTEXT_FORGE, - CONTEXT_RECIPE, - CONTEXT_EFFECT - }; - public ScriptHookController() { Instance = this; initializeBuiltInHooks(); } private void initializeBuiltInHooks() { - // Initialize empty lists for all contexts - for (String context : CONTEXTS) { - builtInHooks.put(context, new ArrayList<>()); - addonHooks.put(context, new ArrayList<>()); + // Initialize empty lists for all contexts from ScriptContext + for (ScriptContext context : ScriptContext.values()) { + if (!context.hookContext.isEmpty()) { + builtInHooks.put(context.hookContext, new ArrayList<>()); + addonHooks.put(context.hookContext, new ArrayList<>()); + } } // NPC hooks - registerBuiltIn(CONTEXT_NPC, + registerBuiltIn(ScriptContext.NPC, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.INTERACT.function, @@ -76,7 +68,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Player hooks - registerBuiltIn(CONTEXT_PLAYER, + registerBuiltIn(ScriptContext.PLAYER, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.INTERACT.function, @@ -150,7 +142,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Block hooks - registerBuiltIn(CONTEXT_BLOCK, + registerBuiltIn(ScriptContext.BLOCK, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.INTERACT.function, @@ -167,7 +159,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Item hooks - registerBuiltIn(CONTEXT_ITEM, + registerBuiltIn(ScriptContext.ITEM, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.TOSSED.function, @@ -183,7 +175,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Linked item hooks - registerBuiltIn(CONTEXT_LINKED_ITEM, + registerBuiltIn(ScriptContext.LINKED_ITEM, EnumScriptType.LINKED_ITEM_BUILD.function, EnumScriptType.LINKED_ITEM_VERSION.function, EnumScriptType.INIT.function, @@ -201,24 +193,32 @@ public class ScriptHookController implements IScriptHookHandler { ); // Recipe hooks - registerBuiltIn(CONTEXT_RECIPE, + registerBuiltIn(ScriptContext.RECIPE, RecipeScript.ScriptType.PRE.function, RecipeScript.ScriptType.POST.function ); // Effect hooks - registerBuiltIn(CONTEXT_EFFECT, + registerBuiltIn(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_ADD.function, EnumScriptType.ON_EFFECT_TICK.function, EnumScriptType.ON_EFFECT_REMOVE.function ); - // Forge hooks - just init, the rest are dynamically discovered - registerBuiltIn(CONTEXT_FORGE, - EnumScriptType.INIT.function + // Forge hooks - init + CNPC-specific hooks, the rest are dynamically discovered from Forge event classes + registerBuiltIn(ScriptContext.FORGE, + EnumScriptType.INIT.function, + EnumScriptType.CNPC_NATURAL_SPAWN.function ); } + /** + * Register built-in hooks for a ScriptContext. + */ + private void registerBuiltIn(ScriptContext context, String... hooks) { + registerBuiltIn(context.hookContext, hooks); + } + private void registerBuiltIn(String context, String... hooks) { List list = builtInHooks.get(context); if (list != null) { @@ -322,7 +322,7 @@ public class ScriptHookController implements IScriptHookHandler { @Override public String[] getContexts() { - return CONTEXTS.clone(); + return builtInHooks.keySet().toArray(new String[0]); } private boolean isBuiltInHook(String context, String functionName) { diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 37f58453d..677b3efde 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -13,7 +13,6 @@ import noppes.npcs.EventHooks; import noppes.npcs.LogWriter; import noppes.npcs.NBTTags; import noppes.npcs.api.IWorld; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigDebug; import noppes.npcs.config.ConfigScript; @@ -218,11 +217,6 @@ public class DataScript implements IScriptHandlerPacket { return ScriptContext.NPC; } - @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_NPC; - } - @Override public void requestData() { EventScriptPacket.Get(); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index b2ae04b98..821e542b4 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -3,22 +3,24 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.scripted.event.player.PlayerEvent; /** * Script handler for CustomEffect scripts. * Manages a single script container for effect hooks (onEffectAdd, onEffectTick, onEffectRemove). */ public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { - + /** The effect ID for packet communication. -1 if not bound to an effect. */ private int effectId = -1; - + /** * Create an unbound EffectScript (for server-side use). */ public EffectScript() { } - + /** * Create an EffectScript bound to a specific effect (for GUI use). * @param effectId The ID of the CustomEffect @@ -26,22 +28,27 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP public EffectScript(int effectId) { this.effectId = effectId; } - + @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_EFFECT; + public ScriptContext getContext() { + return ScriptContext.EFFECT; } - - + + @Override + public String noticeString() { + return effectId >= 0 ? "CustomEffect[" + effectId + "]" : "CustomEffect"; + } + + @Override public void requestData() { - if (effectId >= 0) + if (effectId >= 0) EffectScriptPacket.Get(effectId); } - + @Override public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { - if (effectId >= 0) + if (effectId >= 0) EffectScriptPacket.Save(effectId, index, totalCount, nbt); } diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index 0ecdf27c0..1c6f09da7 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -13,8 +13,6 @@ import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.world.ChunkWatchEvent; import net.minecraftforge.event.world.WorldEvent; import noppes.npcs.EventHooks; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; @@ -26,89 +24,65 @@ import java.io.IOException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; -public class ForgeDataScript implements IScriptHandlerPacket { +public class ForgeDataScript extends MultiScriptHandler { private static final Object HOOK_LOCK = new Object(); private static List cachedHooks; - private List scripts = new ArrayList<>(); - private String scriptLanguage = "ECMAScript"; - public long lastInited = -1L; + private long lastForgeUpdate = -1L; - private boolean enabled = false; public ForgeDataScript() { } - public void clear() { - this.scripts = new ArrayList<>(); - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); - } else { - this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - return compound; - } - - - public void callScript(String type, Event event) { - if (this.isEnabled()) { - if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastForgeUpdate > this.lastForgeUpdate) { - this.lastInited = ScriptController.Instance.lastLoaded; - this.lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; - - for (IScriptUnit script : this.scripts) { - if (script instanceof ScriptContainer) - ((ScriptContainer) script).errored = false; - } - - if (!type.equals("init")) { - EventHooks.onForgeInit(this); - } - } - - for (IScriptUnit script : this.scripts) { - if (script == null || script.hasErrored() || !script.hasCode()) - continue; - script.run(type, event); - } - } - } - public boolean isEnabled() { return this.enabled && ConfigScript.GlobalForgeScripts && ScriptController.HasStart && this.scripts.size() > 0; } + @Override + protected boolean canRunScripts() { + return isEnabled(); + } + @Override public ScriptContext getContext() { return ScriptContext.FORGE; } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_FORGE; + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastForgeUpdate > lastForgeUpdate; + } + + @Override + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; + + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; + } + } + + @Override + public void callScript(String type, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + if (!type.equals("init")) { + EventHooks.onForgeInit(this); + } + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(type, event); + } } @Override @@ -120,9 +94,11 @@ public class ForgeDataScript implements IScriptHandlerPacket { if (cachedHooks != null) return new ArrayList<>(cachedHooks); - List hookList = new ArrayList<>(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE)); + // Start with built-in + addon hooks from ScriptHookController + List hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(ScriptContext.FORGE.hookContext)); - ArrayList list = new ArrayList(); + // Dynamically discover Forge event classes + ArrayList list = new ArrayList<>(); try { list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); @@ -131,51 +107,50 @@ public class ForgeDataScript implements IScriptHandlerPacket { e.printStackTrace(); } - for (ClassPath.ClassInfo e1 : list) { - ClassPath.ClassInfo classLoader = e1; - Class infoClass = classLoader.load(); - ArrayList c = new ArrayList(Arrays.asList(infoClass.getDeclaredClasses())); - if (c.isEmpty()) { - c.add(infoClass); + for (ClassPath.ClassInfo classInfo : list) { + Class infoClass = classInfo.load(); + List> classes = new ArrayList<>(Arrays.asList(infoClass.getDeclaredClasses())); + if (classes.isEmpty()) { + classes.add(infoClass); } - Iterator var10 = c.iterator(); - while (var10.hasNext()) { - Class c1 = (Class) var10.next(); - if (!EntityEvent.EntityConstructing.class.isAssignableFrom(c1) - && !WorldEvent.PotentialSpawns.class.isAssignableFrom(c1) - && !TickEvent.RenderTickEvent.class.isAssignableFrom(c1) - && !TickEvent.ClientTickEvent.class.isAssignableFrom(c1) - && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(c1) - && !ItemTooltipEvent.class.isAssignableFrom(c1) - && Event.class.isAssignableFrom(c1) - && !Modifier.isAbstract(c1.getModifiers()) - && Modifier.isPublic(c1.getModifiers()) - && !ChunkEvent.class.isAssignableFrom(c1) - && !ChunkWatchEvent.class.isAssignableFrom(c1) - && !ChunkDataEvent.class.isAssignableFrom(c1)) { - String eventName = c1.getName(); - int i = eventName.lastIndexOf("."); - eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", "")); - hookList.add(eventName); + for (Class eventClass : classes) { + if (isValidForgeEvent(eventClass)) { + String eventName = eventClass.getName(); + int lastDot = eventName.lastIndexOf("."); + eventName = StringUtils.uncapitalize(eventName.substring(lastDot + 1).replace("$", "")); + + if (!hookList.contains(eventName)) { + hookList.add(eventName); + } } } } - hookList.add("onCNPCNaturalSpawn"); - - for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) { - if (!hookList.contains(addonHook)) { - hookList.add(addonHook); - } - } - cachedHooks = hookList; } return new ArrayList<>(cachedHooks); } + /** + * Check if a class is a valid Forge event that should be exposed as a hook. + */ + private boolean isValidForgeEvent(Class eventClass) { + return Event.class.isAssignableFrom(eventClass) + && Modifier.isPublic(eventClass.getModifiers()) + && !Modifier.isAbstract(eventClass.getModifiers()) + && !EntityEvent.EntityConstructing.class.isAssignableFrom(eventClass) + && !WorldEvent.PotentialSpawns.class.isAssignableFrom(eventClass) + && !TickEvent.RenderTickEvent.class.isAssignableFrom(eventClass) + && !TickEvent.ClientTickEvent.class.isAssignableFrom(eventClass) + && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(eventClass) + && !ItemTooltipEvent.class.isAssignableFrom(eventClass) + && !ChunkEvent.class.isAssignableFrom(eventClass) + && !ChunkWatchEvent.class.isAssignableFrom(eventClass) + && !ChunkDataEvent.class.isAssignableFrom(eventClass); + } + @Override public void requestData() { ForgeScriptPacket.Get(); @@ -186,33 +161,8 @@ public class ForgeDataScript implements IScriptHandlerPacket { ForgeScriptPacket.Save(index, totalCount, nbt); } + @Override public boolean isClient() { return false; } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean bo) { - this.enabled = bo; - } - - public String getLanguage() { - return this.scriptLanguage; - } - - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - - } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index 5561c255c..805ae7c8d 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -1,41 +1,20 @@ package noppes.npcs.controllers.data; -import com.google.common.base.Preconditions; -import cpw.mods.fml.common.eventhandler.Event; import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.util.ScriptToStringHelper; -import javax.annotation.CheckForNull; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; -import java.util.TreeMap; - -public class GlobalNPCDataScript implements IScriptHandlerPacket { - public List scripts = new ArrayList<>(); - public String scriptLanguage = "ECMAScript"; +public class GlobalNPCDataScript extends MultiScriptHandler { private EntityNPCInterface npc; private ICustomNpc npcAPI; - public long lastInited = -1L; private long lastNpcUpdate = -1L; - public boolean enabled = false; public GlobalNPCDataScript(EntityNPCInterface npc) { if (npc != null) { @@ -43,35 +22,9 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { } } - public void clear() { - this.scripts = new ArrayList<>(); - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); - } else { - this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - return compound; + @Override + protected boolean canRunScripts() { + return isEnabled(); } public boolean isEnabled() { @@ -84,8 +37,14 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_NPC; + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastGlobalNpcUpdate > lastNpcUpdate; + } + + @Override + protected void reInitScripts() { + super.reInitScripts(); + lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; } @Override @@ -98,62 +57,18 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { GlobalNPCScriptPacket.Save(index, totalCount, nbt); } + @Override + public boolean isClient() { + return this.npc != null && this.npc.isClientWorld(); + } @Override - public void callScript(String hookName, Event event) { - if (this.isEnabled()) { - if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastGlobalNpcUpdate > this.lastNpcUpdate) { - this.lastInited = ScriptController.Instance.lastLoaded; - this.lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; - - for (IScriptUnit script : this.scripts) { - if (script instanceof ScriptContainer) - ((ScriptContainer) script).errored = false; - } - } - - for (IScriptUnit script : this.scripts) { - if (script == null || script.hasErrored() || !script.hasCode()) - continue; - script.run(hookName, event); - } - } - } - - public boolean isClient() { - return this.npc.isClientWorld(); - } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean bo) { - this.enabled = bo; - } - - public String getLanguage() { - return this.scriptLanguage; - } - - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - public String noticeString() { if (this.npc == null) { return "Global script"; } else { BlockPos pos = new BlockPos(this.npc); - return GlobalNPCDataScript.toStringHelper(this.npc).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); + return ScriptToStringHelper.toStringHelper(this.npc).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } } @@ -163,198 +78,4 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { } return this.npcAPI; } - - - public static final class ToStringHelper { - private final String className; - private final GlobalNPCDataScript.ToStringHelper.ValueHolder holderHead; - private GlobalNPCDataScript.ToStringHelper.ValueHolder holderTail; - private boolean omitNullValues; - private boolean omitEmptyValues; - - private ToStringHelper(String className) { - this.holderHead = new GlobalNPCDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderHead; - this.omitNullValues = false; - this.omitEmptyValues = false; - this.className = (String) Preconditions.checkNotNull(className); - } - - public GlobalNPCDataScript.ToStringHelper omitNullValues() { - this.omitNullValues = true; - return this; - } - - public GlobalNPCDataScript.ToStringHelper add(String name, @CheckForNull Object value) { - return this.addHolder(name, value); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, boolean value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, char value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, double value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, float value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, int value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, long value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(@CheckForNull Object value) { - return this.addHolder(value); - } - - public GlobalNPCDataScript.ToStringHelper addValue(boolean value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(char value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(double value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(float value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(int value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(long value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - private static boolean isEmpty(Object value) { - if (value instanceof CharSequence) { - return ((CharSequence) value).length() == 0; - } else if (value instanceof Collection) { - return ((Collection) value).isEmpty(); - } else if (value instanceof Map) { - return ((Map) value).isEmpty(); - } else if (value instanceof Optional) { - return !((Optional) value).isPresent(); - } else if (value instanceof OptionalInt) { - return !((OptionalInt) value).isPresent(); - } else if (value instanceof OptionalLong) { - return !((OptionalLong) value).isPresent(); - } else if (value instanceof OptionalDouble) { - return !((OptionalDouble) value).isPresent(); - } else if (value instanceof com.google.common.base.Optional) { - return !((com.google.common.base.Optional) value).isPresent(); - } else if (value.getClass().isArray()) { - return Array.getLength(value) == 0; - } else { - return false; - } - } - - public String toString() { - boolean omitNullValuesSnapshot = this.omitNullValues; - boolean omitEmptyValuesSnapshot = this.omitEmptyValues; - String nextSeparator = ""; - StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); - for (GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { - Object value = valueHolder.value; - if (!(valueHolder instanceof GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder)) { - if (value == null) { - if (omitNullValuesSnapshot) { - continue; - } - } else if (omitEmptyValuesSnapshot && isEmpty(value)) { - continue; - } - } - builder.append(nextSeparator); - nextSeparator = ", "; - if (valueHolder.name != null) { - builder.append(valueHolder.name).append('='); - } - if (value != null && value.getClass().isArray()) { - Object[] objectArray = new Object[]{value}; - String arrayString = Arrays.deepToString(objectArray); - builder.append(arrayString, 1, arrayString.length() - 1); - } else { - builder.append(value); - } - } - return builder.append('}').toString(); - } - - private GlobalNPCDataScript.ToStringHelper.ValueHolder addHolder() { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = new GlobalNPCDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private GlobalNPCDataScript.ToStringHelper addHolder(@CheckForNull Object value) { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - return this; - } - - private GlobalNPCDataScript.ToStringHelper addHolder(String name, @CheckForNull Object value) { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder addUnconditionalHolder() { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = new GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private GlobalNPCDataScript.ToStringHelper addUnconditionalHolder(Object value) { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - return this; - } - - private GlobalNPCDataScript.ToStringHelper addUnconditionalHolder(String name, Object value) { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private static final class UnconditionalValueHolder extends GlobalNPCDataScript.ToStringHelper.ValueHolder { - private UnconditionalValueHolder() { - super(); - } - } - - private static class ValueHolder { - @CheckForNull - String name; - @CheckForNull - Object value; - @CheckForNull - GlobalNPCDataScript.ToStringHelper.ValueHolder next; - - private ValueHolder() { - } - } - } - - public static GlobalNPCDataScript.ToStringHelper toStringHelper(Object self) { - return new GlobalNPCDataScript.ToStringHelper(self.getClass().getSimpleName()); - } } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index b89c725a9..93892f2a6 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -134,10 +134,14 @@ public interface IScriptHandler { * Get the hook context identifier for this handler. * Used by ScriptHookController to look up available hooks. * - * @return The context string (e.g., IScriptHookHandler.CONTEXT_EFFECT), or empty if none + * By default, derives from getContext().hookContext. + * Override if a different hook context is needed. + * + * @return The context string (e.g., "effect", "npc"), or empty if none */ default String getHookContext() { - return ""; + ScriptContext ctx = getContext(); + return ctx != null ? ctx.hookContext : ""; } /** diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 4d01e1c46..1ee4537fb 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -2,7 +2,6 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; /** @@ -28,14 +27,9 @@ public class LinkedItemScript extends SingleScriptHandler implements IScriptHand this.linkedItemId = linkedItemId; } - @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_LINKED_ITEM; - } - @Override public ScriptContext getContext() { - return ScriptContext.ITEM; + return ScriptContext.LINKED_ITEM; } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java new file mode 100644 index 000000000..19758708d --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -0,0 +1,141 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NBTTags; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +/** + * Base class for script handlers that manage multiple script containers (tabs). + * Provides shared NBT serialization and common script execution patterns. + * + * Subclasses should: + * - Override getContext() to return the appropriate ScriptContext + * - Override getHookContext() if not using ScriptContext's hookContext + * - Override requestData() and sendSavePacket() for packet communication + * - Override isEnabled() if custom enable logic is needed + */ +public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { + + /** Tracks when scripts were last updated (for reload detection) */ + protected long lastUpdate = -1L; + + /** Console output storage */ + protected Map console = new TreeMap<>(); + + // ==================== NBT SERIALIZATION ==================== + + /** + * Read script data from NBT using the Tab0/Tab1/etc. format. + */ + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("Scripts")) { + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); + } else { + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); + } + + this.scriptLanguage = compound.getString("ScriptLanguage"); + normalizeLanguage(); + + this.enabled = compound.getBoolean("ScriptEnabled"); + + if (compound.hasKey("ScriptConsole")) { + this.console = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); + } + } + + /** + * Write script data to NBT using the Tab0/Tab1/etc. format. + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setInteger("TotalScripts", this.scripts.size()); + for (int i = 0; i < this.scripts.size(); i++) { + compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); + } + compound.setString("ScriptLanguage", this.scriptLanguage); + compound.setBoolean("ScriptEnabled", this.enabled); + compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(console)); + return compound; + } + + /** + * 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 (!ScriptController.Instance.languages.isEmpty()) { + this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; + } else { + this.scriptLanguage = "ECMAScript"; + } + } + } + + // ==================== SCRIPT EXECUTION ==================== + + /** + * Check if scripts need to be re-initialized due to reload. + * Subclasses can override to add additional update tracking (e.g., lastPlayerUpdate). + * + * @return true if scripts should be re-initialized + */ + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited; + } + + /** + * Called when scripts need re-initialization. + * Clears error state on all script containers. + */ + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).errored = false; + } + } + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) { + continue; + } + script.run(hookName, event); + } + } + + // ==================== CONSOLE HANDLING ==================== + + @Override + public Map getConsoleText() { + return console; + } + + @Override + public void clearConsole() { + console.clear(); + } + + // ==================== UTILITY ==================== + + public void clear() { + this.scripts = new ArrayList<>(); + this.console = new TreeMap<>(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index c329c4c6e..c5721f4c5 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -1,6 +1,5 @@ package noppes.npcs.controllers.data; -import com.google.common.base.Preconditions; import cpw.mods.fml.common.eventhandler.Event; import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import net.minecraft.entity.player.EntityPlayer; @@ -8,7 +7,6 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; @@ -18,35 +16,23 @@ import noppes.npcs.controllers.ScriptController; import noppes.npcs.janino.impl.JaninoPlayerScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerEvent; +import noppes.npcs.util.ScriptToStringHelper; -import javax.annotation.CheckForNull; -import java.lang.reflect.Array; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; import java.util.TreeMap; -public class PlayerDataScript implements IScriptHandlerPacket { - public List scripts = new ArrayList<>(); - public String scriptLanguage = "ECMAScript"; +public class PlayerDataScript extends MultiScriptHandler { private EntityPlayer player; private IPlayer playerAPI; private long lastPlayerUpdate = 0L; - public long lastInited = -1; - public boolean hadInteract = true; - private boolean enabled = false; - - private static Map console = new TreeMap(); - private static List errored = new ArrayList(); + // Static console/errored for IndividualPlayerScripts mode + private static Map staticConsole = new TreeMap<>(); + private static List errored = new ArrayList<>(); public PlayerDataScript(EntityPlayer player) { if (player != null) { @@ -54,38 +40,23 @@ public class PlayerDataScript implements IScriptHandlerPacket { } } + @Override public void clear() { - console = new TreeMap(); - errored = new ArrayList(); + staticConsole = new TreeMap<>(); + errored = new ArrayList<>(); scripts = new ArrayList<>(); } + @Override public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); - } else { - this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - console = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); + super.readFromNBT(compound); + staticConsole = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); } + @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(console)); + super.writeToNBT(compound); + compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(staticConsole)); return compound; } @@ -94,13 +65,13 @@ public class PlayerDataScript implements IScriptHandlerPacket { } @Override - public ScriptContext getContext() { - return ScriptContext.PLAYER; + protected boolean canRunScripts() { + return isEnabled(); } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_PLAYER; + public ScriptContext getContext() { + return ScriptContext.PLAYER; } @Override @@ -113,7 +84,6 @@ public class PlayerDataScript implements IScriptHandlerPacket { PlayerScriptPacket.Save(index, totalCount, nbt); } - @Override public void callScript(String hookName, Event event) { if (!isEnabled()) @@ -147,8 +117,8 @@ public class PlayerDataScript implements IScriptHandlerPacket { errored.add(i); } for (Entry entry : script.getConsole().entrySet()) { - if (!console.containsKey(entry.getKey())) - console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + if (!staticConsole.containsKey(entry.getKey())) + staticConsole.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); } script.clearConsole(); } @@ -176,41 +146,34 @@ public class PlayerDataScript implements IScriptHandlerPacket { } } + @Override public boolean isClient() { return this.player != null && this.player.isClientWorld(); } + @Override public boolean getEnabled() { return ScriptController.Instance.playerScripts.enabled; } + @Override public void setEnabled(boolean bo) { ScriptController.Instance.playerScripts.enabled = bo; this.enabled = bo; } + @Override public String getLanguage() { return ScriptController.Instance.playerScripts.scriptLanguage; } - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - + @Override public String noticeString() { if (this.player == null) { return "Global script"; } BlockPos pos = new BlockPos(this.player); - return PlayerDataScript.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); + return ScriptToStringHelper.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } public IPlayer getPlayer() { @@ -220,213 +183,21 @@ public class PlayerDataScript implements IScriptHandlerPacket { return this.playerAPI; } + @Override public Map getConsoleText() { if (ConfigScript.IndividualPlayerScripts) { - return console; + return staticConsole; } - return IScriptHandlerPacket.super.getConsoleText(); + return super.getConsoleText(); } @Override public void clearConsole() { if (ConfigScript.IndividualPlayerScripts) { - console.clear(); + staticConsole.clear(); return; } - IScriptHandlerPacket.super.clearConsole(); - } - - public static final class ToStringHelper { - private final String className; - private final PlayerDataScript.ToStringHelper.ValueHolder holderHead; - private PlayerDataScript.ToStringHelper.ValueHolder holderTail; - private boolean omitNullValues; - private boolean omitEmptyValues; - - private ToStringHelper(String className) { - this.holderHead = new PlayerDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderHead; - this.omitNullValues = false; - this.omitEmptyValues = false; - this.className = (String) Preconditions.checkNotNull(className); - } - - public PlayerDataScript.ToStringHelper omitNullValues() { - this.omitNullValues = true; - return this; - } - - public PlayerDataScript.ToStringHelper add(String name, @CheckForNull Object value) { - return this.addHolder(name, value); - } - - public PlayerDataScript.ToStringHelper add(String name, boolean value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, char value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, double value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, float value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, int value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, long value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(@CheckForNull Object value) { - return this.addHolder(value); - } - - public PlayerDataScript.ToStringHelper addValue(boolean value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(char value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(double value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(float value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(int value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(long value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - private static boolean isEmpty(Object value) { - if (value instanceof CharSequence) { - return ((CharSequence) value).length() == 0; - } else if (value instanceof Collection) { - return ((Collection) value).isEmpty(); - } else if (value instanceof Map) { - return ((Map) value).isEmpty(); - } else if (value instanceof Optional) { - return !((Optional) value).isPresent(); - } else if (value instanceof OptionalInt) { - return !((OptionalInt) value).isPresent(); - } else if (value instanceof OptionalLong) { - return !((OptionalLong) value).isPresent(); - } else if (value instanceof OptionalDouble) { - return !((OptionalDouble) value).isPresent(); - } else if (value instanceof com.google.common.base.Optional) { - return !((com.google.common.base.Optional) value).isPresent(); - } else if (value.getClass().isArray()) { - return Array.getLength(value) == 0; - } else { - return false; - } - } - - public String toString() { - boolean omitNullValuesSnapshot = this.omitNullValues; - boolean omitEmptyValuesSnapshot = this.omitEmptyValues; - String nextSeparator = ""; - StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); - for (PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { - Object value = valueHolder.value; - if (!(valueHolder instanceof PlayerDataScript.ToStringHelper.UnconditionalValueHolder)) { - if (value == null) { - if (omitNullValuesSnapshot) { - continue; - } - } else if (omitEmptyValuesSnapshot && isEmpty(value)) { - continue; - } - } - builder.append(nextSeparator); - nextSeparator = ", "; - if (valueHolder.name != null) { - builder.append(valueHolder.name).append('='); - } - if (value != null && value.getClass().isArray()) { - Object[] objectArray = new Object[]{value}; - String arrayString = Arrays.deepToString(objectArray); - builder.append(arrayString, 1, arrayString.length() - 1); - } else { - builder.append(value); - } - } - return builder.append('}').toString(); - } - - private PlayerDataScript.ToStringHelper.ValueHolder addHolder() { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = new PlayerDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private PlayerDataScript.ToStringHelper addHolder(@CheckForNull Object value) { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - return this; - } - - private PlayerDataScript.ToStringHelper addHolder(String name, @CheckForNull Object value) { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private PlayerDataScript.ToStringHelper.UnconditionalValueHolder addUnconditionalHolder() { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = new PlayerDataScript.ToStringHelper.UnconditionalValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private PlayerDataScript.ToStringHelper addUnconditionalHolder(Object value) { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - return this; - } - - private PlayerDataScript.ToStringHelper addUnconditionalHolder(String name, Object value) { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private static final class UnconditionalValueHolder extends PlayerDataScript.ToStringHelper.ValueHolder { - private UnconditionalValueHolder() { - super(); - } - } - - private static class ValueHolder { - @CheckForNull - String name; - @CheckForNull - Object value; - @CheckForNull - PlayerDataScript.ToStringHelper.ValueHolder next; - - private ValueHolder() { - } - } - } - - public static PlayerDataScript.ToStringHelper toStringHelper(Object self) { - return new PlayerDataScript.ToStringHelper(self.getClass().getSimpleName()); + super.clearConsole(); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index ab5c67fb5..803a1b30d 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -2,7 +2,7 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; /** * Script handler for Recipe scripts (both carpentry and anvil). @@ -33,8 +33,8 @@ public class RecipeScript extends SingleScriptHandler implements IScriptHandlerP } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_RECIPE; + public ScriptContext getContext() { + return ScriptContext.RECIPE; } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index def6ca7cf..4bb555375 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -64,4 +64,11 @@ public abstract class ScriptHandler implements IScriptHandler { public List getScripts() { return this.scripts; } + + /** + * Reset the lastInited timestamp to force re-initialization on next script call. + */ + public void resetLastInited() { + this.lastInited = -1; + } } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 2e17555d3..0ca2fa778 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -8,7 +8,6 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.IScriptUnit; @@ -72,11 +71,6 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus return ScriptContext.ITEM; } - @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_ITEM; - } - @Override public void requestData() { ItemScriptErrorPacket.Get(); diff --git a/src/main/java/noppes/npcs/util/ScriptToStringHelper.java b/src/main/java/noppes/npcs/util/ScriptToStringHelper.java new file mode 100644 index 000000000..d34e5c1cd --- /dev/null +++ b/src/main/java/noppes/npcs/util/ScriptToStringHelper.java @@ -0,0 +1,210 @@ +package noppes.npcs.util; + +import com.google.common.base.Preconditions; + +import javax.annotation.CheckForNull; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +/** + * Utility class for building toString representations of script handlers. + * Used by GlobalNPCDataScript and PlayerDataScript for notice strings. + */ +public final class ScriptToStringHelper { + private final String className; + private final ValueHolder holderHead; + private ValueHolder holderTail; + private boolean omitNullValues; + private boolean omitEmptyValues; + + private ScriptToStringHelper(String className) { + this.holderHead = new ValueHolder(); + this.holderTail = this.holderHead; + this.omitNullValues = false; + this.omitEmptyValues = false; + this.className = Preconditions.checkNotNull(className); + } + + public static ScriptToStringHelper toStringHelper(Object self) { + return new ScriptToStringHelper(self.getClass().getSimpleName()); + } + + public ScriptToStringHelper omitNullValues() { + this.omitNullValues = true; + return this; + } + + public ScriptToStringHelper add(String name, @CheckForNull Object value) { + return this.addHolder(name, value); + } + + public ScriptToStringHelper add(String name, boolean value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, char value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, double value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, float value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, int value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, long value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper addValue(@CheckForNull Object value) { + return this.addHolder(value); + } + + public ScriptToStringHelper addValue(boolean value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(char value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(double value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(float value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(int value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(long value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + private static boolean isEmpty(Object value) { + if (value instanceof CharSequence) { + return ((CharSequence) value).length() == 0; + } else if (value instanceof Collection) { + return ((Collection) value).isEmpty(); + } else if (value instanceof Map) { + return ((Map) value).isEmpty(); + } else if (value instanceof Optional) { + return !((Optional) value).isPresent(); + } else if (value instanceof OptionalInt) { + return !((OptionalInt) value).isPresent(); + } else if (value instanceof OptionalLong) { + return !((OptionalLong) value).isPresent(); + } else if (value instanceof OptionalDouble) { + return !((OptionalDouble) value).isPresent(); + } else if (value instanceof com.google.common.base.Optional) { + return !((com.google.common.base.Optional) value).isPresent(); + } else if (value.getClass().isArray()) { + return Array.getLength(value) == 0; + } else { + return false; + } + } + + public String toString() { + boolean omitNullValuesSnapshot = this.omitNullValues; + boolean omitEmptyValuesSnapshot = this.omitEmptyValues; + String nextSeparator = ""; + StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); + for (ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { + Object value = valueHolder.value; + if (!(valueHolder instanceof UnconditionalValueHolder)) { + if (value == null) { + if (omitNullValuesSnapshot) { + continue; + } + } else if (omitEmptyValuesSnapshot && isEmpty(value)) { + continue; + } + } + builder.append(nextSeparator); + nextSeparator = ", "; + if (valueHolder.name != null) { + builder.append(valueHolder.name).append('='); + } + if (value != null && value.getClass().isArray()) { + Object[] objectArray = new Object[]{value}; + String arrayString = Arrays.deepToString(objectArray); + builder.append(arrayString, 1, arrayString.length() - 1); + } else { + builder.append(value); + } + } + return builder.append('}').toString(); + } + + private ValueHolder addHolder() { + ValueHolder valueHolder = new ValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addHolder(@CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addHolder(String name, @CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private UnconditionalValueHolder addUnconditionalHolder() { + UnconditionalValueHolder valueHolder = new UnconditionalValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addUnconditionalHolder(Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addUnconditionalHolder(String name, Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private static final class UnconditionalValueHolder extends ValueHolder { + private UnconditionalValueHolder() { + super(); + } + } + + private static class ValueHolder { + @CheckForNull + String name; + @CheckForNull + Object value; + @CheckForNull + ValueHolder next; + + private ValueHolder() { + } + } +} From 9ed588728b6af0196aa03166d8678fb9aec746ad Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 15:04:44 -0500 Subject: [PATCH 44/64] Update ScriptForgeEventHandler.java --- src/main/java/noppes/npcs/ScriptForgeEventHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java index 649557b43..4294a6d9e 100644 --- a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java @@ -36,6 +36,10 @@ public class ScriptForgeEventHandler { @SubscribeEvent public void forgeEntity(Event event) { if (CustomNpcs.getServer() != null && ScriptController.Instance.forgeScripts.isEnabled()) { + if(FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT){ + return; + } + if (event instanceof TickEvent && !(((TickEvent) event).side == Side.SERVER && ((TickEvent) event).phase == TickEvent.Phase.START)) { return; } From 73f0d4e441409257056e640c4c36dccb9aff6c12 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:14:50 +0100 Subject: [PATCH 45/64] Bought goatee some braincells He lost his from AI usage. --- .../java/noppes/npcs/janino/JaninoScript.java | 42 ++++++++++--------- .../npcs/janino/impl/JaninoPlayerScript.java | 4 +- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8a217b8d0..a2202271e 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -52,11 +52,11 @@ public abstract class JaninoScript implements IScriptUnit { private final String[] defaultImports; - protected JaninoScript(Class type, String[] defaultImports) { + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; - this.builder = IScriptBodyBuilder.getBuilder(type, CustomNpcs.getClientCompiler()) - .setDefaultImports(defaultImports); + this.builder = IScriptBodyBuilder.getBuilder(type, isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) + .setDefaultImports(defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); @@ -67,6 +67,10 @@ public abstract class JaninoScript implements IScriptUnit { this.hookResolver = new JaninoHookResolver(type); } + protected JaninoScript(Class type, String[] defaultImports) { + this(type, defaultImports, true); + } + protected T getUnsafe() { return scriptBody.get(); } @@ -237,9 +241,9 @@ public abstract class JaninoScript implements IScriptUnit { */ @Override public void run(EnumScriptType type, Event event) { - if (type == null) + if (type == null) return; - + callFunction(type.function, event); } @@ -254,7 +258,7 @@ public abstract class JaninoScript implements IScriptUnit { public void run(String hookName, Object event) { callFunction(hookName, event); } - + @Override public String getScript() { return this.script; @@ -266,7 +270,7 @@ public abstract class JaninoScript implements IScriptUnit { this.evaluated = false; hookResolver.clearResolutionCaches(); } - + @Override public List getExternalScripts() { return this.externalScripts; @@ -278,17 +282,17 @@ public abstract class JaninoScript implements IScriptUnit { this.evaluated = false; hookResolver.clearResolutionCaches(); } - + @Override public TreeMap getConsole() { return this.console; } - + @Override public void clearConsole() { console.clear(); } - + @Override public void appendConsole(String message) { if (message != null && !message.isEmpty()) { @@ -304,18 +308,18 @@ public abstract class JaninoScript implements IScriptUnit { } } } - + @Override public String getLanguage() { return this.language; } - + @Override public void setLanguage(String language) { // JaninoScript is always Java, ignore attempts to change // This method exists for IScriptUnit interface compatibility } - + @Override public boolean hasCode() { if (!externalScripts.isEmpty()) @@ -330,11 +334,11 @@ public abstract class JaninoScript implements IScriptUnit { Method method = hookResolver.getMethodForDisplayName(hookName); if (method != null) return generateMethodStub(method); - + // Fallback for unknown hook names - generate basic override return String.format("public void %s() {\n \n}\n", hookName); } - + /** * Generates a stub string for a given Method. */ @@ -367,7 +371,7 @@ public abstract class JaninoScript implements IScriptUnit { typeCount.put(baseName, count); paramName = count == 1 ? baseName : baseName + (count - 1); } - + return typeName + " " + paramName; }) .collect(Collectors.joining(", ")); @@ -429,14 +433,14 @@ public abstract class JaninoScript implements IScriptUnit { return compound; } - + @Override public boolean hasErrored() { return this.errored; } - + @Override public void setErrored(boolean errored) { this.errored = errored; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index 15d822615..0e1f3b004 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -17,9 +17,9 @@ import noppes.npcs.janino.annotations.ScriptHook; * Janino (Java) script implementation for player events. */ public class JaninoPlayerScript extends JaninoScript { - + public JaninoPlayerScript() { - super(Functions.class, null); + super(Functions.class, null, false); } /** From cb364823a9aef9486e1fee917f8661e1264f853b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 22:28:20 +0200 Subject: [PATCH 46/64] Added ScriptContext based registerHook --- .../noppes/npcs/controllers/ScriptHookController.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index bc20eccff..a919de972 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -3,7 +3,6 @@ package noppes.npcs.controllers; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.RecipeScript; import java.util.ArrayList; @@ -230,6 +229,16 @@ public class ScriptHookController implements IScriptHookHandler { } } + @Override + public void registerHooks(ScriptContext context, String functionName) { + registerHook(context.hookContext, functionName); + } + + @Override + public void registerHooks(ScriptContext context, String... functionNames) { + registerHooks(context.hookContext, functionNames); + } + @Override public void registerHook(String context, String functionName) { if (context == null || functionName == null || functionName.isEmpty()) { From a18292b8ed579a9626159ff80d6b85e5666ee580 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 22:29:26 +0200 Subject: [PATCH 47/64] Used dynamic janino compiler for server side script handlers --- src/main/java/noppes/npcs/janino/JaninoScript.java | 2 +- src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a2202271e..f1a8f58e7 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -68,7 +68,7 @@ public abstract class JaninoScript implements IScriptUnit { } protected JaninoScript(Class type, String[] defaultImports) { - this(type, defaultImports, true); + this(type, defaultImports, false); } protected T getUnsafe() { diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index 0e1f3b004..5c644665d 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -19,7 +19,7 @@ import noppes.npcs.janino.annotations.ScriptHook; public class JaninoPlayerScript extends JaninoScript { public JaninoPlayerScript() { - super(Functions.class, null, false); + super(Functions.class, null); } /** From 13cb94d5e2e180404308247571b8537979aa3028 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 22:57:04 +0200 Subject: [PATCH 48/64] Forgot API ScriptHookController --- .../controllers/data/MultiScriptHandler.java | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java index 19758708d..d175eb3b9 100644 --- a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -21,13 +21,7 @@ import java.util.TreeMap; * - Override isEnabled() if custom enable logic is needed */ public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { - - /** Tracks when scripts were last updated (for reload detection) */ - protected long lastUpdate = -1L; - - /** Console output storage */ - protected Map console = new TreeMap<>(); - + // ==================== NBT SERIALIZATION ==================== /** @@ -44,10 +38,6 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip normalizeLanguage(); this.enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptConsole")) { - this.console = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); - } } /** @@ -60,7 +50,6 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip } compound.setString("ScriptLanguage", this.scriptLanguage); compound.setBoolean("ScriptEnabled", this.enabled); - compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(console)); return compound; } @@ -119,23 +108,4 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip script.run(hookName, event); } } - - // ==================== CONSOLE HANDLING ==================== - - @Override - public Map getConsoleText() { - return console; - } - - @Override - public void clearConsole() { - console.clear(); - } - - // ==================== UTILITY ==================== - - public void clear() { - this.scripts = new ArrayList<>(); - this.console = new TreeMap<>(); - } } From dfcab1e413a9c024bba42fb0013e3e6fa83b776b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 23:05:47 +0200 Subject: [PATCH 49/64] Moved clear to ScriptHandler --- .../java/noppes/npcs/controllers/data/PlayerDataScript.java | 3 +-- src/main/java/noppes/npcs/controllers/data/ScriptHandler.java | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index c5721f4c5..6549ed1e5 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -40,11 +40,10 @@ public class PlayerDataScript extends MultiScriptHandler { } } - @Override public void clear() { + super.clear(); staticConsole = new TreeMap<>(); errored = new ArrayList<>(); - scripts = new ArrayList<>(); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index 4bb555375..dd57b05bb 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -65,6 +65,9 @@ public abstract class ScriptHandler implements IScriptHandler { return this.scripts; } + public void clear() { + this.scripts = new ArrayList<>(); + } /** * Reset the lastInited timestamp to force re-initialization on next script call. */ From e6221c094070eeac161dc1a59defac592be13b38 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 16:24:48 -0500 Subject: [PATCH 50/64] Test Janino --- .../npcs/janino/JaninoHookResolver.java | 113 +++++++- .../java/noppes/npcs/janino/JaninoScript.java | 11 +- .../npcs/janino/impl/JaninoPlayerScript.java | 252 +++++------------- 3 files changed, 184 insertions(+), 192 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index 2c8eeb543..b77233b12 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -16,6 +16,15 @@ import java.util.stream.Collectors; * The resolver keeps simple caches for previously resolved signatures and * supports mapping enum-based hook names (via the {@code @ScriptHook} * annotation) to methods. + * + *

Dynamic Hook Support

+ * This resolver supports two modes: + *
    + *
  • Interface-only: Only methods defined in the interface are found (legacy behavior)
  • + *
  • Compiled class: Methods on the actual compiled script class are found, + * allowing users to define custom methods for Forge events, addon hooks, etc.
  • + *
+ * Use {@link #resolveHookMethod(String, Object[], Object)} to search the compiled instance. */ public final class JaninoHookResolver { private final Class interfaceType; @@ -30,6 +39,18 @@ public final class JaninoHookResolver { */ private final Map signatureCache = new HashMap<>(); + /** + * Cache for methods found on compiled classes (keyed by class + signature). + * Separate from signatureCache to handle recompilation. + */ + private final Map compiledClassCache = new HashMap<>(); + + /** + * Track which compiled class we've cached methods for. + * If the class changes (recompilation), we clear the cache. + */ + private Class lastCompiledClass; + /* Lazily built list of available hooks and a mapping for display names */ private boolean hookListInitialized; /* List of names displayed in GUI hooks list (accounts for overload names) */ @@ -52,10 +73,12 @@ public final class JaninoHookResolver { */ public void clearResolutionCaches() { signatureCache.clear(); + compiledClassCache.clear(); + lastCompiledClass = null; } /** - * Resolve a hook method by name and runtime argument values. + * Resolve a hook method by name and runtime argument values (interface-only mode). * * This first checks any enum-mapped methods (via {@code @ScriptHook}), * then a signature-based cache and finally attempts full candidate @@ -66,16 +89,79 @@ public final class JaninoHookResolver { * @return the resolved {@link Method} or {@code null} if none matched */ public Method resolveHookMethod(String hookName, Object[] args) { + return resolveHookMethod(hookName, args, null); + } + + /** + * Resolve a hook method by name, searching both the compiled class and interface. + * + *

This enables dynamic hook support: users can define methods for any hook + * (Forge events, addon hooks, etc.) without needing them in the interface definition.

+ * + *

Resolution order:

+ *
    + *
  1. Enum-mapped methods (via {@code @ScriptHook})
  2. + *
  3. Compiled class methods (user-defined, including custom hooks)
  4. + *
  5. Interface methods (base hooks with defaults)
  6. + *
  7. Fallback to no-arg versions
  8. + *
+ * + *

Performance: Method lookups are cached. First call per hook does reflection + * (~microseconds), subsequent calls use cache (essentially free).

+ * + * @param hookName hook method name (or enum function name) + * @param args runtime arguments to use for overload resolution + * @param compiledInstance the compiled script instance (or null for interface-only) + * @return the resolved {@link Method} or {@code null} if none matched + */ + public Method resolveHookMethod(String hookName, Object[] args, Object compiledInstance) { if (hookName == null || hookName.isEmpty()) return null; + // 1. Check enum-mapped methods first (fastest path) Method method = enumHookLookup.get(hookName); if (method != null) return method; Object[] safeArgs = args == null ? new Object[0] : args; - String signatureKey = buildSignatureKey(hookName, safeArgs); + + // 2. If we have a compiled instance, search its class first + if (compiledInstance != null) { + Class compiledClass = compiledInstance.getClass(); + + // Clear cache if compiled class changed (recompilation) + if (lastCompiledClass != compiledClass) { + compiledClassCache.clear(); + lastCompiledClass = compiledClass; + } + + // Check compiled class cache + method = compiledClassCache.get(signatureKey); + if (method != null) + return method; + + // Search compiled class for matching method + method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); + if (method != null) { + compiledClassCache.put(signatureKey, method); + return method; + } + + // Try no-arg version on compiled class + String noArgKey = buildSignatureKey(hookName, new Object[0]); + method = compiledClassCache.get(noArgKey); + if (method != null) + return method; + + method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); + if (method != null) { + compiledClassCache.put(noArgKey, method); + return method; + } + } + + // 3. Fall back to interface-only search method = signatureCache.get(signatureKey); if (method != null) return method; @@ -197,17 +283,32 @@ public final class JaninoHookResolver { /** * Internal resolution routine. Attempts to find the best matching method - * on the provided interface by comparing parameter counts and a simple + * on the provided class by comparing parameter counts and a simple * match scoring function (see {@link #scoreMethodMatch}). + * + *

This searches all public methods (including inherited) to support + * both interface-defined hooks and user-defined custom methods.

+ * + * @param targetClass the class to search (interface or compiled class) + * @param hookName the method name to find + * @param args runtime arguments for overload resolution + * @return the best matching method, or null if none found */ - private static Method resolveHookMethodInternal(Class interfaceType, String hookName, Object[] args) { + private static Method resolveHookMethodInternal(Class targetClass, String hookName, Object[] args) { Object[] safeArgs = args == null ? new Object[0] : args; Method best = null; int bestScore = Integer.MAX_VALUE; - for (Method method : interfaceType.getDeclaredMethods()) { - if (!method.getName().equals(hookName) || Modifier.isFinal(method.getModifiers())) + // Use getMethods() to include inherited methods (important for compiled classes) + for (Method method : targetClass.getMethods()) { + if (!method.getName().equals(hookName)) + continue; + + // Skip final methods and Object methods + if (Modifier.isFinal(method.getModifiers())) + continue; + if (method.getDeclaringClass() == Object.class) continue; Class[] paramTypes = method.getParameterTypes(); diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a2202271e..955aed3bc 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -202,15 +202,18 @@ public abstract class JaninoScript implements IScriptUnit { if (hookName == null || hookName.isEmpty()) return null; - Method method = hookResolver.resolveHookMethod(hookName, args); - if (method == null) - return null; - + // Ensure compiled first so we can search the compiled class ensureCompiled(); T t = getUnsafe(); if (t == null) return null; + // Pass compiled instance to enable dynamic hook resolution + // This allows users to define methods for ANY hook (Forge events, addon hooks, etc.) + Method method = hookResolver.resolveHookMethod(hookName, args, t); + if (method == null) + return null; + Method invokeMethod = method; Object[] invokeArgs = args == null ? new Object[0] : args; diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index 0e1f3b004..10cb6d556 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -1,13 +1,6 @@ package noppes.npcs.janino.impl; -import noppes.npcs.api.event.IAnimationEvent; -import noppes.npcs.api.event.ICustomGuiEvent; -import noppes.npcs.api.event.ICustomNPCsEvent; -import noppes.npcs.api.event.IDialogEvent; -import noppes.npcs.api.event.IFactionEvent; -import noppes.npcs.api.event.IPartyEvent; import noppes.npcs.api.event.IPlayerEvent; -import noppes.npcs.api.event.IQuestEvent; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.janino.JaninoScript; import noppes.npcs.janino.annotations.ParamName; @@ -15,6 +8,35 @@ import noppes.npcs.janino.annotations.ScriptHook; /** * Janino (Java) script implementation for player events. + * + *

Dynamic Hook Support

+ * With the new dynamic hook resolution, users can define methods for ANY hook - + * not just those defined in the Functions interface. Simply write a public method + * with the hook name and matching parameter type: + * + *
{@code
+ * // Standard hook (defined in interface)
+ * public void init(IPlayerEvent.InitEvent event) {
+ *     // ...
+ * }
+ *
+ * // Custom addon hook (NOT in interface - still works!)
+ * public void myAddonHook(MyAddonEvent event) {
+ *     // ...
+ * }
+ *
+ * // Any hook by name
+ * public void someCustomHook(Object event) {
+ *     // ...
+ * }
+ * }
+ * + * The interface below defines core hooks for: + *
    + *
  • EnumScriptType mapping (so "init" maps to init() method)
  • + *
  • GUI hook list display
  • + *
  • Stub generation with correct parameter types
  • + *
*/ public class JaninoPlayerScript extends JaninoScript { @@ -23,236 +45,102 @@ public class JaninoPlayerScript extends JaninoScript Date: Thu, 22 Jan 2026 16:59:13 -0500 Subject: [PATCH 51/64] Potential Fix to Usuable Type names --- .../controllers/ScriptHookController.java | 18 +++++++--- .../java/noppes/npcs/janino/JaninoScript.java | 34 ++++++++++++++++--- .../npcs/janino/impl/JaninoNpcScript.java | 15 +++++++- .../npcs/janino/impl/JaninoPlayerScript.java | 19 ++++++++++- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index a919de972..c5af0929b 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -229,14 +229,24 @@ public class ScriptHookController implements IScriptHookHandler { } } - @Override - public void registerHooks(ScriptContext context, String functionName) { + /** + * Convenience method to register a hook using ScriptContext. + * @param context The script context + * @param functionName The hook function name + */ + public void registerHook(ScriptContext context, String functionName) { registerHook(context.hookContext, functionName); } - @Override + /** + * Convenience method to register multiple hooks using ScriptContext. + * @param context The script context + * @param functionNames The hook function names + */ public void registerHooks(ScriptContext context, String... functionNames) { - registerHooks(context.hookContext, functionNames); + for (String fn : functionNames) { + registerHook(context.hookContext, fn); + } } @Override diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 7d61ead92..a7e6389aa 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -344,6 +344,7 @@ public abstract class JaninoScript implements IScriptUnit { /** * Generates a stub string for a given Method. + * Handles nested classes properly (e.g., IPlayerEvent.InitEvent instead of just InitEvent). */ public static String generateMethodStub(Method method) { String mods = Modifier.toString(method.getModifiers()); @@ -352,13 +353,14 @@ public abstract class JaninoScript implements IScriptUnit { if (!mods.isEmpty()) mods += " "; - String returnTypeStr = method.getReturnType().getSimpleName(); + String returnTypeStr = getUsableTypeName(method.getReturnType()); String name = method.getName(); Map typeCount = new HashMap<>(); String params = Arrays.stream(method.getParameters()) .map(p -> { - String typeName = p.getType().getSimpleName(); + String typeName = getUsableTypeName(p.getType()); + String simpleTypeName = p.getType().getSimpleName(); // Check for @ParamName annotation first ParamName paramNameAnnotation = p.getAnnotation(ParamName.class); @@ -368,8 +370,8 @@ public abstract class JaninoScript implements IScriptUnit { // Use annotated name paramName = paramNameAnnotation.value(); } else { - // Fall back to generated name - String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + // Fall back to generated name based on simple name + String baseName = Character.toLowerCase(simpleTypeName.charAt(0)) + simpleTypeName.substring(1); int count = typeCount.getOrDefault(baseName, 0) + 1; typeCount.put(baseName, count); paramName = count == 1 ? baseName : baseName + (count - 1); @@ -397,6 +399,30 @@ public abstract class JaninoScript implements IScriptUnit { return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); } + /** + * Gets a usable type name for stub generation. + * For nested classes like IPlayerEvent.InitEvent, returns "IPlayerEvent.InitEvent" + * For top-level classes, returns the simple name. + * For primitives, returns the primitive name. + */ + private static String getUsableTypeName(Class type) { + if (type.isPrimitive() || type.getEnclosingClass() == null) { + return type.getSimpleName(); + } + + // Build the nested class chain: OuterClass.InnerClass.DeeperClass + StringBuilder sb = new StringBuilder(); + Class current = type; + while (current != null) { + if (sb.length() > 0) { + sb.insert(0, "."); + } + sb.insert(0, current.getSimpleName()); + current = current.getEnclosingClass(); + } + return sb.toString(); + } + public List getHookList() { return hookResolver.getDisplayHookList(); diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 5b3ec4dbd..1af8dea35 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -14,8 +14,21 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoNpcScript extends JaninoScript { + /** + * Default imports for NPC scripts. + * These are automatically available in user code without explicit import statements. + */ + private static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.event.INpcEvent", + "noppes.npcs.api.event.IProjectileEvent", + "noppes.npcs.api.event.IAbilityEvent", + "noppes.npcs.api.entity.ICustomNpc", + "noppes.npcs.api.entity.IPlayer", + "noppes.npcs.api.NpcAPI" + }; + public JaninoNpcScript() { - super(Functions.class, null); + super(Functions.class, DEFAULT_IMPORTS); } /** diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index d535f2e24..c755c700c 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -40,8 +40,25 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoPlayerScript extends JaninoScript { + /** + * Default imports for player scripts. + * These are automatically available in user code without explicit import statements. + */ + private static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.event.IPlayerEvent", + "noppes.npcs.api.event.IQuestEvent", + "noppes.npcs.api.event.IDialogEvent", + "noppes.npcs.api.event.IFactionEvent", + "noppes.npcs.api.event.IPartyEvent", + "noppes.npcs.api.event.ICustomGuiEvent", + "noppes.npcs.api.event.IAnimationEvent", + "noppes.npcs.api.event.ICustomNPCsEvent", + "noppes.npcs.api.entity.IPlayer", + "noppes.npcs.api.NpcAPI" + }; + public JaninoPlayerScript() { - super(Functions.class, null); + super(Functions.class, DEFAULT_IMPORTS, false); } /** From f4a5bbd1f6405de4f1aa6202e470506218e08691 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 17:28:12 -0500 Subject: [PATCH 52/64] Janino Kam --- src/api | 2 +- .../java/noppes/npcs/janino/JaninoScript.java | 53 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/api b/src/api index c962f678a..b90df20fc 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit c962f678a4f1bb83194d805cf3b5681fd5c70b1a +Subproject commit b90df20fc42f11f7ff69e79792881ebc209735eb diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a7e6389aa..8734304d9 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -338,8 +338,57 @@ public abstract class JaninoScript implements IScriptUnit { if (method != null) return generateMethodStub(method); - // Fallback for unknown hook names - generate basic override - return String.format("public void %s() {\n \n}\n", hookName); + // Fallback: try to find the event class by naming convention + String eventType = findEventTypeForHook(hookName); + if (eventType != null) { + return String.format("public void %s(%s event) {\n \n}\n", hookName, eventType); + } + + // Last resort: generic Object parameter + return String.format("public void %s(Object event) {\n \n}\n", hookName); + } + + /** + * Tries to find the event type for a hook by searching API event interfaces. + * Uses naming convention: hookName "questStart" -> event class "QuestStartEvent" + * + * @param hookName The hook name (e.g., "questStart", "dialogOpen") + * @return The usable type name (e.g., "IQuestEvent.QuestStartEvent") or null if not found + */ + private String findEventTypeForHook(String hookName) { + if (hookName == null || hookName.isEmpty()) + return null; + + // Convert hook name to expected event class name + // questStart -> QuestStartEvent + String expectedEventName = Character.toUpperCase(hookName.charAt(0)) + hookName.substring(1) + "Event"; + + // Search in known API event interfaces + Class[] eventInterfaces = { + noppes.npcs.api.event.IPlayerEvent.class, + noppes.npcs.api.event.INpcEvent.class, + noppes.npcs.api.event.IQuestEvent.class, + noppes.npcs.api.event.IDialogEvent.class, + noppes.npcs.api.event.IFactionEvent.class, + noppes.npcs.api.event.IPartyEvent.class, + noppes.npcs.api.event.ICustomGuiEvent.class, + noppes.npcs.api.event.IAnimationEvent.class, + noppes.npcs.api.event.IBlockEvent.class, + noppes.npcs.api.event.IItemEvent.class, + noppes.npcs.api.event.IProjectileEvent.class, + noppes.npcs.api.event.IAbilityEvent.class, + noppes.npcs.api.event.IForgeEvent.class + }; + + for (Class eventInterface : eventInterfaces) { + for (Class nested : eventInterface.getDeclaredClasses()) { + if (nested.getSimpleName().equals(expectedEventName)) { + return getUsableTypeName(nested); + } + } + } + + return null; } /** From bee4d18bc6aeef536c99d5b74f602bb77b262627 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 17:59:03 -0500 Subject: [PATCH 53/64] Hook Context --- src/api | 2 +- .../npcs/controllers/HookDefinition.java | 307 +++++++++ .../controllers/ScriptHookController.java | 624 +++++++++++++----- .../java/noppes/npcs/janino/JaninoScript.java | 156 +++-- .../npcs/janino/impl/JaninoNpcScript.java | 6 + .../npcs/janino/impl/JaninoPlayerScript.java | 6 + 6 files changed, 903 insertions(+), 198 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/HookDefinition.java diff --git a/src/api b/src/api index b90df20fc..7a337a570 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit b90df20fc42f11f7ff69e79792881ebc209735eb +Subproject commit 7a337a5704c442fb98a0fd4e0ba26210cb54af98 diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java new file mode 100644 index 000000000..b8b78fe70 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -0,0 +1,307 @@ +package noppes.npcs.controllers; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import noppes.npcs.api.handler.IHookDefinition; +import noppes.npcs.janino.annotations.ParamName; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +/** + * Implementation of {@link IHookDefinition} with a fluent builder API. + * + *

Usage Examples

+ *
{@code
+ * // Full definition with class reference
+ * HookDefinition.builder("onDBCTransform")
+ *     .eventClass(IDBCEvent.TransformEvent.class)
+ *     .paramNames("event")
+ *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ *     .cancelable(true)
+ *     .build();
+ *
+ * // String-based (avoids classloading issues)
+ * HookDefinition.builder("onDBCPowerUp")
+ *     .eventClass("com.dbc.api.event.IDBCEvent$PowerUpEvent")
+ *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ *     .build();
+ *
+ * // Simple hook (backward compat, no metadata)
+ * HookDefinition.simple("legacyHook");
+ * }
+ */ +public class HookDefinition implements IHookDefinition { + + private final String hookName; + private final String eventClassName; + private final String[] paramNames; + private final String[] requiredImports; + private final boolean cancelable; + + // Cached resolved class (transient - not serialized) + private transient Class cachedEventClass; + private transient boolean classResolutionAttempted; + + private HookDefinition(Builder builder) { + this.hookName = builder.hookName; + this.eventClassName = builder.eventClassName; + this.paramNames = builder.paramNames != null ? builder.paramNames : new String[]{"event"}; + this.requiredImports = builder.requiredImports != null ? builder.requiredImports : new String[0]; + this.cancelable = builder.cancelable; + } + + // ==================== IHookDefinition Implementation ==================== + + @Override + public String hookName() { + return hookName; + } + + @Override + public String eventClassName() { + return eventClassName; + } + + @Override + public String[] paramNames() { + return paramNames; + } + + @Override + public String[] requiredImports() { + return requiredImports; + } + + @Override + public boolean isCancelable() { + return cancelable; + } + + @Override + public Class getEventClass() { + if (classResolutionAttempted) { + return cachedEventClass; + } + + classResolutionAttempted = true; + if (eventClassName == null || eventClassName.isEmpty()) { + return null; + } + + try { + cachedEventClass = Class.forName(eventClassName); + } catch (ClassNotFoundException e) { + cachedEventClass = null; + } + + return cachedEventClass; + } + + // ==================== Factory Methods ==================== + + /** + * Create a new builder for a hook definition. + * + * @param hookName The hook function name + * @return A new builder instance + */ + public static Builder builder(String hookName) { + return new Builder(hookName); + } + + /** + * Create a simple hook definition with just a name (no metadata). + * For backward compatibility with legacy hook registration. + * + * @param hookName The hook function name + * @return A minimal hook definition + */ + public static HookDefinition simple(String hookName) { + return builder(hookName).build(); + } + + /** + * Create a hook definition from an interface method. + * Automatically extracts metadata from the method signature and annotations. + * + * @param hookName The hook name (may differ from method name) + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(String hookName, Method method) { + Builder builder = builder(hookName); + + // Extract event type from first parameter + if (method.getParameterCount() > 0) { + Class eventType = method.getParameterTypes()[0]; + builder.eventClass(eventType); + + // Extract @Cancelable annotation + if (eventType.isAnnotationPresent(Cancelable.class)) { + builder.cancelable(true); + } + + // Build required imports from event type + String importName = getImportForClass(eventType); + if (importName != null) { + builder.requiredImports(importName); + } + } + + // Extract parameter names from @ParamName annotations + Parameter[] params = method.getParameters(); + if (params.length > 0) { + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + ParamName annotation = params[i].getAnnotation(ParamName.class); + if (annotation != null) { + names[i] = annotation.value(); + } else { + names[i] = params[i].getType().getSimpleName().toLowerCase(); + } + } + builder.paramNames(names); + } + + return builder.build(); + } + + /** + * Get the import statement needed for a class. + * For nested classes, returns the enclosing class. + */ + private static String getImportForClass(Class clazz) { + if (clazz == null || clazz.isPrimitive()) { + return null; + } + + // For nested classes, get the top-level enclosing class + Class enclosing = clazz; + while (enclosing.getEnclosingClass() != null) { + enclosing = enclosing.getEnclosingClass(); + } + + String name = enclosing.getName(); + // Skip java.lang classes + if (name.startsWith("java.lang.")) { + return null; + } + + return name; + } + + // ==================== Builder ==================== + + /** + * Builder for creating HookDefinition instances with a fluent API. + */ + public static class Builder { + private final String hookName; + private String eventClassName; + private String[] paramNames; + private String[] requiredImports; + private boolean cancelable; + + private Builder(String hookName) { + if (hookName == null || hookName.isEmpty()) { + throw new IllegalArgumentException("hookName cannot be null or empty"); + } + this.hookName = hookName; + } + + /** + * Set the event class by name (avoids classloading issues). + * + * @param className Full qualified class name (use $ for nested classes) + * @return this builder + */ + public Builder eventClass(String className) { + this.eventClassName = className; + return this; + } + + /** + * Set the event class directly (convenience method). + * The class name will be extracted automatically. + * + * @param clazz The event class + * @return this builder + */ + public Builder eventClass(Class clazz) { + if (clazz != null) { + this.eventClassName = clazz.getName(); + // Auto-detect cancelable + if (clazz.isAnnotationPresent(Cancelable.class)) { + this.cancelable = true; + } + } + return this; + } + + /** + * Set parameter names for stub generation. + * + * @param names Parameter names + * @return this builder + */ + public Builder paramNames(String... names) { + this.paramNames = names; + return this; + } + + /** + * Set required imports for this hook's event type. + * + * @param imports Fully qualified class names to import + * @return this builder + */ + public Builder requiredImports(String... imports) { + this.requiredImports = imports; + return this; + } + + /** + * Set whether the event is cancelable. + * + * @param cancelable true if the event can be canceled + * @return this builder + */ + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + return this; + } + + /** + * Build the hook definition. + * + * @return A new HookDefinition instance + */ + public HookDefinition build() { + return new HookDefinition(this); + } + } + + // ==================== Object Methods ==================== + + @Override + public String toString() { + return "HookDefinition{" + + "hookName='" + hookName + '\'' + + ", eventClassName='" + eventClassName + '\'' + + ", cancelable=" + cancelable + + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof HookDefinition)) return false; + HookDefinition other = (HookDefinition) obj; + return hookName.equals(other.hookName); + } + + @Override + public int hashCode() { + return hookName.hashCode(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index c5af0929b..abefb0930 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,5 +1,6 @@ package noppes.npcs.controllers; +import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; @@ -14,17 +15,33 @@ import java.util.Map; /** * Controller for managing script hooks. * Allows addon mods to register custom hooks that appear in script editor GUIs. + * + *

Rich Hook Registration

+ * Supports full hook definitions with metadata for proper stub generation: + *
{@code
+ * ScriptHookController.Instance.registerHookDefinition("player",
+ *     HookDefinition.builder("onDBCTransform")
+ *         .eventClass(IDBCEvent.TransformEvent.class)
+ *         .requiredImports("com.dbc.api.event.IDBCEvent")
+ *         .build());
+ * }
*/ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Built-in hooks per context + // Built-in hooks per context (legacy string list) private final Map> builtInHooks = new HashMap<>(); - // Addon-registered hooks per context + // Addon-registered hooks per context (legacy string list) private final Map> addonHooks = new HashMap<>(); + // Rich hook definitions per context (NEW) + private final Map> hookDefinitions = new HashMap<>(); + + // Revision counter for cache invalidation (NEW) + private int hookRevision = 0; + public ScriptHookController() { Instance = this; initializeBuiltInHooks(); @@ -36,160 +53,24 @@ public class ScriptHookController implements IScriptHookHandler { if (!context.hookContext.isEmpty()) { builtInHooks.put(context.hookContext, new ArrayList<>()); addonHooks.put(context.hookContext, new ArrayList<>()); + hookDefinitions.put(context.hookContext, new HashMap<>()); } } - // NPC hooks - registerBuiltIn(ScriptContext.NPC, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.DIALOG.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLED.function, - EnumScriptType.ATTACK_MELEE.function, - EnumScriptType.ATTACK_SWING.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TARGET.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.KILLS.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.TIMER.function, - EnumScriptType.TARGET_LOST.function, - EnumScriptType.PROJECTILE_TICK.function, - EnumScriptType.PROJECTILE_IMPACT.function, - EnumScriptType.ABILITY_TICK.function, - EnumScriptType.ABILITY_EXECUTE.function, - EnumScriptType.ABILITY_HIT.function, - EnumScriptType.ABILITY_START.function, - EnumScriptType.ABILITY_INTERRUPT.function, - EnumScriptType.ABILITY_COMPLETE.function - ); + // NPC hooks with full definitions + initializeNpcHooks(); - // Player hooks - registerBuiltIn(ScriptContext.PLAYER, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.ATTACKED.function, - EnumScriptType.DAMAGED_ENTITY.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLS.function, - EnumScriptType.KILLED.function, - EnumScriptType.DROP.function, - EnumScriptType.RESPAWN.function, - EnumScriptType.BREAK_BLOCK.function, - EnumScriptType.CHAT.function, - EnumScriptType.LOGIN.function, - EnumScriptType.LOGOUT.function, - EnumScriptType.KEY_PRESSED.function, - EnumScriptType.MOUSE_CLICKED.function, - EnumScriptType.TOSS.function, - EnumScriptType.PICKUP.function, - EnumScriptType.PICKUP_XP.function, - EnumScriptType.RANGED_CHARGE.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TIMER.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function, - EnumScriptType.CONTAINER_OPEN.function, - EnumScriptType.USE_HOE.function, - EnumScriptType.BONEMEAL.function, - EnumScriptType.FILL_BUCKET.function, - EnumScriptType.JUMP.function, - EnumScriptType.FALL.function, - EnumScriptType.WAKE_UP.function, - EnumScriptType.SLEEP.function, - EnumScriptType.PLAYSOUND.function, - EnumScriptType.LIGHTNING.function, - EnumScriptType.CHANGED_DIM.function, - EnumScriptType.QUEST_START.function, - EnumScriptType.QUEST_COMPLETED.function, - EnumScriptType.QUEST_TURNIN.function, - EnumScriptType.FACTION_POINTS.function, - EnumScriptType.DIALOG_OPEN.function, - EnumScriptType.DIALOG_OPTION.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.SCRIPT_COMMAND.function, - EnumScriptType.CUSTOM_GUI_CLOSED.function, - EnumScriptType.CUSTOM_GUI_BUTTON.function, - EnumScriptType.CUSTOM_GUI_SLOT.function, - EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, - EnumScriptType.CUSTOM_GUI_SCROLL.function, - EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, - EnumScriptType.PARTY_QUEST_COMPLETED.function, - EnumScriptType.PARTY_QUEST_SET.function, - EnumScriptType.PARTY_QUEST_TURNED_IN.function, - EnumScriptType.PARTY_INVITE.function, - EnumScriptType.PARTY_KICK.function, - EnumScriptType.PARTY_LEAVE.function, - EnumScriptType.PARTY_DISBAND.function, - EnumScriptType.ANIMATION_START.function, - EnumScriptType.ANIMATION_END.function, - EnumScriptType.ANIMATION_FRAME_ENTER.function, - EnumScriptType.ANIMATION_FRAME_EXIT.function, - EnumScriptType.PROFILE_CHANGE.function, - EnumScriptType.PROFILE_REMOVE.function, - EnumScriptType.PROFILE_CREATE.function, - EnumScriptType.ON_EFFECT_ADD.function, - EnumScriptType.ON_EFFECT_TICK.function, - EnumScriptType.ON_EFFECT_REMOVE.function - ); + // Player hooks with full definitions + initializePlayerHooks(); - // Block hooks - registerBuiltIn(ScriptContext.BLOCK, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.FALLEN_UPON.function, - EnumScriptType.REDSTONE.function, - EnumScriptType.BROKEN.function, - EnumScriptType.EXPLODED.function, - EnumScriptType.RAIN_FILLED.function, - EnumScriptType.NEIGHBOR_CHANGED.function, - EnumScriptType.CLICKED.function, - EnumScriptType.HARVESTED.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.TIMER.function - ); + // Block hooks with full definitions + initializeBlockHooks(); - // Item hooks - registerBuiltIn(ScriptContext.ITEM, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); + // Item hooks with full definitions + initializeItemHooks(); - // Linked item hooks - registerBuiltIn(ScriptContext.LINKED_ITEM, - EnumScriptType.LINKED_ITEM_BUILD.function, - EnumScriptType.LINKED_ITEM_VERSION.function, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); + // Linked item hooks with full definitions + initializeLinkedItemHooks(); // Recipe hooks registerBuiltIn(ScriptContext.RECIPE, @@ -211,6 +92,372 @@ public class ScriptHookController implements IScriptHookHandler { ); } + private void initializeNpcHooks() { + String ctx = ScriptContext.NPC.hookContext; + String npcImport = "noppes.npcs.api.event.INpcEvent"; + String projImport = "noppes.npcs.api.event.IProjectileEvent"; + String abilityImport = "noppes.npcs.api.event.IAbilityEvent"; + + // Core lifecycle + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); + + // Interaction + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG.function, + "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, + "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, + "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); + + // Combat + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, + "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, + "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, + "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_MELEE.function, + "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_SWING.function, + "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, + "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TARGET.function, + "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TARGET_LOST.function, + "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); + + // Projectile events + registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_TICK.function, + "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_IMPACT.function, + "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); + + // Ability events + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_START.function, + "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_EXECUTE.function, + "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_HIT.function, + "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_TICK.function, + "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_INTERRUPT.function, + "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_COMPLETE.function, + "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); + } + + private void initializePlayerHooks() { + String ctx = ScriptContext.PLAYER.hookContext; + String playerImport = "noppes.npcs.api.event.IPlayerEvent"; + String questImport = "noppes.npcs.api.event.IQuestEvent"; + String dialogImport = "noppes.npcs.api.event.IDialogEvent"; + String factionImport = "noppes.npcs.api.event.IFactionEvent"; + String partyImport = "noppes.npcs.api.event.IPartyEvent"; + String guiImport = "noppes.npcs.api.event.ICustomGuiEvent"; + String animImport = "noppes.npcs.api.event.IAnimationEvent"; + + // Core lifecycle + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); + + // Combat + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACKED.function, + "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, + "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED_ENTITY.function, + "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, + "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, + "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); + + // Interaction + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.BREAK_BLOCK.function, + "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CHAT.function, + "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); + + // Connection + registerBuiltInDefinition(ctx, EnumScriptType.LOGIN.function, + "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.LOGOUT.function, + "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RESPAWN.function, + "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); + + // Input + registerBuiltInDefinition(ctx, EnumScriptType.KEY_PRESSED.function, + "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.MOUSE_CLICKED.function, + "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); + + // Items + registerBuiltInDefinition(ctx, EnumScriptType.PICKUP.function, + "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSS.function, + "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DROP.function, + "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKUP_XP.function, + "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CONTAINER_OPEN.function, + "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); + + // Movement + registerBuiltInDefinition(ctx, EnumScriptType.JUMP.function, + "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FALL.function, + "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CHANGED_DIM.function, + "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); + + // World + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_CHARGE.function, + "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, + "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.USE_HOE.function, + "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.BONEMEAL.function, + "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FILL_BUCKET.function, + "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.WAKE_UP.function, + "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.SLEEP.function, + "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PLAYSOUND.function, + "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.LIGHTNING.function, + "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.SCRIPT_COMMAND.function, + "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); + + // Quest events + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_START.function, + "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_COMPLETED.function, + "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_TURNIN.function, + "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); + + // Dialog events + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPEN.function, + "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPTION.function, + "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, + "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); + + // Faction events + registerBuiltInDefinition(ctx, EnumScriptType.FACTION_POINTS.function, + "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); + + // Custom GUI events + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, + "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, + "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, + "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, + "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, + "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, + "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); + + // Party events + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_SET.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_INVITE.function, + "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_KICK.function, + "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_LEAVE.function, + "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_DISBAND.function, + "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); + + // Animation events + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_START.function, + "noppes.npcs.api.event.IAnimationEvent$Started", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_END.function, + "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, + "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, + "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); + + // Profile events + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CHANGE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_REMOVE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CREATE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); + + // Effect events (for player context) + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_ADD.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_TICK.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); + } + + private void initializeBlockHooks() { + String ctx = ScriptContext.BLOCK.hookContext; + String blockImport = "noppes.npcs.api.event.IBlockEvent"; + + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.FALLEN_UPON.function, + "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.REDSTONE.function, + "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.BROKEN.function, + "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.EXPLODED.function, + "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.RAIN_FILLED.function, + "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, + "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.CLICKED.function, + "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.HARVESTED.function, + "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, + "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); + } + + private void initializeItemHooks() { + String ctx = ScriptContext.ITEM.hookContext; + String itemImport = "noppes.npcs.api.event.IItemEvent"; + + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, + "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, + "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, + "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + } + + private void initializeLinkedItemHooks() { + String ctx = ScriptContext.LINKED_ITEM.hookContext; + String itemImport = "noppes.npcs.api.event.IItemEvent"; + String linkedImport = "noppes.npcs.api.event.ILinkedItemEvent"; + + // Linked item specific hooks + registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, + "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); + registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, + "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); + + // Standard item hooks + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, + "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, + "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, + "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + } + + /** + * Register a built-in hook with full definition metadata. + */ + private void registerBuiltInDefinition(String context, String hookName, String eventClassName, String... imports) { + HookDefinition def = HookDefinition.builder(hookName) + .eventClass(eventClassName) + .requiredImports(imports) + .build(); + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new HashMap<>(); + hookDefinitions.put(context, contextDefs); + } + contextDefs.put(hookName, def); + + // Also register in legacy string list + registerBuiltIn(context, hookName); + } + /** * Register built-in hooks for a ScriptContext. */ @@ -229,6 +476,72 @@ public class ScriptHookController implements IScriptHookHandler { } } + // ==================== RICH HOOK REGISTRATION (NEW API) ==================== + + @Override + public void registerHookDefinition(String context, IHookDefinition definition) { + if (context == null || definition == null || definition.hookName() == null) { + return; + } + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new HashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + // Store as HookDefinition (implementation) + if (definition instanceof HookDefinition) { + contextDefs.put(definition.hookName(), (HookDefinition) definition); + } else { + // Convert foreign IHookDefinition to HookDefinition + HookDefinition def = HookDefinition.builder(definition.hookName()) + .eventClass(definition.eventClassName()) + .paramNames(definition.paramNames()) + .requiredImports(definition.requiredImports()) + .cancelable(definition.isCancelable()) + .build(); + contextDefs.put(definition.hookName(), def); + } + + // Also add to legacy addon hooks list for backward compatibility + registerHook(context, definition.hookName()); + + // Increment revision for cache invalidation + hookRevision++; + } + + @Override + public IHookDefinition getHookDefinition(String context, String hookName) { + if (context == null || hookName == null) { + return null; + } + + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null ? contextDefs.get(hookName) : null; + } + + @Override + public List getAllHookDefinitions(String context) { + if (context == null) { + return Collections.emptyList(); + } + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) { + return Collections.emptyList(); + } + + return new ArrayList<>(contextDefs.values()); + } + + @Override + public int getHookRevision() { + return hookRevision; + } + + // ==================== CONVENIENCE METHODS ==================== + /** * Convenience method to register a hook using ScriptContext. * @param context The script context @@ -248,7 +561,18 @@ public class ScriptHookController implements IScriptHookHandler { registerHook(context.hookContext, fn); } } - + + /** + * Convenience method to register a hook definition using ScriptContext. + * @param context The script context + * @param definition The hook definition + */ + public void registerHookDefinition(ScriptContext context, IHookDefinition definition) { + registerHookDefinition(context.hookContext, definition); + } + + // ==================== LEGACY HOOK REGISTRATION ==================== + @Override public void registerHook(String context, String functionName) { if (context == null || functionName == null || functionName.isEmpty()) { diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8734304d9..6f6d6e55b 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -6,9 +6,11 @@ import io.github.somehussar.janinoloader.api.script.IScriptClassBody; 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.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.janino.annotations.ParamName; import org.codehaus.commons.compiler.InternalCompilerException; @@ -52,6 +54,10 @@ public abstract class JaninoScript implements IScriptUnit { private final String[] defaultImports; + // Hook definition cache for fast lookup (NEW) + private Map hookDefinitionCache; + private int lastHookRevision = -1; + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; @@ -71,6 +77,79 @@ public abstract class JaninoScript implements IScriptUnit { this(type, defaultImports, false); } + // ==================== HOOK CONTEXT (Abstract) ==================== + + /** + * Get the hook context identifier for this script type. + * Used to look up hook definitions from the registry. + * + * @return Context string, e.g., "npc", "player", "block" + */ + protected abstract String getHookContext(); + + // ==================== HOOK DEFINITION CACHE ==================== + + /** + * Get hook definition from cache, rebuilding cache if revision changed. + * + * @param hookName The hook name to look up + * @return The hook definition, or null if not found + */ + protected IHookDefinition getHookDefinition(String hookName) { + if (ScriptHookController.Instance == null) { + return null; + } + + int currentRevision = ScriptHookController.Instance.getHookRevision(); + if (hookDefinitionCache == null || currentRevision != lastHookRevision) { + rebuildHookDefinitionCache(); + lastHookRevision = currentRevision; + } + + return hookDefinitionCache.get(hookName); + } + + /** + * Rebuild the hook definition cache from the registry. + */ + private void rebuildHookDefinitionCache() { + hookDefinitionCache = new HashMap<>(); + String context = getHookContext(); + + if (context == null || context.isEmpty() || ScriptHookController.Instance == null) { + return; + } + + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + hookDefinitionCache.put(def.hookName(), def); + } + } + + /** + * Get all required imports from hook definitions plus default imports. + * + * @return Array of all import statements needed + */ + protected String[] collectAllImports() { + Set allImports = new LinkedHashSet<>(); + + // Add default imports first + Collections.addAll(allImports, defaultImports); + + // Add imports from all hook definitions for this context + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + String[] imports = def.requiredImports(); + if (imports != null) { + Collections.addAll(allImports, imports); + } + } + } + + return allImports.toArray(new String[0]); + } + protected T getUnsafe() { return scriptBody.get(); } @@ -119,11 +198,16 @@ public abstract class JaninoScript implements IScriptUnit { } /** - * Feed the code into the engine and compile it + * Feed the code into the engine and compile it. + * Rebuilds imports from hook definitions before compilation. */ public void compileScript(String code) { try { + // Rebuild imports from current hook definitions + defaults + String[] allImports = collectAllImports(); + builder.setDefaultImports(allImports); + scriptBody.setScript(code); } catch (InternalCompilerException e ) { Throwable parentCause = null; @@ -330,65 +414,43 @@ public abstract class JaninoScript implements IScriptUnit { return script != null && !script.isEmpty(); } - @Override - public String generateHookStub(String hookName, Object hookData) { - hookResolver.clearResolutionCaches(); + @Override + public String generateHookStub(String hookName, Object hookData) { + hookResolver.clearResolutionCaches(); - Method method = hookResolver.getMethodForDisplayName(hookName); - if (method != null) - return generateMethodStub(method); + // 1. Try interface method (existing @ScriptHook annotated methods) + Method method = hookResolver.getMethodForDisplayName(hookName); + if (method != null) + return generateMethodStub(method); - // Fallback: try to find the event class by naming convention - String eventType = findEventTypeForHook(hookName); - if (eventType != null) { - return String.format("public void %s(%s event) {\n \n}\n", hookName, eventType); + // 2. Look up in hook definition registry (addon hooks, dynamic hooks) + IHookDefinition def = getHookDefinition(hookName); + if (def != null) { + return generateStubFromDefinition(def); } - // Last resort: generic Object parameter + // 3. Last resort: generic Object parameter return String.format("public void %s(Object event) {\n \n}\n", hookName); } /** - * Tries to find the event type for a hook by searching API event interfaces. - * Uses naming convention: hookName "questStart" -> event class "QuestStartEvent" + * Generate a method stub from a hook definition. * - * @param hookName The hook name (e.g., "questStart", "dialogOpen") - * @return The usable type name (e.g., "IQuestEvent.QuestStartEvent") or null if not found + * @param def The hook definition containing type info + * @return Generated stub code */ - private String findEventTypeForHook(String hookName) { - if (hookName == null || hookName.isEmpty()) - return null; + private String generateStubFromDefinition(IHookDefinition def) { + String typeName = def.getUsableTypeName(); + String[] params = def.paramNames(); + String paramName = (params != null && params.length > 0) ? params[0] : "event"; - // Convert hook name to expected event class name - // questStart -> QuestStartEvent - String expectedEventName = Character.toUpperCase(hookName.charAt(0)) + hookName.substring(1) + "Event"; - - // Search in known API event interfaces - Class[] eventInterfaces = { - noppes.npcs.api.event.IPlayerEvent.class, - noppes.npcs.api.event.INpcEvent.class, - noppes.npcs.api.event.IQuestEvent.class, - noppes.npcs.api.event.IDialogEvent.class, - noppes.npcs.api.event.IFactionEvent.class, - noppes.npcs.api.event.IPartyEvent.class, - noppes.npcs.api.event.ICustomGuiEvent.class, - noppes.npcs.api.event.IAnimationEvent.class, - noppes.npcs.api.event.IBlockEvent.class, - noppes.npcs.api.event.IItemEvent.class, - noppes.npcs.api.event.IProjectileEvent.class, - noppes.npcs.api.event.IAbilityEvent.class, - noppes.npcs.api.event.IForgeEvent.class - }; - - for (Class eventInterface : eventInterfaces) { - for (Class nested : eventInterface.getDeclaredClasses()) { - if (nested.getSimpleName().equals(expectedEventName)) { - return getUsableTypeName(nested); - } - } + if (typeName == null || typeName.isEmpty()) { + return String.format("public void %s(Object %s) {\n \n}\n", + def.hookName(), paramName); } - return null; + return String.format("public void %s(%s %s) {\n \n}\n", + def.hookName(), typeName, paramName); } /** diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 1af8dea35..637374775 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -17,6 +17,7 @@ public class JaninoNpcScript extends JaninoScript { /** * Default imports for NPC scripts. * These are automatically available in user code without explicit import statements. + * Additional imports from registered hook definitions are added at compile time. */ private static final String[] DEFAULT_IMPORTS = { "noppes.npcs.api.event.INpcEvent", @@ -31,6 +32,11 @@ public class JaninoNpcScript extends JaninoScript { super(Functions.class, DEFAULT_IMPORTS); } + @Override + protected String getHookContext() { + return "npc"; + } + /** * Interface defining the hook methods available for NPC scripts. * Each method corresponds to an NPC event from ScriptHookController CONTEXT_NPC. diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index c755c700c..d29b1b15e 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -43,6 +43,7 @@ public class JaninoPlayerScript extends JaninoScript Date: Thu, 22 Jan 2026 18:44:41 -0500 Subject: [PATCH 54/64] No @ScriptHook needed anymore --- .../npcs/janino/JaninoHookResolver.java | 273 +++++------------- .../java/noppes/npcs/janino/JaninoScript.java | 154 ++-------- .../npcs/janino/annotations/ScriptHook.java | 22 -- .../npcs/janino/impl/JaninoNpcScript.java | 114 +------- .../npcs/janino/impl/JaninoPlayerScript.java | 154 +--------- 5 files changed, 108 insertions(+), 609 deletions(-) delete mode 100644 src/main/java/noppes/npcs/janino/annotations/ScriptHook.java diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index b77233b12..87b969e25 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -1,47 +1,24 @@ package noppes.npcs.janino; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.janino.annotations.ScriptHook; - +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; -import java.util.stream.Collectors; /** - * Resolves script hook methods for a given interface type using runtime + * Resolves script hook methods for compiled script classes using runtime * argument values. Resolution favors exact matches and then the "closest" * assignable parameter types (measured by inheritance distance). * - * The resolver keeps simple caches for previously resolved signatures and - * supports mapping enum-based hook names (via the {@code @ScriptHook} - * annotation) to methods. - * - *

Dynamic Hook Support

- * This resolver supports two modes: - *
    - *
  • Interface-only: Only methods defined in the interface are found (legacy behavior)
  • - *
  • Compiled class: Methods on the actual compiled script class are found, - * allowing users to define custom methods for Forge events, addon hooks, etc.
  • - *
- * Use {@link #resolveHookMethod(String, Object[], Object)} to search the compiled instance. + * Methods are resolved dynamically from the compiled class - no interface + * definitions required. */ public final class JaninoHookResolver { private final Class interfaceType; - /* Methods explicitly mapped from EnumScriptType via @ScriptHook */ - private final Map enumHookLookup = new HashMap<>(); - /** - * Cache for resolved hook methods. Keys are generated by {@link #buildSignatureKey} - * and represent the unique combination of hook name and runtime argument types. - * This avoids expensive recursive reflection lookups on every hook call. - */ - private final Map signatureCache = new HashMap<>(); - - /** - * Cache for methods found on compiled classes (keyed by class + signature). - * Separate from signatureCache to handle recompilation. + * Cache for methods found on compiled classes (keyed by signature). */ private final Map compiledClassCache = new HashMap<>(); @@ -51,12 +28,13 @@ public final class JaninoHookResolver { */ private Class lastCompiledClass; - /* Lazily built list of available hooks and a mapping for display names */ - private boolean hookListInitialized; - /* List of names displayed in GUI hooks list (accounts for overload names) */ - private final List displayHookList = new ArrayList<>(); - /* Display name -> Method map */ - private final Map displayHookMethods = new HashMap<>(); + // MethodHandle caches + private final Map compiledHandleCache = new HashMap<>(); + + // Method -> MethodHandle for identity-based deduplication + private final Map methodToHandle = new IdentityHashMap<>(); + + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); /** * Create a resolver for the provided script interface type. @@ -64,7 +42,6 @@ public final class JaninoHookResolver { */ public JaninoHookResolver(Class interfaceType) { this.interfaceType = Objects.requireNonNull(interfaceType, "interfaceType"); - initializeEnumHookMethods(); } /** @@ -72,114 +49,64 @@ public final class JaninoHookResolver { * method resolution. Call when scripts or available methods change. */ public void clearResolutionCaches() { - signatureCache.clear(); compiledClassCache.clear(); + compiledHandleCache.clear(); + methodToHandle.clear(); lastCompiledClass = null; } /** - * Resolve a hook method by name and runtime argument values (interface-only mode). - * - * This first checks any enum-mapped methods (via {@code @ScriptHook}), - * then a signature-based cache and finally attempts full candidate - * resolution using {@link #resolveHookMethodInternal}. - * - * @param hookName hook method name (or enum function name) - * @param args runtime arguments to use for overload resolution - * @return the resolved {@link Method} or {@code null} if none matched - */ - public Method resolveHookMethod(String hookName, Object[] args) { - return resolveHookMethod(hookName, args, null); - } - - /** - * Resolve a hook method by name, searching both the compiled class and interface. - * - *

This enables dynamic hook support: users can define methods for any hook - * (Forge events, addon hooks, etc.) without needing them in the interface definition.

+ * Resolve a hook method by name, searching the compiled class. * *

Resolution order:

*
    - *
  1. Enum-mapped methods (via {@code @ScriptHook})
  2. - *
  3. Compiled class methods (user-defined, including custom hooks)
  4. - *
  5. Interface methods (base hooks with defaults)
  6. + *
  7. Compiled class methods (user-defined)
  8. *
  9. Fallback to no-arg versions
  10. *
* - *

Performance: Method lookups are cached. First call per hook does reflection - * (~microseconds), subsequent calls use cache (essentially free).

- * - * @param hookName hook method name (or enum function name) + * @param hookName hook method name * @param args runtime arguments to use for overload resolution - * @param compiledInstance the compiled script instance (or null for interface-only) + * @param compiledInstance the compiled script instance * @return the resolved {@link Method} or {@code null} if none matched */ public Method resolveHookMethod(String hookName, Object[] args, Object compiledInstance) { - if (hookName == null || hookName.isEmpty()) + if (hookName == null || hookName.isEmpty() || compiledInstance == null) return null; - // 1. Check enum-mapped methods first (fastest path) - Method method = enumHookLookup.get(hookName); - if (method != null) - return method; - Object[] safeArgs = args == null ? new Object[0] : args; String signatureKey = buildSignatureKey(hookName, safeArgs); - // 2. If we have a compiled instance, search its class first - if (compiledInstance != null) { - Class compiledClass = compiledInstance.getClass(); + Class compiledClass = compiledInstance.getClass(); - // Clear cache if compiled class changed (recompilation) - if (lastCompiledClass != compiledClass) { - compiledClassCache.clear(); - lastCompiledClass = compiledClass; - } - - // Check compiled class cache - method = compiledClassCache.get(signatureKey); - if (method != null) - return method; - - // Search compiled class for matching method - method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); - if (method != null) { - compiledClassCache.put(signatureKey, method); - return method; - } - - // Try no-arg version on compiled class - String noArgKey = buildSignatureKey(hookName, new Object[0]); - method = compiledClassCache.get(noArgKey); - if (method != null) - return method; - - method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); - if (method != null) { - compiledClassCache.put(noArgKey, method); - return method; - } + // Clear cache if compiled class changed (recompilation) + if (lastCompiledClass != compiledClass) { + compiledClassCache.clear(); + compiledHandleCache.clear(); + methodToHandle.clear(); + lastCompiledClass = compiledClass; } - // 3. Fall back to interface-only search - method = signatureCache.get(signatureKey); + // Check compiled class cache + Method method = compiledClassCache.get(signatureKey); if (method != null) return method; - method = resolveHookMethodInternal(interfaceType, hookName, safeArgs); + // Search compiled class for matching method + method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); if (method != null) { - signatureCache.put(signatureKey, method); + compiledClassCache.put(signatureKey, method); return method; } - String noArgSignatureKey = buildSignatureKey(hookName, new Object[0]); - method = signatureCache.get(noArgSignatureKey); + // Try no-arg version on compiled class + String noArgKey = buildSignatureKey(hookName, new Object[0]); + method = compiledClassCache.get(noArgKey); if (method != null) return method; - method = resolveHookMethodInternal(interfaceType, hookName, new Object[0]); + method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); if (method != null) { - signatureCache.put(noArgSignatureKey, method); + compiledClassCache.put(noArgKey, method); return method; } @@ -187,83 +114,55 @@ public final class JaninoHookResolver { } /** - * Returns an immutable copy of the available hook display names. + * Resolve a MethodHandle for the given hook. Faster than Method.invoke(). + * Uses existing Method resolution, then converts to cached MethodHandle. */ - public List getDisplayHookList() { - ensureHookListCreated(); - return new ArrayList<>(displayHookList); - } - - public Method getMethodForDisplayName(String displayName) { - if (displayName == null || displayName.isEmpty()) + public MethodHandle resolveHookHandle(String hookName, Object[] args, Object compiledInstance) { + if (hookName == null || hookName.isEmpty() || compiledInstance == null) return null; - ensureHookListCreated(); - return displayHookMethods.get(displayName); - } + Object[] safeArgs = args == null ? new Object[0] : args; + String signatureKey = buildSignatureKey(hookName, safeArgs); - /** - * Builds the human-readable hook list and display-name -> Method map. - * The list is computed lazily on first access and cached. - */ - private void ensureHookListCreated() { - if (hookListInitialized) - return; + Class compiledClass = compiledInstance.getClass(); - Map> grouped = Arrays.stream(interfaceType.getDeclaredMethods()) - .collect(Collectors.groupingBy(Method::getName)); + // Clear cache if compiled class changed (recompilation) + if (lastCompiledClass != compiledClass) { + compiledClassCache.clear(); + compiledHandleCache.clear(); + methodToHandle.clear(); + lastCompiledClass = compiledClass; + } - for (Map.Entry> entry : grouped.entrySet()) { - List methods = entry.getValue(); + // Check handle cache + MethodHandle handle = compiledHandleCache.get(signatureKey); + if (handle != null) + return handle; - methods.sort(Comparator.comparingInt(Method::getParameterCount)); + // Resolve Method first + Method method = resolveHookMethod(hookName, args, compiledInstance); + if (method == null) + return null; - boolean first = true; - for (Method m : methods) { - if (Modifier.isFinal(m.getModifiers())) - continue; - - String displayName = first ? m.getName() : - m.getName() + "(" + Arrays.stream(m.getParameterTypes()) - .map(Class::getSimpleName) - .collect(Collectors.joining(", ")) + ")"; - - displayHookList.add(displayName); - displayHookMethods.put(displayName, m); - first = false; + // Convert Method to MethodHandle (deduplicated by Method identity) + handle = methodToHandle.get(method); + if (handle == null) { + try { + handle = LOOKUP.unreflect(method); + methodToHandle.put(method, handle); + } catch (IllegalAccessException e) { + return null; } } - hookListInitialized = true; + compiledHandleCache.put(signatureKey, handle); + return handle; } /** - * Maps EnumScriptType common names to their respective methods using - * the {@code @ScriptHook} annotation. + * Build a cache key for a hook name and runtime argument list. */ - private void initializeEnumHookMethods() { - for (Method method : interfaceType.getDeclaredMethods()) { - ScriptHook annotation = method.getAnnotation(ScriptHook.class); - if (annotation == null) - continue; - - for (EnumScriptType hookType : annotation.value()) { - enumHookLookup.put(hookType.function, method); - } - } - } - - /** - * Build a cache key for a hook name and runtime argument list. This key - * encodes the runtime classes of the provided arguments and is used for - * quick cache lookups. Note: keys are based on concrete runtime classes - * which may cause fragmentation when many subclasses are used. - * - * @param hookName hook name - * @param args runtime arguments - * @return a string key representing the hook signature - */ - public static String buildSignatureKey(String hookName, Object[] args) { + private static String buildSignatureKey(String hookName, Object[] args) { StringBuilder sb = new StringBuilder(); sb.append(hookName).append('#'); @@ -284,15 +183,7 @@ public final class JaninoHookResolver { /** * Internal resolution routine. Attempts to find the best matching method * on the provided class by comparing parameter counts and a simple - * match scoring function (see {@link #scoreMethodMatch}). - * - *

This searches all public methods (including inherited) to support - * both interface-defined hooks and user-defined custom methods.

- * - * @param targetClass the class to search (interface or compiled class) - * @param hookName the method name to find - * @param args runtime arguments for overload resolution - * @return the best matching method, or null if none found + * match scoring function. */ private static Method resolveHookMethodInternal(Class targetClass, String hookName, Object[] args) { Object[] safeArgs = args == null ? new Object[0] : args; @@ -300,7 +191,6 @@ public final class JaninoHookResolver { Method best = null; int bestScore = Integer.MAX_VALUE; - // Use getMethods() to include inherited methods (important for compiled classes) for (Method method : targetClass.getMethods()) { if (!method.getName().equals(hookName)) continue; @@ -331,13 +221,8 @@ public final class JaninoHookResolver { } /** - * Score how well a method's parameter types match the provided runtime - * arguments. Lower scores are better; {@link Integer#MAX_VALUE} indicates - * an incompatible parameter. - * - * @param paramTypes method parameter types - * @param args runtime argument values - * @return score (lower is better) or Integer.MAX_VALUE for no match + * Score how well a method's parameter types match the provided runtime arguments. + * Lower scores are better; Integer.MAX_VALUE indicates an incompatible parameter. */ private static int scoreMethodMatch(Class[] paramTypes, Object[] args) { int score = 0; @@ -392,16 +277,6 @@ public final class JaninoHookResolver { return type; } - /** - * Compute a simple inheritance distance between a child and parent class - * by walking superclasses. Returns a large value if {@code parent} is - * not assignable from {@code child}. - * - * Note: the matching algorithm uses superclass traversal to compute a - * distance metric; interface inheritance is not explored which can make - * matches against interfaces less optimal. Consider improving - * `inheritanceDistance` if interface-based scoring is required. - */ private static int inheritanceDistance(Class child, Class parent) { if (child.equals(parent)) return 0; diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 6f6d6e55b..8093b8224 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -12,18 +12,15 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.janino.annotations.ParamName; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.lang.invoke.MethodHandle; import java.security.*; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; public abstract class JaninoScript implements IScriptUnit { public boolean errored = false; @@ -286,26 +283,27 @@ public abstract class JaninoScript implements IScriptUnit { if (hookName == null || hookName.isEmpty()) return null; - // Ensure compiled first so we can search the compiled class ensureCompiled(); T t = getUnsafe(); if (t == null) return null; - // Pass compiled instance to enable dynamic hook resolution - // This allows users to define methods for ANY hook (Forge events, addon hooks, etc.) - Method method = hookResolver.resolveHookMethod(hookName, args, t); - if (method == null) - return null; - - Method invokeMethod = method; Object[] invokeArgs = args == null ? new Object[0] : args; + MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, t); + if (handle == null) + return null; + + final MethodHandle finalHandle = handle; try { return sandbox.confine((PrivilegedAction) () -> { try { - return invokeMethod.invoke(t, invokeArgs); - } catch (Exception e) { + // Prepend receiver to args for virtual method call + Object[] fullArgs = new Object[invokeArgs.length + 1]; + fullArgs[0] = t; + System.arraycopy(invokeArgs, 0, fullArgs, 1, invokeArgs.length); + return finalHandle.invokeWithArguments(fullArgs); + } catch (Throwable e) { appendConsole("Error calling hook " + hookName + ": " + e.getMessage()); return null; } @@ -416,127 +414,31 @@ public abstract class JaninoScript implements IScriptUnit { @Override public String generateHookStub(String hookName, Object hookData) { - hookResolver.clearResolutionCaches(); - - // 1. Try interface method (existing @ScriptHook annotated methods) - Method method = hookResolver.getMethodForDisplayName(hookName); - if (method != null) - return generateMethodStub(method); - - // 2. Look up in hook definition registry (addon hooks, dynamic hooks) + // Look up in hook definition registry IHookDefinition def = getHookDefinition(hookName); if (def != null) { - return generateStubFromDefinition(def); - } + String typeName = def.getUsableTypeName(); + String[] params = def.paramNames(); + String paramName = (params != null && params.length > 0) ? params[0] : "event"; - // 3. Last resort: generic Object parameter - return String.format("public void %s(Object event) {\n \n}\n", hookName); - } - - /** - * Generate a method stub from a hook definition. - * - * @param def The hook definition containing type info - * @return Generated stub code - */ - private String generateStubFromDefinition(IHookDefinition def) { - String typeName = def.getUsableTypeName(); - String[] params = def.paramNames(); - String paramName = (params != null && params.length > 0) ? params[0] : "event"; - - if (typeName == null || typeName.isEmpty()) { - return String.format("public void %s(Object %s) {\n \n}\n", - def.hookName(), paramName); - } - - return String.format("public void %s(%s %s) {\n \n}\n", - def.hookName(), typeName, paramName); - } - - /** - * Generates a stub string for a given Method. - * Handles nested classes properly (e.g., IPlayerEvent.InitEvent instead of just InitEvent). - */ - public static String generateMethodStub(Method method) { - String mods = Modifier.toString(method.getModifiers()); - // Remove 'abstract' from modifiers for implementation - mods = mods.replace("abstract ", "").replace("abstract", "").trim(); - if (!mods.isEmpty()) - mods += " "; - - String returnTypeStr = getUsableTypeName(method.getReturnType()); - String name = method.getName(); - - Map typeCount = new HashMap<>(); - String params = Arrays.stream(method.getParameters()) - .map(p -> { - String typeName = getUsableTypeName(p.getType()); - String simpleTypeName = p.getType().getSimpleName(); - - // Check for @ParamName annotation first - ParamName paramNameAnnotation = p.getAnnotation(ParamName.class); - String paramName; - - if (paramNameAnnotation != null) { - // Use annotated name - paramName = paramNameAnnotation.value(); - } else { - // Fall back to generated name based on simple name - String baseName = Character.toLowerCase(simpleTypeName.charAt(0)) + simpleTypeName.substring(1); - int count = typeCount.getOrDefault(baseName, 0) + 1; - typeCount.put(baseName, count); - paramName = count == 1 ? baseName : baseName + (count - 1); - } - - return typeName + " " + paramName; - }) - .collect(Collectors.joining(", ")); - - String body; - Class returnType = method.getReturnType(); - if (returnType == void.class) - body = ""; - else if (returnType.isPrimitive()) { - if (returnType == boolean.class) - body = " return false;"; - else if (returnType == char.class) - body = " return '\\0';"; - else - body = " return 0;"; - } else { - body = " return null;"; - } - - return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); - } - - /** - * Gets a usable type name for stub generation. - * For nested classes like IPlayerEvent.InitEvent, returns "IPlayerEvent.InitEvent" - * For top-level classes, returns the simple name. - * For primitives, returns the primitive name. - */ - private static String getUsableTypeName(Class type) { - if (type.isPrimitive() || type.getEnclosingClass() == null) { - return type.getSimpleName(); - } - - // Build the nested class chain: OuterClass.InnerClass.DeeperClass - StringBuilder sb = new StringBuilder(); - Class current = type; - while (current != null) { - if (sb.length() > 0) { - sb.insert(0, "."); + if (typeName != null && !typeName.isEmpty()) { + return String.format("public void %s(%s %s) {\n \n}\n", + def.hookName(), typeName, paramName); } - sb.insert(0, current.getSimpleName()); - current = current.getEnclosingClass(); } - return sb.toString(); + + // Fallback: generic Object parameter + return String.format("public void %s(Object event) {\n \n}\n", hookName); } public List getHookList() { - return hookResolver.getDisplayHookList(); + // Get hooks from registry based on context + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + return ScriptHookController.Instance.getAllHooks(context); + } + return Collections.emptyList(); } diff --git a/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java b/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java deleted file mode 100644 index d4720b5d8..000000000 --- a/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java +++ /dev/null @@ -1,22 +0,0 @@ -package noppes.npcs.janino.annotations; - -import noppes.npcs.constants.EnumScriptType; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to mark script hook methods and associate them with their EnumScriptType. - * Used by JaninoScript to automatically build a map of hook types to methods. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface ScriptHook { - /** - * The script type(s) this method handles. - * Multiple types can be specified for methods that handle multiple hooks. - */ - EnumScriptType[] value(); -} diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 637374775..4445ed8c6 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -1,35 +1,15 @@ package noppes.npcs.janino.impl; -import noppes.npcs.api.event.IAbilityEvent; -import noppes.npcs.api.event.INpcEvent; -import noppes.npcs.api.event.IProjectileEvent; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.janino.JaninoScript; -import noppes.npcs.janino.annotations.ParamName; -import noppes.npcs.janino.annotations.ScriptHook; /** * Janino (Java) script implementation for NPC events. - * This allows NPCs to use compiled Java code instead of ECMAScript. + * Hook methods are resolved dynamically by name - no interface definition required. */ public class JaninoNpcScript extends JaninoScript { - /** - * Default imports for NPC scripts. - * These are automatically available in user code without explicit import statements. - * Additional imports from registered hook definitions are added at compile time. - */ - private static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.event.INpcEvent", - "noppes.npcs.api.event.IProjectileEvent", - "noppes.npcs.api.event.IAbilityEvent", - "noppes.npcs.api.entity.ICustomNpc", - "noppes.npcs.api.entity.IPlayer", - "noppes.npcs.api.NpcAPI" - }; - public JaninoNpcScript() { - super(Functions.class, DEFAULT_IMPORTS); + super(Functions.class, null); } @Override @@ -37,93 +17,5 @@ public class JaninoNpcScript extends JaninoScript { return "npc"; } - /** - * Interface defining the hook methods available for NPC scripts. - * Each method corresponds to an NPC event from ScriptHookController CONTEXT_NPC. - * Methods are annotated with @ScriptHook to map them to their EnumScriptType. - */ - public interface Functions { - // Core lifecycle events - @ScriptHook(EnumScriptType.INIT) - default void init(@ParamName("event") INpcEvent.InitEvent event) {} - - @ScriptHook(EnumScriptType.TICK) - default void tick(@ParamName("event") INpcEvent.UpdateEvent event) {} - - - // Interaction events - @ScriptHook(EnumScriptType.INTERACT) - default void interact(@ParamName("event") INpcEvent.InteractEvent event) {} - - @ScriptHook(EnumScriptType.DIALOG) - default void dialog(@ParamName("event") INpcEvent.DialogEvent event) {} - - @ScriptHook(EnumScriptType.DIALOG_CLOSE) - default void dialogClose(@ParamName("event") INpcEvent.DialogClosedEvent event) {} - - - // Combat events - damage - @ScriptHook(EnumScriptType.DAMAGED) - default void damaged(@ParamName("event") INpcEvent.DamagedEvent event) {} - - @ScriptHook(EnumScriptType.KILLED) - default void killed(@ParamName("event") INpcEvent.DiedEvent event) {} - - @ScriptHook(EnumScriptType.KILLS) - default void kills(@ParamName("event") INpcEvent.KilledEntityEvent event) {} - - - // Combat events - attacks - @ScriptHook(EnumScriptType.ATTACK_MELEE) - default void meleeAttack(@ParamName("event") INpcEvent.MeleeAttackEvent event) {} - - @ScriptHook(EnumScriptType.ATTACK_SWING) - default void meleeSwing(@ParamName("event") INpcEvent.SwingEvent event) {} - - @ScriptHook(EnumScriptType.RANGED_LAUNCHED) - default void rangedLaunched(@ParamName("event") INpcEvent.RangedLaunchedEvent event) {} - - - // Targeting events - @ScriptHook(EnumScriptType.TARGET) - default void target(@ParamName("event") INpcEvent.TargetEvent event) {} - - @ScriptHook(EnumScriptType.TARGET_LOST) - default void targetLost(@ParamName("event") INpcEvent.TargetLostEvent event) {} - - // Other NPC events - @ScriptHook(EnumScriptType.COLLIDE) - default void collide(@ParamName("event") INpcEvent.CollideEvent event) {} - - @ScriptHook(EnumScriptType.TIMER) - default void timer(@ParamName("event") INpcEvent.TimerEvent event) {} - - - // Projectile events - @ScriptHook(EnumScriptType.PROJECTILE_TICK) - default void projectileTick(@ParamName("event") IProjectileEvent.UpdateEvent event) {} - - @ScriptHook(EnumScriptType.PROJECTILE_IMPACT) - default void projectileImpact(@ParamName("event") IProjectileEvent.ImpactEvent event) {} - - - // Ability events - @ScriptHook(EnumScriptType.ABILITY_START) - default void abilityStart(@ParamName("event") IAbilityEvent.StartEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_EXECUTE) - default void abilityExecute(@ParamName("event") IAbilityEvent.ExecuteEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_HIT) - default void abilityHit(@ParamName("event") IAbilityEvent.HitEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_TICK) - default void abilityTick(@ParamName("event") IAbilityEvent.TickEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_INTERRUPT) - default void abilityInterrupt(@ParamName("event") IAbilityEvent.InterruptEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_COMPLETE) - default void abilityComplete(@ParamName("event") IAbilityEvent.CompleteEvent event) {} - } + public interface Functions {} } diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index d29b1b15e..79c7718b6 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -1,65 +1,15 @@ package noppes.npcs.janino.impl; -import noppes.npcs.api.event.IPlayerEvent; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.janino.JaninoScript; -import noppes.npcs.janino.annotations.ParamName; -import noppes.npcs.janino.annotations.ScriptHook; /** * Janino (Java) script implementation for player events. - * - *

Dynamic Hook Support

- * With the new dynamic hook resolution, users can define methods for ANY hook - - * not just those defined in the Functions interface. Simply write a public method - * with the hook name and matching parameter type: - * - *
{@code
- * // Standard hook (defined in interface)
- * public void init(IPlayerEvent.InitEvent event) {
- *     // ...
- * }
- *
- * // Custom addon hook (NOT in interface - still works!)
- * public void myAddonHook(MyAddonEvent event) {
- *     // ...
- * }
- *
- * // Any hook by name
- * public void someCustomHook(Object event) {
- *     // ...
- * }
- * }
- * - * The interface below defines core hooks for: - *
    - *
  • EnumScriptType mapping (so "init" maps to init() method)
  • - *
  • GUI hook list display
  • - *
  • Stub generation with correct parameter types
  • - *
+ * Hook methods are resolved dynamically by name - no interface definition required. */ public class JaninoPlayerScript extends JaninoScript { - /** - * Default imports for player scripts. - * These are automatically available in user code without explicit import statements. - * Additional imports from registered hook definitions are added at compile time. - */ - private static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.event.IPlayerEvent", - "noppes.npcs.api.event.IQuestEvent", - "noppes.npcs.api.event.IDialogEvent", - "noppes.npcs.api.event.IFactionEvent", - "noppes.npcs.api.event.IPartyEvent", - "noppes.npcs.api.event.ICustomGuiEvent", - "noppes.npcs.api.event.IAnimationEvent", - "noppes.npcs.api.event.ICustomNPCsEvent", - "noppes.npcs.api.entity.IPlayer", - "noppes.npcs.api.NpcAPI" - }; - public JaninoPlayerScript() { - super(Functions.class, DEFAULT_IMPORTS, false); + super(Functions.class, null, false); } @Override @@ -67,103 +17,5 @@ public class JaninoPlayerScript extends JaninoScript Date: Thu, 22 Jan 2026 19:03:28 -0500 Subject: [PATCH 55/64] Cache Handle for O(1) lookups --- .../controllers/ScriptHookController.java | 626 ++++++------------ .../controllers/data/JaninoScriptHandler.java | 7 - .../npcs/janino/JaninoHookResolver.java | 240 +++---- .../java/noppes/npcs/janino/JaninoScript.java | 52 +- 4 files changed, 285 insertions(+), 640 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index abefb0930..8ecc8d43f 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -6,11 +6,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.RecipeScript; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Controller for managing script hooks. @@ -30,16 +26,10 @@ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Built-in hooks per context (legacy string list) - private final Map> builtInHooks = new HashMap<>(); - - // Addon-registered hooks per context (legacy string list) - private final Map> addonHooks = new HashMap<>(); - - // Rich hook definitions per context (NEW) + // Hook definitions per context - single source of truth private final Map> hookDefinitions = new HashMap<>(); - // Revision counter for cache invalidation (NEW) + // Revision counter for cache invalidation private int hookRevision = 0; public ScriptHookController() { @@ -48,48 +38,40 @@ public class ScriptHookController implements IScriptHookHandler { } private void initializeBuiltInHooks() { - // Initialize empty lists for all contexts from ScriptContext + // Initialize empty maps for all contexts from ScriptContext for (ScriptContext context : ScriptContext.values()) { if (!context.hookContext.isEmpty()) { - builtInHooks.put(context.hookContext, new ArrayList<>()); - addonHooks.put(context.hookContext, new ArrayList<>()); - hookDefinitions.put(context.hookContext, new HashMap<>()); + hookDefinitions.put(context.hookContext, new LinkedHashMap<>()); } } - // NPC hooks with full definitions + // NPC hooks initializeNpcHooks(); - // Player hooks with full definitions + // Player hooks initializePlayerHooks(); - // Block hooks with full definitions + // Block hooks initializeBlockHooks(); - // Item hooks with full definitions + // Item hooks initializeItemHooks(); - // Linked item hooks with full definitions + // Linked item hooks initializeLinkedItemHooks(); // Recipe hooks - registerBuiltIn(ScriptContext.RECIPE, - RecipeScript.ScriptType.PRE.function, - RecipeScript.ScriptType.POST.function - ); + registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.PRE.function, null, null); + registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.POST.function, null, null); // Effect hooks - registerBuiltIn(ScriptContext.EFFECT, - EnumScriptType.ON_EFFECT_ADD.function, - EnumScriptType.ON_EFFECT_TICK.function, - EnumScriptType.ON_EFFECT_REMOVE.function - ); + registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_ADD.function, null, null); + registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_TICK.function, null, null); + registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_REMOVE.function, null, null); - // Forge hooks - init + CNPC-specific hooks, the rest are dynamically discovered from Forge event classes - registerBuiltIn(ScriptContext.FORGE, - EnumScriptType.INIT.function, - EnumScriptType.CNPC_NATURAL_SPAWN.function - ); + // Forge hooks + registerHookDef(ScriptContext.FORGE, EnumScriptType.INIT.function, null, null); + registerHookDef(ScriptContext.FORGE, EnumScriptType.CNPC_NATURAL_SPAWN.function, null, null); } private void initializeNpcHooks() { @@ -99,60 +81,37 @@ public class ScriptHookController implements IScriptHookHandler { String abilityImport = "noppes.npcs.api.event.IAbilityEvent"; // Core lifecycle - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, - "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); // Interaction - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG.function, - "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, - "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, - "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); + registerHookDef(ctx, EnumScriptType.DIALOG.function, "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); + registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); // Combat - registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, - "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, - "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, - "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_MELEE.function, - "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_SWING.function, - "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, - "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TARGET.function, - "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TARGET_LOST.function, - "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); + registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); + registerHookDef(ctx, EnumScriptType.ATTACK_MELEE.function, "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); + registerHookDef(ctx, EnumScriptType.ATTACK_SWING.function, "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); + registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TARGET.function, "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TARGET_LOST.function, "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); // Projectile events - registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_TICK.function, - "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); - registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_IMPACT.function, - "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); + registerHookDef(ctx, EnumScriptType.PROJECTILE_TICK.function, "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); + registerHookDef(ctx, EnumScriptType.PROJECTILE_IMPACT.function, "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); // Ability events - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_START.function, - "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_EXECUTE.function, - "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_HIT.function, - "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_TICK.function, - "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_INTERRUPT.function, - "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_COMPLETE.function, - "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_START.function, "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_EXECUTE.function, "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_HIT.function, "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_TICK.function, "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_INTERRUPT.function, "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_COMPLETE.function, "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); } private void initializePlayerHooks() { @@ -166,238 +125,143 @@ public class ScriptHookController implements IScriptHookHandler { String animImport = "noppes.npcs.api.event.IAnimationEvent"; // Core lifecycle - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, - "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); + registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); // Combat - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, - "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACKED.function, - "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, - "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED_ENTITY.function, - "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, - "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, - "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); + registerHookDef(ctx, EnumScriptType.ATTACKED.function, "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.DAMAGED_ENTITY.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); + registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); + registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); // Interaction - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, - "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.BREAK_BLOCK.function, - "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.CHAT.function, - "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); + registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); + registerHookDef(ctx, EnumScriptType.BREAK_BLOCK.function, "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); + registerHookDef(ctx, EnumScriptType.CHAT.function, "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); // Connection - registerBuiltInDefinition(ctx, EnumScriptType.LOGIN.function, - "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.LOGOUT.function, - "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.RESPAWN.function, - "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); + registerHookDef(ctx, EnumScriptType.LOGIN.function, "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); + registerHookDef(ctx, EnumScriptType.LOGOUT.function, "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); + registerHookDef(ctx, EnumScriptType.RESPAWN.function, "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); // Input - registerBuiltInDefinition(ctx, EnumScriptType.KEY_PRESSED.function, - "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.MOUSE_CLICKED.function, - "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.KEY_PRESSED.function, "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.MOUSE_CLICKED.function, "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); // Items - registerBuiltInDefinition(ctx, EnumScriptType.PICKUP.function, - "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.TOSS.function, - "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.DROP.function, - "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PICKUP_XP.function, - "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.CONTAINER_OPEN.function, - "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); + registerHookDef(ctx, EnumScriptType.PICKUP.function, "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); + registerHookDef(ctx, EnumScriptType.TOSS.function, "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); + registerHookDef(ctx, EnumScriptType.DROP.function, "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); + registerHookDef(ctx, EnumScriptType.PICKUP_XP.function, "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); + registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.CONTAINER_OPEN.function, "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); // Movement - registerBuiltInDefinition(ctx, EnumScriptType.JUMP.function, - "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.FALL.function, - "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.CHANGED_DIM.function, - "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); + registerHookDef(ctx, EnumScriptType.JUMP.function, "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); + registerHookDef(ctx, EnumScriptType.FALL.function, "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); + registerHookDef(ctx, EnumScriptType.CHANGED_DIM.function, "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); // World - registerBuiltInDefinition(ctx, EnumScriptType.RANGED_CHARGE.function, - "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, - "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.USE_HOE.function, - "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.BONEMEAL.function, - "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.FILL_BUCKET.function, - "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.WAKE_UP.function, - "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.SLEEP.function, - "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PLAYSOUND.function, - "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.LIGHTNING.function, - "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.SCRIPT_COMMAND.function, - "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); + registerHookDef(ctx, EnumScriptType.RANGED_CHARGE.function, "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); + registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.USE_HOE.function, "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); + registerHookDef(ctx, EnumScriptType.BONEMEAL.function, "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); + registerHookDef(ctx, EnumScriptType.FILL_BUCKET.function, "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); + registerHookDef(ctx, EnumScriptType.WAKE_UP.function, "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); + registerHookDef(ctx, EnumScriptType.SLEEP.function, "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); + registerHookDef(ctx, EnumScriptType.PLAYSOUND.function, "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); + registerHookDef(ctx, EnumScriptType.LIGHTNING.function, "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); + registerHookDef(ctx, EnumScriptType.SCRIPT_COMMAND.function, "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); // Quest events - registerBuiltInDefinition(ctx, EnumScriptType.QUEST_START.function, - "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); - registerBuiltInDefinition(ctx, EnumScriptType.QUEST_COMPLETED.function, - "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); - registerBuiltInDefinition(ctx, EnumScriptType.QUEST_TURNIN.function, - "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); + registerHookDef(ctx, EnumScriptType.QUEST_START.function, "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); + registerHookDef(ctx, EnumScriptType.QUEST_COMPLETED.function, "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); + registerHookDef(ctx, EnumScriptType.QUEST_TURNIN.function, "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); // Dialog events - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPEN.function, - "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPTION.function, - "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, - "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); + registerHookDef(ctx, EnumScriptType.DIALOG_OPEN.function, "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); + registerHookDef(ctx, EnumScriptType.DIALOG_OPTION.function, "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); + registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); // Faction events - registerBuiltInDefinition(ctx, EnumScriptType.FACTION_POINTS.function, - "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); + registerHookDef(ctx, EnumScriptType.FACTION_POINTS.function, "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); // Custom GUI events - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, - "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, - "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, - "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, - "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, - "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, - "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); // Party events - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, - "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_SET.function, - "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, - "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_INVITE.function, - "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_KICK.function, - "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_LEAVE.function, - "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_DISBAND.function, - "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_QUEST_SET.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_INVITE.function, "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_KICK.function, "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_LEAVE.function, "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_DISBAND.function, "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); // Animation events - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_START.function, - "noppes.npcs.api.event.IAnimationEvent$Started", animImport); - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_END.function, - "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, - "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, - "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_START.function, "noppes.npcs.api.event.IAnimationEvent$Started", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_END.function, "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); // Profile events - registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CHANGE.function, - "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_REMOVE.function, - "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CREATE.function, - "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); + registerHookDef(ctx, EnumScriptType.PROFILE_CHANGE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); + registerHookDef(ctx, EnumScriptType.PROFILE_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); + registerHookDef(ctx, EnumScriptType.PROFILE_CREATE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); // Effect events (for player context) - registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_ADD.function, - "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_TICK.function, - "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, - "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); + registerHookDef(ctx, EnumScriptType.ON_EFFECT_ADD.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); + registerHookDef(ctx, EnumScriptType.ON_EFFECT_TICK.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); + registerHookDef(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); } private void initializeBlockHooks() { String ctx = ScriptContext.BLOCK.hookContext; String blockImport = "noppes.npcs.api.event.IBlockEvent"; - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.FALLEN_UPON.function, - "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.REDSTONE.function, - "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.BROKEN.function, - "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.EXPLODED.function, - "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.RAIN_FILLED.function, - "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, - "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.CLICKED.function, - "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.HARVESTED.function, - "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, - "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, - "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); + registerHookDef(ctx, EnumScriptType.FALLEN_UPON.function, "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); + registerHookDef(ctx, EnumScriptType.REDSTONE.function, "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); + registerHookDef(ctx, EnumScriptType.BROKEN.function, "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); + registerHookDef(ctx, EnumScriptType.EXPLODED.function, "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.RAIN_FILLED.function, "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); + registerHookDef(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.CLICKED.function, "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.HARVESTED.function, "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); } private void initializeItemHooks() { String ctx = ScriptContext.ITEM.hookContext; String itemImport = "noppes.npcs.api.event.IItemEvent"; - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, - "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, - "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, - "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, - "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, - "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); } private void initializeLinkedItemHooks() { @@ -406,95 +270,64 @@ public class ScriptHookController implements IScriptHookHandler { String linkedImport = "noppes.npcs.api.event.ILinkedItemEvent"; // Linked item specific hooks - registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, - "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); - registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, - "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); + registerHookDef(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); + registerHookDef(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); // Standard item hooks - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, - "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, - "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, - "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, - "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, - "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); } /** - * Register a built-in hook with full definition metadata. + * Internal helper to register a hook definition. */ - private void registerBuiltInDefinition(String context, String hookName, String eventClassName, String... imports) { - HookDefinition def = HookDefinition.builder(hookName) - .eventClass(eventClassName) - .requiredImports(imports) - .build(); - + private void registerHookDef(String context, String hookName, String eventClassName, String... imports) { Map contextDefs = hookDefinitions.get(context); if (contextDefs == null) { - contextDefs = new HashMap<>(); + contextDefs = new LinkedHashMap<>(); hookDefinitions.put(context, contextDefs); } - contextDefs.put(hookName, def); - // Also register in legacy string list - registerBuiltIn(context, hookName); - } - - /** - * Register built-in hooks for a ScriptContext. - */ - private void registerBuiltIn(ScriptContext context, String... hooks) { - registerBuiltIn(context.hookContext, hooks); - } - - private void registerBuiltIn(String context, String... hooks) { - List list = builtInHooks.get(context); - if (list != null) { - for (String hook : hooks) { - if (!list.contains(hook)) { - list.add(hook); - } - } + HookDefinition.Builder builder = HookDefinition.builder(hookName); + if (eventClassName != null) { + builder.eventClass(eventClassName); } + if (imports != null && imports.length > 0 && imports[0] != null) { + builder.requiredImports(imports); + } + contextDefs.put(hookName, builder.build()); } - // ==================== RICH HOOK REGISTRATION (NEW API) ==================== + private void registerHookDef(ScriptContext context, String hookName, String eventClassName, String... imports) { + registerHookDef(context.hookContext, hookName, eventClassName, imports); + } + + // ==================== PUBLIC API ==================== @Override public void registerHookDefinition(String context, IHookDefinition definition) { - if (context == null || definition == null || definition.hookName() == null) { + if (context == null || definition == null || definition.hookName() == null) return; - } Map contextDefs = hookDefinitions.get(context); if (contextDefs == null) { - contextDefs = new HashMap<>(); + contextDefs = new LinkedHashMap<>(); hookDefinitions.put(context, contextDefs); } - // Store as HookDefinition (implementation) if (definition instanceof HookDefinition) { contextDefs.put(definition.hookName(), (HookDefinition) definition); } else { - // Convert foreign IHookDefinition to HookDefinition HookDefinition def = HookDefinition.builder(definition.hookName()) .eventClass(definition.eventClassName()) .paramNames(definition.paramNames()) @@ -504,18 +337,13 @@ public class ScriptHookController implements IScriptHookHandler { contextDefs.put(definition.hookName(), def); } - // Also add to legacy addon hooks list for backward compatibility - registerHook(context, definition.hookName()); - - // Increment revision for cache invalidation hookRevision++; } @Override public IHookDefinition getHookDefinition(String context, String hookName) { - if (context == null || hookName == null) { + if (context == null || hookName == null) return null; - } Map contextDefs = hookDefinitions.get(context); return contextDefs != null ? contextDefs.get(hookName) : null; @@ -523,14 +351,12 @@ public class ScriptHookController implements IScriptHookHandler { @Override public List getAllHookDefinitions(String context) { - if (context == null) { + if (context == null) return Collections.emptyList(); - } Map contextDefs = hookDefinitions.get(context); - if (contextDefs == null || contextDefs.isEmpty()) { + if (contextDefs == null || contextDefs.isEmpty()) return Collections.emptyList(); - } return new ArrayList<>(contextDefs.values()); } @@ -542,60 +368,44 @@ public class ScriptHookController implements IScriptHookHandler { // ==================== CONVENIENCE METHODS ==================== - /** - * Convenience method to register a hook using ScriptContext. - * @param context The script context - * @param functionName The hook function name - */ public void registerHook(ScriptContext context, String functionName) { registerHook(context.hookContext, functionName); } - /** - * Convenience method to register multiple hooks using ScriptContext. - * @param context The script context - * @param functionNames The hook function names - */ public void registerHooks(ScriptContext context, String... functionNames) { for (String fn : functionNames) { registerHook(context.hookContext, fn); } } - /** - * Convenience method to register a hook definition using ScriptContext. - * @param context The script context - * @param definition The hook definition - */ public void registerHookDefinition(ScriptContext context, IHookDefinition definition) { registerHookDefinition(context.hookContext, definition); } - // ==================== LEGACY HOOK REGISTRATION ==================== + // ==================== LEGACY API (IScriptHookHandler) ==================== @Override public void registerHook(String context, String functionName) { - if (context == null || functionName == null || functionName.isEmpty()) { + if (context == null || functionName == null || functionName.isEmpty()) return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); } - List hooks = addonHooks.get(context); - if (hooks == null) { - // Custom context - create it - hooks = new ArrayList<>(); - addonHooks.put(context, hooks); - } - - if (!hooks.contains(functionName) && !isBuiltInHook(context, functionName)) { - hooks.add(functionName); + // Only add if not already present + if (!contextDefs.containsKey(functionName)) { + contextDefs.put(functionName, HookDefinition.builder(functionName).build()); + hookRevision++; } } @Override public void registerHooks(String functionName, String... contexts) { - if (functionName == null || functionName.isEmpty() || contexts == null) { + if (functionName == null || functionName.isEmpty() || contexts == null) return; - } for (String context : contexts) { registerHook(context, functionName); @@ -604,85 +414,57 @@ public class ScriptHookController implements IScriptHookHandler { @Override public void unregisterHook(String context, String functionName) { - if (context == null || functionName == null) { + if (context == null || functionName == null) return; - } - List hooks = addonHooks.get(context); - if (hooks != null) { - hooks.remove(functionName); + Map contextDefs = hookDefinitions.get(context); + if (contextDefs != null && contextDefs.remove(functionName) != null) { + hookRevision++; } } @Override public void unregisterHookFromAll(String functionName) { - if (functionName == null) { + if (functionName == null) return; - } - for (List hooks : addonHooks.values()) { - hooks.remove(functionName); + for (Map contextDefs : hookDefinitions.values()) { + if (contextDefs.remove(functionName) != null) { + hookRevision++; + } } } @Override public List getAddonHooks(String context) { - List hooks = addonHooks.get(context); - return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList(); + // With unified storage, we can't distinguish addon vs built-in + // Return empty for backwards compatibility + return Collections.emptyList(); } @Override public List getAllHooks(String context) { - List result = new ArrayList<>(); + if (context == null) + return Collections.emptyList(); - // Add built-in hooks - List builtIn = builtInHooks.get(context); - if (builtIn != null) { - result.addAll(builtIn); - } + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) + return Collections.emptyList(); - // Add addon hooks - List addon = addonHooks.get(context); - if (addon != null) { - for (String hook : addon) { - if (!result.contains(hook)) { - result.add(hook); - } - } - } - - return result; + return new ArrayList<>(contextDefs.keySet()); } @Override public boolean hasHook(String context, String functionName) { - if (context == null || functionName == null) { + if (context == null || functionName == null) return false; - } - return isBuiltInHook(context, functionName) || isAddonHook(context, functionName); + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null && contextDefs.containsKey(functionName); } @Override public String[] getContexts() { - return builtInHooks.keySet().toArray(new String[0]); - } - - private boolean isBuiltInHook(String context, String functionName) { - List hooks = builtInHooks.get(context); - return hooks != null && hooks.contains(functionName); - } - - private boolean isAddonHook(String context, String functionName) { - List hooks = addonHooks.get(context); - return hooks != null && hooks.contains(functionName); - } - - /** - * Get built-in hooks for a context (used by GUIs for backwards compatibility). - */ - public List getBuiltInHooks(String context) { - List hooks = builtInHooks.get(context); - return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList(); + return hookDefinitions.keySet().toArray(new String[0]); } } diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index 28bd3af4f..fb354e590 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -48,16 +48,9 @@ public class JaninoScriptHandler> extends ScriptHandle } public void deleteScript() { - unloadScript(); script = null; } - public void unloadScript() { - if (script != null) { - script.unload(); - } - } - // -------------------- NBT helpers -------------------- public void readFromNBT(NBTTagCompound compound) { diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index 87b969e25..2ff8b4afb 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -7,20 +7,16 @@ import java.lang.reflect.Modifier; import java.util.*; /** - * Resolves script hook methods for compiled script classes using runtime - * argument values. Resolution favors exact matches and then the "closest" - * assignable parameter types (measured by inheritance distance). - * - * Methods are resolved dynamically from the compiled class - no interface - * definitions required. + * Resolves script hook methods for compiled script classes. + * Optimized for high-frequency hooks like TICK with fast O(1) lookups. */ public final class JaninoHookResolver { - private final Class interfaceType; /** - * Cache for methods found on compiled classes (keyed by signature). + * Primary cache: hookName -> CachedHandle + * Fast O(1) lookup without string building for non-overloaded hooks. */ - private final Map compiledClassCache = new HashMap<>(); + private final Map handleCache = new HashMap<>(); /** * Track which compiled class we've cached methods for. @@ -28,164 +24,78 @@ public final class JaninoHookResolver { */ private Class lastCompiledClass; - // MethodHandle caches - private final Map compiledHandleCache = new HashMap<>(); - - // Method -> MethodHandle for identity-based deduplication - private final Map methodToHandle = new IdentityHashMap<>(); - private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); /** - * Create a resolver for the provided script interface type. - * @param interfaceType non-null interface class that scripts implement + * Cached handle with expected parameter count for quick validation. */ - public JaninoHookResolver(Class interfaceType) { - this.interfaceType = Objects.requireNonNull(interfaceType, "interfaceType"); + private static final class CachedHandle { + final MethodHandle handle; + final int paramCount; + + CachedHandle(MethodHandle handle, int paramCount) { + this.handle = handle; + this.paramCount = paramCount; + } + } + + public JaninoHookResolver() { } /** - * Clears internal resolution caches so future calls perform fresh - * method resolution. Call when scripts or available methods change. + * Clears internal resolution caches. */ public void clearResolutionCaches() { - compiledClassCache.clear(); - compiledHandleCache.clear(); - methodToHandle.clear(); + handleCache.clear(); lastCompiledClass = null; } /** - * Resolve a hook method by name, searching the compiled class. - * - *

Resolution order:

- *
    - *
  1. Compiled class methods (user-defined)
  2. - *
  3. Fallback to no-arg versions
  4. - *
- * - * @param hookName hook method name - * @param args runtime arguments to use for overload resolution - * @param compiledInstance the compiled script instance - * @return the resolved {@link Method} or {@code null} if none matched - */ - public Method resolveHookMethod(String hookName, Object[] args, Object compiledInstance) { - if (hookName == null || hookName.isEmpty() || compiledInstance == null) - return null; - - Object[] safeArgs = args == null ? new Object[0] : args; - String signatureKey = buildSignatureKey(hookName, safeArgs); - - Class compiledClass = compiledInstance.getClass(); - - // Clear cache if compiled class changed (recompilation) - if (lastCompiledClass != compiledClass) { - compiledClassCache.clear(); - compiledHandleCache.clear(); - methodToHandle.clear(); - lastCompiledClass = compiledClass; - } - - // Check compiled class cache - Method method = compiledClassCache.get(signatureKey); - if (method != null) - return method; - - // Search compiled class for matching method - method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); - if (method != null) { - compiledClassCache.put(signatureKey, method); - return method; - } - - // Try no-arg version on compiled class - String noArgKey = buildSignatureKey(hookName, new Object[0]); - method = compiledClassCache.get(noArgKey); - if (method != null) - return method; - - method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); - if (method != null) { - compiledClassCache.put(noArgKey, method); - return method; - } - - return null; - } - - /** - * Resolve a MethodHandle for the given hook. Faster than Method.invoke(). - * Uses existing Method resolution, then converts to cached MethodHandle. + * Resolve a MethodHandle for the given hook. + * Optimized for repeated calls with same hook name - O(1) after first resolution. */ public MethodHandle resolveHookHandle(String hookName, Object[] args, Object compiledInstance) { if (hookName == null || hookName.isEmpty() || compiledInstance == null) return null; - Object[] safeArgs = args == null ? new Object[0] : args; - String signatureKey = buildSignatureKey(hookName, safeArgs); - Class compiledClass = compiledInstance.getClass(); // Clear cache if compiled class changed (recompilation) if (lastCompiledClass != compiledClass) { - compiledClassCache.clear(); - compiledHandleCache.clear(); - methodToHandle.clear(); + handleCache.clear(); lastCompiledClass = compiledClass; } - // Check handle cache - MethodHandle handle = compiledHandleCache.get(signatureKey); - if (handle != null) - return handle; + int argCount = args == null ? 0 : args.length; - // Resolve Method first - Method method = resolveHookMethod(hookName, args, compiledInstance); + // Fast path: check primary cache by hookName only + CachedHandle cached = handleCache.get(hookName); + if (cached != null) { + // Verify arg count matches (handles most cases without full signature check) + if (cached.paramCount == argCount) { + return cached.handle; + } + // Arg count mismatch - need full resolution for potential overload + } + + // Slow path: resolve method and cache + Method method = resolveMethod(compiledClass, hookName, args); if (method == null) return null; - // Convert Method to MethodHandle (deduplicated by Method identity) - handle = methodToHandle.get(method); - if (handle == null) { - try { - handle = LOOKUP.unreflect(method); - methodToHandle.put(method, handle); - } catch (IllegalAccessException e) { - return null; - } + try { + MethodHandle handle = LOOKUP.unreflect(method); + handleCache.put(hookName, new CachedHandle(handle, method.getParameterCount())); + return handle; + } catch (IllegalAccessException e) { + return null; } - - compiledHandleCache.put(signatureKey, handle); - return handle; } /** - * Build a cache key for a hook name and runtime argument list. + * Find the best matching method on the compiled class. */ - private static String buildSignatureKey(String hookName, Object[] args) { - StringBuilder sb = new StringBuilder(); - sb.append(hookName).append('#'); - - if (args == null || args.length == 0) - return sb.append('0').toString(); - - sb.append(args.length).append(':'); - for (int i = 0; i < args.length; i++) { - Object arg = args[i]; - if (i > 0) - sb.append(','); - - sb.append(arg == null ? "null" : arg.getClass().getName()); - } - return sb.toString(); - } - - /** - * Internal resolution routine. Attempts to find the best matching method - * on the provided class by comparing parameter counts and a simple - * match scoring function. - */ - private static Method resolveHookMethodInternal(Class targetClass, String hookName, Object[] args) { + private Method resolveMethod(Class targetClass, String hookName, Object[] args) { Object[] safeArgs = args == null ? new Object[0] : args; Method best = null; @@ -202,18 +112,31 @@ public final class JaninoHookResolver { continue; Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != safeArgs.length) - continue; - int score = scoreMethodMatch(paramTypes, safeArgs); - if (score == Integer.MAX_VALUE) - continue; + // Try exact arg count match first + if (paramTypes.length == safeArgs.length) { + int score = scoreMethodMatch(paramTypes, safeArgs); + if (score != Integer.MAX_VALUE && score < bestScore) { + bestScore = score; + best = method; + if (score == 0) + return best; // Perfect match + } + } + } - if (score < bestScore) { - bestScore = score; - best = method; - if (score == 0) - return best; + // If no match with args, try no-arg version + if (best == null && safeArgs.length > 0) { + for (Method method : targetClass.getMethods()) { + if (!method.getName().equals(hookName)) + continue; + if (Modifier.isFinal(method.getModifiers())) + continue; + if (method.getDeclaringClass() == Object.class) + continue; + if (method.getParameterCount() == 0) { + return method; + } } } @@ -221,8 +144,8 @@ public final class JaninoHookResolver { } /** - * Score how well a method's parameter types match the provided runtime arguments. - * Lower scores are better; Integer.MAX_VALUE indicates an incompatible parameter. + * Score how well parameter types match runtime arguments. + * Lower is better; MAX_VALUE means incompatible. */ private static int scoreMethodMatch(Class[] paramTypes, Object[] args) { int score = 0; @@ -233,7 +156,6 @@ public final class JaninoHookResolver { if (arg == null) { if (paramType.isPrimitive()) return Integer.MAX_VALUE; - score += 1; continue; } @@ -251,36 +173,26 @@ public final class JaninoHookResolver { return Integer.MAX_VALUE; } - return score; } private static Class boxType(Class type) { if (!type.isPrimitive()) return type; - if (type == boolean.class) - return Boolean.class; - if (type == byte.class) - return Byte.class; - if (type == short.class) - return Short.class; - if (type == int.class) - return Integer.class; - if (type == long.class) - return Long.class; - if (type == float.class) - return Float.class; - if (type == double.class) - return Double.class; - if (type == char.class) - return Character.class; + if (type == boolean.class) return Boolean.class; + if (type == byte.class) return Byte.class; + if (type == short.class) return Short.class; + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == float.class) return Float.class; + if (type == double.class) return Double.class; + if (type == char.class) return Character.class; return type; } private static int inheritanceDistance(Class child, Class parent) { if (child.equals(parent)) return 0; - if (!parent.isAssignableFrom(child)) return 1000; diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8093b8224..3ce614632 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -16,10 +16,9 @@ import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; import java.lang.invoke.MethodHandle; -import java.security.*; +import java.security.Permissions; +import java.security.PrivilegedAction; import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { @@ -67,7 +66,7 @@ public abstract class JaninoScript implements IScriptUnit { this.scriptBody = builder.build(); - this.hookResolver = new JaninoHookResolver(type); + this.hookResolver = new JaninoHookResolver(); } protected JaninoScript(Class type, String[] defaultImports) { @@ -151,49 +150,6 @@ public abstract class JaninoScript implements IScriptUnit { return scriptBody.get(); } - public R call(Function fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return null; - -// CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); -// ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); - - try { - return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); -// return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( -// new ProtectionDomain[] { -// pd -// } -// )); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - return null; - } - } - - public void run(Consumer fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return; - - try { - sandbox.confine((PrivilegedAction) () -> { - fn.accept(t); - return null; - }); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - } - } - - public void unload() { - } - /** * Feed the code into the engine and compile it. * Rebuilds imports from hook definitions before compilation. @@ -205,6 +161,8 @@ public abstract class JaninoScript implements IScriptUnit { String[] allImports = collectAllImports(); builder.setDefaultImports(allImports); + // Rebuild scriptBody to apply new imports + this.scriptBody = builder.build(); scriptBody.setScript(code); } catch (InternalCompilerException e ) { Throwable parentCause = null; From a7d58bfb4ec6b019204dfc786684964e66329e60 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 19:38:04 -0500 Subject: [PATCH 56/64] Re-Add Call Run --- .../java/noppes/npcs/janino/JaninoScript.java | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 3ce614632..6de7b7a5a 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -19,6 +19,8 @@ import java.lang.invoke.MethodHandle; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { @@ -58,7 +60,7 @@ public abstract class JaninoScript implements IScriptUnit { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; this.builder = IScriptBodyBuilder.getBuilder(type, isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) - .setDefaultImports(defaultImports); + .setDefaultImports(this.defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); @@ -135,7 +137,8 @@ public abstract class JaninoScript implements IScriptUnit { // Add imports from all hook definitions for this context String context = getHookContext(); if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + java.util.List defs = ScriptHookController.Instance.getAllHookDefinitions(context); + for (IHookDefinition def : defs) { String[] imports = def.requiredImports(); if (imports != null) { Collections.addAll(allImports, imports); @@ -272,6 +275,49 @@ public abstract class JaninoScript implements IScriptUnit { } } + public R call(Function fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return null; + +// CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); +// ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); + + try { + return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); +// return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( +// new ProtectionDomain[] { +// pd +// } +// )); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + return null; + } + } + + public void run(Consumer fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return; + + try { + sandbox.confine((PrivilegedAction) () -> { + fn.accept(t); + return null; + }); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + } + } + + public void unload() { + } + // ==================== IScriptUnit IMPLEMENTATION ==================== From f8dc9ec1e4e8c648b529d9baafc6866cc162a6d3 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 19:43:05 -0500 Subject: [PATCH 57/64] Update JaninoScript.java --- src/main/java/noppes/npcs/janino/JaninoScript.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 6de7b7a5a..a97aed889 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -137,8 +137,7 @@ public abstract class JaninoScript implements IScriptUnit { // Add imports from all hook definitions for this context String context = getHookContext(); if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - java.util.List defs = ScriptHookController.Instance.getAllHookDefinitions(context); - for (IHookDefinition def : defs) { + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { String[] imports = def.requiredImports(); if (imports != null) { Collections.addAll(allImports, imports); From 7ba2dc72d7ecf4fe610da906a82ed9ffa02984ca Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 19:55:57 -0500 Subject: [PATCH 58/64] Goatee loves this --- src/api | 2 +- .../npcs/controllers/HookDefinition.java | 147 ++--- .../controllers/ScriptHookController.java | 582 ++++++++---------- 3 files changed, 308 insertions(+), 423 deletions(-) diff --git a/src/api b/src/api index 7a337a570..64fe8794e 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 7a337a5704c442fb98a0fd4e0ba26210cb54af98 +Subproject commit 64fe8794e71d82f09449e6c916c7902fe890cab7 diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java index b8b78fe70..b5a61ceab 100644 --- a/src/main/java/noppes/npcs/controllers/HookDefinition.java +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -2,32 +2,23 @@ package noppes.npcs.controllers; import cpw.mods.fml.common.eventhandler.Cancelable; import noppes.npcs.api.handler.IHookDefinition; -import noppes.npcs.janino.annotations.ParamName; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; /** * Implementation of {@link IHookDefinition} with a fluent builder API. * *

Usage Examples

*
{@code
- * // Full definition with class reference
- * HookDefinition.builder("onDBCTransform")
- *     .eventClass(IDBCEvent.TransformEvent.class)
- *     .paramNames("event")
- *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ * // Recommended: auto-derives imports from event class
+ * HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class);
+ *
+ * // Full control with builder
+ * HookDefinition.builder("onDBCPowerUp")
+ *     .eventClass(IDBCEvent.PowerUpEvent.class)
  *     .cancelable(true)
  *     .build();
  *
- * // String-based (avoids classloading issues)
- * HookDefinition.builder("onDBCPowerUp")
- *     .eventClass("com.dbc.api.event.IDBCEvent$PowerUpEvent")
- *     .requiredImports("com.dbc.api.event.IDBCEvent")
- *     .build();
- *
- * // Simple hook (backward compat, no metadata)
- * HookDefinition.simple("legacyHook");
+ * // Simple hook (no event metadata)
+ * HookDefinition.simple("customHook");
  * }
*/ public class HookDefinition implements IHookDefinition { @@ -38,7 +29,6 @@ public class HookDefinition implements IHookDefinition { private final String[] requiredImports; private final boolean cancelable; - // Cached resolved class (transient - not serialized) private transient Class cachedEventClass; private transient boolean classResolutionAttempted; @@ -100,18 +90,19 @@ public class HookDefinition implements IHookDefinition { // ==================== Factory Methods ==================== /** - * Create a new builder for a hook definition. + * Create a hook definition from a hook name and event class. + * Automatically derives imports from the event class. * * @param hookName The hook function name - * @return A new builder instance + * @param eventClass The event class (e.g., INpcEvent.InitEvent.class) + * @return A hook definition with auto-derived imports */ - public static Builder builder(String hookName) { - return new Builder(hookName); + public static HookDefinition of(String hookName, Class eventClass) { + return builder(hookName).eventClass(eventClass).build(); } /** - * Create a simple hook definition with just a name (no metadata). - * For backward compatibility with legacy hook registration. + * Create a simple hook definition with just a name (no event metadata). * * @param hookName The hook function name * @return A minimal hook definition @@ -121,49 +112,13 @@ public class HookDefinition implements IHookDefinition { } /** - * Create a hook definition from an interface method. - * Automatically extracts metadata from the method signature and annotations. + * Create a new builder for a hook definition. * - * @param hookName The hook name (may differ from method name) - * @param method The method to extract metadata from - * @return A hook definition with extracted metadata + * @param hookName The hook function name + * @return A new builder instance */ - public static HookDefinition fromMethod(String hookName, Method method) { - Builder builder = builder(hookName); - - // Extract event type from first parameter - if (method.getParameterCount() > 0) { - Class eventType = method.getParameterTypes()[0]; - builder.eventClass(eventType); - - // Extract @Cancelable annotation - if (eventType.isAnnotationPresent(Cancelable.class)) { - builder.cancelable(true); - } - - // Build required imports from event type - String importName = getImportForClass(eventType); - if (importName != null) { - builder.requiredImports(importName); - } - } - - // Extract parameter names from @ParamName annotations - Parameter[] params = method.getParameters(); - if (params.length > 0) { - String[] names = new String[params.length]; - for (int i = 0; i < params.length; i++) { - ParamName annotation = params[i].getAnnotation(ParamName.class); - if (annotation != null) { - names[i] = annotation.value(); - } else { - names[i] = params[i].getType().getSimpleName().toLowerCase(); - } - } - builder.paramNames(names); - } - - return builder.build(); + public static Builder builder(String hookName) { + return new Builder(hookName); } /** @@ -182,7 +137,6 @@ public class HookDefinition implements IHookDefinition { } String name = enclosing.getName(); - // Skip java.lang classes if (name.startsWith("java.lang.")) { return null; } @@ -192,9 +146,6 @@ public class HookDefinition implements IHookDefinition { // ==================== Builder ==================== - /** - * Builder for creating HookDefinition instances with a fluent API. - */ public static class Builder { private final String hookName; private String eventClassName; @@ -210,27 +161,18 @@ public class HookDefinition implements IHookDefinition { } /** - * Set the event class by name (avoids classloading issues). - * - * @param className Full qualified class name (use $ for nested classes) - * @return this builder - */ - public Builder eventClass(String className) { - this.eventClassName = className; - return this; - } - - /** - * Set the event class directly (convenience method). - * The class name will be extracted automatically. - * - * @param clazz The event class - * @return this builder + * Set the event class directly. + * Automatically derives: class name, required imports, and cancelable status. */ public Builder eventClass(Class clazz) { if (clazz != null) { this.eventClassName = clazz.getName(); - // Auto-detect cancelable + + String importName = getImportForClass(clazz); + if (importName != null) { + this.requiredImports = new String[]{importName}; + } + if (clazz.isAnnotationPresent(Cancelable.class)) { this.cancelable = true; } @@ -238,11 +180,16 @@ public class HookDefinition implements IHookDefinition { return this; } + /** + * Set the event class by name (for addon classes that may not be loaded). + */ + public Builder eventClass(String className) { + this.eventClassName = className; + return this; + } + /** * Set parameter names for stub generation. - * - * @param names Parameter names - * @return this builder */ public Builder paramNames(String... names) { this.paramNames = names; @@ -250,10 +197,7 @@ public class HookDefinition implements IHookDefinition { } /** - * Set required imports for this hook's event type. - * - * @param imports Fully qualified class names to import - * @return this builder + * Set required imports (only needed if using eventClass(String)). */ public Builder requiredImports(String... imports) { this.requiredImports = imports; @@ -262,20 +206,12 @@ public class HookDefinition implements IHookDefinition { /** * Set whether the event is cancelable. - * - * @param cancelable true if the event can be canceled - * @return this builder */ public Builder cancelable(boolean cancelable) { this.cancelable = cancelable; return this; } - /** - * Build the hook definition. - * - * @return A new HookDefinition instance - */ public HookDefinition build() { return new HookDefinition(this); } @@ -285,19 +221,14 @@ public class HookDefinition implements IHookDefinition { @Override public String toString() { - return "HookDefinition{" + - "hookName='" + hookName + '\'' + - ", eventClassName='" + eventClassName + '\'' + - ", cancelable=" + cancelable + - '}'; + return "HookDefinition{hookName='" + hookName + "', eventClass='" + eventClassName + "'}"; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof HookDefinition)) return false; - HookDefinition other = (HookDefinition) obj; - return hookName.equals(other.hookName); + return hookName.equals(((HookDefinition) obj).hookName); } @Override diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index 8ecc8d43f..f89177c9e 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,5 +1,6 @@ package noppes.npcs.controllers; +import noppes.npcs.api.event.*; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; @@ -8,28 +9,24 @@ import noppes.npcs.controllers.data.RecipeScript; import java.util.*; +import static noppes.npcs.constants.EnumScriptType.*; +import static noppes.npcs.constants.ScriptContext.*; + /** * Controller for managing script hooks. * Allows addon mods to register custom hooks that appear in script editor GUIs. * - *

Rich Hook Registration

- * Supports full hook definitions with metadata for proper stub generation: + *

Addon Hook Registration

*
{@code
- * ScriptHookController.Instance.registerHookDefinition("player",
- *     HookDefinition.builder("onDBCTransform")
- *         .eventClass(IDBCEvent.TransformEvent.class)
- *         .requiredImports("com.dbc.api.event.IDBCEvent")
- *         .build());
+ * ScriptHookController.Instance.registerHook("player",
+ *     HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class));
  * }
*/ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Hook definitions per context - single source of truth private final Map> hookDefinitions = new HashMap<>(); - - // Revision counter for cache invalidation private int hookRevision = 0; public ScriptHookController() { @@ -37,286 +34,268 @@ public class ScriptHookController implements IScriptHookHandler { initializeBuiltInHooks(); } + // ==================== INITIALIZATION ==================== + private void initializeBuiltInHooks() { - // Initialize empty maps for all contexts from ScriptContext for (ScriptContext context : ScriptContext.values()) { if (!context.hookContext.isEmpty()) { hookDefinitions.put(context.hookContext, new LinkedHashMap<>()); } } - // NPC hooks initializeNpcHooks(); - - // Player hooks initializePlayerHooks(); - - // Block hooks initializeBlockHooks(); - - // Item hooks initializeItemHooks(); - - // Linked item hooks initializeLinkedItemHooks(); - - // Recipe hooks - registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.PRE.function, null, null); - registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.POST.function, null, null); - - // Effect hooks - registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_ADD.function, null, null); - registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_TICK.function, null, null); - registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_REMOVE.function, null, null); - - // Forge hooks - registerHookDef(ScriptContext.FORGE, EnumScriptType.INIT.function, null, null); - registerHookDef(ScriptContext.FORGE, EnumScriptType.CNPC_NATURAL_SPAWN.function, null, null); + initializeRecipeHooks(); + initializeEffectHooks(); + initializeForgeHooks(); } private void initializeNpcHooks() { - String ctx = ScriptContext.NPC.hookContext; - String npcImport = "noppes.npcs.api.event.INpcEvent"; - String projImport = "noppes.npcs.api.event.IProjectileEvent"; - String abilityImport = "noppes.npcs.api.event.IAbilityEvent"; - // Core lifecycle - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); + hook(NPC, INIT, INpcEvent.InitEvent.class); + hook(NPC, TICK, INpcEvent.UpdateEvent.class); + hook(NPC, TIMER, INpcEvent.TimerEvent.class); // Interaction - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); - registerHookDef(ctx, EnumScriptType.DIALOG.function, "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); - registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); + hook(NPC, INTERACT, INpcEvent.InteractEvent.class); + hook(NPC, DIALOG, INpcEvent.DialogEvent.class); + hook(NPC, DIALOG_CLOSE, INpcEvent.DialogClosedEvent.class); + hook(NPC, COLLIDE, INpcEvent.CollideEvent.class); // Combat - registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); - registerHookDef(ctx, EnumScriptType.ATTACK_MELEE.function, "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); - registerHookDef(ctx, EnumScriptType.ATTACK_SWING.function, "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); - registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TARGET.function, "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TARGET_LOST.function, "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); + hook(NPC, DAMAGED, INpcEvent.DamagedEvent.class); + hook(NPC, KILLED, INpcEvent.DiedEvent.class); + hook(NPC, KILLS, INpcEvent.KilledEntityEvent.class); + hook(NPC, ATTACK_MELEE, INpcEvent.MeleeAttackEvent.class); + hook(NPC, ATTACK_SWING, INpcEvent.SwingEvent.class); + hook(NPC, RANGED_LAUNCHED, INpcEvent.RangedLaunchedEvent.class); + hook(NPC, TARGET, INpcEvent.TargetEvent.class); + hook(NPC, TARGET_LOST, INpcEvent.TargetLostEvent.class); - // Projectile events - registerHookDef(ctx, EnumScriptType.PROJECTILE_TICK.function, "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); - registerHookDef(ctx, EnumScriptType.PROJECTILE_IMPACT.function, "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); + // Projectile + hook(NPC, PROJECTILE_TICK, IProjectileEvent.UpdateEvent.class); + hook(NPC, PROJECTILE_IMPACT, IProjectileEvent.ImpactEvent.class); - // Ability events - registerHookDef(ctx, EnumScriptType.ABILITY_START.function, "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_EXECUTE.function, "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_HIT.function, "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_TICK.function, "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_INTERRUPT.function, "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_COMPLETE.function, "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); + // Ability + hook(NPC, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(NPC, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(NPC, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(NPC, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); } private void initializePlayerHooks() { - String ctx = ScriptContext.PLAYER.hookContext; - String playerImport = "noppes.npcs.api.event.IPlayerEvent"; - String questImport = "noppes.npcs.api.event.IQuestEvent"; - String dialogImport = "noppes.npcs.api.event.IDialogEvent"; - String factionImport = "noppes.npcs.api.event.IFactionEvent"; - String partyImport = "noppes.npcs.api.event.IPartyEvent"; - String guiImport = "noppes.npcs.api.event.ICustomGuiEvent"; - String animImport = "noppes.npcs.api.event.IAnimationEvent"; - // Core lifecycle - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); - registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); + hook(PLAYER, INIT, IPlayerEvent.InitEvent.class); + hook(PLAYER, TICK, IPlayerEvent.UpdateEvent.class); + hook(PLAYER, TIMER, IPlayerEvent.TimerEvent.class); // Combat - registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); - registerHookDef(ctx, EnumScriptType.ATTACKED.function, "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.DAMAGED_ENTITY.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); - registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); - registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); + hook(PLAYER, ATTACK, IPlayerEvent.AttackEvent.class); + hook(PLAYER, ATTACKED, IPlayerEvent.AttackedEvent.class); + hook(PLAYER, DAMAGED, IPlayerEvent.DamagedEvent.class); + hook(PLAYER, DAMAGED_ENTITY, IPlayerEvent.DamagedEntityEvent.class); + hook(PLAYER, KILLS, IPlayerEvent.KilledEntityEvent.class); + hook(PLAYER, KILLED, IPlayerEvent.DiedEvent.class); // Interaction - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); - registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); - registerHookDef(ctx, EnumScriptType.BREAK_BLOCK.function, "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); - registerHookDef(ctx, EnumScriptType.CHAT.function, "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); + hook(PLAYER, INTERACT, IPlayerEvent.InteractEvent.class); + hook(PLAYER, RIGHT_CLICK, IPlayerEvent.RightClickEvent.class); + hook(PLAYER, BREAK_BLOCK, IPlayerEvent.BreakEvent.class); + hook(PLAYER, CHAT, IPlayerEvent.ChatEvent.class); // Connection - registerHookDef(ctx, EnumScriptType.LOGIN.function, "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); - registerHookDef(ctx, EnumScriptType.LOGOUT.function, "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); - registerHookDef(ctx, EnumScriptType.RESPAWN.function, "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); + hook(PLAYER, LOGIN, IPlayerEvent.LoginEvent.class); + hook(PLAYER, LOGOUT, IPlayerEvent.LogoutEvent.class); + hook(PLAYER, RESPAWN, IPlayerEvent.RespawnEvent.class); // Input - registerHookDef(ctx, EnumScriptType.KEY_PRESSED.function, "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.MOUSE_CLICKED.function, "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); + hook(PLAYER, KEY_PRESSED, IPlayerEvent.KeyPressedEvent.class); + hook(PLAYER, MOUSE_CLICKED, IPlayerEvent.MouseClickedEvent.class); // Items - registerHookDef(ctx, EnumScriptType.PICKUP.function, "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); - registerHookDef(ctx, EnumScriptType.TOSS.function, "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); - registerHookDef(ctx, EnumScriptType.DROP.function, "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); - registerHookDef(ctx, EnumScriptType.PICKUP_XP.function, "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); - registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.CONTAINER_OPEN.function, "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); + hook(PLAYER, PICKUP, IPlayerEvent.PickUpEvent.class); + hook(PLAYER, TOSS, IPlayerEvent.TossEvent.class); + hook(PLAYER, DROP, IPlayerEvent.DropEvent.class); + hook(PLAYER, PICKUP_XP, IPlayerEvent.PickupXPEvent.class); + hook(PLAYER, START_USING_ITEM, IPlayerEvent.StartUsingItem.class); + hook(PLAYER, USING_ITEM, IPlayerEvent.UsingItem.class); + hook(PLAYER, STOP_USING_ITEM, IPlayerEvent.StopUsingItem.class); + hook(PLAYER, FINISH_USING_ITEM, IPlayerEvent.FinishUsingItem.class); + hook(PLAYER, CONTAINER_OPEN, IPlayerEvent.ContainerOpen.class); // Movement - registerHookDef(ctx, EnumScriptType.JUMP.function, "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); - registerHookDef(ctx, EnumScriptType.FALL.function, "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); - registerHookDef(ctx, EnumScriptType.CHANGED_DIM.function, "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); + hook(PLAYER, JUMP, IPlayerEvent.JumpEvent.class); + hook(PLAYER, FALL, IPlayerEvent.FallEvent.class); + hook(PLAYER, CHANGED_DIM, IPlayerEvent.ChangedDimension.class); // World - registerHookDef(ctx, EnumScriptType.RANGED_CHARGE.function, "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); - registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.USE_HOE.function, "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); - registerHookDef(ctx, EnumScriptType.BONEMEAL.function, "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); - registerHookDef(ctx, EnumScriptType.FILL_BUCKET.function, "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); - registerHookDef(ctx, EnumScriptType.WAKE_UP.function, "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); - registerHookDef(ctx, EnumScriptType.SLEEP.function, "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); - registerHookDef(ctx, EnumScriptType.PLAYSOUND.function, "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); - registerHookDef(ctx, EnumScriptType.LIGHTNING.function, "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); - registerHookDef(ctx, EnumScriptType.SCRIPT_COMMAND.function, "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); + hook(PLAYER, RANGED_CHARGE, IPlayerEvent.RangedChargeEvent.class); + hook(PLAYER, RANGED_LAUNCHED, IPlayerEvent.RangedLaunchedEvent.class); + hook(PLAYER, USE_HOE, IPlayerEvent.UseHoeEvent.class); + hook(PLAYER, BONEMEAL, IPlayerEvent.BonemealEvent.class); + hook(PLAYER, FILL_BUCKET, IPlayerEvent.FillBucketEvent.class); + hook(PLAYER, WAKE_UP, IPlayerEvent.WakeUpEvent.class); + hook(PLAYER, SLEEP, IPlayerEvent.SleepEvent.class); + hook(PLAYER, PLAYSOUND, IPlayerEvent.SoundEvent.class); + hook(PLAYER, LIGHTNING, IPlayerEvent.LightningEvent.class); + hook(PLAYER, SCRIPT_COMMAND, ICustomNPCsEvent.ScriptedCommandEvent.class); - // Quest events - registerHookDef(ctx, EnumScriptType.QUEST_START.function, "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); - registerHookDef(ctx, EnumScriptType.QUEST_COMPLETED.function, "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); - registerHookDef(ctx, EnumScriptType.QUEST_TURNIN.function, "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); + // Quest + hook(PLAYER, QUEST_START, IQuestEvent.QuestStartEvent.class); + hook(PLAYER, QUEST_COMPLETED, IQuestEvent.QuestCompletedEvent.class); + hook(PLAYER, QUEST_TURNIN, IQuestEvent.QuestTurnedInEvent.class); - // Dialog events - registerHookDef(ctx, EnumScriptType.DIALOG_OPEN.function, "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); - registerHookDef(ctx, EnumScriptType.DIALOG_OPTION.function, "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); - registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); + // Dialog + hook(PLAYER, DIALOG_OPEN, IDialogEvent.DialogOpen.class); + hook(PLAYER, DIALOG_OPTION, IDialogEvent.DialogOption.class); + hook(PLAYER, DIALOG_CLOSE, IDialogEvent.DialogClosed.class); - // Faction events - registerHookDef(ctx, EnumScriptType.FACTION_POINTS.function, "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); + // Faction + hook(PLAYER, FACTION_POINTS, IFactionEvent.FactionPoints.class); - // Custom GUI events - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); + // Custom GUI + hook(PLAYER, CUSTOM_GUI_CLOSED, ICustomGuiEvent.CloseEvent.class); + hook(PLAYER, CUSTOM_GUI_BUTTON, ICustomGuiEvent.ButtonEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT, ICustomGuiEvent.SlotEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT_CLICKED, ICustomGuiEvent.SlotClickEvent.class); + hook(PLAYER, CUSTOM_GUI_SCROLL, ICustomGuiEvent.ScrollEvent.class); + hook(PLAYER, CUSTOM_GUI_TEXTFIELD, ICustomGuiEvent.UnfocusedEvent.class); - // Party events - registerHookDef(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_QUEST_SET.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_INVITE.function, "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_KICK.function, "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_LEAVE.function, "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_DISBAND.function, "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); + // Party + hook(PLAYER, PARTY_QUEST_COMPLETED, IPartyEvent.PartyQuestCompletedEvent.class); + hook(PLAYER, PARTY_QUEST_SET, IPartyEvent.PartyQuestSetEvent.class); + hook(PLAYER, PARTY_QUEST_TURNED_IN, IPartyEvent.PartyQuestTurnedInEvent.class); + hook(PLAYER, PARTY_INVITE, IPartyEvent.PartyInviteEvent.class); + hook(PLAYER, PARTY_KICK, IPartyEvent.PartyKickEvent.class); + hook(PLAYER, PARTY_LEAVE, IPartyEvent.PartyLeaveEvent.class); + hook(PLAYER, PARTY_DISBAND, IPartyEvent.PartyDisbandEvent.class); - // Animation events - registerHookDef(ctx, EnumScriptType.ANIMATION_START.function, "noppes.npcs.api.event.IAnimationEvent$Started", animImport); - registerHookDef(ctx, EnumScriptType.ANIMATION_END.function, "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); - registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); - registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); + // Animation + hook(PLAYER, ANIMATION_START, IAnimationEvent.Started.class); + hook(PLAYER, ANIMATION_END, IAnimationEvent.Ended.class); + hook(PLAYER, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class); + hook(PLAYER, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class); - // Profile events - registerHookDef(ctx, EnumScriptType.PROFILE_CHANGE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); - registerHookDef(ctx, EnumScriptType.PROFILE_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); - registerHookDef(ctx, EnumScriptType.PROFILE_CREATE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); + // Profile + hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class); + hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class); + hook(PLAYER, PROFILE_CREATE, IPlayerEvent.ProfileEvent.Create.class); - // Effect events (for player context) - registerHookDef(ctx, EnumScriptType.ON_EFFECT_ADD.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); - registerHookDef(ctx, EnumScriptType.ON_EFFECT_TICK.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); - registerHookDef(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); + // Effect (player context) + hook(PLAYER, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(PLAYER, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(PLAYER, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); } private void initializeBlockHooks() { - String ctx = ScriptContext.BLOCK.hookContext; - String blockImport = "noppes.npcs.api.event.IBlockEvent"; - - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); - registerHookDef(ctx, EnumScriptType.FALLEN_UPON.function, "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); - registerHookDef(ctx, EnumScriptType.REDSTONE.function, "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); - registerHookDef(ctx, EnumScriptType.BROKEN.function, "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); - registerHookDef(ctx, EnumScriptType.EXPLODED.function, "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.RAIN_FILLED.function, "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); - registerHookDef(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.CLICKED.function, "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.HARVESTED.function, "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); + hook(BLOCK, INIT, IBlockEvent.InitEvent.class); + hook(BLOCK, TICK, IBlockEvent.UpdateEvent.class); + hook(BLOCK, INTERACT, IBlockEvent.InteractEvent.class); + hook(BLOCK, FALLEN_UPON, IBlockEvent.EntityFallenUponEvent.class); + hook(BLOCK, REDSTONE, IBlockEvent.RedstoneEvent.class); + hook(BLOCK, BROKEN, IBlockEvent.BreakEvent.class); + hook(BLOCK, EXPLODED, IBlockEvent.ExplodedEvent.class); + hook(BLOCK, RAIN_FILLED, IBlockEvent.RainFillEvent.class); + hook(BLOCK, NEIGHBOR_CHANGED, IBlockEvent.NeighborChangedEvent.class); + hook(BLOCK, CLICKED, IBlockEvent.ClickedEvent.class); + hook(BLOCK, HARVESTED, IBlockEvent.HarvestedEvent.class); + hook(BLOCK, COLLIDE, IBlockEvent.CollidedEvent.class); + hook(BLOCK, TIMER, IBlockEvent.TimerEvent.class); } private void initializeItemHooks() { - String ctx = ScriptContext.ITEM.hookContext; - String itemImport = "noppes.npcs.api.event.IItemEvent"; - - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + hook(ITEM, INIT, IItemEvent.InitEvent.class); + hook(ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); } private void initializeLinkedItemHooks() { - String ctx = ScriptContext.LINKED_ITEM.hookContext; - String itemImport = "noppes.npcs.api.event.IItemEvent"; - String linkedImport = "noppes.npcs.api.event.ILinkedItemEvent"; - - // Linked item specific hooks - registerHookDef(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); - registerHookDef(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); + // Linked item specific + hook(LINKED_ITEM, LINKED_ITEM_BUILD, ILinkedItemEvent.BuildEvent.class); + hook(LINKED_ITEM, LINKED_ITEM_VERSION, ILinkedItemEvent.VersionChangeEvent.class); // Standard item hooks - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + hook(LINKED_ITEM, INIT, IItemEvent.InitEvent.class); + hook(LINKED_ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(LINKED_ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(LINKED_ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(LINKED_ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(LINKED_ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(LINKED_ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(LINKED_ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(LINKED_ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(LINKED_ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(LINKED_ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(LINKED_ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); } - /** - * Internal helper to register a hook definition. - */ - private void registerHookDef(String context, String hookName, String eventClassName, String... imports) { - Map contextDefs = hookDefinitions.get(context); - if (contextDefs == null) { - contextDefs = new LinkedHashMap<>(); - hookDefinitions.put(context, contextDefs); - } - - HookDefinition.Builder builder = HookDefinition.builder(hookName); - if (eventClassName != null) { - builder.eventClass(eventClassName); - } - if (imports != null && imports.length > 0 && imports[0] != null) { - builder.requiredImports(imports); - } - contextDefs.put(hookName, builder.build()); + private void initializeRecipeHooks() { + hook(RECIPE, RecipeScript.ScriptType.PRE.function, IRecipeEvent.Pre.class); + hook(RECIPE, RecipeScript.ScriptType.POST.function, IRecipeEvent.Post.class); } - private void registerHookDef(ScriptContext context, String hookName, String eventClassName, String... imports) { - registerHookDef(context.hookContext, hookName, eventClassName, imports); + private void initializeEffectHooks() { + // Effect context uses IPlayerEvent.EffectEvent (same as player context) + hook(EFFECT, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(EFFECT, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(EFFECT, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); + } + + private void initializeForgeHooks() { + hook(FORGE, INIT, IForgeEvent.InitEvent.class); + hook(FORGE, FORGE_WORLD, IForgeEvent.WorldEvent.class); + hook(FORGE, FORGE_ENTITY, IForgeEvent.EntityEvent.class); + hook(FORGE, CNPC_NATURAL_SPAWN, ICustomNPCsEvent.CNPCNaturalSpawnEvent.class); + } + + // ==================== INTERNAL REGISTRATION ==================== + + private void hook(ScriptContext context, EnumScriptType type, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.of(type.function, eventClass)); + } + + private void hook(ScriptContext context, EnumScriptType type) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.simple(type.function)); + } + + private void hook(ScriptContext context, String hookName) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.simple(hookName)); + } + + private void hook(ScriptContext context, String hookName, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.of(hookName, eventClass)); } // ==================== PUBLIC API ==================== - @Override - public void registerHookDefinition(String context, IHookDefinition definition) { - if (context == null || definition == null || definition.hookName() == null) + /** + * Register a hook with event class (auto-derives imports). + * For addon mods to register custom hooks. + * + * @param context Hook context (e.g., "npc", "player") + * @param hookName Function name in scripts + * @param eventClass The event class (imports auto-derived from enclosing class) + */ + public void registerHook(String context, String hookName, Class eventClass) { + if (context == null || hookName == null) return; Map contextDefs = hookDefinitions.get(context); @@ -325,19 +304,70 @@ public class ScriptHookController implements IScriptHookHandler { hookDefinitions.put(context, contextDefs); } + contextDefs.put(hookName, HookDefinition.of(hookName, eventClass)); + hookRevision++; + } + + public void registerHook(ScriptContext context, String hookName, Class eventClass) { + registerHook(context.hookContext, hookName, eventClass); + } + + /** + * Register a simple hook with no event class metadata. + */ + public void registerHook(String context, String hookName) { + if (context == null || hookName == null) + return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + if (!contextDefs.containsKey(hookName)) { + contextDefs.put(hookName, HookDefinition.simple(hookName)); + hookRevision++; + } + } + + public void registerHook(ScriptContext context, String hookName) { + registerHook(context.hookContext, hookName); + } + + /** + * Register a hook definition for addon mods. + */ + public void registerHook(String context, HookDefinition definition) { + if (context == null || definition == null) + return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + contextDefs.put(definition.hookName(), definition); + hookRevision++; + } + + public void registerHook(ScriptContext context, HookDefinition definition) { + registerHook(context.hookContext, definition); + } + + @Override + public void registerHookDefinition(String context, IHookDefinition definition) { if (definition instanceof HookDefinition) { - contextDefs.put(definition.hookName(), (HookDefinition) definition); - } else { - HookDefinition def = HookDefinition.builder(definition.hookName()) + registerHook(context, (HookDefinition) definition); + } else if (definition != null) { + registerHook(context, HookDefinition.builder(definition.hookName()) .eventClass(definition.eventClassName()) .paramNames(definition.paramNames()) .requiredImports(definition.requiredImports()) .cancelable(definition.isCancelable()) - .build(); - contextDefs.put(definition.hookName(), def); + .build()); } - - hookRevision++; } @Override @@ -361,87 +391,6 @@ public class ScriptHookController implements IScriptHookHandler { return new ArrayList<>(contextDefs.values()); } - @Override - public int getHookRevision() { - return hookRevision; - } - - // ==================== CONVENIENCE METHODS ==================== - - public void registerHook(ScriptContext context, String functionName) { - registerHook(context.hookContext, functionName); - } - - public void registerHooks(ScriptContext context, String... functionNames) { - for (String fn : functionNames) { - registerHook(context.hookContext, fn); - } - } - - public void registerHookDefinition(ScriptContext context, IHookDefinition definition) { - registerHookDefinition(context.hookContext, definition); - } - - // ==================== LEGACY API (IScriptHookHandler) ==================== - - @Override - public void registerHook(String context, String functionName) { - if (context == null || functionName == null || functionName.isEmpty()) - return; - - Map contextDefs = hookDefinitions.get(context); - if (contextDefs == null) { - contextDefs = new LinkedHashMap<>(); - hookDefinitions.put(context, contextDefs); - } - - // Only add if not already present - if (!contextDefs.containsKey(functionName)) { - contextDefs.put(functionName, HookDefinition.builder(functionName).build()); - hookRevision++; - } - } - - @Override - public void registerHooks(String functionName, String... contexts) { - if (functionName == null || functionName.isEmpty() || contexts == null) - return; - - for (String context : contexts) { - registerHook(context, functionName); - } - } - - @Override - public void unregisterHook(String context, String functionName) { - if (context == null || functionName == null) - return; - - Map contextDefs = hookDefinitions.get(context); - if (contextDefs != null && contextDefs.remove(functionName) != null) { - hookRevision++; - } - } - - @Override - public void unregisterHookFromAll(String functionName) { - if (functionName == null) - return; - - for (Map contextDefs : hookDefinitions.values()) { - if (contextDefs.remove(functionName) != null) { - hookRevision++; - } - } - } - - @Override - public List getAddonHooks(String context) { - // With unified storage, we can't distinguish addon vs built-in - // Return empty for backwards compatibility - return Collections.emptyList(); - } - @Override public List getAllHooks(String context) { if (context == null) @@ -455,12 +404,17 @@ public class ScriptHookController implements IScriptHookHandler { } @Override - public boolean hasHook(String context, String functionName) { - if (context == null || functionName == null) + public boolean hasHook(String context, String hookName) { + if (context == null || hookName == null) return false; Map contextDefs = hookDefinitions.get(context); - return contextDefs != null && contextDefs.containsKey(functionName); + return contextDefs != null && contextDefs.containsKey(hookName); + } + + @Override + public int getHookRevision() { + return hookRevision; } @Override From 9c9bcc822859b8bb96fc04c2ede1292cf9b9267d Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 20:17:37 -0500 Subject: [PATCH 59/64] Update JaninoScript.java --- .../java/noppes/npcs/janino/JaninoScript.java | 452 ++++++------------ 1 file changed, 133 insertions(+), 319 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a97aed889..b04031973 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -19,172 +19,64 @@ import java.lang.invoke.MethodHandle; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { - public boolean errored = false; - private String language = "Java"; - public TreeMap console = new TreeMap(); - /** - * Text written in the script GUI - */ + public boolean errored = false; public String script = ""; - /** - * This script's selected external files - * stored in "world/customnpcs/scripts/java" - */ public List externalScripts = new ArrayList<>(); - /** - * To evaluate if script/externalScripts were changed or not, - * and compile the fullScript freshly if so. - */ + public TreeMap console = new TreeMap<>(); public boolean evaluated; public final Class type; - protected final Sandbox sandbox; public final IScriptBodyBuilder builder; + protected final Sandbox sandbox; protected IScriptClassBody scriptBody; - private final JaninoHookResolver hookResolver; - + private final JaninoHookResolver hookResolver = new JaninoHookResolver(); private final String[] defaultImports; - // Hook definition cache for fast lookup (NEW) - private Map hookDefinitionCache; + private Map hookDefCache; private int lastHookRevision = -1; + private int lastSeenGlobalRevision; + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; - this.builder = IScriptBodyBuilder.getBuilder(type, isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) + this.builder = IScriptBodyBuilder.getBuilder(type, + isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) .setDefaultImports(this.defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); - sandbox = new Sandbox(permissions); - + this.sandbox = new Sandbox(permissions); this.scriptBody = builder.build(); - - this.hookResolver = new JaninoHookResolver(); } protected JaninoScript(Class type, String[] defaultImports) { this(type, defaultImports, false); } - // ==================== HOOK CONTEXT (Abstract) ==================== - - /** - * Get the hook context identifier for this script type. - * Used to look up hook definitions from the registry. - * - * @return Context string, e.g., "npc", "player", "block" - */ protected abstract String getHookContext(); - // ==================== HOOK DEFINITION CACHE ==================== + // ==================== COMPILATION ==================== - /** - * Get hook definition from cache, rebuilding cache if revision changed. - * - * @param hookName The hook name to look up - * @return The hook definition, or null if not found - */ - protected IHookDefinition getHookDefinition(String hookName) { - if (ScriptHookController.Instance == null) { - return null; - } - - int currentRevision = ScriptHookController.Instance.getHookRevision(); - if (hookDefinitionCache == null || currentRevision != lastHookRevision) { - rebuildHookDefinitionCache(); - lastHookRevision = currentRevision; - } - - return hookDefinitionCache.get(hookName); - } - - /** - * Rebuild the hook definition cache from the registry. - */ - private void rebuildHookDefinitionCache() { - hookDefinitionCache = new HashMap<>(); - String context = getHookContext(); - - if (context == null || context.isEmpty() || ScriptHookController.Instance == null) { - return; - } - - for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { - hookDefinitionCache.put(def.hookName(), def); - } - } - - /** - * Get all required imports from hook definitions plus default imports. - * - * @return Array of all import statements needed - */ - protected String[] collectAllImports() { - Set allImports = new LinkedHashSet<>(); - - // Add default imports first - Collections.addAll(allImports, defaultImports); - - // Add imports from all hook definitions for this context - String context = getHookContext(); - if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { - String[] imports = def.requiredImports(); - if (imports != null) { - Collections.addAll(allImports, imports); - } - } - } - - return allImports.toArray(new String[0]); - } - - protected T getUnsafe() { - return scriptBody.get(); - } - - /** - * Feed the code into the engine and compile it. - * Rebuilds imports from hook definitions before compilation. - */ public void compileScript(String code) { - try { - // Rebuild imports from current hook definitions + defaults - String[] allImports = collectAllImports(); - builder.setDefaultImports(allImports); - - // Rebuild scriptBody to apply new imports + String[] imports = collectImportsForCode(code); + builder.setDefaultImports(imports); this.scriptBody = builder.build(); scriptBody.setScript(code); - } catch (InternalCompilerException e ) { - Throwable parentCause = null; - Throwable cause = e; - + } catch (InternalCompilerException e) { appendConsole("Compilation error: " + e.getMessage()); - - while (cause.getCause() != null) { parentCause = cause; cause = cause.getCause(); } - Throwable rootCause = cause; - - if (e != cause) { - appendConsole("\n"); - - if (parentCause != null) - appendConsole(parentCause.getMessage()); - appendConsole(rootCause.getMessage()); - } + Throwable cause = e; + while (cause.getCause() != null) cause = cause.getCause(); + if (e != cause) appendConsole(cause.getMessage()); hookResolver.clearResolutionCaches(); } catch (Exception e) { - appendConsole("Unknown error: " + e.getMessage()); + appendConsole("Error: " + e.getMessage()); hookResolver.clearResolutionCaches(); } } @@ -195,7 +87,6 @@ public abstract class JaninoScript implements IScriptUnit { public void ensureCompiled() { int global = ScriptController.Instance.globalRevision; - if (!evaluated || global != lastSeenGlobalRevision) { compileScript(); lastSeenGlobalRevision = global; @@ -203,221 +94,123 @@ public abstract class JaninoScript implements IScriptUnit { } } - /** - * globalRevision is incremented on server - * each time externalScripts are updated - * then sent to client. - */ - private int lastSeenGlobalRevision; - private String getFullCode() { StringBuilder sb = new StringBuilder(); - - if (ConfigScript.RunLoadedScriptsFirst) - this.appendExternalScripts(sb); - - // Main script - if (this.script != null && !this.script.isEmpty()) - sb.append(this.script).append("\n"); - - if (!ConfigScript.RunLoadedScriptsFirst) - this.appendExternalScripts(sb); - + if (ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); + if (script != null && !script.isEmpty()) sb.append(script).append("\n"); + if (!ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); return sb.toString(); } private void appendExternalScripts(StringBuilder sb) { - for (String scriptName : externalScripts) { - String code = ScriptController.Instance.scripts.get(scriptName); - if (code != null && !code.isEmpty()) - sb.append(code).append("\n"); + for (String name : externalScripts) { + String code = ScriptController.Instance.scripts.get(name); + if (code != null && !code.isEmpty()) sb.append(code).append("\n"); } } - public void callByHookName(String hookName, Object arg) { - callFunction(hookName, arg); + // ==================== SMART IMPORTS ==================== + + private String[] collectImportsForCode(String code) { + Set imports = new LinkedHashSet<>(); + Collections.addAll(imports, defaultImports); + + if (ScriptHookController.Instance == null) { + return imports.toArray(new String[0]); + } + + String context = getHookContext(); + if (context == null || context.isEmpty()) { + return imports.toArray(new String[0]); + } + + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + if (code.contains(def.hookName() + "(")) { + String[] hookImports = def.requiredImports(); + if (hookImports != null) { + Collections.addAll(imports, hookImports); + } + } + } + + return imports.toArray(new String[0]); } + // ==================== HOOK EXECUTION ==================== + @Override public Object callFunction(String hookName, Object... args) { - if (hookName == null || hookName.isEmpty()) - return null; + if (hookName == null || hookName.isEmpty()) return null; ensureCompiled(); - T t = getUnsafe(); - if (t == null) - return null; + T instance = scriptBody.get(); + if (instance == null) return null; Object[] invokeArgs = args == null ? new Object[0] : args; + MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, instance); + if (handle == null) return null; - MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, t); - if (handle == null) - return null; - - final MethodHandle finalHandle = handle; try { return sandbox.confine((PrivilegedAction) () -> { try { - // Prepend receiver to args for virtual method call Object[] fullArgs = new Object[invokeArgs.length + 1]; - fullArgs[0] = t; + fullArgs[0] = instance; System.arraycopy(invokeArgs, 0, fullArgs, 1, invokeArgs.length); - return finalHandle.invokeWithArguments(fullArgs); + return handle.invokeWithArguments(fullArgs); } catch (Throwable e) { - appendConsole("Error calling hook " + hookName + ": " + e.getMessage()); + appendConsole("Error in " + hookName + ": " + e.getMessage()); return null; } }); } catch (Exception e) { - appendConsole("Runtime Error in hook " + hookName + ": " + e.getMessage()); + appendConsole("Runtime error in " + hookName + ": " + e.getMessage()); return null; } } - public R call(Function fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return null; - -// CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); -// ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); - - try { - return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); -// return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( -// new ProtectionDomain[] { -// pd -// } -// )); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - return null; - } - } - - public void run(Consumer fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return; - - try { - sandbox.confine((PrivilegedAction) () -> { - fn.accept(t); - return null; - }); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - } - } - - public void unload() { - } - - - // ==================== IScriptUnit IMPLEMENTATION ==================== - - /** - * Execute this script unit for the given hook type and event. - * Implementation of IScriptUnit.run(EnumScriptType, Event) - * - * @param type The script hook type - * @param event The event object - */ @Override public void run(EnumScriptType type, Event event) { - if (type == null) - return; - - callFunction(type.function, event); + if (type != null) callFunction(type.function, event); } - /** - * Execute this script unit for the given hook name and event. - * Implementation of IScriptUnit.run(String, Event) - * - * @param hookName The hook name (e.g., "init", "interact") - * @param event The event object - */ @Override public void run(String hookName, Object event) { callFunction(hookName, event); } - @Override - public String getScript() { - return this.script; + // ==================== HOOK DEFINITIONS ==================== + + protected IHookDefinition getHookDefinition(String hookName) { + if (ScriptHookController.Instance == null) return null; + + int rev = ScriptHookController.Instance.getHookRevision(); + if (hookDefCache == null || rev != lastHookRevision) { + rebuildHookDefCache(); + lastHookRevision = rev; + } + return hookDefCache.get(hookName); } - @Override - public void setScript(String script) { - this.script = script; - this.evaluated = false; - hookResolver.clearResolutionCaches(); - } + private void rebuildHookDefCache() { + hookDefCache = new HashMap<>(); + String context = getHookContext(); + if (context == null || context.isEmpty() || ScriptHookController.Instance == null) return; - @Override - public List getExternalScripts() { - return this.externalScripts; - } - - @Override - public void setExternalScripts(List externalScripts) { - this.externalScripts = externalScripts; - this.evaluated = false; - hookResolver.clearResolutionCaches(); - } - - @Override - public TreeMap getConsole() { - return this.console; - } - - @Override - public void clearConsole() { - console.clear(); - } - - @Override - public void appendConsole(String message) { - if (message != null && !message.isEmpty()) { - long time = System.currentTimeMillis(); - if (this.console.containsKey(time)) { - message = (String) this.console.get(time) + "\n" + message; - } - - this.console.put(time, message); - - while (this.console.size() > 40) { - this.console.remove(this.console.firstKey()); - } + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + hookDefCache.put(def.hookName(), def); } } - @Override - public String getLanguage() { - return this.language; - } - - @Override - public void setLanguage(String language) { - // JaninoScript is always Java, ignore attempts to change - // This method exists for IScriptUnit interface compatibility - } - - @Override - public boolean hasCode() { - if (!externalScripts.isEmpty()) - return true; - return script != null && !script.isEmpty(); + public List getHookList() { + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + return ScriptHookController.Instance.getAllHooks(context); + } + return Collections.emptyList(); } @Override public String generateHookStub(String hookName, Object hookData) { - // Look up in hook definition registry IHookDefinition def = getHookDefinition(hookName); if (def != null) { String typeName = def.getUsableTypeName(); @@ -429,34 +222,69 @@ public abstract class JaninoScript implements IScriptUnit { def.hookName(), typeName, paramName); } } - - // Fallback: generic Object parameter return String.format("public void %s(Object event) {\n \n}\n", hookName); } + // ==================== IScriptUnit ==================== - public List getHookList() { - // Get hooks from registry based on context - String context = getHookContext(); - if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - return ScriptHookController.Instance.getAllHooks(context); - } - return Collections.emptyList(); + @Override public String getScript() { return script; } + + @Override + public void setScript(String script) { + this.script = script; + this.evaluated = false; + hookResolver.clearResolutionCaches(); } + @Override public List getExternalScripts() { return externalScripts; } + + @Override + public void setExternalScripts(List scripts) { + this.externalScripts = scripts; + this.evaluated = false; + hookResolver.clearResolutionCaches(); + } + + @Override public TreeMap getConsole() { return console; } + @Override public void clearConsole() { console.clear(); } + + @Override + public void appendConsole(String message) { + if (message == null || message.isEmpty()) return; + long time = System.currentTimeMillis(); + if (console.containsKey(time)) { + message = console.get(time) + "\n" + message; + } + console.put(time, message); + while (console.size() > 40) { + console.remove(console.firstKey()); + } + } + + @Override public String getLanguage() { return "Java"; } + @Override public void setLanguage(String language) { } + + @Override + public boolean hasCode() { + return !externalScripts.isEmpty() || (script != null && !script.isEmpty()); + } + + @Override public boolean hasErrored() { return errored; } + @Override public void setErrored(boolean errored) { this.errored = errored; } + + // ==================== NBT ==================== @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_JANINO); - compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); - compound.setTag("externalScripts", NBTTags.nbtStringList(this.externalScripts)); + compound.setTag("console", NBTTags.NBTLongStringMap(console)); + compound.setTag("externalScripts", NBTTags.nbtStringList(externalScripts)); compound.setString("script", script); return compound; } @Override public void readFromNBT(NBTTagCompound compound) { - // Read legacy enabled field but ignore it (handler owns enabled now) this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); setScript(compound.getString("script")); @@ -464,28 +292,14 @@ public abstract class JaninoScript implements IScriptUnit { public static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { if (compound.hasKey("Script")) { - if (script == null) - script = factory.get(); - + if (script == null) script = factory.get(); script.readFromNBT(compound.getCompoundTag("Script")); } return script; } public static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { - if (script != null) - compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); - + if (script != null) compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); return compound; } - - @Override - public boolean hasErrored() { - return this.errored; - } - - @Override - public void setErrored(boolean errored) { - this.errored = errored; - } } From 587d435a6fab35d346355bdd7e3f7fd305b7c261 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 23 Jan 2026 03:27:40 +0200 Subject: [PATCH 60/64] Added removed methods --- .../java/noppes/npcs/janino/JaninoScript.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index b04031973..7cae0bdbb 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -19,6 +19,8 @@ import java.lang.invoke.MethodHandle; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { @@ -61,6 +63,54 @@ public abstract class JaninoScript implements IScriptUnit { protected abstract String getHookContext(); + protected T getUnsafe() { + return scriptBody.get(); + } + + public R call(Function fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return null; + + // CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); + // ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); + + try { + return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); + // return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( + // new ProtectionDomain[] { + // pd + // } + // )); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + return null; + } + } + + public void run(Consumer fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return; + + try { + sandbox.confine((PrivilegedAction) () -> { + fn.accept(t); + return null; + }); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + } + } + + public void unload() { + } + + // ==================== COMPILATION ==================== public void compileScript(String code) { From 0f1a6028f3284c888d3aaa5d05a3a700fbd55c67 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 20:53:48 -0500 Subject: [PATCH 61/64] Event Janino Script --- .../npcs/controllers/data/DataScript.java | 4 ++-- .../controllers/data/PlayerDataScript.java | 4 ++-- .../noppes/npcs/janino/EventJaninoScript.java | 11 ++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 6 +++++- .../npcs/janino/impl/JaninoNpcScript.java | 21 ------------------- .../npcs/janino/impl/JaninoPlayerScript.java | 21 ------------------- 6 files changed, 20 insertions(+), 47 deletions(-) create mode 100644 src/main/java/noppes/npcs/janino/EventJaninoScript.java delete mode 100644 src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java delete mode 100644 src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 677b3efde..ede4a4c96 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -21,7 +21,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.janino.impl.JaninoNpcScript; +import noppes.npcs.janino.EventJaninoScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.ScriptWorld; import noppes.npcs.scripted.constants.EntityType; @@ -314,6 +314,6 @@ public class DataScript implements IScriptHandlerPacket { } @Override public IScriptUnit createJaninoScriptUnit() { - return new JaninoNpcScript(); + return new EventJaninoScript(ScriptContext.NPC); } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 6549ed1e5..38d9e8447 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -13,7 +13,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.janino.impl.JaninoPlayerScript; +import noppes.npcs.janino.EventJaninoScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.util.ScriptToStringHelper; @@ -201,6 +201,6 @@ public class PlayerDataScript extends MultiScriptHandler { @Override public IScriptUnit createJaninoScriptUnit() { - return new JaninoPlayerScript(); + return new EventJaninoScript(ScriptContext.PLAYER); } } diff --git a/src/main/java/noppes/npcs/janino/EventJaninoScript.java b/src/main/java/noppes/npcs/janino/EventJaninoScript.java new file mode 100644 index 000000000..2ac7dd218 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/EventJaninoScript.java @@ -0,0 +1,11 @@ +package noppes.npcs.janino; + +import noppes.npcs.constants.ScriptContext; + +public class EventJaninoScript extends JaninoScript { + + public EventJaninoScript(ScriptContext context) { + super(Object.class, null, false); + this.context = context; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index b04031973..d8dfb4007 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -9,6 +9,7 @@ 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; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; @@ -23,6 +24,7 @@ import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { + public ScriptContext context = ScriptContext.GLOBAL; public boolean errored = false; public String script = ""; public List externalScripts = new ArrayList<>(); @@ -59,7 +61,9 @@ public abstract class JaninoScript implements IScriptUnit { this(type, defaultImports, false); } - protected abstract String getHookContext(); + protected String getHookContext(){ + return this.context.hookContext; + } // ==================== COMPILATION ==================== diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java deleted file mode 100644 index 4445ed8c6..000000000 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ /dev/null @@ -1,21 +0,0 @@ -package noppes.npcs.janino.impl; - -import noppes.npcs.janino.JaninoScript; - -/** - * Janino (Java) script implementation for NPC events. - * Hook methods are resolved dynamically by name - no interface definition required. - */ -public class JaninoNpcScript extends JaninoScript { - - public JaninoNpcScript() { - super(Functions.class, null); - } - - @Override - protected String getHookContext() { - return "npc"; - } - - public interface Functions {} -} diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java deleted file mode 100644 index 79c7718b6..000000000 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ /dev/null @@ -1,21 +0,0 @@ -package noppes.npcs.janino.impl; - -import noppes.npcs.janino.JaninoScript; - -/** - * Janino (Java) script implementation for player events. - * Hook methods are resolved dynamically by name - no interface definition required. - */ -public class JaninoPlayerScript extends JaninoScript { - - public JaninoPlayerScript() { - super(Functions.class, null, false); - } - - @Override - protected String getHookContext() { - return "player"; - } - - public interface Functions {} -} From d560c23241c9ae35e17e8fec9edb351d3af86449 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 22:01:13 -0500 Subject: [PATCH 62/64] @ParamName support for Hook Definitions in Overlay Scripts --- .../npcs/controllers/HookDefinition.java | 63 ++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 116 +++++++++++++++++- 2 files changed, 177 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java index b5a61ceab..9d8c34fa6 100644 --- a/src/main/java/noppes/npcs/controllers/HookDefinition.java +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -2,6 +2,10 @@ package noppes.npcs.controllers; import cpw.mods.fml.common.eventhandler.Cancelable; import noppes.npcs.api.handler.IHookDefinition; +import noppes.npcs.janino.annotations.ParamName; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; /** * Implementation of {@link IHookDefinition} with a fluent builder API. @@ -121,6 +125,65 @@ public class HookDefinition implements IHookDefinition { return new Builder(hookName); } + /** + * Create a hook definition from an interface method. + * Automatically extracts metadata from the method signature and @ParamName annotations. + * This provides backward compatibility with the annotation-based hook definition approach. + * + * @param hookName The hook name (may differ from method name) + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(String hookName, Method method) { + Builder builder = builder(hookName); + + // Extract event type from first parameter + if (method.getParameterCount() > 0) { + Class eventType = method.getParameterTypes()[0]; + builder.eventClass(eventType); + + // Auto-detect cancelable from event class + if (eventType.isAnnotationPresent(Cancelable.class)) { + builder.cancelable(true); + } + + // Build required imports from event type + String importName = getImportForClass(eventType); + if (importName != null) { + builder.requiredImports(importName); + } + } + + // Extract parameter names from @ParamName annotations + Parameter[] params = method.getParameters(); + if (params.length > 0) { + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + ParamName annotation = params[i].getAnnotation(ParamName.class); + if (annotation != null) { + names[i] = annotation.value(); + } else { + // Fallback: use lowercase simple type name + names[i] = Character.toLowerCase(params[i].getType().getSimpleName().charAt(0)) + + params[i].getType().getSimpleName().substring(1); + } + } + builder.paramNames(names); + } + + return builder.build(); + } + + /** + * Create a hook definition from a method using its name as the hook name. + * + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(Method method) { + return fromMethod(method.getName(), method); + } + /** * Get the import statement needed for a class. * For nested classes, returns the enclosing class. diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 1deda1a94..46d0bf164 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -8,6 +8,8 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.HookDefinition; +import noppes.npcs.janino.annotations.ParamName; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; @@ -17,6 +19,8 @@ import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; @@ -256,15 +260,29 @@ public abstract class JaninoScript implements IScriptUnit { } public List getHookList() { + Set hooks = new LinkedHashSet<>(); + + // 1. Add hooks from ScriptHookController (new system) String context = getHookContext(); if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - return ScriptHookController.Instance.getAllHooks(context); + hooks.addAll(ScriptHookController.Instance.getAllHooks(context)); } - return Collections.emptyList(); + + // 2. Add methods from interface type (backward compatibility with @ParamName) + if (type != null) { + for (Method m : type.getDeclaredMethods()) { + if (!Modifier.isFinal(m.getModifiers()) && !hooks.contains(m.getName())) { + hooks.add(m.getName()); + } + } + } + + return new ArrayList<>(hooks); } @Override public String generateHookStub(String hookName, Object hookData) { + // 1. Try HookDefinition from registry (new system) IHookDefinition def = getHookDefinition(hookName); if (def != null) { String typeName = def.getUsableTypeName(); @@ -276,9 +294,103 @@ public abstract class JaninoScript implements IScriptUnit { def.hookName(), typeName, paramName); } } + + // 2. Fallback: Try interface method with @ParamName (backward compatibility) + Method method = findInterfaceMethod(hookName); + if (method != null) { + return generateMethodStub(method); + } + + // 3. Final fallback: generic stub return String.format("public void %s(Object event) {\n \n}\n", hookName); } + /** + * Find a method in the type interface by name. + */ + private Method findInterfaceMethod(String methodName) { + if (type == null || methodName == null) return null; + + for (Method m : type.getDeclaredMethods()) { + if (m.getName().equals(methodName) && !Modifier.isFinal(m.getModifiers())) { + return m; + } + } + return null; + } + + /** + * Generate a method stub from a Method, supporting @ParamName annotations. + * Provides backward compatibility with interface-based hook definitions. + */ + public static String generateMethodStub(Method method) { + String mods = Modifier.toString(method.getModifiers()); + mods = mods.replace("abstract ", "").replace("abstract", "") + .replace("default ", "").replace("default", "").trim(); + if (!mods.isEmpty()) mods += " "; + + String returnTypeStr = getUsableTypeName(method.getReturnType()); + String name = method.getName(); + + Map typeCount = new HashMap<>(); + StringBuilder params = new StringBuilder(); + for (int i = 0; i < method.getParameters().length; i++) { + java.lang.reflect.Parameter p = method.getParameters()[i]; + String typeName = getUsableTypeName(p.getType()); + + // Check for @ParamName annotation first + ParamName annotation = p.getAnnotation(ParamName.class); + String paramName; + if (annotation != null) { + paramName = annotation.value(); + } else { + // Fallback to generated name + String baseName = Character.toLowerCase(p.getType().getSimpleName().charAt(0)) + + p.getType().getSimpleName().substring(1); + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + paramName = count == 1 ? baseName : baseName + (count - 1); + } + + if (i > 0) params.append(", "); + params.append(typeName).append(" ").append(paramName); + } + + String body; + Class returnType = method.getReturnType(); + if (returnType == void.class) { + body = ""; + } else if (returnType.isPrimitive()) { + if (returnType == boolean.class) body = " return false;"; + else if (returnType == char.class) body = " return '\\0';"; + else body = " return 0;"; + } else { + body = " return null;"; + } + + return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + + /** + * Gets a usable type name for stub generation. + * For nested classes, returns "OuterClass.InnerClass" format. + */ + private static String getUsableTypeName(Class type) { + if (type.isPrimitive() || type.getEnclosingClass() == null) { + return type.getSimpleName(); + } + + // Build the nested class chain + StringBuilder sb = new StringBuilder(); + Class current = type; + while (current != null) { + if (sb.length() > 0) sb.insert(0, "."); + sb.insert(0, current.getSimpleName()); + current = current.getEnclosingClass(); + } + return sb.toString(); + } + // ==================== IScriptUnit ==================== @Override public String getScript() { return script; } From 31aac0d6b3e9fa475f1bffc162a89e275df66c58 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 22:52:04 -0500 Subject: [PATCH 63/64] GUI fixes and small bug fixes to Script GUIs, Overlay GUI, etc --- .../noppes/npcs/client/gui/GuiScript.java | 53 +++++++++++++------ .../client/gui/script/GuiScriptInterface.java | 6 ++- .../npcs/client/gui/util/GuiNPCInterface.java | 10 ++-- .../client/gui/util/GuiScriptTextArea.java | 14 +++-- 4 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index b98e94ac4..137ccf5a1 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -80,16 +80,18 @@ public class GuiScript extends GuiScriptInterface { public void initGui() { super.initGui(); this.guiTop += 10; - - // ==================== TOP BUTTONS ==================== - GuiMenuTopButton top; - addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); - addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); - addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); - top.active = showScript; - addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); - top.active = !showScript; - addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + + // ==================== TOP BUTTONS (hidden in fullscreen) ==================== + if (!isFullscreen) { + GuiMenuTopButton top; + addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); + addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); + addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); + top.active = showScript; + addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); + top.active = !showScript; + addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + } if (showScript) { initScriptView(); @@ -166,10 +168,8 @@ public class GuiScript extends GuiScriptInterface { int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); - // Set language and script context for proper syntax highlighting and autocomplete - activeArea.setLanguage(script.getLanguage()); - activeArea.setScriptContext(ScriptContext.NPC); - + // ==================== RIGHT PANEL (hidden in fullscreen) ==================== + if (!isFullscreen) { addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); addButton(new GuiNpcButton(100, guiLeft + 315, guiTop + 25, 50, 20, "gui.copy")); @@ -183,11 +183,13 @@ public class GuiScript extends GuiScriptInterface { scroll.setList(container.scripts); addScroll(scroll); } + } private void initSettingsView() { addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); - getTopButton(14).active = true; + if (getTopButton(14) != null) + getTopButton(14).active = true; GuiNpcTextArea consoleArea = new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText()); consoleArea.canEdit = false; @@ -269,7 +271,7 @@ public class GuiScript extends GuiScriptInterface { } if (guibutton.id == 16) { close(); - GuiScriptInterface.open(this, new DataScript(npc)); + GuiScriptInterface.open(this, this.script); } if (guibutton.id == 17) { close(); @@ -342,5 +344,24 @@ public class GuiScript extends GuiScriptInterface { initGui(); } } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw fullscreen button when in script view (GuiScript uses 0-based activeTab) + if (showScript) { + fullscreenButton.draw(mouseX, mouseY); + } + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check fullscreen button first when in script view + if (showScript && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + super.mouseClicked(mouseX, mouseY, mouseButton); + } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 5c3ee2596..15d3e8e76 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -354,7 +354,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb super.drawScreen(mouseX, mouseY, partialTicks); // Draw fullscreen button on top of everything when on script editor tab - if (this.activeTab > 0) { + // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves + if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer()) { fullscreenButton.draw(mouseX, mouseY); } } @@ -364,7 +365,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { // Check fullscreen button first when on script editor tab - if (this.activeTab > 0 && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves + if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer() && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { return; } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 823f6a3be..585ee8515 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -108,7 +108,7 @@ public abstract class GuiNPCInterface extends GuiScreen { } //Important for GuiScriptTextArea - for (GuiNpcTextField tf : textfields.values()) + for (GuiNpcTextField tf : textfields.values()) tf.initGui(); } @@ -264,8 +264,8 @@ public abstract class GuiNPCInterface extends GuiScreen { else close(); } - - + + if (subgui != null) subgui.keyTyped(c, i); for (GuiNpcTextField tf : textfields.values()) @@ -274,7 +274,7 @@ public abstract class GuiNPCInterface extends GuiScreen { for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) { guiScrollableComponent.keyTyped(c, i); } - + } public void onGuiClosed() { @@ -284,7 +284,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public void closeOnEsc(Supplier close) { this.closeOnEscSupplier = close; } - + public void close() { if(GuiNpcTextField.activeTextfield != null) GuiNpcTextField.unfocus(); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 99e6ce480..c73f2bebf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -493,12 +493,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); - // Handle mouse wheel scroll - int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); - if (listener instanceof GuiNPCInterface) { - ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + // Handle mouse wheel scroll - only consume when mouse is over this text area + boolean isMouseOverTextArea = xMouse >= x && xMouse < x + width && yMouse >= y && yMouse < y + height; + int wheelDelta = 0; + if (isMouseOverTextArea) { + wheelDelta = Mouse.getDWheel(); + if (listener instanceof GuiNPCInterface) { + ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + } boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; - if (wheelDelta != 0 && canScroll) + if (wheelDelta != 0 && canScroll) scroll.applyWheelScroll(wheelDelta, maxScroll); } From a9b602713c064bb6e50a75543416cb857966572a Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 23:07:38 -0500 Subject: [PATCH 64/64] Large Cleanup to Code Imports and Formatting --- src/api | 2 +- .../npcs/controllers/ProfileController.java | 1 - .../npcs/controllers/SyncController.java | 2 +- .../controllers/data/ability/Ability.java | 421 ++++++++++++----- .../data/ability/AbilityController.java | 27 +- .../controllers/data/ability/Condition.java | 55 ++- .../data/ability/TargetingMode.java | 28 +- .../data/ability/telegraph/Telegraph.java | 156 +++++-- .../ability/telegraph/TelegraphInstance.java | 110 +++-- .../ability/telegraph/TelegraphManager.java | 7 +- .../data/ability/telegraph/TelegraphType.java | 24 +- .../data/ability/type/AbilityBeam.java | 105 +++-- .../data/ability/type/AbilityCharge.java | 51 ++- .../data/ability/type/AbilityCutter.java | 135 ++++-- .../data/ability/type/AbilityDash.java | 52 ++- .../data/ability/type/AbilityGuard.java | 84 +++- .../data/ability/type/AbilityHazard.java | 223 ++++++--- .../data/ability/type/AbilityHeal.java | 66 ++- .../data/ability/type/AbilityHeavyHit.java | 64 ++- .../data/ability/type/AbilityOrb.java | 189 ++++++-- .../data/ability/type/AbilityProjectile.java | 85 +++- .../data/ability/type/AbilityShockwave.java | 69 ++- .../data/ability/type/AbilitySlam.java | 58 ++- .../data/ability/type/AbilityTeleport.java | 118 +++-- .../data/ability/type/AbilityTrap.java | 212 +++++++-- .../data/ability/type/AbilityVortex.java | 119 +++-- .../kamkeel/npcs/network/PacketHandler.java | 23 +- .../data/ability/TelegraphSpawnPacket.java | 1 - .../request/script/ScriptFilesPacket.java | 4 +- .../java/kamkeel/npcs/util/BukkitUtil.java | 4 + .../java/kamkeel/npcs/util/VaultUtil.java | 46 +- src/main/java/noppes/npcs/CustomNpcs.java | 10 +- src/main/java/noppes/npcs/DataAbilities.java | 47 +- src/main/java/noppes/npcs/EventHooks.java | 33 +- src/main/java/noppes/npcs/NBTTags.java | 1 - .../java/noppes/npcs/NoppesStringUtils.java | 2 +- .../noppes/npcs/ScriptForgeEventHandler.java | 2 +- .../noppes/npcs/ScriptPlayerEventHandler.java | 3 +- .../java/noppes/npcs/ServerEventsHandler.java | 2 - .../java/noppes/npcs/ServerTickHandler.java | 2 +- .../npcs/blocks/tiles/TileScripted.java | 6 +- .../npcs/client/ClientCacheHandler.java | 2 +- .../java/noppes/npcs/client/ClientProxy.java | 8 +- .../noppes/npcs/client/ClientTickHandler.java | 4 +- .../client/CustomNpcResourceListener.java | 2 +- .../client/controllers/MusicController.java | 1 + .../noppes/npcs/client/gui/GuiScript.java | 79 ++-- .../noppes/npcs/client/gui/GuiScriptList.java | 2 +- .../npcs/client/gui/SubGuiEffectGeneral.java | 2 +- .../npcs/client/gui/SubGuiPlayerData.java | 29 +- .../client/gui/advanced/GuiNPCAbilities.java | 13 +- .../gui/advanced/SubGuiAbilityConfig.java | 42 +- .../gui/advanced/SubGuiAbilityLoad.java | 6 +- .../gui/advanced/SubGuiConditionEdit.java | 8 +- .../advanced/ability/SubGuiAbilityBeam.java | 38 +- .../advanced/ability/SubGuiAbilityCharge.java | 18 +- .../advanced/ability/SubGuiAbilityCutter.java | 46 +- .../advanced/ability/SubGuiAbilityDash.java | 10 +- .../advanced/ability/SubGuiAbilityGuard.java | 22 +- .../advanced/ability/SubGuiAbilityHazard.java | 42 +- .../advanced/ability/SubGuiAbilityHeal.java | 26 +- .../ability/SubGuiAbilityHeavyHit.java | 22 +- .../advanced/ability/SubGuiAbilityOrb.java | 42 +- .../ability/SubGuiAbilityProjectile.java | 30 +- .../ability/SubGuiAbilityShockwave.java | 22 +- .../advanced/ability/SubGuiAbilitySlam.java | 22 +- .../ability/SubGuiAbilityTeleport.java | 38 +- .../advanced/ability/SubGuiAbilityTrap.java | 34 +- .../advanced/ability/SubGuiAbilityVortex.java | 42 +- .../gui/global/GuiNpcManageAbilities.java | 16 +- .../gui/global/GuiNpcManageRecipes.java | 2 +- .../client/gui/item/SubGuiLinkedItem.java | 6 +- .../client/gui/mainmenu/GuiNpcAdvanced.java | 4 +- .../client/gui/script/EventGuiScriptList.java | 2 +- .../client/gui/script/GuiJaninoScript.java | 36 +- .../client/gui/script/GuiNPCEventScripts.java | 2 +- .../client/gui/script/GuiScriptInterface.java | 114 +++-- .../client/gui/script/SubGuiSelectList.java | 2 +- .../gui/select/GuiTextureSelection.java | 1 + .../npcs/client/gui/util/GuiDiagram.java | 2 +- .../npcs/client/gui/util/GuiNPCInterface.java | 6 +- .../npcs/client/gui/util/GuiNpcButton.java | 1 + .../npcs/client/gui/util/GuiNpcTextField.java | 4 +- .../client/gui/util/GuiScriptTextArea.java | 185 ++++---- .../npcs/client/gui/util/GuiScrollWindow.java | 2 + .../noppes/npcs/client/gui/util/GuiUtil.java | 2 +- .../gui/util/IAbilityConfigCallback.java | 1 + .../gui/util/key/OverlayKeyPresetViewer.java | 10 +- .../gui/util/script/BracketMatcher.java | 98 ++-- .../gui/util/script/ClassPathFinder.java | 49 +- .../gui/util/script/CommentHandler.java | 73 +-- .../gui/util/script/CursorNavigation.java | 95 ++-- .../client/gui/util/script/FormatHelper.java | 428 +++++++++--------- .../gui/util/script/GoToLineDialog.java | 38 +- .../client/gui/util/script/IndentHelper.java | 145 +++--- .../gui/util/script/JavaTextContainer.java | 190 +++++--- .../client/gui/util/script/MethodBlock.java | 46 +- .../client/gui/util/script/PackageFinder.java | 1 - .../util/script/RenameRefactorHandler.java | 54 +-- .../client/gui/util/script/ScrollState.java | 102 +++-- .../gui/util/script/SearchReplaceBar.java | 396 ++++++++-------- .../gui/util/script/SelectionState.java | 92 ++-- .../client/gui/util/script/UndoManager.java | 55 ++- .../java/noppes/npcs/client/key/KeyEvent.java | 29 +- .../noppes/npcs/client/key/KeyPreset.java | 14 +- .../npcs/client/key/KeyPresetManager.java | 2 +- .../client/key/impl/ScriptEditorKeys.java | 14 +- .../noppes/npcs/client/model/ModelPony.java | 2 +- .../renderer/ImageBufferDownloadAlt.java | 2 +- .../npcs/client/renderer/RenderNPCPony.java | 2 +- .../blocks/BlockScriptedRenderer.java | 2 +- .../java/noppes/npcs/config/GlyphCache.java | 8 +- .../java/noppes/npcs/config/StringCache.java | 3 +- .../noppes/npcs/constants/ScriptContext.java | 92 ++-- .../npcs/controllers/AnimationController.java | 5 +- .../npcs/controllers/HookDefinition.java | 6 +- .../controllers/LinkedItemController.java | 9 +- .../npcs/controllers/MarketRegistry.java | 2 +- .../npcs/controllers/ScriptContainer.java | 25 +- .../controllers/ScriptHookController.java | 37 +- .../npcs/controllers/data/Animation.java | 32 +- .../npcs/controllers/data/CustomEffect.java | 6 +- .../npcs/controllers/data/DataScript.java | 1 + .../npcs/controllers/data/EffectScript.java | 7 +- .../npcs/controllers/data/IScriptHandler.java | 26 +- .../data/IScriptHandlerPacket.java | 4 +- .../npcs/controllers/data/IScriptUnit.java | 74 +-- .../controllers/data/JaninoScriptHandler.java | 14 +- .../npcs/controllers/data/LinkedItem.java | 4 +- .../controllers/data/LinkedItemScript.java | 25 +- .../controllers/data/MultiScriptHandler.java | 6 +- .../controllers/data/PlayerCurrencyData.java | 2 + .../npcs/controllers/data/RecipeScript.java | 33 +- .../npcs/controllers/data/ScriptHandler.java | 1 + .../controllers/data/SingleScriptHandler.java | 8 +- .../npcs/controllers/data/TraderStock.java | 14 +- .../npcs/controllers/data/action/Action.java | 26 +- .../data/action/ActionManager.java | 32 +- .../controllers/data/action/ActionQueue.java | 15 +- .../npcs/entity/EntityNPCInterface.java | 9 +- .../noppes/npcs/items/ItemElementalStaff.java | 2 +- .../noppes/npcs/items/ItemNpcScripter.java | 2 +- src/main/java/noppes/npcs/items/ItemOrb.java | 2 +- .../npcs/janino/JaninoHookResolver.java | 3 +- .../java/noppes/npcs/janino/JaninoScript.java | 62 ++- .../npcs/janino/annotations/ParamName.java | 10 +- .../java/noppes/npcs/roles/RoleTrader.java | 5 +- .../java/noppes/npcs/scripted/NpcAPI.java | 21 +- .../npcs/scripted/entity/ScriptPlayer.java | 7 +- .../npcs/scripted/event/AbilityEvent.java | 2 +- .../npcs/scripted/item/ScriptCustomItem.java | 4 +- .../java/noppes/npcs/util/NPCMountUtil.java | 2 +- src/main/java/noppes/npcs/util/ValueUtil.java | 2 +- .../janino/AdvancedClassFilter.java | 3 + .../janino/EntityUnloadListener.java | 2 +- 155 files changed, 4089 insertions(+), 2194 deletions(-) diff --git a/src/api b/src/api index 64fe8794e..a6b4a131c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 64fe8794e71d82f09449e6c916c7902fe890cab7 +Subproject commit a6b4a131c843f27bbd3c561168f18498cb9d59ec diff --git a/src/main/java/kamkeel/npcs/controllers/ProfileController.java b/src/main/java/kamkeel/npcs/controllers/ProfileController.java index 73f16ebf6..f7ac06bb1 100644 --- a/src/main/java/kamkeel/npcs/controllers/ProfileController.java +++ b/src/main/java/kamkeel/npcs/controllers/ProfileController.java @@ -1,6 +1,5 @@ package kamkeel.npcs.controllers; -import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.controllers.data.profile.EnumProfileOperation; import kamkeel.npcs.controllers.data.profile.IProfileData; diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 9617f4d12..3247498b5 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -2,9 +2,9 @@ package kamkeel.npcs.controllers; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.addon.DBCAddon; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 4d62a934f..48cf46cb3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -15,8 +15,8 @@ import noppes.npcs.DataAbilities; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; @@ -89,33 +89,46 @@ public abstract class Ability implements IAbility { // ABSTRACT METHODS // ═══════════════════════════════════════════════════════════════════ - /** Called first tick of ACTIVE phase */ + /** + * Called first tick of ACTIVE phase + */ public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world); - /** Called every tick of ACTIVE phase */ + /** + * Called every tick of ACTIVE phase + */ public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick); - /** Write type-specific config to NBT */ + /** + * Write type-specific config to NBT + */ public abstract void writeTypeNBT(NBTTagCompound nbt); - /** Read type-specific config from NBT */ + /** + * Read type-specific config from NBT + */ public abstract void readTypeNBT(NBTTagCompound nbt); // ═══════════════════════════════════════════════════════════════════ // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {} - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {} - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {} + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } /** * Apply damage to an entity with ability hit event support. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param npc The NPC executing the ability * @param hitEntity The entity being hit - * @param damage The damage amount + * @param damage The damage amount * @param knockback The horizontal knockback * @return true if damage was applied (not cancelled), false if cancelled */ @@ -160,17 +173,17 @@ public abstract class Ability implements IAbility { * Apply damage to an entity with ability hit event support and custom knockback direction. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability - * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback + * @param npc The NPC executing the ability + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback * @param knockbackDirX The X component of knockback direction (normalized) * @param knockbackDirZ The Z component of knockback direction (normalized) * @return true if damage was applied (not cancelled), false if cancelled */ protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, - float damage, float knockback, - double knockbackDirX, double knockbackDirZ) { + float damage, float knockback, + double knockbackDirX, double knockbackDirZ) { DataAbilities dataAbilities = npc.abilities; AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( this, currentTarget, hitEntity, damage, knockback, 0.0f); @@ -206,53 +219,77 @@ public abstract class Ability implements IAbility { hitEntity.addVelocity(x, 0.1D, z); hitEntity.velocityChanged = true; } - public float getTelegraphRadius() { return 5.0f; } - public float getTelegraphLength() { return 5.0f; } - public float getTelegraphWidth() { return 2.0f; } - public float getTelegraphAngle() { return 45.0f; } + + public float getTelegraphRadius() { + return 5.0f; + } + + public float getTelegraphLength() { + return 5.0f; + } + + public float getTelegraphWidth() { + return 2.0f; + } + + public float getTelegraphAngle() { + return 45.0f; + } /** * Returns true if this ability controls NPC movement during ACTIVE phase. * When true, AI pathfinding will be blocked during ability execution. * Override in abilities that move the NPC (Charge, Dash, Slam, etc.) */ - public boolean hasAbilityMovement() { return false; } + public boolean hasAbilityMovement() { + return false; + } /** * Returns true if the targeting mode for this ability type is locked and cannot be changed. * Most abilities have fixed targeting (e.g., Heal is always SELF, Charge is always AGGRO_TARGET). * Override and return false for abilities with flexible targeting. */ - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } /** * Returns the allowed targeting modes for this ability, or null if all modes are allowed. * Override in abilities that only support specific targeting modes. */ - public TargetingMode[] getAllowedTargetingModes() { return null; } + public TargetingMode[] getAllowedTargetingModes() { + return null; + } /** * Returns true if the telegraph type for this ability is locked and cannot be changed. * Telegraph type is inherent to how the ability works (e.g., Charge uses LINE, Slam uses CIRCLE). */ - public boolean isTelegraphTypeLocked() { return true; } + public boolean isTelegraphTypeLocked() { + return true; + } /** * Returns true if this ability type has custom settings that need a Type-specific tab. * Override to return true if the ability has settings beyond the base Ability fields. */ - public boolean hasTypeSettings() { return false; } + public boolean hasTypeSettings() { + return false; + } /** * Get the number of rows needed for type-specific settings in the GUI. * Each row is approximately 24 pixels. Used for GUI layout. */ - public int getTypeSettingsRowCount() { return 0; } + public int getTypeSettingsRowCount() { + return 0; + } /** * Creates the GUI for configuring this ability. * Override this method to provide a custom GUI subclass for type-specific settings. - * + *

* Third-party mods can extend SubGuiAbilityConfig and override initTypeTab(), * handleTypeButton(), and handleTypeTextField() to provide custom type settings. * @@ -268,7 +305,7 @@ public abstract class Ability implements IAbility { * Create a telegraph instance for this ability. * Override for custom telegraph shapes. * - * @param npc The caster + * @param npc The caster * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ @@ -283,9 +320,9 @@ public abstract class Ability implements IAbility { // Determine position based on targeting mode boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF || - targetingMode == TargetingMode.SELF || - telegraphType == TelegraphType.LINE || - telegraphType == TelegraphType.CONE; + targetingMode == TargetingMode.SELF || + telegraphType == TelegraphType.LINE || + telegraphType == TelegraphType.CONE; if (positionAtNpc) { x = npc.posX; @@ -347,10 +384,10 @@ public abstract class Ability implements IAbility { * Find the ground level at a given position. * Searches downward from the given Y to find a solid block. * - * @param world The world - * @param x X coordinate + * @param world The world + * @param x X coordinate * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate + * @param z Z coordinate * @return The Y coordinate of the ground surface */ protected double findGroundLevel(World world, double x, double startY, double z) { @@ -391,7 +428,9 @@ public abstract class Ability implements IAbility { // EXECUTION LOGIC (called by CombatHandler) // ═══════════════════════════════════════════════════════════════════ - /** Start executing this ability */ + /** + * Start executing this ability + */ public void start(EntityLivingBase target) { this.phase = AbilityPhase.WINDUP; this.currentTick = 0; @@ -399,7 +438,9 @@ public abstract class Ability implements IAbility { this.executionStartTime = System.currentTimeMillis(); } - /** Tick the ability. Returns true if phase changed. */ + /** + * Tick the ability. Returns true if phase changed. + */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; @@ -432,7 +473,9 @@ public abstract class Ability implements IAbility { return false; } - /** Interrupt this ability */ + /** + * Interrupt this ability + */ public void interrupt() { phase = AbilityPhase.IDLE; currentTick = 0; @@ -440,17 +483,23 @@ public abstract class Ability implements IAbility { telegraphInstance = null; } - /** Start cooldown timer */ + /** + * Start cooldown timer + */ public void startCooldown() { cooldownEndTime = System.currentTimeMillis() + (cooldownTicks * 50L); } - /** Check if on cooldown */ + /** + * Check if on cooldown + */ public boolean isOnCooldown() { return System.currentTimeMillis() < cooldownEndTime; } - /** Check if executing */ + /** + * Check if executing + */ public boolean isExecuting() { return phase != AbilityPhase.IDLE; } @@ -487,7 +536,9 @@ public abstract class Ability implements IAbility { return true; } - /** Reset all execution state (called on combat end) */ + /** + * Reset all execution state (called on combat end) + */ public void reset() { phase = AbilityPhase.IDLE; currentTick = 0; @@ -507,7 +558,9 @@ public abstract class Ability implements IAbility { return true; } - /** Full eligibility check */ + /** + * Full eligibility check + */ public boolean canUse(EntityNPCInterface npc, EntityLivingBase target) { if (!enabled) return false; if (isOnCooldown()) return false; @@ -634,110 +687,266 @@ public abstract class Ability implements IAbility { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public String getName() { return name; } - public void setName(String name) { this.name = name; } + public void setId(String id) { + this.id = id; + } - public String getTypeId() { return typeId; } + public String getName() { + return name; + } - public int getWeight() { return weight; } - public void setWeight(int weight) { this.weight = weight; } + public void setName(String name) { + this.name = name; + } - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getTypeId() { + return typeId; + } - public TargetingMode getTargetingMode() { return targetingMode; } - public void setTargetingMode(TargetingMode targetingMode) { this.targetingMode = targetingMode; } + public int getWeight() { + return weight; + } - public float getMinRange() { return minRange; } - public void setMinRange(float minRange) { this.minRange = minRange; } + public void setWeight(int weight) { + this.weight = weight; + } - public float getMaxRange() { return maxRange; } - public void setMaxRange(float maxRange) { this.maxRange = maxRange; } + public boolean isEnabled() { + return enabled; + } - public int getCooldownTicks() { return cooldownTicks; } - public void setCooldownTicks(int cooldownTicks) { this.cooldownTicks = cooldownTicks; } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } - public int getWindUpTicks() { return windUpTicks; } - public void setWindUpTicks(int windUpTicks) { this.windUpTicks = windUpTicks; } + public TargetingMode getTargetingMode() { + return targetingMode; + } - public int getActiveTicks() { return activeTicks; } - public void setActiveTicks(int activeTicks) { this.activeTicks = activeTicks; } + public void setTargetingMode(TargetingMode targetingMode) { + this.targetingMode = targetingMode; + } - public int getRecoveryTicks() { return recoveryTicks; } - public void setRecoveryTicks(int recoveryTicks) { this.recoveryTicks = recoveryTicks; } + public float getMinRange() { + return minRange; + } - public boolean isInterruptible() { return interruptible; } - public void setInterruptible(boolean interruptible) { this.interruptible = interruptible; } + public void setMinRange(float minRange) { + this.minRange = minRange; + } - public boolean isLockMovement() { return lockMovement; } - public void setLockMovement(boolean lockMovement) { this.lockMovement = lockMovement; } + public float getMaxRange() { + return maxRange; + } - public int getWindUpColor() { return windUpColor; } - public void setWindUpColor(int windUpColor) { this.windUpColor = windUpColor; } + public void setMaxRange(float maxRange) { + this.maxRange = maxRange; + } - public int getActiveColor() { return activeColor; } - public void setActiveColor(int activeColor) { this.activeColor = activeColor; } + public int getCooldownTicks() { + return cooldownTicks; + } - /** @deprecated Use getWindUpColor() instead */ + public void setCooldownTicks(int cooldownTicks) { + this.cooldownTicks = cooldownTicks; + } + + public int getWindUpTicks() { + return windUpTicks; + } + + public void setWindUpTicks(int windUpTicks) { + this.windUpTicks = windUpTicks; + } + + public int getActiveTicks() { + return activeTicks; + } + + public void setActiveTicks(int activeTicks) { + this.activeTicks = activeTicks; + } + + public int getRecoveryTicks() { + return recoveryTicks; + } + + public void setRecoveryTicks(int recoveryTicks) { + this.recoveryTicks = recoveryTicks; + } + + public boolean isInterruptible() { + return interruptible; + } + + public void setInterruptible(boolean interruptible) { + this.interruptible = interruptible; + } + + public boolean isLockMovement() { + return lockMovement; + } + + public void setLockMovement(boolean lockMovement) { + this.lockMovement = lockMovement; + } + + public int getWindUpColor() { + return windUpColor; + } + + public void setWindUpColor(int windUpColor) { + this.windUpColor = windUpColor; + } + + public int getActiveColor() { + return activeColor; + } + + public void setActiveColor(int activeColor) { + this.activeColor = activeColor; + } + + /** + * @deprecated Use getWindUpColor() instead + */ @Deprecated - public int getTelegraphColor() { return windUpColor; } - /** @deprecated Use setWindUpColor() instead */ + public int getTelegraphColor() { + return windUpColor; + } + + /** + * @deprecated Use setWindUpColor() instead + */ @Deprecated - public void setTelegraphColor(int telegraphColor) { this.windUpColor = telegraphColor; } + public void setTelegraphColor(int telegraphColor) { + this.windUpColor = telegraphColor; + } - public String getWindUpSound() { return windUpSound; } - public void setWindUpSound(String windUpSound) { this.windUpSound = windUpSound; } + public String getWindUpSound() { + return windUpSound; + } - public String getActiveSound() { return activeSound; } - public void setActiveSound(String activeSound) { this.activeSound = activeSound; } + public void setWindUpSound(String windUpSound) { + this.windUpSound = windUpSound; + } - public int getWindUpAnimationId() { return windUpAnimationId; } - public void setWindUpAnimationId(int windUpAnimationId) { this.windUpAnimationId = windUpAnimationId; } + public String getActiveSound() { + return activeSound; + } - public int getActiveAnimationId() { return activeAnimationId; } - public void setActiveAnimationId(int activeAnimationId) { this.activeAnimationId = activeAnimationId; } + public void setActiveSound(String activeSound) { + this.activeSound = activeSound; + } - /** @deprecated Use getWindUpSound() instead */ + public int getWindUpAnimationId() { + return windUpAnimationId; + } + + public void setWindUpAnimationId(int windUpAnimationId) { + this.windUpAnimationId = windUpAnimationId; + } + + public int getActiveAnimationId() { + return activeAnimationId; + } + + public void setActiveAnimationId(int activeAnimationId) { + this.activeAnimationId = activeAnimationId; + } + + /** + * @deprecated Use getWindUpSound() instead + */ @Deprecated - public String getCastSound() { return windUpSound; } - /** @deprecated Use setWindUpSound() instead */ + public String getCastSound() { + return windUpSound; + } + + /** + * @deprecated Use setWindUpSound() instead + */ @Deprecated - public void setCastSound(String castSound) { this.windUpSound = castSound; } + public void setCastSound(String castSound) { + this.windUpSound = castSound; + } - /** @deprecated Use getWindUpAnimationId() instead */ + /** + * @deprecated Use getWindUpAnimationId() instead + */ @Deprecated - public int getAnimationId() { return windUpAnimationId; } - /** @deprecated Use setWindUpAnimationId() instead */ + public int getAnimationId() { + return windUpAnimationId; + } + + /** + * @deprecated Use setWindUpAnimationId() instead + */ @Deprecated - public void setAnimationId(int animationId) { this.windUpAnimationId = animationId; } + public void setAnimationId(int animationId) { + this.windUpAnimationId = animationId; + } - public boolean isShowTelegraph() { return showTelegraph; } - public void setShowTelegraph(boolean showTelegraph) { this.showTelegraph = showTelegraph; } + public boolean isShowTelegraph() { + return showTelegraph; + } - public TelegraphType getTelegraphType() { return telegraphType; } - public void setTelegraphType(TelegraphType telegraphType) { this.telegraphType = telegraphType; } + public void setShowTelegraph(boolean showTelegraph) { + this.showTelegraph = showTelegraph; + } - public float getTelegraphHeightOffset() { return telegraphHeightOffset; } - public void setTelegraphHeightOffset(float telegraphHeightOffset) { this.telegraphHeightOffset = telegraphHeightOffset; } + public TelegraphType getTelegraphType() { + return telegraphType; + } - public AbilityPhase getPhase() { return phase; } + public void setTelegraphType(TelegraphType telegraphType) { + this.telegraphType = telegraphType; + } + + public float getTelegraphHeightOffset() { + return telegraphHeightOffset; + } + + public void setTelegraphHeightOffset(float telegraphHeightOffset) { + this.telegraphHeightOffset = telegraphHeightOffset; + } + + public AbilityPhase getPhase() { + return phase; + } /** * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY). * Required for IAbility interface. */ @Override - public int getPhaseInt() { return phase.ordinal(); } + public int getPhaseInt() { + return phase.ordinal(); + } - public int getCurrentTick() { return currentTick; } - public EntityLivingBase getCurrentTarget() { return currentTarget; } - public NBTTagCompound getCustomData() { return customData; } + public int getCurrentTick() { + return currentTick; + } - public List getConditions() { return conditions; } - public void addCondition(Condition c) { conditions.add(c); } + public EntityLivingBase getCurrentTarget() { + return currentTarget; + } + + public NBTTagCompound getCustomData() { + return customData; + } + + public List getConditions() { + return conditions; + } + + public void addCondition(Condition c) { + conditions.add(c); + } // ═══════════════════════════════════════════════════════════════════ // IAbility API METHODS diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 685e5403d..1a9bbd522 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -1,6 +1,20 @@ package kamkeel.npcs.controllers.data.ability; -import kamkeel.npcs.controllers.data.ability.type.*; +import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; +import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; +import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; +import kamkeel.npcs.controllers.data.ability.type.AbilityDash; +import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; +import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; +import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; +import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; +import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; +import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; +import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; +import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; +import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; +import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; +import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -8,13 +22,17 @@ import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.util.NBTJsonUtil; import java.io.File; -import java.util.*; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import java.util.function.Supplier; /** * Controller for ability types. Allows external mods to register custom ability types. * Initialize during FMLInitializationEvent. - * + *

* Also manages saved ability presets that can be reused across NPCs. */ public class AbilityController implements IAbilityHandler { @@ -211,7 +229,7 @@ public class AbilityController implements IAbilityHandler { * Call during FMLInitializationEvent. * * @param factoryKey The factory key (e.g., "cnpc:slam", "mymod:custom_ability") - * @param factory Factory lambda that creates a new instance + * @param factory Factory lambda that creates a new instance */ public void registerType(String factoryKey, Supplier factory) { if (factories.containsKey(factoryKey)) { @@ -252,6 +270,7 @@ public class AbilityController implements IAbilityHandler { /** * Create a new empty ability of the given type. + * * @param factoryKey The factory key (e.g., "cnpc:slam") or the ability typeId (e.g., "ability.cnpc.slam") */ public Ability create(String factoryKey) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java index 70b3b290f..8d078421a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java @@ -13,7 +13,7 @@ public interface Condition { /** * Check if this condition is met. * - * @param npc The NPC that would use the ability + * @param npc The NPC that would use the ability * @param target The current target (may be null for self-targeting abilities) * @return true if the condition is satisfied */ @@ -80,7 +80,8 @@ public interface Condition { class ConditionHPAbove implements Condition { private float threshold = 0.5f; // 50% - public ConditionHPAbove() {} + public ConditionHPAbove() { + } public ConditionHPAbove(float threshold) { this.threshold = threshold; @@ -92,7 +93,9 @@ public interface Condition { } @Override - public String getTypeId() { return "hp_above"; } + public String getTypeId() { + return "hp_above"; + } @Override public NBTTagCompound writeNBT() { @@ -114,7 +117,8 @@ public interface Condition { class ConditionHPBelow implements Condition { private float threshold = 0.5f; - public ConditionHPBelow() {} + public ConditionHPBelow() { + } public ConditionHPBelow(float threshold) { this.threshold = threshold; @@ -126,7 +130,9 @@ public interface Condition { } @Override - public String getTypeId() { return "hp_below"; } + public String getTypeId() { + return "hp_below"; + } @Override public NBTTagCompound writeNBT() { @@ -148,7 +154,8 @@ public interface Condition { class ConditionTargetHPAbove implements Condition { private float threshold = 0.5f; - public ConditionTargetHPAbove() {} + public ConditionTargetHPAbove() { + } public ConditionTargetHPAbove(float threshold) { this.threshold = threshold; @@ -161,7 +168,9 @@ public interface Condition { } @Override - public String getTypeId() { return "target_hp_above"; } + public String getTypeId() { + return "target_hp_above"; + } @Override public NBTTagCompound writeNBT() { @@ -183,7 +192,8 @@ public interface Condition { class ConditionTargetHPBelow implements Condition { private float threshold = 0.5f; - public ConditionTargetHPBelow() {} + public ConditionTargetHPBelow() { + } public ConditionTargetHPBelow(float threshold) { this.threshold = threshold; @@ -196,7 +206,9 @@ public interface Condition { } @Override - public String getTypeId() { return "target_hp_below"; } + public String getTypeId() { + return "target_hp_below"; + } @Override public NBTTagCompound writeNBT() { @@ -220,7 +232,8 @@ public interface Condition { private int requiredHits = 3; private int withinTicks = 60; - public ConditionHitCount() {} + public ConditionHitCount() { + } public ConditionHitCount(int requiredHits, int withinTicks) { this.requiredHits = requiredHits; @@ -237,7 +250,9 @@ public interface Condition { } @Override - public String getTypeId() { return "hit_count"; } + public String getTypeId() { + return "hit_count"; + } @Override public NBTTagCompound writeNBT() { @@ -254,10 +269,20 @@ public interface Condition { withinTicks = nbt.getInteger("withinTicks"); } - public int getRequiredHits() { return requiredHits; } - public void setRequiredHits(int hits) { this.requiredHits = hits; } + public int getRequiredHits() { + return requiredHits; + } - public int getWithinTicks() { return withinTicks; } - public void setWithinTicks(int ticks) { this.withinTicks = ticks; } + public void setRequiredHits(int hits) { + this.requiredHits = hits; + } + + public int getWithinTicks() { + return withinTicks; + } + + public void setWithinTicks(int ticks) { + this.withinTicks = ticks; + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java index a639702e1..7a2e43b64 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java @@ -4,24 +4,38 @@ package kamkeel.npcs.controllers.data.ability; * Defines how an ability selects its target(s). */ public enum TargetingMode { - /** Target the current aggro target */ + /** + * Target the current aggro target + */ AGGRO_TARGET, - /** Target self (for buffs, heals, guards) */ + /** + * Target self (for buffs, heals, guards) + */ SELF, - /** AOE centered on self */ + /** + * AOE centered on self + */ AOE_SELF, - /** AOE centered on target */ + /** + * AOE centered on target + */ AOE_TARGET, - /** Random nearby enemy */ + /** + * Random nearby enemy + */ RANDOM_ENEMY, - /** Lowest HP enemy in range */ + /** + * Lowest HP enemy in range + */ LOWEST_HP, - /** Highest threat enemy */ + /** + * Highest threat enemy + */ HIGHEST_THREAT } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java index 1b42b55e1..b8f92de17 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java @@ -35,7 +35,8 @@ public class Telegraph { private boolean followsCaster = false; private float offsetForward = 0.0f; - public Telegraph() {} + public Telegraph() { + } public Telegraph(String id, TelegraphType type) { this.id = id; @@ -149,54 +150,139 @@ public class Telegraph { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public TelegraphType getType() { return type; } - public void setType(TelegraphType type) { this.type = type; } + public void setId(String id) { + this.id = id; + } - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public TelegraphType getType() { + return type; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public void setType(TelegraphType type) { + this.type = type; + } - public float getLength() { return length; } - public void setLength(float length) { this.length = length; } + public float getRadius() { + return radius; + } - public float getWidth() { return width; } - public void setWidth(float width) { this.width = width; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getAngle() { return angle; } - public void setAngle(float angle) { this.angle = angle; } + public float getInnerRadius() { + return innerRadius; + } - public int getDurationTicks() { return durationTicks; } - public void setDurationTicks(int durationTicks) { this.durationTicks = durationTicks; } + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } - public int getColor() { return color; } - public void setColor(int color) { this.color = color; } + public float getLength() { + return length; + } - public int getWarningColor() { return warningColor; } - public void setWarningColor(int warningColor) { this.warningColor = warningColor; } + public void setLength(float length) { + this.length = length; + } - public int getWarningStartTick() { return warningStartTick; } - public void setWarningStartTick(int warningStartTick) { this.warningStartTick = warningStartTick; } + public float getWidth() { + return width; + } - public boolean isAnimated() { return animated; } - public void setAnimated(boolean animated) { this.animated = animated; } + public void setWidth(float width) { + this.width = width; + } - public float getHeightOffset() { return heightOffset; } - public void setHeightOffset(float heightOffset) { this.heightOffset = heightOffset; } + public float getAngle() { + return angle; + } - public boolean isAtTarget() { return atTarget; } - public void setAtTarget(boolean atTarget) { this.atTarget = atTarget; } + public void setAngle(float angle) { + this.angle = angle; + } - public boolean isFollowsTarget() { return followsTarget; } - public void setFollowsTarget(boolean followsTarget) { this.followsTarget = followsTarget; } + public int getDurationTicks() { + return durationTicks; + } - public boolean isFollowsCaster() { return followsCaster; } - public void setFollowsCaster(boolean followsCaster) { this.followsCaster = followsCaster; } + public void setDurationTicks(int durationTicks) { + this.durationTicks = durationTicks; + } - public float getOffsetForward() { return offsetForward; } - public void setOffsetForward(float offsetForward) { this.offsetForward = offsetForward; } + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public int getWarningColor() { + return warningColor; + } + + public void setWarningColor(int warningColor) { + this.warningColor = warningColor; + } + + public int getWarningStartTick() { + return warningStartTick; + } + + public void setWarningStartTick(int warningStartTick) { + this.warningStartTick = warningStartTick; + } + + public boolean isAnimated() { + return animated; + } + + public void setAnimated(boolean animated) { + this.animated = animated; + } + + public float getHeightOffset() { + return heightOffset; + } + + public void setHeightOffset(float heightOffset) { + this.heightOffset = heightOffset; + } + + public boolean isAtTarget() { + return atTarget; + } + + public void setAtTarget(boolean atTarget) { + this.atTarget = atTarget; + } + + public boolean isFollowsTarget() { + return followsTarget; + } + + public void setFollowsTarget(boolean followsTarget) { + this.followsTarget = followsTarget; + } + + public boolean isFollowsCaster() { + return followsCaster; + } + + public void setFollowsCaster(boolean followsCaster) { + this.followsCaster = followsCaster; + } + + public float getOffsetForward() { + return offsetForward; + } + + public void setOffsetForward(float offsetForward) { + this.offsetForward = offsetForward; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java index b16a9f0f2..2d705fb15 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java @@ -55,6 +55,7 @@ public class TelegraphInstance { /** * Called every tick to update telegraph state. + * * @param world The world (for entity lookup) * @return false if telegraph should be removed */ @@ -90,10 +91,10 @@ public class TelegraphInstance { * Find the ground level at a given position. * Searches downward from the given Y to find a solid block. * - * @param world The world - * @param x X coordinate + * @param world The world + * @param x X coordinate * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate + * @param z Z coordinate * @return The Y coordinate of the ground surface */ private double findGroundLevel(World world, double x, double startY, double z) { @@ -207,42 +208,99 @@ public class TelegraphInstance { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getInstanceId() { return instanceId; } - public void setInstanceId(String instanceId) { this.instanceId = instanceId; } + public String getInstanceId() { + return instanceId; + } - public Telegraph getTelegraph() { return telegraph; } - public void setTelegraph(Telegraph telegraph) { this.telegraph = telegraph; } + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } - public double getX() { return x; } - public void setX(double x) { this.x = x; } + public Telegraph getTelegraph() { + return telegraph; + } - public double getY() { return y; } - public void setY(double y) { this.y = y; } + public void setTelegraph(Telegraph telegraph) { + this.telegraph = telegraph; + } - public double getZ() { return z; } - public void setZ(double z) { this.z = z; } + public double getX() { + return x; + } - public float getYaw() { return yaw; } - public void setYaw(float yaw) { this.yaw = yaw; } + public void setX(double x) { + this.x = x; + } - public int getEntityIdToFollow() { return entityIdToFollow; } - public void setEntityIdToFollow(int entityIdToFollow) { this.entityIdToFollow = entityIdToFollow; } + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public int getEntityIdToFollow() { + return entityIdToFollow; + } + + public void setEntityIdToFollow(int entityIdToFollow) { + this.entityIdToFollow = entityIdToFollow; + } /** * Locks the telegraph at its current position. * Stops following any entity - used when ability commits to action. */ - public void lockPosition() { this.entityIdToFollow = -1; } + public void lockPosition() { + this.entityIdToFollow = -1; + } - public int getCasterEntityId() { return casterEntityId; } - public void setCasterEntityId(int casterEntityId) { this.casterEntityId = casterEntityId; } + public int getCasterEntityId() { + return casterEntityId; + } - public int getRemainingTicks() { return remainingTicks; } - public void setRemainingTicks(int remainingTicks) { this.remainingTicks = remainingTicks; } + public void setCasterEntityId(int casterEntityId) { + this.casterEntityId = casterEntityId; + } - public int getTotalTicks() { return totalTicks; } - public void setTotalTicks(int totalTicks) { this.totalTicks = totalTicks; } + public int getRemainingTicks() { + return remainingTicks; + } - public boolean isWarning() { return isWarning; } - public void setWarning(boolean warning) { isWarning = warning; } + public void setRemainingTicks(int remainingTicks) { + this.remainingTicks = remainingTicks; + } + + public int getTotalTicks() { + return totalTicks; + } + + public void setTotalTicks(int totalTicks) { + this.totalTicks = totalTicks; + } + + public boolean isWarning() { + return isWarning; + } + + public void setWarning(boolean warning) { + isWarning = warning; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java index b82afffd2..3dc4f9e0f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java @@ -12,12 +12,15 @@ import java.util.concurrent.ConcurrentHashMap; */ public class TelegraphManager { - /** Client-side instance for rendering */ + /** + * Client-side instance for rendering + */ public static TelegraphManager ClientInstance; private final ConcurrentHashMap telegraphs = new ConcurrentHashMap<>(); - public TelegraphManager() {} + public TelegraphManager() { + } /** * Initialize the client-side manager. diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java index dee9a249a..c7e4978b6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java @@ -4,21 +4,33 @@ package kamkeel.npcs.controllers.data.ability.telegraph; * Types of telegraph shapes for visual attack warnings. */ public enum TelegraphType { - /** Filled circle (uses radius) */ + /** + * Filled circle (uses radius) + */ CIRCLE, - /** Donut shape (uses radius and innerRadius) */ + /** + * Donut shape (uses radius and innerRadius) + */ RING, - /** Rectangle (uses length, width) */ + /** + * Rectangle (uses length, width) + */ LINE, - /** Fan/wedge shape (uses length, angle) */ + /** + * Fan/wedge shape (uses length, angle) + */ CONE, - /** Single point indicator */ + /** + * Single point indicator + */ POINT, - /** No telegraph */ + /** + * No telegraph + */ NONE } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index f094a4f0d..a071fcb30 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -2,19 +2,17 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -61,21 +59,25 @@ public class AbilityBeam extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityBeam(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -184,7 +186,7 @@ public class AbilityBeam extends Ability { } private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, - double dirX, double dirZ) { + double dirX, double dirZ) { // Project entity position onto beam line double entityX = entity.posX - startX; double entityZ = entity.posZ - startZ; @@ -209,7 +211,7 @@ public class AbilityBeam extends Ability { } private void spawnBeamParticles(World world, double startX, double startY, double startZ, - double dirX, double dirZ) { + double dirX, double dirZ) { // Spawn particles along beam line for (int i = 0; i < beamLength; i++) { double px = startX + dirX * i; @@ -259,30 +261,75 @@ public class AbilityBeam extends Ability { } // Getters & Setters - public float getBeamLength() { return beamLength; } - public void setBeamLength(float beamLength) { this.beamLength = beamLength; } + public float getBeamLength() { + return beamLength; + } - public float getBeamWidth() { return beamWidth; } - public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public void setBeamLength(float beamLength) { + this.beamLength = beamLength; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getBeamWidth() { + return beamWidth; + } - public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public void setBeamWidth(float beamWidth) { + this.beamWidth = beamWidth; + } - public boolean isPiercing() { return piercing; } - public void setPiercing(boolean piercing) { this.piercing = piercing; } + public float getDamage() { + return damage; + } - public float getSweepAngle() { return sweepAngle; } - public void setSweepAngle(float sweepAngle) { this.sweepAngle = sweepAngle; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getSweepSpeed() { return sweepSpeed; } - public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public int getDamageInterval() { + return damageInterval; + } - public boolean isSweepBackAndForth() { return sweepBackAndForth; } - public void setSweepBackAndForth(boolean sweepBackAndForth) { this.sweepBackAndForth = sweepBackAndForth; } + public void setDamageInterval(int damageInterval) { + this.damageInterval = damageInterval; + } - public boolean isLockOnTarget() { return lockOnTarget; } - public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; } + public boolean isPiercing() { + return piercing; + } + + public void setPiercing(boolean piercing) { + this.piercing = piercing; + } + + public float getSweepAngle() { + return sweepAngle; + } + + public void setSweepAngle(float sweepAngle) { + this.sweepAngle = sweepAngle; + } + + public float getSweepSpeed() { + return sweepSpeed; + } + + public void setSweepSpeed(float sweepSpeed) { + this.sweepSpeed = sweepSpeed; + } + + public boolean isSweepBackAndForth() { + return sweepBackAndForth; + } + + public void setSweepBackAndForth(boolean sweepBackAndForth) { + this.sweepBackAndForth = sweepBackAndForth; + } + + public boolean isLockOnTarget() { + return lockOnTarget; + } + + public void setLockOnTarget(boolean lockOnTarget) { + this.lockOnTarget = lockOnTarget; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 41e288c5b..abe05c643 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -11,12 +11,11 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCharge; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -59,7 +58,9 @@ public class AbilityCharge extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -68,11 +69,13 @@ public class AbilityCharge extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -150,7 +153,7 @@ public class AbilityCharge extends Ability { // Calculate distance traveled double distanceTraveled = Math.sqrt( Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(npc.posZ - startZ, 2) ); // Check if reached max distance @@ -305,15 +308,35 @@ public class AbilityCharge extends Ability { } // Getters & Setters - public float getChargeSpeed() { return chargeSpeed; } - public void setChargeSpeed(float chargeSpeed) { this.chargeSpeed = chargeSpeed; } + public float getChargeSpeed() { + return chargeSpeed; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public void setChargeSpeed(float chargeSpeed) { + this.chargeSpeed = chargeSpeed; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public float getDamage() { + return damage; + } - public float getHitWidth() { return hitWidth; } - public void setHitWidth(float hitWidth) { this.hitWidth = hitWidth; } + public void setDamage(float damage) { + this.damage = damage; + } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public float getHitWidth() { + return hitWidth; + } + + public void setHitWidth(float hitWidth) { + this.hitWidth = hitWidth; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index b258e6127..89ff0e3f0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -12,9 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCutter; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -67,7 +67,9 @@ public class AbilityCutter extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -76,21 +78,29 @@ public class AbilityCutter extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return range; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } @Override - public float getTelegraphLength() { return range; } + public float getTelegraphRadius() { + return range; + } @Override - public float getTelegraphAngle() { return arcAngle; } + public float getTelegraphLength() { + return range; + } + + @Override + public float getTelegraphAngle() { + return arcAngle; + } @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { @@ -145,7 +155,7 @@ public class AbilityCutter extends Ability { hitEntities.add(entity.getEntityId()); - float distFactor = 1.0f - ((float)dist / maxDist) * 0.3f; + float distFactor = 1.0f - ((float) dist / maxDist) * 0.3f; float actualDamage = damage * distFactor; // Apply damage with scripted event support @@ -223,38 +233,95 @@ public class AbilityCutter extends Ability { } // Getters & Setters - public float getArcAngle() { return arcAngle; } - public void setArcAngle(float arcAngle) { this.arcAngle = arcAngle; } + public float getArcAngle() { + return arcAngle; + } - public float getRange() { return range; } - public void setRange(float range) { this.range = range; } + public void setArcAngle(float arcAngle) { + this.arcAngle = arcAngle; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getRange() { + return range; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setRange(float range) { + this.range = range; + } - public SweepMode getSweepMode() { return sweepMode; } - public void setSweepMode(SweepMode sweepMode) { this.sweepMode = sweepMode; } + public float getDamage() { + return damage; + } - public float getSweepSpeed() { return sweepSpeed; } - public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public float getKnockback() { + return knockback; + } - public int getPoisonDurationSeconds() { return poisonDurationSeconds; } - public void setPoisonDurationSeconds(int poisonDurationSeconds) { this.poisonDurationSeconds = poisonDurationSeconds; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public SweepMode getSweepMode() { + return sweepMode; + } - public boolean isPiercing() { return piercing; } - public void setPiercing(boolean piercing) { this.piercing = piercing; } + public void setSweepMode(SweepMode sweepMode) { + this.sweepMode = sweepMode; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public float getSweepSpeed() { + return sweepSpeed; + } - public float getCurrentRotation() { return currentRotation; } + public void setSweepSpeed(float sweepSpeed) { + this.sweepSpeed = sweepSpeed; + } + + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getPoisonDurationSeconds() { + return poisonDurationSeconds; + } + + public void setPoisonDurationSeconds(int poisonDurationSeconds) { + this.poisonDurationSeconds = poisonDurationSeconds; + } + + public int getPoisonLevel() { + return poisonLevel; + } + + public void setPoisonLevel(int poisonLevel) { + this.poisonLevel = poisonLevel; + } + + public boolean isPiercing() { + return piercing; + } + + public void setPiercing(boolean piercing) { + this.piercing = piercing; + } + + public float getInnerRadius() { + return innerRadius; + } + + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } + + public float getCurrentRotation() { + return currentRotation; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index db34b0bb8..0c0f9cf2c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -2,17 +2,16 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.Random; @@ -100,21 +99,25 @@ public class AbilityDash extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityDash(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -163,7 +166,7 @@ public class AbilityDash extends Ability { // Calculate distance traveled double distanceTraveled = Math.sqrt( Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(npc.posZ - startZ, 2) ); // Check if reached max distance @@ -246,14 +249,31 @@ public class AbilityDash extends Ability { } // Getters & Setters - public DashMode getDashMode() { return dashMode; } - public void setDashMode(DashMode dashMode) { this.dashMode = dashMode; } + public DashMode getDashMode() { + return dashMode; + } - public float getDashDistance() { return dashDistance; } - public void setDashDistance(float dashDistance) { this.dashDistance = dashDistance; } + public void setDashMode(DashMode dashMode) { + this.dashMode = dashMode; + } - public float getDashSpeed() { return dashSpeed; } - public void setDashSpeed(float dashSpeed) { this.dashSpeed = dashSpeed; } + public float getDashDistance() { + return dashDistance; + } - public DashDirection getChosenDirection() { return chosenDirection; } + public void setDashDistance(float dashDistance) { + this.dashDistance = dashDistance; + } + + public float getDashSpeed() { + return dashSpeed; + } + + public void setDashSpeed(float dashSpeed) { + this.dashSpeed = dashSpeed; + } + + public DashDirection getChosenDirection() { + return chosenDirection; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 2ef0a446c..2427255fe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -2,17 +2,16 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.Random; @@ -62,21 +61,25 @@ public class AbilityGuard extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityGuard(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.SELF }; + return new TargetingMode[]{TargetingMode.SELF}; } @Override @@ -99,7 +102,7 @@ public class AbilityGuard extends Ability { * This should be called from the damage handling code. * * @param attacker The entity that attacked - * @param damage The damage amount (after reduction) + * @param damage The damage amount (after reduction) */ public void onDamageTaken(EntityNPCInterface npc, EntityLivingBase attacker, DamageSource source, float damage) { if (!canCounter || attacker == null) return; @@ -212,24 +215,59 @@ public class AbilityGuard extends Ability { } // Getters & Setters - public float getDamageReduction() { return damageReduction; } - public void setDamageReduction(float damageReduction) { this.damageReduction = damageReduction; } + public float getDamageReduction() { + return damageReduction; + } - public boolean isCanCounter() { return canCounter; } - public void setCanCounter(boolean canCounter) { this.canCounter = canCounter; } + public void setDamageReduction(float damageReduction) { + this.damageReduction = damageReduction; + } - public CounterType getCounterType() { return counterType; } - public void setCounterType(CounterType counterType) { this.counterType = counterType; } + public boolean isCanCounter() { + return canCounter; + } - public float getCounterValue() { return counterValue; } - public void setCounterValue(float counterValue) { this.counterValue = counterValue; } + public void setCanCounter(boolean canCounter) { + this.canCounter = canCounter; + } - public float getCounterChance() { return counterChance; } - public void setCounterChance(float counterChance) { this.counterChance = counterChance; } + public CounterType getCounterType() { + return counterType; + } - public String getCounterSound() { return counterSound; } - public void setCounterSound(String counterSound) { this.counterSound = counterSound; } + public void setCounterType(CounterType counterType) { + this.counterType = counterType; + } - public int getCounterAnimationId() { return counterAnimationId; } - public void setCounterAnimationId(int counterAnimationId) { this.counterAnimationId = counterAnimationId; } + public float getCounterValue() { + return counterValue; + } + + public void setCounterValue(float counterValue) { + this.counterValue = counterValue; + } + + public float getCounterChance() { + return counterChance; + } + + public void setCounterChance(float counterChance) { + this.counterChance = counterChance; + } + + public String getCounterSound() { + return counterSound; + } + + public void setCounterSound(String counterSound) { + this.counterSound = counterSound; + } + + public int getCounterAnimationId() { + return counterAnimationId; + } + + public void setCounterAnimationId(int counterAnimationId) { + this.counterAnimationId = counterAnimationId; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index d211e7516..b8ce70929 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; @@ -17,6 +13,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -90,25 +89,31 @@ public class AbilityHazard extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityHazard(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return radius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphRadius() { + return radius; + } @Override public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { @@ -414,68 +419,176 @@ public class AbilityHazard extends Ability { } // Getters & Setters - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public float getRadius() { + return radius; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getConeAngle() { return coneAngle; } - public void setConeAngle(float coneAngle) { this.coneAngle = coneAngle; } + public float getInnerRadius() { + return innerRadius; + } - public HazardShape getShape() { return shape; } - public void setShape(HazardShape shape) { this.shape = shape; } + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } - public PlacementMode getPlacement() { return placement; } - public void setPlacement(PlacementMode placement) { this.placement = placement; } + public float getConeAngle() { + return coneAngle; + } - public float getDamagePerTick() { return damagePerTick; } - public void setDamagePerTick(float damagePerTick) { this.damagePerTick = damagePerTick; } + public void setConeAngle(float coneAngle) { + this.coneAngle = coneAngle; + } - public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public HazardShape getShape() { + return shape; + } - public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; } - public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; } + public void setShape(HazardShape shape) { + this.shape = shape; + } - public int getSlownessLevel() { return slownessLevel; } - public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; } + public PlacementMode getPlacement() { + return placement; + } - public int getWeaknessLevel() { return weaknessLevel; } - public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; } + public void setPlacement(PlacementMode placement) { + this.placement = placement; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public float getDamagePerTick() { + return damagePerTick; + } - public int getWitherLevel() { return witherLevel; } - public void setWitherLevel(int witherLevel) { this.witherLevel = witherLevel; } + public void setDamagePerTick(float damagePerTick) { + this.damagePerTick = damagePerTick; + } - public int getBlindnessLevel() { return blindnessLevel; } - public void setBlindnessLevel(int blindnessLevel) { this.blindnessLevel = blindnessLevel; } + public int getDamageInterval() { + return damageInterval; + } - public int getDebuffDuration() { return debuffDuration; } - public void setDebuffDuration(int debuffDuration) { this.debuffDuration = debuffDuration; } + public void setDamageInterval(int damageInterval) { + this.damageInterval = damageInterval; + } - public boolean isAffectsCaster() { return affectsCaster; } - public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; } + public boolean isIgnoreInvulnFrames() { + return ignoreInvulnFrames; + } - public float getHeightAbove() { return heightAbove; } - public void setHeightAbove(float heightAbove) { this.heightAbove = heightAbove; } + public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { + this.ignoreInvulnFrames = ignoreInvulnFrames; + } - public float getHeightBelow() { return heightBelow; } - public void setHeightBelow(float heightBelow) { this.heightBelow = heightBelow; } + public int getSlownessLevel() { + return slownessLevel; + } - public float getMinOffset() { return minOffset; } - public void setMinOffset(float minOffset) { this.minOffset = minOffset; } + public void setSlownessLevel(int slownessLevel) { + this.slownessLevel = slownessLevel; + } - public float getMaxOffset() { return maxOffset; } - public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; } + public int getWeaknessLevel() { + return weaknessLevel; + } - public boolean isRandomOffset() { return randomOffset; } - public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; } + public void setWeaknessLevel(int weaknessLevel) { + this.weaknessLevel = weaknessLevel; + } + + public int getPoisonLevel() { + return poisonLevel; + } + + public void setPoisonLevel(int poisonLevel) { + this.poisonLevel = poisonLevel; + } + + public int getWitherLevel() { + return witherLevel; + } + + public void setWitherLevel(int witherLevel) { + this.witherLevel = witherLevel; + } + + public int getBlindnessLevel() { + return blindnessLevel; + } + + public void setBlindnessLevel(int blindnessLevel) { + this.blindnessLevel = blindnessLevel; + } + + public int getDebuffDuration() { + return debuffDuration; + } + + public void setDebuffDuration(int debuffDuration) { + this.debuffDuration = debuffDuration; + } + + public boolean isAffectsCaster() { + return affectsCaster; + } + + public void setAffectsCaster(boolean affectsCaster) { + this.affectsCaster = affectsCaster; + } + + public float getHeightAbove() { + return heightAbove; + } + + public void setHeightAbove(float heightAbove) { + this.heightAbove = heightAbove; + } + + public float getHeightBelow() { + return heightBelow; + } + + public void setHeightBelow(float heightBelow) { + this.heightBelow = heightBelow; + } + + public float getMinOffset() { + return minOffset; + } + + public void setMinOffset(float minOffset) { + this.minOffset = minOffset; + } + + public float getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(float maxOffset) { + this.maxOffset = maxOffset; + } + + public boolean isRandomOffset() { + return randomOffset; + } + + public void setRandomOffset(boolean randomOffset) { + this.randomOffset = randomOffset; + } // Runtime getters - public double getZoneX() { return zoneX; } - public double getZoneY() { return zoneY; } - public double getZoneZ() { return zoneZ; } + public double getZoneX() { + return zoneX; + } + + public double getZoneY() { + return zoneY; + } + + public double getZoneZ() { + return zoneZ; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 4a393682b..cae5f6815 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -10,9 +10,9 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeal; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; @@ -50,7 +50,9 @@ public class AbilityHeal extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -59,11 +61,13 @@ public class AbilityHeal extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.SELF }; + return new TargetingMode[]{TargetingMode.SELF}; } @Override @@ -197,21 +201,51 @@ public class AbilityHeal extends Ability { } // Getters & Setters - public float getHealAmount() { return healAmount; } - public void setHealAmount(float healAmount) { this.healAmount = healAmount; } + public float getHealAmount() { + return healAmount; + } - public float getHealPercent() { return healPercent; } - public void setHealPercent(float healPercent) { this.healPercent = healPercent; } + public void setHealAmount(float healAmount) { + this.healAmount = healAmount; + } - public boolean isHealSelf() { return healSelf; } - public void setHealSelf(boolean healSelf) { this.healSelf = healSelf; } + public float getHealPercent() { + return healPercent; + } - public boolean isHealAllies() { return healAllies; } - public void setHealAllies(boolean healAllies) { this.healAllies = healAllies; } + public void setHealPercent(float healPercent) { + this.healPercent = healPercent; + } - public float getHealRadius() { return healRadius; } - public void setHealRadius(float healRadius) { this.healRadius = healRadius; } + public boolean isHealSelf() { + return healSelf; + } - public boolean isInstantHeal() { return instantHeal; } - public void setInstantHeal(boolean instantHeal) { this.instantHeal = instantHeal; } + public void setHealSelf(boolean healSelf) { + this.healSelf = healSelf; + } + + public boolean isHealAllies() { + return healAllies; + } + + public void setHealAllies(boolean healAllies) { + this.healAllies = healAllies; + } + + public float getHealRadius() { + return healRadius; + } + + public void setHealRadius(float healRadius) { + this.healRadius = healRadius; + } + + public boolean isInstantHeal() { + return instantHeal; + } + + public void setInstantHeal(boolean instantHeal) { + this.instantHeal = instantHeal; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 288af63d7..caafc9d5d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -14,6 +10,9 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; import net.minecraft.potion.PotionEffect; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; /** @@ -46,21 +45,25 @@ public class AbilityHeavyHit extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityHeavyHit(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -105,18 +108,43 @@ public class AbilityHeavyHit extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getSlownessLevel() { return slownessLevel; } - public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; } + public float getKnockback() { + return knockback; + } - public int getWeaknessLevel() { return weaknessLevel; } - public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public int getPotionDurationSeconds() { return potionDurationSeconds; } - public void setPotionDurationSeconds(int potionDurationSeconds) { this.potionDurationSeconds = potionDurationSeconds; } + public int getSlownessLevel() { + return slownessLevel; + } + + public void setSlownessLevel(int slownessLevel) { + this.slownessLevel = slownessLevel; + } + + public int getWeaknessLevel() { + return weaknessLevel; + } + + public void setWeaknessLevel(int weaknessLevel) { + this.weaknessLevel = weaknessLevel; + } + + public int getPotionDurationSeconds() { + return potionDurationSeconds; + } + + public void setPotionDurationSeconds(int potionDurationSeconds) { + this.potionDurationSeconds = potionDurationSeconds; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index cf1555898..a3677a9db 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -16,6 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -67,21 +66,25 @@ public class AbilityOrb extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityOrb(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -124,8 +127,8 @@ public class AbilityOrb extends Ability { double distTraveled = Math.sqrt( Math.pow(orbX - startX, 2) + - Math.pow(orbY - startY, 2) + - Math.pow(orbZ - startZ, 2) + Math.pow(orbY - startY, 2) + + Math.pow(orbZ - startZ, 2) ); if (distTraveled >= maxDistance) { hasHit = true; @@ -139,8 +142,8 @@ public class AbilityOrb extends Ability { double distToTarget = Math.sqrt( Math.pow(targetX - orbX, 2) + - Math.pow(targetY - orbY, 2) + - Math.pow(targetZ - orbZ, 2) + Math.pow(targetY - orbY, 2) + + Math.pow(targetZ - orbZ, 2) ); if (distToTarget <= homingRange) { @@ -202,7 +205,7 @@ public class AbilityOrb extends Ability { break; } - if (!hasHit && world.getBlock((int)orbX, (int)orbY, (int)orbZ).getMaterial().isSolid()) { + if (!hasHit && world.getBlock((int) orbX, (int) orbY, (int) orbZ).getMaterial().isSolid()) { hasHit = true; if (explosive) { doExplosion(npc, world); @@ -224,12 +227,12 @@ public class AbilityOrb extends Ability { double dist = Math.sqrt( Math.pow(blastTarget.posX - orbX, 2) + - Math.pow(blastTarget.posY - orbY, 2) + - Math.pow(blastTarget.posZ - orbZ, 2) + Math.pow(blastTarget.posY - orbY, 2) + + Math.pow(blastTarget.posZ - orbZ, 2) ); if (dist <= explosionRadius) { - float falloff = 1.0f - (float)(dist / explosionRadius) * explosionDamageFalloff; + float falloff = 1.0f - (float) (dist / explosionRadius) * explosionDamageFalloff; // Apply damage with scripted event support boolean wasHit = applyAbilityDamage(npc, blastTarget, damage * falloff, knockback * falloff); if (wasHit) { @@ -314,54 +317,140 @@ public class AbilityOrb extends Ability { } // Getters & Setters - public float getOrbSpeed() { return orbSpeed; } - public void setOrbSpeed(float orbSpeed) { this.orbSpeed = orbSpeed; } + public float getOrbSpeed() { + return orbSpeed; + } - public float getOrbSize() { return orbSize; } - public void setOrbSize(float orbSize) { this.orbSize = orbSize; } + public void setOrbSpeed(float orbSpeed) { + this.orbSpeed = orbSpeed; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getOrbSize() { + return orbSize; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setOrbSize(float orbSize) { + this.orbSize = orbSize; + } - public float getMaxDistance() { return maxDistance; } - public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public float getDamage() { + return damage; + } - public int getMaxLifetime() { return maxLifetime; } - public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + public void setDamage(float damage) { + this.damage = damage; + } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } + public float getKnockback() { + return knockback; + } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public float getHomingRange() { return homingRange; } - public void setHomingRange(float homingRange) { this.homingRange = homingRange; } + public float getMaxDistance() { + return maxDistance; + } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } + public void setMaxDistance(float maxDistance) { + this.maxDistance = maxDistance; + } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public int getMaxLifetime() { + return maxLifetime; + } - public float getExplosionDamageFalloff() { return explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public void setMaxLifetime(int maxLifetime) { + this.maxLifetime = maxLifetime; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public boolean isHoming() { + return homing; + } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public void setHoming(boolean homing) { + this.homing = homing; + } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public float getHomingStrength() { + return homingStrength; + } + + public void setHomingStrength(float homingStrength) { + this.homingStrength = homingStrength; + } + + public float getHomingRange() { + return homingRange; + } + + public void setHomingRange(float homingRange) { + this.homingRange = homingRange; + } + + public boolean isExplosive() { + return explosive; + } + + public void setExplosive(boolean explosive) { + this.explosive = explosive; + } + + public float getExplosionRadius() { + return explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + this.explosionRadius = explosionRadius; + } + + public float getExplosionDamageFalloff() { + return explosionDamageFalloff; + } + + public void setExplosionDamageFalloff(float explosionDamageFalloff) { + this.explosionDamageFalloff = explosionDamageFalloff; + } + + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getSlowDuration() { + return slowDuration; + } + + public void setSlowDuration(int slowDuration) { + this.slowDuration = slowDuration; + } + + public int getSlowLevel() { + return slowLevel; + } + + public void setSlowLevel(int slowLevel) { + this.slowLevel = slowLevel; + } // Runtime getters for rendering - public double getOrbX() { return orbX; } - public double getOrbY() { return orbY; } - public double getOrbZ() { return orbZ; } - public boolean hasHit() { return hasHit; } + public double getOrbX() { + return orbX; + } + + public double getOrbY() { + return orbY; + } + + public double getOrbZ() { + return orbZ; + } + + public boolean hasHit() { + return hasHit; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 4d2143b36..b47cd244a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -8,11 +8,10 @@ import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -49,7 +48,9 @@ public class AbilityProjectile extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -58,11 +59,13 @@ public class AbilityProjectile extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -187,27 +190,67 @@ public class AbilityProjectile extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getSpeed() { return speed; } - public void setSpeed(float speed) { this.speed = speed; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public float getSpeed() { + return speed; + } - public String getProjectileType() { return projectileType; } - public void setProjectileType(String projectileType) { this.projectileType = projectileType; } + public void setSpeed(float speed) { + this.speed = speed; + } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getKnockback() { + return knockback; + } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } + public String getProjectileType() { + return projectileType; + } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public void setProjectileType(String projectileType) { + this.projectileType = projectileType; + } + + public boolean isExplosive() { + return explosive; + } + + public void setExplosive(boolean explosive) { + this.explosive = explosive; + } + + public float getExplosionRadius() { + return explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + this.explosionRadius = explosionRadius; + } + + public boolean isHoming() { + return homing; + } + + public void setHoming(boolean homing) { + this.homing = homing; + } + + public float getHomingStrength() { + return homingStrength; + } + + public void setHomingStrength(float homingStrength) { + this.homingStrength = homingStrength; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 340b92826..e15cf683a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -12,9 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityShockwave; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -50,7 +50,9 @@ public class AbilityShockwave extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -59,15 +61,19 @@ public class AbilityShockwave extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return pushRadius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + @Override + public float getTelegraphRadius() { + return pushRadius; + } @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { @@ -111,7 +117,7 @@ public class AbilityShockwave extends Ability { } // Scale knockback by distance (closer = stronger) - float distFactor = 1.0f - (float)(dist / pushRadius) * 0.5f; + float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; float finalPush = pushStrength * distFactor; // Apply damage with custom knockback direction boolean wasHit = applyAbilityDamageWithDirection(npc, entity, damage * distFactor, finalPush, dx, dz); @@ -153,19 +159,44 @@ public class AbilityShockwave extends Ability { } // Getters & Setters - public float getPushRadius() { return pushRadius; } - public void setPushRadius(float pushRadius) { this.pushRadius = pushRadius; } + public float getPushRadius() { + return pushRadius; + } - public float getPushStrength() { return pushStrength; } - public void setPushStrength(float pushStrength) { this.pushStrength = pushStrength; } + public void setPushRadius(float pushRadius) { + this.pushRadius = pushRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getPushStrength() { + return pushStrength; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public void setPushStrength(float pushStrength) { + this.pushStrength = pushStrength; + } - public int getMaxTargets() { return maxTargets; } - public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; } + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getMaxTargets() { + return maxTargets; + } + + public void setMaxTargets(int maxTargets) { + this.maxTargets = maxTargets; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 321b9058f..1b0eb246f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -11,18 +11,17 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; -import net.minecraft.util.MathHelper; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySlam; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; /** * Slam ability: NPC leaps toward target in an arc and slams down, dealing AOE damage on landing. - * + *

* Phases: * - WINDUP: NPC prepares to leap, telegraph shows landing zone * - ACTIVE: NPC is in the air, traveling in arc toward destination @@ -62,7 +61,9 @@ public class AbilitySlam extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -80,7 +81,7 @@ public class AbilitySlam extends Ability { @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET }; + return new TargetingMode[]{TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET}; } @Override @@ -317,7 +318,7 @@ public class AbilitySlam extends Ability { } // Ground crack particles - check if block is valid - net.minecraft.block.Block groundBlock = world.getBlock((int)x, (int)y - 1, (int)z); + net.minecraft.block.Block groundBlock = world.getBlock((int) x, (int) y - 1, (int) z); if (groundBlock != null && groundBlock.getMaterial().isSolid()) { int blockId = net.minecraft.block.Block.getIdFromBlock(groundBlock); if (blockId > 0) { @@ -427,18 +428,43 @@ public class AbilitySlam extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getKnockbackStrength() { return knockbackStrength; } - public void setKnockbackStrength(float knockbackStrength) { this.knockbackStrength = knockbackStrength; } + public float getRadius() { + return radius; + } - public float getLeapSpeed() { return leapSpeed; } - public void setLeapSpeed(float leapSpeed) { this.leapSpeed = leapSpeed; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getLeapHeight() { return leapHeight; } - public void setLeapHeight(float leapHeight) { this.leapHeight = leapHeight; } + public float getKnockbackStrength() { + return knockbackStrength; + } + + public void setKnockbackStrength(float knockbackStrength) { + this.knockbackStrength = knockbackStrength; + } + + public float getLeapSpeed() { + return leapSpeed; + } + + public void setLeapSpeed(float leapSpeed) { + this.leapSpeed = leapSpeed; + } + + public float getLeapHeight() { + return leapHeight; + } + + public void setLeapHeight(float leapHeight) { + this.leapHeight = leapHeight; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 72971c572..50d59e729 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.block.Block; @@ -14,10 +10,12 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -73,21 +71,25 @@ public class AbilityTeleport extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityTeleport(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -202,7 +204,7 @@ public class AbilityTeleport extends Ability { } private Vec3 findSafeDestination(World world, double oldX, double oldY, double oldZ, - double destX, double destY, double destZ) { + double destX, double destY, double destZ) { int oldBlockX = (int) Math.floor(oldX); int oldBlockY = (int) Math.floor(oldY); int oldBlockZ = (int) Math.floor(oldZ); @@ -291,7 +293,7 @@ public class AbilityTeleport extends Ability { double checkZ = z1 + dz * d; if (hasLineOfSight(world, x1, y1 + npc.getEyeHeight(), z1, - checkX, checkY + npc.getEyeHeight(), checkZ)) { + checkX, checkY + npc.getEyeHeight(), checkZ)) { int blockX = MathHelper.floor_double(checkX); int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); @@ -330,8 +332,8 @@ public class AbilityTeleport extends Ability { Block headBlock = world.getBlock(x, y + 1, z); return groundBlock.getMaterial().isSolid() && - !feetBlock.getMaterial().isSolid() && - !headBlock.getMaterial().isSolid(); + !feetBlock.getMaterial().isSolid() && + !headBlock.getMaterial().isSolid(); } private void dealDamageAt(EntityNPCInterface npc, World world, double x, double y, double z) { @@ -414,33 +416,83 @@ public class AbilityTeleport extends Ability { } // Getters & Setters - public TeleportMode getMode() { return mode; } - public void setMode(TeleportMode mode) { this.mode = mode; } + public TeleportMode getMode() { + return mode; + } - public int getBlinkCount() { return blinkCount; } - public void setBlinkCount(int blinkCount) { this.blinkCount = blinkCount; } + public void setMode(TeleportMode mode) { + this.mode = mode; + } - public int getBlinkDelayTicks() { return blinkDelayTicks; } - public void setBlinkDelayTicks(int blinkDelayTicks) { this.blinkDelayTicks = blinkDelayTicks; } + public int getBlinkCount() { + return blinkCount; + } - public float getBlinkRadius() { return blinkRadius; } - public void setBlinkRadius(float blinkRadius) { this.blinkRadius = blinkRadius; } + public void setBlinkCount(int blinkCount) { + this.blinkCount = blinkCount; + } - public float getBehindDistance() { return behindDistance; } - public void setBehindDistance(float behindDistance) { this.behindDistance = behindDistance; } + public int getBlinkDelayTicks() { + return blinkDelayTicks; + } - public boolean isRequireLineOfSight() { return requireLineOfSight; } - public void setRequireLineOfSight(boolean requireLineOfSight) { this.requireLineOfSight = requireLineOfSight; } + public void setBlinkDelayTicks(int blinkDelayTicks) { + this.blinkDelayTicks = blinkDelayTicks; + } - public boolean isDamageAtStart() { return damageAtStart; } - public void setDamageAtStart(boolean damageAtStart) { this.damageAtStart = damageAtStart; } + public float getBlinkRadius() { + return blinkRadius; + } - public boolean isDamageAtEnd() { return damageAtEnd; } - public void setDamageAtEnd(boolean damageAtEnd) { this.damageAtEnd = damageAtEnd; } + public void setBlinkRadius(float blinkRadius) { + this.blinkRadius = blinkRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getBehindDistance() { + return behindDistance; + } - public float getDamageRadius() { return damageRadius; } - public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + public void setBehindDistance(float behindDistance) { + this.behindDistance = behindDistance; + } + + public boolean isRequireLineOfSight() { + return requireLineOfSight; + } + + public void setRequireLineOfSight(boolean requireLineOfSight) { + this.requireLineOfSight = requireLineOfSight; + } + + public boolean isDamageAtStart() { + return damageAtStart; + } + + public void setDamageAtStart(boolean damageAtStart) { + this.damageAtStart = damageAtStart; + } + + public boolean isDamageAtEnd() { + return damageAtEnd; + } + + public void setDamageAtEnd(boolean damageAtEnd) { + this.damageAtEnd = damageAtEnd; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + public float getDamageRadius() { + return damageRadius; + } + + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 9d1090da6..021b1f7d6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -13,9 +13,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTrap; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -80,7 +80,9 @@ public class AbilityTrap extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -89,15 +91,19 @@ public class AbilityTrap extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return triggerRadius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphRadius() { + return triggerRadius; + } @Override public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { @@ -362,66 +368,172 @@ public class AbilityTrap extends Ability { } // Getters & Setters - public TrapPlacement getPlacement() { return placement; } - public void setPlacement(TrapPlacement placement) { this.placement = placement; } + public TrapPlacement getPlacement() { + return placement; + } - public float getPlacementDistance() { return placementDistance; } - public void setPlacementDistance(float placementDistance) { this.placementDistance = placementDistance; } + public void setPlacement(TrapPlacement placement) { + this.placement = placement; + } - public float getTriggerRadius() { return triggerRadius; } - public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; } + public float getPlacementDistance() { + return placementDistance; + } - public int getArmTime() { return armTime; } - public void setArmTime(int armTime) { this.armTime = armTime; } + public void setPlacementDistance(float placementDistance) { + this.placementDistance = placementDistance; + } - public int getMaxTriggers() { return maxTriggers; } - public void setMaxTriggers(int maxTriggers) { this.maxTriggers = maxTriggers; } + public float getTriggerRadius() { + return triggerRadius; + } - public int getTriggerCooldown() { return triggerCooldown; } - public void setTriggerCooldown(int triggerCooldown) { this.triggerCooldown = triggerCooldown; } + public void setTriggerRadius(float triggerRadius) { + this.triggerRadius = triggerRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public int getArmTime() { + return armTime; + } - public float getDamageRadius() { return damageRadius; } - public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + public void setArmTime(int armTime) { + this.armTime = armTime; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public int getMaxTriggers() { + return maxTriggers; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public void setMaxTriggers(int maxTriggers) { + this.maxTriggers = maxTriggers; + } - public int getRootDuration() { return rootDuration; } - public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; } + public int getTriggerCooldown() { + return triggerCooldown; + } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public void setTriggerCooldown(int triggerCooldown) { + this.triggerCooldown = triggerCooldown; + } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public float getDamage() { + return damage; + } - public int getPoisonDuration() { return poisonDuration; } - public void setPoisonDuration(int poisonDuration) { this.poisonDuration = poisonDuration; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public float getDamageRadius() { + return damageRadius; + } - public boolean isVisible() { return visible; } - public void setVisible(boolean visible) { this.visible = visible; } + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } - public float getMinOffset() { return minOffset; } - public void setMinOffset(float minOffset) { this.minOffset = minOffset; } + public float getKnockback() { + return knockback; + } - public float getMaxOffset() { return maxOffset; } - public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isRandomOffset() { return randomOffset; } - public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; } + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getRootDuration() { + return rootDuration; + } + + public void setRootDuration(int rootDuration) { + this.rootDuration = rootDuration; + } + + public int getSlowDuration() { + return slowDuration; + } + + public void setSlowDuration(int slowDuration) { + this.slowDuration = slowDuration; + } + + public int getSlowLevel() { + return slowLevel; + } + + public void setSlowLevel(int slowLevel) { + this.slowLevel = slowLevel; + } + + public int getPoisonDuration() { + return poisonDuration; + } + + public void setPoisonDuration(int poisonDuration) { + this.poisonDuration = poisonDuration; + } + + public int getPoisonLevel() { + return poisonLevel; + } + + public void setPoisonLevel(int poisonLevel) { + this.poisonLevel = poisonLevel; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public float getMinOffset() { + return minOffset; + } + + public void setMinOffset(float minOffset) { + this.minOffset = minOffset; + } + + public float getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(float maxOffset) { + this.maxOffset = maxOffset; + } + + public boolean isRandomOffset() { + return randomOffset; + } + + public void setRandomOffset(boolean randomOffset) { + this.randomOffset = randomOffset; + } // Runtime getters - public double getTrapX() { return trapX; } - public double getTrapY() { return trapY; } - public double getTrapZ() { return trapZ; } - public boolean isArmed() { return armed; } + public double getTrapX() { + return trapX; + } + + public double getTrapY() { + return trapY; + } + + public double getTrapZ() { + return trapZ; + } + + public boolean isArmed() { + return armed; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index ccd8d4a19..ab7171461 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -16,6 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -61,25 +60,31 @@ public class AbilityVortex extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityVortex(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return pullRadius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + @Override + public float getTelegraphRadius() { + return pullRadius; + } @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { @@ -249,33 +254,83 @@ public class AbilityVortex extends Ability { } // Getters & Setters - public float getPullRadius() { return pullRadius; } - public void setPullRadius(float pullRadius) { this.pullRadius = pullRadius; } + public float getPullRadius() { + return pullRadius; + } - public float getPullStrength() { return pullStrength; } - public void setPullStrength(float pullStrength) { this.pullStrength = pullStrength; } + public void setPullRadius(float pullRadius) { + this.pullRadius = pullRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getPullStrength() { + return pullStrength; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setPullStrength(float pullStrength) { + this.pullStrength = pullStrength; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public float getDamage() { + return damage; + } - public int getRootDuration() { return rootDuration; } - public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; } + public void setDamage(float damage) { + this.damage = damage; + } - public boolean isAoe() { return aoe; } - public void setAoe(boolean aoe) { this.aoe = aoe; } + public float getKnockback() { + return knockback; + } - public int getMaxTargets() { return maxTargets; } - public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isDamageOnPull() { return damageOnPull; } - public void setDamageOnPull(boolean damageOnPull) { this.damageOnPull = damageOnPull; } + public int getStunDuration() { + return stunDuration; + } - public float getPullDamage() { return pullDamage; } - public void setPullDamage(float pullDamage) { this.pullDamage = pullDamage; } + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getRootDuration() { + return rootDuration; + } + + public void setRootDuration(int rootDuration) { + this.rootDuration = rootDuration; + } + + public boolean isAoe() { + return aoe; + } + + public void setAoe(boolean aoe) { + this.aoe = aoe; + } + + public int getMaxTargets() { + return maxTargets; + } + + public void setMaxTargets(int maxTargets) { + this.maxTargets = maxTargets; + } + + public boolean isDamageOnPull() { + return damageOnPull; + } + + public void setDamageOnPull(boolean damageOnPull) { + this.damageOnPull = damageOnPull; + } + + public float getPullDamage() { + return pullDamage; + } + + public void setPullDamage(float pullDamage) { + this.pullDamage = pullDamage; + } } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index c87a5aa2d..1405b44c9 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,8 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; import kamkeel.npcs.network.packets.data.gui.GuiErrorPacket; import kamkeel.npcs.network.packets.data.gui.GuiOpenBookPacket; @@ -49,8 +51,6 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; @@ -64,6 +64,7 @@ import kamkeel.npcs.network.packets.player.SaveBookPacket; import kamkeel.npcs.network.packets.player.SaveSignPacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; +import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.network.packets.player.TransportPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiButtonPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiClosePacket; @@ -75,7 +76,6 @@ import kamkeel.npcs.network.packets.player.item.MagicCyclesPacket; import kamkeel.npcs.network.packets.player.profile.ProfileChangePacket; import kamkeel.npcs.network.packets.player.profile.ProfileCreatePacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetInfoPacket; -import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetPacket; import kamkeel.npcs.network.packets.player.profile.ProfileRemovePacket; import kamkeel.npcs.network.packets.player.profile.ProfileRenamePacket; @@ -88,6 +88,9 @@ import kamkeel.npcs.network.packets.request.MountPacket; import kamkeel.npcs.network.packets.request.TileEntityGetPacket; import kamkeel.npcs.network.packets.request.TileEntitySavePacket; import kamkeel.npcs.network.packets.request.TraderMarketSavePacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; @@ -141,9 +144,6 @@ import kamkeel.npcs.network.packets.request.linked.LinkedItemSavePacket; import kamkeel.npcs.network.packets.request.linked.LinkedNPCAddPacket; import kamkeel.npcs.network.packets.request.linked.LinkedNPCRemovePacket; import kamkeel.npcs.network.packets.request.linked.LinkedSetPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; import kamkeel.npcs.network.packets.request.magic.MagicCycleRemovePacket; import kamkeel.npcs.network.packets.request.magic.MagicCycleSavePacket; import kamkeel.npcs.network.packets.request.magic.MagicGetAllPacket; @@ -216,7 +216,16 @@ import kamkeel.npcs.network.packets.request.recipe.RecipesGetPacket; import kamkeel.npcs.network.packets.request.role.RoleCompanionUpdatePacket; import kamkeel.npcs.network.packets.request.role.RoleGetPacket; import kamkeel.npcs.network.packets.request.role.RoleSavePacket; -import kamkeel.npcs.network.packets.request.script.*; +import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; +import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; +import kamkeel.npcs.network.packets.request.script.EventScriptPacket; +import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; +import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; +import kamkeel.npcs.network.packets.request.script.NPCScriptPacket; +import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; +import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; +import kamkeel.npcs.network.packets.request.script.ScriptFilesPacket; +import kamkeel.npcs.network.packets.request.script.ScriptInfoPacket; import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java index bbf342ade..e9c846764 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java @@ -3,7 +3,6 @@ package kamkeel.npcs.network.packets.data.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 74bf8bd5e..37383ea6c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -3,7 +3,9 @@ package kamkeel.npcs.network.packets.request.script; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.network.*; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumRequestPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; diff --git a/src/main/java/kamkeel/npcs/util/BukkitUtil.java b/src/main/java/kamkeel/npcs/util/BukkitUtil.java index 78cd49c82..bc35cae6c 100644 --- a/src/main/java/kamkeel/npcs/util/BukkitUtil.java +++ b/src/main/java/kamkeel/npcs/util/BukkitUtil.java @@ -95,6 +95,7 @@ public class BukkitUtil { /** * Checks if a specific Bukkit plugin is enabled. + * * @param pluginName the plugin name to check * @return true if the plugin is enabled */ @@ -112,6 +113,7 @@ public class BukkitUtil { /** * Gets a Bukkit OfflinePlayer by name. + * * @param playerName the player name * @return the OfflinePlayer object, or null if not available */ @@ -128,6 +130,7 @@ public class BukkitUtil { /** * Gets a Bukkit Player by name (online players only). + * * @param playerName the player name * @return the Player object, or null if not online or not available */ @@ -144,6 +147,7 @@ public class BukkitUtil { /** * Gets a service provider from Bukkit's ServicesManager. + * * @param serviceClass the service class to get * @return the service provider, or null if not available */ diff --git a/src/main/java/kamkeel/npcs/util/VaultUtil.java b/src/main/java/kamkeel/npcs/util/VaultUtil.java index c7ef92e97..64743ffba 100644 --- a/src/main/java/kamkeel/npcs/util/VaultUtil.java +++ b/src/main/java/kamkeel/npcs/util/VaultUtil.java @@ -174,6 +174,7 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. + * * @param player the player to check * @return true if the player has an account */ @@ -183,6 +184,7 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. + * * @param playerName the player name to check * @return true if the player has an account */ @@ -203,6 +205,7 @@ public class VaultUtil { /** * Gets the balance of a player. + * * @param player the player * @return the player's balance, or 0 if not available */ @@ -212,6 +215,7 @@ public class VaultUtil { /** * Gets the balance of a player by name. + * * @param playerName the player name * @return the player's balance, or 0 if not available */ @@ -232,6 +236,7 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. + * * @param player the player * @param amount the amount to check * @return true if the player has at least the specified amount @@ -242,8 +247,9 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. + * * @param playerName the player name - * @param amount the amount to check + * @param amount the amount to check * @return true if the player has at least the specified amount */ public static boolean has(String playerName, double amount) { @@ -263,6 +269,7 @@ public class VaultUtil { /** * Withdraws money from a player's account. + * * @param player the player * @param amount the amount to withdraw * @return true if the transaction was successful @@ -273,8 +280,9 @@ public class VaultUtil { /** * Withdraws money from a player's account. + * * @param playerName the player name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean withdrawMoney(String playerName, double amount) { @@ -297,6 +305,7 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). + * * @param player the player * @param amount the amount to add * @return true if the transaction was successful @@ -307,8 +316,9 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). + * * @param playerName the player name - * @param amount the amount to add + * @param amount the amount to add * @return true if the transaction was successful */ public static boolean addMoney(String playerName, double amount) { @@ -332,6 +342,7 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. + * * @param player the player * @param amount the new balance * @return true if the transaction was successful @@ -343,8 +354,9 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. + * * @param playerName the player name - * @param amount the new balance + * @param amount the new balance * @return true if the transaction was successful */ public static boolean setBalance(String playerName, double amount) { @@ -364,6 +376,7 @@ public class VaultUtil { /** * Formats an amount according to the economy plugin's settings. + * * @param amount the amount to format * @return the formatted string, or the raw number as string if not available */ @@ -380,6 +393,7 @@ public class VaultUtil { /** * Gets the plural currency name (e.g., "Dollars"). + * * @return the plural currency name, or "coins" if not available */ public static String getCurrencyNamePlural() { @@ -395,6 +409,7 @@ public class VaultUtil { /** * Gets the singular currency name (e.g., "Dollar"). + * * @return the singular currency name, or "coin" if not available */ public static String getCurrencyNameSingular() { @@ -410,6 +425,7 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. + * * @param player the player * @return true if the account was created or already exists */ @@ -419,6 +435,7 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. + * * @param playerName the player name * @return true if the account was created or already exists */ @@ -455,6 +472,7 @@ public class VaultUtil { /** * Gets a list of all bank names. + * * @return list of bank names, or empty list if not available */ @SuppressWarnings("unchecked") @@ -471,6 +489,7 @@ public class VaultUtil { /** * Gets the balance of a bank. + * * @param bankName the bank name * @return the bank balance, or 0 if not available */ @@ -488,8 +507,9 @@ public class VaultUtil { /** * Checks if a bank has at least the specified amount. + * * @param bankName the bank name - * @param amount the amount to check + * @param amount the amount to check * @return true if the bank has at least the specified amount */ public static boolean bankHas(String bankName, double amount) { @@ -506,8 +526,9 @@ public class VaultUtil { /** * Withdraws money from a bank. + * * @param bankName the bank name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean bankWithdraw(String bankName, double amount) { @@ -524,8 +545,9 @@ public class VaultUtil { /** * Deposits money into a bank. + * * @param bankName the bank name - * @param amount the amount to deposit + * @param amount the amount to deposit * @return true if the transaction was successful */ public static boolean bankDeposit(String bankName, double amount) { @@ -542,8 +564,9 @@ public class VaultUtil { /** * Transfers money from one player to another. - * @param from the player to take money from - * @param to the player to give money to + * + * @param from the player to take money from + * @param to the player to give money to * @param amount the amount to transfer * @return true if the transfer was successful */ @@ -553,9 +576,10 @@ public class VaultUtil { /** * Transfers money from one player to another. + * * @param fromPlayerName the player name to take money from - * @param toPlayerName the player name to give money to - * @param amount the amount to transfer + * @param toPlayerName the player name to give money to + * @param amount the amount to transfer * @return true if the transfer was successful */ public static boolean transfer(String fromPlayerName, String toPlayerName, double amount) { diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 604ba26a0..c5b82d72a 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -4,7 +4,13 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.Mod.EventHandler; import cpw.mods.fml.common.SidedProxy; -import cpw.mods.fml.common.event.*; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLLoadCompleteEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.event.FMLServerAboutToStartEvent; +import cpw.mods.fml.common.event.FMLServerStartedEvent; +import cpw.mods.fml.common.event.FMLServerStartingEvent; +import cpw.mods.fml.common.event.FMLServerStoppedEvent; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.registry.EntityRegistry; import cpw.mods.fml.relauncher.Side; @@ -18,6 +24,7 @@ import kamkeel.npcs.command.profile.CommandProfile; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; import kamkeel.npcs.network.PacketHandler; @@ -68,7 +75,6 @@ import noppes.npcs.controllers.SpawnController; import noppes.npcs.controllers.TagController; import noppes.npcs.controllers.TransportController; import noppes.npcs.enchants.EnchantInterface; -import kamkeel.npcs.controllers.data.ability.AbilityController; import noppes.npcs.entity.EntityChairMount; import noppes.npcs.entity.EntityCustomNpc; import noppes.npcs.entity.EntityMagicProjectile; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index d9655b20a..3bed6075f 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -1,8 +1,8 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; @@ -35,32 +35,48 @@ public class DataAbilities { // CONFIGURATION (saved to NBT) // ═══════════════════════════════════════════════════════════════════ - /** List of abilities this NPC can use */ + /** + * List of abilities this NPC can use + */ private List abilities = new ArrayList<>(); - /** Whether the ability system is enabled for this NPC */ + /** + * Whether the ability system is enabled for this NPC + */ public boolean enabled = false; - /** Minimum ticks between ability selections (global cooldown) */ + /** + * Minimum ticks between ability selections (global cooldown) + */ public int globalCooldown = 20; // ═══════════════════════════════════════════════════════════════════ // RUNTIME STATE (not saved) // ═══════════════════════════════════════════════════════════════════ - /** Currently executing ability (null if none) */ + /** + * Currently executing ability (null if none) + */ private transient Ability currentAbility; - /** Per-ability cooldown timers (ability ID -> world time when cooldown ends) */ + /** + * Per-ability cooldown timers (ability ID -> world time when cooldown ends) + */ private transient Map cooldowns = new HashMap<>(); - /** Ticks until next ability can be selected (global cooldown timer) */ + /** + * Ticks until next ability can be selected (global cooldown timer) + */ private transient int globalCooldownTimer = 0; - /** Last target used for ability execution */ + /** + * Last target used for ability execution + */ private transient EntityLivingBase lastTarget; - /** Recent hit timestamps for hit count condition */ + /** + * Recent hit timestamps for hit count condition + */ private transient List recentHitTimes = new ArrayList<>(); // ═══════════════════════════════════════════════════════════════════ @@ -532,11 +548,11 @@ public class DataAbilities { * Fire an ability hit event. Called by abilities when they hit an entity. * Returns null if the event was cancelled, otherwise returns the (possibly modified) event. * - * @param ability The ability doing the hit - * @param target The original target of the ability - * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback + * @param ability The ability doing the hit + * @param target The original target of the ability + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback * @param knockbackUp The vertical knockback (deprecated, ignored) * @return The event (with possibly modified values), or null if cancelled */ @@ -636,7 +652,7 @@ public class DataAbilities { /** * Check if ability is controlling NPC movement (AI pathfinding should be blocked). * Called by AI tasks to determine if they should try to path. - * + *

* Rules: * - WINDUP phase: Block AI if lockMovement is true * - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true @@ -672,6 +688,7 @@ public class DataAbilities { /** * Check if NPC movement should be blocked due to ability execution. + * * @deprecated Use isAbilityControllingMovement() instead */ @Deprecated diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index f72bbdab7..5e086abce 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -38,7 +38,6 @@ import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.Dialog; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptBlockHandler; import noppes.npcs.controllers.data.IScriptHandler; @@ -265,7 +264,7 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.InitEvent event = new noppes.npcs.scripted.event.NpcEvent.InitEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INIT, event); npc.script.callScript(EnumScriptType.INIT, event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); npc.advanced.soulStoneInit = false; } @@ -276,7 +275,7 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.UpdateEvent event = new noppes.npcs.scripted.event.NpcEvent.UpdateEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TICK, event); npc.script.callScript(EnumScriptType.TICK, event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCDialog(EntityNPCInterface npc, EntityPlayer player, int dialogId, int optionId, Dialog dialog) { @@ -297,7 +296,7 @@ public class EventHooks { NpcEvent.DialogClosedEvent event = new NpcEvent.DialogClosedEvent(npc.wrappedNPC, player, dialogId, optionId, dialog); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DIALOG_CLOSE, event); npc.script.callScript(EnumScriptType.DIALOG_CLOSE, event, "player", event.getPlayer(), "dialog", event.getDialogId(), "option", event.getOptionId(), "dialogObj", event.getDialog()); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCInteract(EntityNPCInterface npc, EntityPlayer player) { @@ -307,7 +306,7 @@ public class EventHooks { NpcEvent.InteractEvent event = new NpcEvent.InteractEvent(npc.wrappedNPC, player); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INTERACT, event); boolean result = npc.script.callScript(EnumScriptType.INTERACT, event, "player", player); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); return result; } @@ -318,7 +317,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_MELEE, event); npc.script.callScript(EnumScriptType.ATTACK_MELEE, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCMeleeSwing(EntityNPCInterface npc, NpcEvent.SwingEvent event) { @@ -327,7 +326,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_SWING, event); npc.script.callScript(EnumScriptType.ATTACK_SWING, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCRangedAttack(EntityNPCInterface npc, NpcEvent.RangedLaunchedEvent event) { @@ -337,7 +336,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.RANGED_LAUNCHED, event); npc.script.callScript(EnumScriptType.RANGED_LAUNCHED, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCKilledEntity(EntityNPCInterface npc, EntityLivingBase entity) { @@ -347,7 +346,7 @@ public class EventHooks { NpcEvent.KilledEntityEvent event = new NpcEvent.KilledEntityEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLS, event); npc.script.callScript(EnumScriptType.KILLS, event, "target", entity); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCTarget(EntityNPCInterface npc, NpcEvent.TargetEvent event) { @@ -356,7 +355,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET, event); npc.script.callScript(EnumScriptType.TARGET, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCTargetLost(EntityNPCInterface npc, EntityLivingBase prevtarget, EntityLivingBase newTarget) { @@ -366,7 +365,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET_LOST, event); npc.script.callScript(EnumScriptType.TARGET_LOST, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCCollide(EntityNPCInterface npc, Entity entity) { @@ -376,7 +375,7 @@ public class EventHooks { NpcEvent.CollideEvent event = new NpcEvent.CollideEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.COLLIDE, event); npc.script.callScript(EnumScriptType.COLLIDE, event, "entity", entity); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCDamaged(EntityNPCInterface npc, NpcEvent.DamagedEvent event) { @@ -385,7 +384,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DAMAGED, event); npc.script.callScript(EnumScriptType.DAMAGED, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCKilled(EntityNPCInterface npc, NpcEvent.DiedEvent event) { @@ -394,7 +393,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLED, event); npc.script.callScript(EnumScriptType.KILLED, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCTimer(EntityNPCInterface npc, int id) { @@ -404,7 +403,7 @@ public class EventHooks { NpcEvent.TimerEvent event = new NpcEvent.TimerEvent(npc.wrappedNPC, id); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TIMER, event); npc.script.callScript(EnumScriptType.TIMER, event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static void onProjectileTick(EntityProjectile projectile) { @@ -422,7 +421,7 @@ public class EventHooks { EntityNPCInterface npc = (EntityNPCInterface) thrower; npc.script.callScript(EnumScriptType.PROJECTILE_TICK, event); - } + } } NpcAPI.EVENT_BUS.post(event); @@ -442,7 +441,7 @@ public class EventHooks { EntityNPCInterface npc = (EntityNPCInterface) thrower; npc.script.callScript(EnumScriptType.PROJECTILE_IMPACT, event); - } + } } NpcAPI.EVENT_BUS.post(event); diff --git a/src/main/java/noppes/npcs/NBTTags.java b/src/main/java/noppes/npcs/NBTTags.java index fff8d1c5d..6eec4c14d 100644 --- a/src/main/java/noppes/npcs/NBTTags.java +++ b/src/main/java/noppes/npcs/NBTTags.java @@ -6,7 +6,6 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagDouble; import net.minecraft.nbt.NBTTagInt; import net.minecraft.nbt.NBTTagList; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; diff --git a/src/main/java/noppes/npcs/NoppesStringUtils.java b/src/main/java/noppes/npcs/NoppesStringUtils.java index cd7031ef0..ddc11d5c6 100644 --- a/src/main/java/noppes/npcs/NoppesStringUtils.java +++ b/src/main/java/noppes/npcs/NoppesStringUtils.java @@ -4,7 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.StatCollector; import noppes.npcs.entity.EntityNPCInterface; -import java.awt.*; +import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.DataFlavor; diff --git a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java index 4294a6d9e..abc5a80ee 100644 --- a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java @@ -36,7 +36,7 @@ public class ScriptForgeEventHandler { @SubscribeEvent public void forgeEntity(Event event) { if (CustomNpcs.getServer() != null && ScriptController.Instance.forgeScripts.isEnabled()) { - if(FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT){ + if (FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT) { return; } diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index e9531296a..a37e6ba08 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -62,7 +62,6 @@ import noppes.npcs.items.ItemNpcTool; import noppes.npcs.quests.QuestItem; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.item.ScriptCustomizableItem; -import noppes.npcs.scripted.item.ScriptItemStack; import static noppes.npcs.config.ConfigMain.TrackedQuestUpdateFrequency; @@ -549,7 +548,7 @@ public class ScriptPlayerEventHandler { if (event.source.getEntity() instanceof EntityPlayerMP) { EntityPlayerMP playerMP = (EntityPlayerMP) event.source.getEntity(); - if(playerMP.getHeldItem() != null) { + if (playerMP.getHeldItem() != null) { IItemStack itemStack = NpcAPI.Instance().getIItemStack(playerMP.getHeldItem()); if (itemStack instanceof ScriptCustomizableItem) { ScriptCustomizableItem sci = (ScriptCustomizableItem) itemStack; diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 47a1148df..b6671b72a 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -51,7 +51,6 @@ import noppes.npcs.controllers.ServerCloneController; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Line; -import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.MarkData; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -60,7 +59,6 @@ import noppes.npcs.controllers.data.Quest; import noppes.npcs.controllers.data.QuestData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.items.ItemExcalibur; -import noppes.npcs.items.ItemLinked; import noppes.npcs.items.ItemShield; import noppes.npcs.items.ItemSoulstoneEmpty; import noppes.npcs.quests.QuestKill; diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index eb5018871..93acda2ea 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,8 +10,8 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.world.WorldServer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; +import noppes.npcs.entity.EntityNPCInterface; public class ServerTickHandler { diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index b9558dd93..64dbb767b 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -28,8 +28,8 @@ import noppes.npcs.client.renderer.blocks.BlockScriptedRenderer; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.IScriptBlockHandler; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.data.DataTimers; import noppes.npcs.scripted.BlockScriptedWrapper; @@ -39,8 +39,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; public class TileScripted extends TileEntity implements IScriptBlockHandler, IScriptHandlerPacket { public List scripts = new ArrayList<>(); @@ -449,7 +447,7 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler, ISc public List getScripts() { return scripts; } - + @SideOnly(Side.CLIENT) @Override diff --git a/src/main/java/noppes/npcs/client/ClientCacheHandler.java b/src/main/java/noppes/npcs/client/ClientCacheHandler.java index 2c6a368c2..30b14be83 100644 --- a/src/main/java/noppes/npcs/client/ClientCacheHandler.java +++ b/src/main/java/noppes/npcs/client/ClientCacheHandler.java @@ -1,5 +1,6 @@ package noppes.npcs.client; +import kamkeel.npcs.network.enums.EnumSyncType; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.controllers.MusicController; @@ -15,7 +16,6 @@ import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.SkinOverlay; -import kamkeel.npcs.network.enums.EnumSyncType; import noppes.npcs.util.CacheHashMap; import java.util.EnumMap; diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 249836b80..656efc97b 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -6,6 +6,8 @@ import cpw.mods.fml.client.registry.RenderingRegistry; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ObfuscationReflectionHelper; import kamkeel.npcs.addon.client.DBCClient; +import kamkeel.npcs.client.renderer.TelegraphRenderer; +import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.model.ModelBiped; @@ -138,9 +140,6 @@ import noppes.npcs.client.renderer.RenderNpcCrystal; import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; -import kamkeel.npcs.client.renderer.TelegraphRenderer; -import noppes.npcs.scripted.item.ScriptCustomItem; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; import noppes.npcs.client.renderer.blocks.BlockBeamRenderer; @@ -214,6 +213,7 @@ import noppes.npcs.entity.data.ModelPartData; import noppes.npcs.items.ItemLinked; import noppes.npcs.items.ItemNpcTool; import noppes.npcs.items.ItemScripted; +import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.input.Keyboard; import tconstruct.client.tabs.InventoryTabCustomNpc; import tconstruct.client.tabs.InventoryTabVanilla; @@ -635,7 +635,7 @@ public class ClientProxy extends CommonProxy { return GuiScriptInterface.create(null, (TileScripted) tile); } - + @Override public void spawnParticle(EntityLivingBase player, String string, Object... ob) { diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 7c5ec98f2..78a473f41 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,11 +5,12 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.InputDevicePacket; -import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; +import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import net.minecraft.client.Minecraft; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -18,7 +19,6 @@ import net.minecraft.world.World; import noppes.npcs.CustomNpcs; import noppes.npcs.client.controllers.MusicController; import noppes.npcs.client.controllers.ScriptSoundController; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import noppes.npcs.client.gui.hud.ClientHudManager; import noppes.npcs.client.gui.hud.CompassHudComponent; import noppes.npcs.client.gui.hud.EnumHudComponent; diff --git a/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java b/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java index 132805af1..2f2bc3115 100644 --- a/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java +++ b/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java @@ -11,7 +11,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/controllers/MusicController.java b/src/main/java/noppes/npcs/client/controllers/MusicController.java index 98c30f2ac..172152f51 100644 --- a/src/main/java/noppes/npcs/client/controllers/MusicController.java +++ b/src/main/java/noppes/npcs/client/controllers/MusicController.java @@ -199,6 +199,7 @@ public class MusicController { /** * Stops a specific sound by its resource key. * This is used to stop dialog sounds without affecting Bard music. + * * @param soundKey The sound resource key to stop */ public void stopSound(String soundKey) { diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 137ccf5a1..8d960e142 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -5,34 +5,41 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiConfirmOpenLink; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; import net.minecraft.server.MinecraftServer; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextArea; +import noppes.npcs.client.gui.util.GuiScriptTextArea; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.DataScript; import noppes.npcs.entity.EntityNPCInterface; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; /** * NPC Script GUI - extends GuiScriptInterface but with unique behavior: * - Uses scroll-based tab selection instead of top buttons - * - Shows settings/script toggle + * - Shows settings/script toggle * - Uses DataScript with getNPCScript(index) access pattern * - Has console filtering by hook type */ public class GuiScript extends GuiScriptInterface { public boolean showScript = false; public DataScript script; - + private static int activeConsole = 0; - + public GuiScript(EntityNPCInterface npc) { super(); this.npc = npc; @@ -40,7 +47,7 @@ public class GuiScript extends GuiScriptInterface { this.handler = script; this.useScrollTabs = true; this.useSettingsToggle = true; - + // Initialize hook list for NPC scripts hookList.add("script.init"); hookList.add("script.update"); @@ -57,20 +64,20 @@ public class GuiScript extends GuiScriptInterface { hookList.add("script.targetLost"); hookList.add("script.projectileTick"); hookList.add("script.projectileImpact"); - + NPCScriptPacket.Get(); } - + @Override protected ScriptContext getScriptContext() { return ScriptContext.NPC; } - + @Override protected int getActiveScriptIndex() { return this.activeTab; } - + @Override protected ScriptContainer getCurrentContainer() { return script.getNPCScript(activeTab); @@ -99,13 +106,13 @@ public class GuiScript extends GuiScriptInterface { initSettingsView(); } } - + private void initScriptView() { ScriptContainer container = getCurrentContainer(); - + // ==================== CALCULATE VIEWPORT BOUNDS ==================== int editorX, editorY, editorWidth, editorHeight; - + if (isFullscreen) { // Fullscreen: viewport fills screen with configured padding FullscreenConfig.paddingTop = 30; @@ -123,11 +130,11 @@ public class GuiScript extends GuiScriptInterface { editorWidth = 239; editorHeight = 208; } - + // ==================== HOOKS SCROLL (hidden in fullscreen) ==================== if (!isFullscreen) { addLabel(new GuiNpcLabel(0, "script.hooks", guiLeft + 4, guiTop + 5)); - + GuiCustomScroll hooks = new GuiCustomScroll(this, 1); hooks.setSize(68, 198); hooks.guiLeft = guiLeft + 4; @@ -142,28 +149,28 @@ public class GuiScript extends GuiScriptInterface { GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + container == null ? "" : container.script); activeArea.setListener(this); this.closeOnEsc(activeArea::closeOnEsc); textAreas.put(idx, activeArea); } else { - activeArea.init(editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + activeArea.init(editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); } activeArea.setLanguage(script.getLanguage()); activeArea.setScriptContext(getScriptContext()); - + // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { if (e.isPress()) { toggleFullscreen(); } }); - + activeArea.enableCodeHighlighting(); addTextField(activeArea); - + // Initialize fullscreen button int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); @@ -184,35 +191,35 @@ public class GuiScript extends GuiScriptInterface { addScroll(scroll); } } - - + + private void initSettingsView() { addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); if (getTopButton(14) != null) getTopButton(14).active = true; - + GuiNpcTextArea consoleArea = new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText()); consoleArea.canEdit = false; addTextField(consoleArea); - + addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); List consoleOptions = new ArrayList<>(); consoleOptions.add("All"); consoleOptions.addAll(hookList); - addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, - consoleOptions.toArray(new String[0]), activeConsole)); + addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, + consoleOptions.toArray(new String[0]), activeConsole)); addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); List languageOptions = getLanguageOptions(); - addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, - languageOptions.toArray(new String[0]), getLanguageIndex(languageOptions))); + addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, + languageOptions.toArray(new String[0]), getLanguageIndex(languageOptions))); getButton(103).enabled = languageOptions.size() > 0; addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); - addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, - new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); + addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, + new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); if (MinecraftServer.getServer() != null) addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); @@ -280,12 +287,12 @@ public class GuiScript extends GuiScriptInterface { NoppesStringUtils.setClipboardContents(getTextField(2).getText()); } if (guibutton.id == 101) { - displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), - StatCollector.translateToLocal("gui.sure"), 101)); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), + StatCollector.translateToLocal("gui.sure"), 101)); } if (guibutton.id == 102) { - displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), - StatCollector.translateToLocal("gui.sure"), 102)); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), + StatCollector.translateToLocal("gui.sure"), 102)); } if (guibutton.id == 103) { script.scriptLanguage = ((GuiNpcButton) guibutton).displayString; diff --git a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java index 547f75f83..a13f2a056 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java @@ -20,7 +20,7 @@ public class GuiScriptList extends SubGuiInterface { public GuiScriptList(List scripts, ScriptContainer container) { this(scripts, (IScriptUnit) container); } - + public GuiScriptList(List scripts, IScriptUnit scriptUnit) { this.scriptUnit = scriptUnit; setBackground("menubg.png"); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java index db1b516ea..cfde598de 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java @@ -8,7 +8,6 @@ import net.minecraft.util.ResourceLocation; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.gui.global.GuiNPCManageEffects; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -18,6 +17,7 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.client.renderer.ImageData; import noppes.npcs.controllers.data.CustomEffect; +import noppes.npcs.controllers.data.EffectScript; import org.lwjgl.opengl.GL11; import java.util.ArrayList; diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java index 9025023b1..c3d85c8ad 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java @@ -36,7 +36,7 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Fields - //////////////////////////////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////////////////////////////// // Current tab (10=Quest, 11=Dialog, 12=Transport, 13=Bank, 14=Faction, 15=Magic) private int currentTab = 10; @@ -99,7 +99,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Constructor - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// public SubGuiPlayerData(String playerName) { this.playerName = playerName; xSize = 420; @@ -112,7 +113,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Layout Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// private int getPaddedLeft() { return guiLeft + leftPadding; } @@ -135,7 +137,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Component Setup Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// private void setupTopButtons() { // Show General Display Information GuiMenuTopButton playerTab = new GuiMenuTopButton(-10, guiLeft + 4, guiTop - 10, playerName); @@ -192,7 +195,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // initGui() & Tab Initialization - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void initGui() { super.initGui(); @@ -489,7 +493,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Drawing Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { super.drawScreen(mouseX, mouseY, partialTicks); @@ -525,7 +530,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Mouse & Resizing Handlers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { int hitMargin = 3; @@ -672,7 +678,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Key & Search Field Handling - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void keyTyped(char c, int i) { super.keyTyped(c, i); @@ -894,7 +901,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Action Handling - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void actionPerformed(GuiButton button) { if (button.id == -5) { @@ -1043,7 +1051,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Data Setting Methods (IPlayerDataInfo) - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void setQuestData(Map questCategories, Map questActive, Map questFinished) { this.questCatData = new HashMap<>(questCategories); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 584bd8238..0f41347d4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -10,7 +10,18 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.resources.I18n; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.constants.EnumScrollData; import noppes.npcs.entity.EntityNPCInterface; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index c88c18095..1146ea4ae 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -10,7 +10,14 @@ import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.select.GuiAnimationSelection; import noppes.npcs.client.gui.select.GuiSoundSelection; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +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.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.List; @@ -18,13 +25,13 @@ import java.util.List; /** * Base SubGui for editing ability configuration with tabbed interface. * Uses GuiMenuTopButton for navigation tabs. - * + *

* Tabs: * - General: Name, Weight, Lock Movement, Interruptible, Timing * - Type: Type-specific settings per Ability (override in subclasses) * - Target: Min/Max Range, Targeting Mode, Conditions (up to 3) * - Effects: Sounds, Animations, Telegraph settings, Colors - * + *

* Subclasses should override initTypeTab() and handleTypeButton()/handleTypeTextField() * to provide type-specific settings UI. */ @@ -376,13 +383,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi String typeId = cond.getTypeId(); switch (typeId) { case "hp_above": - return StatCollector.translateToLocal("condition.hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hp_below": - return StatCollector.translateToLocal("condition.hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "target_hp_above": - return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "target_hp_below": - return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hit_count": Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond; return StatCollector.translateToLocal("condition.hit_count") + ": " + hitCount.getRequiredHits() + "/" + hitCount.getWithinTicks() + "t"; @@ -566,8 +573,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // General tab buttons else if (id == 14) { interruptible = ((GuiNpcButton) guibutton).getValue() == 1; - } - else if (id == 16) { + } else if (id == 16) { lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; } @@ -619,12 +625,10 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 20) { showTelegraph = ((GuiNpcButton) guibutton).getValue() == 1; initGui(); - } - else if (id == 22) { + } else if (id == 22) { editingColorId = 22; setSubGui(new SubGuiColorSelector(windUpColor)); - } - else if (id == 24) { + } else if (id == 24) { editingColorId = 24; setSubGui(new SubGuiColorSelector(activeColor)); } @@ -633,8 +637,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 30) { editingSoundId = 30; setSubGui(new GuiSoundSelection(windUpSound)); - } - else if (id == 31) { + } else if (id == 31) { editingSoundId = 31; setSubGui(new GuiSoundSelection(activeSound)); } @@ -642,8 +645,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 35) { windUpSound = ""; initGui(); - } - else if (id == 36) { + } else if (id == 36) { activeSound = ""; initGui(); } @@ -651,8 +653,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 32) { editingAnimationId = 32; setSubGui(new GuiAnimationSelection(windUpAnimationId)); - } - else if (id == 33) { + } else if (id == 33) { editingAnimationId = 33; setSubGui(new GuiAnimationSelection(activeAnimationId)); } @@ -661,8 +662,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi windUpAnimationId = -1; windUpAnimationName = null; initGui(); - } - else if (id == 38) { + } else if (id == 38) { activeAnimationId = -1; activeAnimationName = null; initGui(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index 069c9f1b4..c48683070 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -3,7 +3,11 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java index f0d4f2734..3d2874dbc 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -2,7 +2,11 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Condition; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; /** * SubGui for editing a single ability condition. @@ -106,7 +110,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi } else { // HP threshold field (as percentage) addLabel(new GuiNpcLabel(2, "condition.percent", labelX, y + 5)); - int percent = (int)(threshold * 100); + int percent = (int) (threshold * 100); GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(percent)); thresholdField.setIntegersOnly(); thresholdField.setMinMaxDefault(1, 99, 50); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java index fb3e6b074..ce56f809e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Beam ability type-specific settings. @@ -72,21 +72,39 @@ public class SubGuiAbilityBeam extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 106: beam.setPiercing(value == 1); break; - case 107: beam.setLockOnTarget(value == 1); break; - case 108: beam.setSweepBackAndForth(value == 1); break; + case 106: + beam.setPiercing(value == 1); + break; + case 107: + beam.setLockOnTarget(value == 1); + break; + case 108: + beam.setSweepBackAndForth(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: beam.setDamage(parseFloat(field, beam.getDamage())); break; - case 101: beam.setDamageInterval(field.getInteger()); break; - case 102: beam.setBeamLength(parseFloat(field, beam.getBeamLength())); break; - case 103: beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); break; - case 104: beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); break; - case 105: beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); break; + case 100: + beam.setDamage(parseFloat(field, beam.getDamage())); + break; + case 101: + beam.setDamageInterval(field.getInteger()); + break; + case 102: + beam.setBeamLength(parseFloat(field, beam.getBeamLength())); + break; + case 103: + beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); + break; + case 104: + beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); + break; + case 105: + beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java index d3ff5fd81..4d7486c5e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Charge ability type-specific settings. @@ -50,10 +50,18 @@ public class SubGuiAbilityCharge extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: charge.setDamage(parseFloat(field, charge.getDamage())); break; - case 101: charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); break; - case 102: charge.setKnockback(parseFloat(field, charge.getKnockback())); break; - case 104: charge.setHitWidth(parseFloat(field, charge.getHitWidth())); break; + case 100: + charge.setDamage(parseFloat(field, charge.getDamage())); + break; + case 101: + charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); + break; + case 102: + charge.setKnockback(parseFloat(field, charge.getKnockback())); + break; + case 104: + charge.setHitWidth(parseFloat(field, charge.getHitWidth())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index d917282f2..514391f8a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Cutter ability type-specific settings. @@ -82,23 +82,45 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 106: cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); break; - case 107: cutter.setPiercing(value == 1); break; + case 106: + cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); + break; + case 107: + cutter.setPiercing(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: cutter.setDamage(parseFloat(field, cutter.getDamage())); break; - case 101: cutter.setRange(parseFloat(field, cutter.getRange())); break; - case 102: cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); break; - case 103: cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); break; - case 104: cutter.setKnockback(parseFloat(field, cutter.getKnockback())); break; - case 105: cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); break; - case 108: cutter.setStunDuration(field.getInteger()); break; - case 109: cutter.setPoisonDurationSeconds(field.getInteger()); break; - case 110: cutter.setPoisonLevel(field.getInteger()); break; + case 100: + cutter.setDamage(parseFloat(field, cutter.getDamage())); + break; + case 101: + cutter.setRange(parseFloat(field, cutter.getRange())); + break; + case 102: + cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); + break; + case 103: + cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); + break; + case 104: + cutter.setKnockback(parseFloat(field, cutter.getKnockback())); + break; + case 105: + cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); + break; + case 108: + cutter.setStunDuration(field.getInteger()); + break; + case 109: + cutter.setPoisonDurationSeconds(field.getInteger()); + break; + case 110: + cutter.setPoisonLevel(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java index 7ae0ea1f5..26bfd6610 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityDash; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Dash ability type-specific settings. @@ -52,8 +52,12 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 101: dash.setDashDistance(parseFloat(field, dash.getDashDistance())); break; - case 102: dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); break; + case 101: + dash.setDashDistance(parseFloat(field, dash.getDashDistance())); + break; + case 102: + dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java index 4858445fb..c5240d862 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Guard ability type-specific settings. @@ -86,11 +86,21 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); break; - case 103: guard.setCounterValue(parseFloat(field, guard.getCounterValue())); break; - case 104: guard.setCounterChance(parseFloat(field, guard.getCounterChance())); break; - case 105: guard.setCounterSound(field.getText()); break; - case 106: guard.setCounterAnimationId(field.getInteger()); break; + case 100: + guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); + break; + case 103: + guard.setCounterValue(parseFloat(field, guard.getCounterValue())); + break; + case 104: + guard.setCounterChance(parseFloat(field, guard.getCounterChance())); + break; + case 105: + guard.setCounterSound(field.getText()); + break; + case 106: + guard.setCounterAnimationId(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index def67e71d..e44eb6984 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Hazard ability type-specific settings. @@ -77,22 +77,42 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 100: hazard.setShape(AbilityHazard.HazardShape.values()[value]); break; - case 101: hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); break; - case 109: hazard.setAffectsCaster(value == 1); break; + case 100: + hazard.setShape(AbilityHazard.HazardShape.values()[value]); + break; + case 101: + hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); + break; + case 109: + hazard.setAffectsCaster(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 102: hazard.setRadius(parseFloat(field, hazard.getRadius())); break; - case 103: hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); break; - case 104: hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); break; - case 105: hazard.setDamageInterval(field.getInteger()); break; - case 106: hazard.setSlownessLevel(field.getInteger()); break; - case 107: hazard.setDebuffDuration(field.getInteger()); break; - case 108: hazard.setPoisonLevel(field.getInteger()); break; + case 102: + hazard.setRadius(parseFloat(field, hazard.getRadius())); + break; + case 103: + hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); + break; + case 104: + hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); + break; + case 105: + hazard.setDamageInterval(field.getInteger()); + break; + case 106: + hazard.setSlownessLevel(field.getInteger()); + break; + case 107: + hazard.setDebuffDuration(field.getInteger()); + break; + case 108: + hazard.setPoisonLevel(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java index 30ccb739e..8b679fb25 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Heal ability type-specific settings. @@ -57,18 +57,30 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 103: heal.setInstantHeal(value == 1); break; - case 104: heal.setHealSelf(value == 1); break; - case 105: heal.setHealAllies(value == 1); break; + case 103: + heal.setInstantHeal(value == 1); + break; + case 104: + heal.setHealSelf(value == 1); + break; + case 105: + heal.setHealAllies(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: heal.setHealAmount(parseFloat(field, heal.getHealAmount())); break; - case 101: heal.setHealPercent(parseFloat(field, heal.getHealPercent())); break; - case 102: heal.setHealRadius(parseFloat(field, heal.getHealRadius())); break; + case 100: + heal.setHealAmount(parseFloat(field, heal.getHealAmount())); + break; + case 101: + heal.setHealPercent(parseFloat(field, heal.getHealPercent())); + break; + case 102: + heal.setHealRadius(parseFloat(field, heal.getHealRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java index 5c0d99c48..ea21127dc 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Heavy Hit ability type-specific settings. @@ -52,11 +52,21 @@ public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); break; - case 101: heavyHit.setPotionDurationSeconds(field.getInteger()); break; - case 102: heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); break; - case 103: heavyHit.setSlownessLevel(field.getInteger()); break; - case 104: heavyHit.setWeaknessLevel(field.getInteger()); break; + case 100: + heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); + break; + case 101: + heavyHit.setPotionDurationSeconds(field.getInteger()); + break; + case 102: + heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); + break; + case 103: + heavyHit.setSlownessLevel(field.getInteger()); + break; + case 104: + heavyHit.setWeaknessLevel(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index fd75700c0..f94e8d0cf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Orb ability type-specific settings. @@ -75,22 +75,42 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 104: orb.setHoming(value == 1); break; - case 106: orb.setExplosive(value == 1); break; + case 104: + orb.setHoming(value == 1); + break; + case 106: + orb.setExplosive(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: orb.setDamage(parseFloat(field, orb.getDamage())); break; - case 101: orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); break; - case 102: orb.setOrbSize(parseFloat(field, orb.getOrbSize())); break; - case 103: orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); break; - case 105: orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); break; - case 107: orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); break; - case 108: orb.setKnockback(parseFloat(field, orb.getKnockback())); break; - case 109: orb.setMaxLifetime(field.getInteger()); break; + case 100: + orb.setDamage(parseFloat(field, orb.getDamage())); + break; + case 101: + orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); + break; + case 102: + orb.setOrbSize(parseFloat(field, orb.getOrbSize())); + break; + case 103: + orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); + break; + case 105: + orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); + break; + case 107: + orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); + break; + case 108: + orb.setKnockback(parseFloat(field, orb.getKnockback())); + break; + case 109: + orb.setMaxLifetime(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java index 88d7bbf00..c9be00e38 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Projectile ability type-specific settings. @@ -79,19 +79,33 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { String[] projTypes = {"fireball", "arrow", "magic"}; projectile.setProjectileType(projTypes[value]); break; - case 104: projectile.setExplosive(value == 1); break; - case 106: projectile.setHoming(value == 1); break; + case 104: + projectile.setExplosive(value == 1); + break; + case 106: + projectile.setHoming(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: projectile.setDamage(parseFloat(field, projectile.getDamage())); break; - case 101: projectile.setSpeed(parseFloat(field, projectile.getSpeed())); break; - case 102: projectile.setKnockback(parseFloat(field, projectile.getKnockback())); break; - case 105: projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); break; - case 107: projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); break; + case 100: + projectile.setDamage(parseFloat(field, projectile.getDamage())); + break; + case 101: + projectile.setSpeed(parseFloat(field, projectile.getSpeed())); + break; + case 102: + projectile.setKnockback(parseFloat(field, projectile.getKnockback())); + break; + case 105: + projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); + break; + case 107: + projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java index 38ab22158..a03a85736 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Shockwave ability type-specific settings. @@ -55,11 +55,21 @@ public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: shockwave.setDamage(parseFloat(field, shockwave.getDamage())); break; - case 101: shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); break; - case 102: shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); break; - case 103: shockwave.setStunDuration(field.getInteger()); break; - case 104: shockwave.setMaxTargets(field.getInteger()); break; + case 100: + shockwave.setDamage(parseFloat(field, shockwave.getDamage())); + break; + case 101: + shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); + break; + case 102: + shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); + break; + case 103: + shockwave.setStunDuration(field.getInteger()); + break; + case 104: + shockwave.setMaxTargets(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java index 7f8c7194e..989202037 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Slam ability type-specific settings. @@ -52,11 +52,21 @@ public class SubGuiAbilitySlam extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: slam.setDamage(parseFloat(field, slam.getDamage())); break; - case 101: slam.setRadius(parseFloat(field, slam.getRadius())); break; - case 102: slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); break; - case 103: slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); break; - case 104: slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); break; + case 100: + slam.setDamage(parseFloat(field, slam.getDamage())); + break; + case 101: + slam.setRadius(parseFloat(field, slam.getRadius())); + break; + case 102: + slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); + break; + case 103: + slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); + break; + case 104: + slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java index 5570d944f..6173f9ded 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Teleport ability type-specific settings. @@ -95,21 +95,39 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { teleport.setMode(AbilityTeleport.TeleportMode.values()[value]); initGui(); break; - case 105: teleport.setRequireLineOfSight(value == 1); break; - case 108: teleport.setDamageAtStart(value == 1); break; - case 109: teleport.setDamageAtEnd(value == 1); break; + case 105: + teleport.setRequireLineOfSight(value == 1); + break; + case 108: + teleport.setDamageAtStart(value == 1); + break; + case 109: + teleport.setDamageAtEnd(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 101: teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); break; - case 102: teleport.setBlinkCount(field.getInteger()); break; - case 103: teleport.setBlinkDelayTicks(field.getInteger()); break; - case 104: teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); break; - case 106: teleport.setDamage(parseFloat(field, teleport.getDamage())); break; - case 107: teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); break; + case 101: + teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); + break; + case 102: + teleport.setBlinkCount(field.getInteger()); + break; + case 103: + teleport.setBlinkDelayTicks(field.getInteger()); + break; + case 104: + teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); + break; + case 106: + teleport.setDamage(parseFloat(field, teleport.getDamage())); + break; + case 107: + teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index 88529fe68..781d0665a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Trap ability type-specific settings. @@ -79,14 +79,30 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 101: trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); break; - case 102: trap.setDamage(parseFloat(field, trap.getDamage())); break; - case 103: trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); break; - case 104: trap.setArmTime(field.getInteger()); break; - case 105: trap.setMaxTriggers(field.getInteger()); break; - case 106: trap.setRootDuration(field.getInteger()); break; - case 107: trap.setStunDuration(field.getInteger()); break; - case 108: trap.setKnockback(parseFloat(field, trap.getKnockback())); break; + case 101: + trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); + break; + case 102: + trap.setDamage(parseFloat(field, trap.getDamage())); + break; + case 103: + trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); + break; + case 104: + trap.setArmTime(field.getInteger()); + break; + case 105: + trap.setMaxTriggers(field.getInteger()); + break; + case 106: + trap.setRootDuration(field.getInteger()); + break; + case 107: + trap.setStunDuration(field.getInteger()); + break; + case 108: + trap.setKnockback(parseFloat(field, trap.getKnockback())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java index a1abbd661..003cef5c6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.IAbilityConfigCallback; /** * GUI for configuring Vortex ability type-specific settings. @@ -75,22 +75,42 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 104: vortex.setAoe(value == 1); break; - case 108: vortex.setDamageOnPull(value == 1); break; + case 104: + vortex.setAoe(value == 1); + break; + case 108: + vortex.setDamageOnPull(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); break; - case 101: vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); break; - case 102: vortex.setDamage(parseFloat(field, vortex.getDamage())); break; - case 103: vortex.setKnockback(parseFloat(field, vortex.getKnockback())); break; - case 105: vortex.setMaxTargets(field.getInteger()); break; - case 106: vortex.setStunDuration(field.getInteger()); break; - case 107: vortex.setRootDuration(field.getInteger()); break; - case 109: vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); break; + case 100: + vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); + break; + case 101: + vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); + break; + case 102: + vortex.setDamage(parseFloat(field, vortex.getDamage())); + break; + case 103: + vortex.setKnockback(parseFloat(field, vortex.getKnockback())); + break; + case 105: + vortex.setMaxTargets(field.getInteger()); + break; + case 106: + vortex.setStunDuration(field.getInteger()); + break; + case 107: + vortex.setRootDuration(field.getInteger()); + break; + case 109: + vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index e5ae8c436..22d0f3103 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -3,13 +3,21 @@ package noppes.npcs.client.gui.global; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.SubGuiAbilityDeleteConfirm; -import noppes.npcs.client.gui.advanced.SubGuiAbilityLoad; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.entity.EntityNPCInterface; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; /** * Global GUI for managing saved ability presets. diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java index f246cb417..40df2d463 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java @@ -13,7 +13,6 @@ import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.SubGuiNpcAvailability; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -32,6 +31,7 @@ import noppes.npcs.constants.EnumScrollData; import noppes.npcs.containers.ContainerManageRecipes; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.entity.EntityNPCInterface; import org.lwjgl.opengl.GL11; diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index fd57c0171..b71f6642a 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -13,7 +13,6 @@ import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.global.GuiNPCManageLinked; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.controllers.data.LinkedItemScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButton; @@ -26,6 +25,7 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.client.renderer.ImageData; import noppes.npcs.controllers.data.LinkedItem; +import noppes.npcs.controllers.data.LinkedItemScript; import org.lwjgl.opengl.GL11; public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListener, GuiYesNoCallback, ISubGuiListener { @@ -120,8 +120,8 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe GuiNpcTextField digSpeedField = new GuiNpcTextField(12, this, x + 110, y, 50, 20, "" + linkedItem.digSpeed); digSpeedField.setIntegersOnly().setMinMaxDefault(0, 20, 1); addTextField(digSpeedField); - GuiNpcTextField attackSpeedField = new GuiNpcTextField(14, this, x +165, y, 50, 20, "" +linkedItem.attackSpeed); - attackSpeedField.setIntegersOnly().setMinMaxDefault(0,999999999,20); + GuiNpcTextField attackSpeedField = new GuiNpcTextField(14, this, x + 165, y, 50, 20, "" + linkedItem.attackSpeed); + attackSpeedField.setIntegersOnly().setMinMaxDefault(0, 999999999, 20); addTextField(attackSpeedField); y += spacing; diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 885c61d94..26942e57f 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -9,11 +9,11 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.advanced.GuiNPCAbilities; import noppes.npcs.client.gui.advanced.GuiNPCAdvancedLinkedNpc; import noppes.npcs.client.gui.advanced.GuiNPCDialogNpcOptions; import noppes.npcs.client.gui.advanced.GuiNPCFactionSetup; import noppes.npcs.client.gui.advanced.GuiNPCLinesMenu; -import noppes.npcs.client.gui.advanced.GuiNPCAbilities; import noppes.npcs.client.gui.advanced.GuiNPCMagic; import noppes.npcs.client.gui.advanced.GuiNPCMarks; import noppes.npcs.client.gui.advanced.GuiNPCNightSetup; @@ -25,8 +25,8 @@ import noppes.npcs.client.gui.roles.GuiNpcConversation; import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; -import noppes.npcs.client.gui.roles.GuiNpcSpawner; import noppes.npcs.client.gui.roles.GuiNpcMount; +import noppes.npcs.client.gui.roles.GuiNpcSpawner; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; import noppes.npcs.client.gui.util.GuiNpcButton; diff --git a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java index cd47fed55..fe6daff23 100644 --- a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java @@ -105,7 +105,7 @@ public class EventGuiScriptList extends SubGuiInterface { public List getScripts() { return this.container.getExternalScripts(); } - + public void save() { } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index acfac310a..9fb601162 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -1,17 +1,38 @@ package noppes.npcs.client.gui.script; -import noppes.npcs.janino.JaninoScript; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.*; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiConfirmOpenLink; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +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.GuiNpcTextArea; +import noppes.npcs.client.gui.util.GuiScriptTextArea; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextChangeListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.janino.JaninoScript; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback, ITextChangeListener, ICustomScrollListener, ISubGuiListener { @@ -21,7 +42,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback public List allExternalScripts = new ArrayList<>(); private final int scriptLimit = 1; public String previousHookClicked = ""; - + // GUI display list List hooklist = new ArrayList<>(); @@ -83,6 +104,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback } } } + public void initGui() { this.ySize = (int) ((double) this.xSize * 0.56); if ((double) this.ySize > (double) this.height * 0.95) { @@ -95,7 +117,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback this.guiTop += 10; int yoffset = (int) ((double) this.ySize * 0.02); GuiMenuTopButton top; - addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize-2 , guiTop - 17, "X")); + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize - 2, guiTop - 17, "X")); this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); int topXoffset = 0; int topYoffset = 0; @@ -135,7 +157,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback textArea.setListener(this); this.closeOnEsc(textArea::closeOnEsc); this.addTextField(textArea); - + int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index cff2c111e..db1338fff 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -1,7 +1,7 @@ package noppes.npcs.client.gui.script; -import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.DataScript; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.entity.EntityNPCInterface; public class GuiNPCEventScripts extends GuiScriptInterface { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 15d3e8e76..82012429f 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -1,7 +1,11 @@ package noppes.npcs.client.gui.script; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.*; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiConfirmOpenLink; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.StatCollector; @@ -21,13 +25,13 @@ import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IJTextAreaListener; import noppes.npcs.client.gui.util.ITextChangeListener; import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; -import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.data.IScriptHandlerPacket; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -50,15 +54,21 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - - /** If true, uses scroll-based tab selection instead of top buttons */ + + /** + * If true, uses scroll-based tab selection instead of top buttons + */ protected boolean useScrollTabs = false; - - /** If true, shows settings/script toggle instead of numbered tabs */ - protected boolean useSettingsToggle = false; + + /** + * If true, shows settings/script toggle instead of numbered tabs + */ + protected boolean useSettingsToggle = false; // ==================== FULLSCREEN MODE ==================== - /** Whether the editor viewport is currently in fullscreen mode */ + /** + * Whether the editor viewport is currently in fullscreen mode + */ public static boolean isFullscreen = false; /** @@ -67,19 +77,29 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * All padding values are in scaled GUI pixels. */ public static class FullscreenConfig { - /** Padding from the top edge of the screen */ + /** + * Padding from the top edge of the screen + */ public static int paddingTop = 20; - /** Padding from the bottom edge of the screen */ + /** + * Padding from the bottom edge of the screen + */ public static int paddingBottom = 20; - /** Padding from the left edge of the screen */ + /** + * Padding from the left edge of the screen + */ public static int paddingLeft = 20; - /** Padding from the right edge of the screen */ + /** + * Padding from the right edge of the screen + */ public static int paddingRight = 20; } - /** Fullscreen toggle button drawn at top-right of viewport */ + /** + * Fullscreen toggle button drawn at top-right of viewport + */ public final FullscreenButton fullscreenButton = new FullscreenButton(); - + public GuiScriptInterface() { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -150,7 +170,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb topXoffset -= top.width + 20 * 22; } this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, - top.yPosition + topYoffset, ta + 1 + "")); + top.yPosition + topYoffset, ta + 1 + "")); topXoffset = 0; topYoffset = 0; scriptLimit = ta + 2; @@ -159,8 +179,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (isSingle && handler.getSingleScript() != null) this.addTopButton( - top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, - "Script")); + top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, + "Script")); else if (this.handler.getScripts().size() < 100) this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); @@ -241,15 +261,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.getScript()); + container == null ? "" : container.getScript()); activeArea.setListener(this); this.closeOnEsc(activeArea::closeOnEsc); textAreas.put(idx, activeArea); } else { activeArea.init(editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.getScript()); + container == null ? "" : container.getScript()); } - + // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); @@ -279,12 +299,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - + // Language toggle button (only if handler supports Janino and there is more than one option) if (handler.supportsJanino() && getLanguageOptions().size() > 1 && container != null) { this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, container.getLanguage())); } - + this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); @@ -303,7 +323,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb */ private void initSettingsTab(int yoffset) { GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, - this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); + this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); var8.canEdit = false; var8.setFocused(true); this.addTextField(var8); @@ -319,7 +339,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.getButton(103).enabled = languageOptions.size() > 0; this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, - new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); + new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); if (this.player.worldObj.isRemote) { this.addButton(new GuiNpcButton(106, var9, this.guiTop + 55, 150, 20, "script.openfolder")); @@ -382,11 +402,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb IScriptUnit container = getCurrentContainer(); if (container == null) return; - + String addString = ""; if (!this.getTextField(2).getText().isEmpty()) addString += "\n"; - + // Generate the appropriate stub based on language // hook is already the proper method name from EnumScriptType.function addString += container.generateHookStub(hook, null); @@ -525,7 +545,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return textAreas.get(idx); return null; } - + /** * Get the current script index for text area storage. * Override in subclasses with different tab logic. @@ -533,7 +553,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected int getActiveScriptIndex() { return this.activeTab - 1; } - + /** * Get the current script container. * Override in subclasses with different container access patterns. @@ -642,14 +662,14 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb String language = container != null ? container.getLanguage() : this.handler.getLanguage(); this.setSubGui(new EventGuiScriptList((List) this.languages.get(language), container)); } - + // Language toggle button - switch between ECMAScript and Java if (guibutton.id == 113) { int idx = getActiveScriptIndex(); if (idx >= 0 && idx < handler.getScripts().size()) { IScriptUnit currentUnit = handler.getScripts().get(idx); IScriptUnit newUnit; - + if (currentUnit.isJanino()) { // Switch from Java to ECMAScript newUnit = new ScriptContainer(handler); @@ -669,7 +689,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb handler.replaceScriptUnit(idx, newUnit); // Clear the cached text area so it recreates with new language textAreas.remove(idx); - + // Reinitialize the GUI initGui(); } @@ -712,7 +732,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } return; } - + loadLanguagesData(compound); } @@ -759,7 +779,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return; this.setScript(); - + if (handler instanceof IScriptHandlerPacket) ((IScriptHandlerPacket) handler).sync(); @@ -790,7 +810,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb IScriptUnit current = getCurrentContainer(); if (current != null) current.ensureCompiled(); - + if (parent != null) { this.save(); parent.setWorldAndResolution(mc, width, height); @@ -838,14 +858,18 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public int size = 12; public boolean hovered = false; - /** Initialize button position relative to viewport bounds */ + /** + * Initialize button position relative to viewport bounds + */ public void initGui(int viewportEndX, int viewportY, int scrollbarOffset) { // Position at top-right, accounting for scrollbar this.x = viewportEndX + scrollbarOffset - size - 4; this.y = viewportY + 4; } - /** Draw the fullscreen toggle button */ + /** + * Draw the fullscreen toggle button + */ public void draw(int mouseX, int mouseY) { hovered = isMouseOver(mouseX, mouseY); int y = this.y + getYOffset(); @@ -877,7 +901,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb drawRect(right, bottom, right + cornerLen, bottom + 1, iconColor); drawRect(right, bottom, right + 1, bottom + cornerLen, iconColor); - + } else { // Draw expand icon (corners pointing outward) ⌐¬⌙⌞ // Top-left corner pointing outward @@ -891,23 +915,27 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb drawRect(x + inset, y + size - inset - 1, x + inset + cornerLen, y + size - inset, iconColor); // Bottom-right corner pointing outward drawRect(x + size - inset - 1, y + size - inset - cornerLen, x + size - inset, y + size - inset, - iconColor); + iconColor); drawRect(x + size - inset - cornerLen, y + size - inset - 1, x + size - inset, y + size - inset, - iconColor); + iconColor); } } - + public void drawRect(int x1, int y1, int x2, int y2, int color) { GuiUtil.drawRectD(x1, y1, x2, y2, color); } - /** Check if mouse is over the button */ + /** + * Check if mouse is over the button + */ public boolean isMouseOver(int mouseX, int mouseY) { int y = this.y + getYOffset(); return mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; } - /** Handle click - returns true if click was consumed */ + /** + * Handle click - returns true if click was consumed + */ public boolean mouseClicked(int mouseX, int mouseY, int button) { if (button == 0 && isMouseOver(mouseX, mouseY)) { toggleFullscreen(); diff --git a/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java index e2812ecb2..2e20a6ed5 100644 --- a/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java +++ b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java @@ -42,7 +42,7 @@ public class SubGuiSelectList extends SubGuiInterface { scroll1.setSize(140, 180); } - addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize-22 , guiTop - 17, "X")); + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize - 22, guiTop - 17, "X")); scroll1.guiLeft = guiLeft + 4; scroll1.guiTop = guiTop + 14; diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java index 4f1bccfaf..3b8fda84d 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java @@ -371,6 +371,7 @@ public class GuiTextureSelection extends SubGuiInterface implements ICustomScrol selectedDomain = domain; this.location = location; } + static class TextureData { String domain; String absoluteName; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java b/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java index 07822888c..3624de03c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java @@ -9,7 +9,7 @@ import noppes.npcs.constants.EnumDiagramLayout; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 585ee8515..8444c5c1d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -109,7 +109,7 @@ public abstract class GuiNPCInterface extends GuiScreen { //Important for GuiScriptTextArea for (GuiNpcTextField tf : textfields.values()) - tf.initGui(); + tf.initGui(); } @Override @@ -208,6 +208,7 @@ public abstract class GuiNPCInterface extends GuiScreen { } } } + @Override public void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { if (subgui != null) { @@ -286,7 +287,7 @@ public abstract class GuiNPCInterface extends GuiScreen { } public void close() { - if(GuiNpcTextField.activeTextfield != null) + if (GuiNpcTextField.activeTextfield != null) GuiNpcTextField.unfocus(); Keyboard.enableRepeatEvents(false); @@ -537,6 +538,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public void drawNpcWithExtras(EntityLivingBase entity, int mouseX, int mouseY, float partialTicks) { drawNpc(entity, mouseX, mouseY, partialTicks); } + public void drawNpc(EntityLivingBase entity, int mouseX, int mouseY, float partialTicks) { if (hasSubGui() && !drawNPConSub) return; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index 37ffdb807..c14d0da83 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -126,6 +126,7 @@ public class GuiNpcButton extends GuiButton { } public AtomicBoolean rightClicked = new AtomicBoolean(); + @Override public boolean mousePressed(Minecraft minecraft, int i, int j) { boolean bo = super.mousePressed(minecraft, i, j); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 1345a584f..91f3ca142 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -38,10 +38,10 @@ public class GuiNpcTextField extends GuiTextField { public GuiNpcTextField(int id, GuiScreen parent, int i, int j, int k, int l, String s) { this(id, parent, Minecraft.getMinecraft().fontRenderer, i, j, k, l, s); } - + public void initGui() { } - + protected boolean charAllowed(char c, int i) { if (!integersOnly || Character.isDigit(c)) return true; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index c73f2bebf..77be0dd7c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -8,8 +8,17 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; -import noppes.npcs.client.gui.util.script.*; +import noppes.npcs.client.gui.util.script.BracketMatcher; +import noppes.npcs.client.gui.util.script.CommentHandler; +import noppes.npcs.client.gui.util.script.CursorNavigation; +import noppes.npcs.client.gui.util.script.GoToLineDialog; +import noppes.npcs.client.gui.util.script.IndentHelper; +import noppes.npcs.client.gui.util.script.JavaTextContainer; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.RenameRefactorHandler; +import noppes.npcs.client.gui.util.script.ScrollState; +import noppes.npcs.client.gui.util.script.SearchReplaceBar; +import noppes.npcs.client.gui.util.script.SelectionState; import noppes.npcs.client.key.impl.ScriptEditorKeys; import noppes.npcs.constants.ScriptContext; import noppes.npcs.util.ValueUtil; @@ -27,7 +36,7 @@ import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; /** * Script text editor component with syntax highlighting, bracket matching, * smooth scrolling, and IDE-like features. - * + *

* Helper classes used: * - ScrollState: smooth scroll animation and state management * - SelectionState: cursor position and text selection management @@ -37,11 +46,11 @@ import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; * - CursorNavigation: cursor movement logic */ public class GuiScriptTextArea extends GuiNpcTextField { - + // ==================== DIMENSIONS & POSITION ==================== public int x; public int y; - + // ==================== STATE FLAGS ==================== public boolean active = false; public boolean enabled = true; @@ -51,7 +60,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean tripleClicked = false; private int clickCount = 0; private long lastClicked = 0L; - + // ==================== TEXT & CONTAINER ==================== public String text = null; public String highlightedWord; @@ -80,12 +89,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== HELPER CLASS INSTANCES ==================== private final ScrollState scroll = new ScrollState(); private final SelectionState selection = new SelectionState(); - + // ==================== UI COMPONENTS ==================== private int cursorCounter; private ITextChangeListener listener; private static int LINE_NUMBER_GUTTER_WIDTH = 25; - + // ==================== UNDO/REDO ==================== public List undoList = new ArrayList<>(); public List redoList = new ArrayList<>(); @@ -97,7 +106,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== SEARCH/REPLACE ==================== public static final SearchReplaceBar searchBar = new SearchReplaceBar(); - + // ==================== GO TO LINE ==================== private final GoToLineDialog goToLineDialog = new GoToLineDialog(); @@ -125,11 +134,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.searchBaseHeight = 0; this.searchAppliedOffset = 0; this.searchBaseInitialized = false; - + KEYS_OVERLAY.openOnClick = true; initGui(); initializeKeyBindings(); } + public void initGui() { int endX = x + width, endY = y + height; int xOffset = hasVerticalScrollbar() ? -8 : -2; @@ -137,11 +147,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS_OVERLAY.borderCol1 = KEYS_OVERLAY.borderCol2 = 0xFF3c3c3c; int overlayWidth = 160; KEYS_OVERLAY.initGui(x + (width - overlayWidth) / 2 + 5, y + height / 10, overlayWidth, - height - height / 5 - 10); + height - height / 5 - 10); KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); - + // Initialize search bar (preserves state across initGui calls) searchBar.initGui(x, y, width); if (searchBar.isVisible()) { // If open @@ -150,7 +160,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!active) // Focus search if opening another script tab & bar is open searchBar.focus(false); } - + // Initialize Go To Line dialog goToLineDialog.initGui(x, y, width); } @@ -193,7 +203,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int visibleLines = effectiveHeight / container.lineHeight; // Calculate how many lines the search bar covers int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil( - (double) searchBarOffset / container.lineHeight) : 0; + (double) searchBarOffset / container.lineHeight) : 0; for (int i = 0; i < container.lines.size(); i++) { LineData ld = container.lines.get(i); @@ -457,12 +467,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) return; clampSelectionBounds(); - + // Dynamically calculate gutter width based on line count digits if (container != null && container.linesCount > 0) { int maxLineNum = container.linesCount; @@ -473,7 +484,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw outer border around entire area int offset = fullscreen() ? 2 : 1; drawRect(x - offset, y - offset - searchBar.getTotalHeight(), x + width + offset, y + height + offset, - 0xffa0a0a0); + 0xffa0a0a0); int searchHeight = searchBar.getTotalHeight(); @@ -483,7 +494,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw text viewport background (starts after gutter) drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000); // Draw separator line between gutter and text area - drawRect(x + LINE_NUMBER_GUTTER_WIDTH-1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41); + drawRect(x + LINE_NUMBER_GUTTER_WIDTH - 1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41); // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) GL11.glEnable(GL11.GL_SCISSOR_TEST); @@ -509,7 +520,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle scrollbar dragging (delegated to ScrollState) if (scroll.isClickScrolling()) scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, getPaddedLineCount(), maxScroll); - + // Update scroll animation scroll.initializeIfNeeded(scroll.getScrolledLine()); scroll.update(maxScroll); @@ -535,8 +546,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Calculate braces next to cursor to highlight int startBracket = 0, endBracket = 0; if (selection.getStartSelection() >= 0 && text != null && text.length() > 0 && - (selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) { - int[] span = BracketMatcher.findBracketSpanAt(text,selection.getStartSelection()); + (selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) { + int[] span = BracketMatcher.findBracketSpanAt(text, selection.getStartSelection()); if (span != null) { startBracket = span[0]; endBracket = span[1]; @@ -544,12 +555,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } List list = new ArrayList<>(container.lines); - + // Build brace spans: {origDepth, open line, close line, adjustedDepth} List braceSpans = BracketMatcher.computeBraceSpans(text, list); // Always highlight unmatched braces (positions in text) List unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text); - + // Determine which exact brace span (openLine/closeLine) to highlight indent guides for int highlightedOpenLine = -1; int highlightedCloseLine = -1; @@ -618,19 +629,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { float fracPixels = (float) (fracOffset * container.lineHeight); GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); - - + + // Expand render range by one line above/below so partially-visible lines are drawn int renderStart = Math.max(0, scroll.getScrolledLine() - 1); // Compute the last line index to render, including the last partially-visible line if any. // Adds the fractional scroll offset (fracOffset * lineHeight) to ensure the bottom line is drawn // when only part of it is visible in the viewport. int renderEnd = (int) Math.min(list.size() - 1, - scroll.getScrolledLine() + container.visibleLines + fracPixels + 1); + scroll.getScrolledLine() + container.visibleLines + fracPixels + 1); // Strings start drawing vertically this much into the line. int stringYOffset = 2; - + // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; @@ -698,7 +709,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - + // Highlight search matches if (searchBar.isVisible()) { List searchMatches = searchBar.getMatches(); @@ -747,8 +758,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (occStart <= occEnd) { // Changed from < to <= to handle empty case int s = ClientProxy.Font.width(line.substring(0, occStart)); int e = isEmpty ? s + 2 : ClientProxy.Font.width( - line.substring(0, occEnd)) + 1; // 2px wide for empty - int occX = x + LINE_NUMBER_GUTTER_WIDTH + s; + line.substring(0, occEnd)) + 1; // 2px wide for empty + int occX = x + LINE_NUMBER_GUTTER_WIDTH + s; int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 2 + e; boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]); @@ -761,12 +772,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { int borderColor = 0xDDFFFFFF; //RED BG drawRect(occX, posY, occEndX, posY + container.lineHeight, 0x33ff0000); - + // Top border drawRect(occX, posY, occEndX, posY + 1, borderColor); - // Bottom border + // Bottom border drawRect(occX, posY + container.lineHeight - 1, occEndX, - posY + container.lineHeight, borderColor); + posY + container.lineHeight, borderColor); // Left border drawRect(occX, posY, occX + 1, posY + container.lineHeight, borderColor); // Right border @@ -778,7 +789,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String currentWord = renameHandler.getCurrentWord(); if (currentWord != null && cursorInWord >= 0 && cursorInWord <= currentWord.length()) { String beforeCursor = currentWord.substring(0, - Math.min(cursorInWord, currentWord.length())); + Math.min(cursorInWord, currentWord.length())); int cursorX = occX + ClientProxy.Font.width(beforeCursor); // drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1, // 0xFFFFFFFF); @@ -789,18 +800,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - + // Highlight the current line (light gray) under any selection if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { - drawRect(x , posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); + drawRect(x, posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); } // Highlight selection if (selection.hasSelection() && selection.getEndSelection() > data.start && selection.getStartSelection() <= data.end) { if (selection.getStartSelection() < data.end) { int s = ClientProxy.Font.width( - line.substring(0, Math.max(selection.getStartSelection() - data.start, 0))); + line.substring(0, Math.max(selection.getStartSelection() - data.start, 0))); int e = ClientProxy.Font.width( - line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1; + line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1; drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); } } @@ -837,11 +848,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { boolean highlighted = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine); int guideColor = highlighted ? 0x9933cc00 : 0x33FFFFFF; - + int topY = y + (drawStart - scroll.getScrolledLine()) * container.lineHeight; int bottomY = y + (endLine - scroll.getScrolledLine() + 1) * container.lineHeight; - if(highlighted) - bottomY-=2; + if (highlighted) + bottomY -= 2; drawRect(gx, topY, gx + 1, bottomY, guideColor); } } @@ -852,15 +863,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { boolean recentInput = selection.hadRecentInput(); if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width( - line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); - drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff); + line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); + drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff); } } } GL11.glPopMatrix(); GL11.glDisable(GL11.GL_SCISSOR_TEST); - + if (hasVerticalScrollbar()) { Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource); int effLines = Math.max(1, getPaddedLineCount()); @@ -875,7 +886,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw search/replace bar (overlays viewport) searchBar.draw(xMouse, yMouse); - + // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); @@ -902,13 +913,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // visible lines (fractional positions) correctly hit that line. double fracPixels = scroll.getFractionalOffset() * container.lineHeight; double yMouseD = yMouse + fracPixels; - + ArrayList list = new ArrayList(this.container.lines); for (int i = 0; i < list.size(); ++i) { LineData data = (LineData) list.get(i); //+1 to account for the fractional line - if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines +1) { + if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines + 1) { double yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight; if (yMouseD >= yPos && yMouseD < yPos + this.container.lineHeight) { int lineWidth = 0; @@ -941,13 +952,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int getCursorLineIndex() { return selection.getCursorLineIndex(container.lines, text != null ? text.length() : 0); } - - + + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) // Only scrolls if cursor is outside the visible area private void scrollToCursor() { if (container == null || container.lines == null || container.lines.isEmpty()) return; - + int lineIdx = getCursorLineIndex(); int visible = Math.max(1, container.visibleLines); int effectiveVisible = Math.max(1, visible - bottomPaddingLines); @@ -1162,27 +1173,27 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS.SEARCH.setTask(e -> { if (!e.isPress() || !openBoxes.get()) return; - + unfocusAll(); searchBar.openSearch(); }); - + // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) // Works in search bar. KEYS.SEARCH_REPLACE.setTask(e -> { if (!e.isPress() || !openBoxes.get()) return; - + unfocusAll(); searchBar.openSearchReplace(); }); - + // GO_TO_LINE: Open go to line dialog (Ctrl+G) // Works in search bar. KEYS.GO_TO_LINE.setTask(e -> { if (!e.isPress() || !openBoxes.get()) return; - + unfocusAll(); goToLineDialog.toggle(); }); @@ -1221,13 +1232,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle rename refactor input first if active if (renameHandler.isActive() && renameHandler.keyTyped(c, i)) return true; - + // Handle Go To Line dialog input first if it has focus - if (goToLineDialog.isVisible() &&goToLineDialog.keyTyped(c, i)) - return true; - + if (goToLineDialog.isVisible() && goToLineDialog.keyTyped(c, i)) + return true; + // Handle search bar input first if it has focus - if (searchBar.isVisible() && searchBar.keyTyped(c, i)) + if (searchBar.isVisible() && searchBar.keyTyped(c, i)) return true; if (!active) @@ -1339,7 +1350,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int cursorPos = selection.getCursorPosition(); int prevNonWs = cursorPos - 1; while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace( - text.charAt(prevNonWs))) { + text.charAt(prevNonWs))) { prevNonWs--; } @@ -1473,7 +1484,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { s = s.substring(0, nearestCondition); // Adjust selection start to match removed characters selection.setStartSelection( - selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); + selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); } setText(s + getSelectionAfterText()); selection.reset(selection.getStartSelection()); @@ -1561,9 +1572,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { char lastChar = before.charAt(before.length() - 1); char firstChar = content.charAt(0); // Avoid adding space when punctuation/brackets are adjacent - if (!Character.isWhitespace(lastChar) && + if (!Character.isWhitespace(lastChar) && lastChar != '{' && lastChar != '(' && lastChar != '[' && - firstChar != '}' && firstChar != ')' && firstChar != ']' && + firstChar != '}' && firstChar != ')' && firstChar != ']' && firstChar != ';' && firstChar != ',' && firstChar != '.' && firstChar != '\n') { spacer = " "; @@ -1584,14 +1595,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (selection.getStartSelection() > 0 && selection.getStartSelection() < text.length()) { char prev = text.charAt(selection.getStartSelection() - 1); char nextc = text.charAt(selection.getStartSelection()); - if ((prev == '(' && nextc == ')') || - (prev == '[' && nextc == ']') || - (prev == '{' && nextc == '}') || - (prev == '\'' && nextc == '\'') || + if ((prev == '(' && nextc == ')') || + (prev == '[' && nextc == ']') || + (prev == '{' && nextc == '}') || + (prev == '\'' && nextc == '\'') || (prev == '"' && nextc == '"')) { String before = text.substring(0, selection.getStartSelection() - 1); String after = selection.getStartSelection() + 1 < text.length() ? text.substring( - selection.getStartSelection() + 1) : ""; + selection.getStartSelection() + 1) : ""; setText(before + after); selection.setStartSelection(selection.getStartSelection() - 1); selection.reset(selection.getStartSelection()); @@ -1757,7 +1768,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // already immediately after the caret, move caret past it instead // of inserting another closer. This prevents duplicate closers // when the editor auto-inserts pairs. - if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { + if ((c == ')' || c == ']' || c == '"' || c == '\'') && after.length() > 0 && after.charAt(0) == c) { // Move caret forward by one (skip over existing closer) selection.reset(before.length() + 1); scrollToCursor(); @@ -1805,10 +1816,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== COMMENT TOGGLING ==================== // Uses CommentHandler helper for comment operations - + private void toggleCommentSelection() { CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection( - text, container.lines, selection.getStartSelection(), selection.getEndSelection()); + text, container.lines, selection.getStartSelection(), selection.getEndSelection()); setText(result.newText); selection.setStartSelection(result.newStartSelection); selection.setEndSelection(result.newEndSelection); @@ -1816,15 +1827,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void toggleCommentLineAtCursor() { CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor( - text, container.lines, selection.getCursorPosition()); + text, container.lines, selection.getCursorPosition()); setText(result.newText); setCursor(result.newCursorPosition, false); } - - public boolean closeOnEsc(){ - return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + + public boolean closeOnEsc() { + return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); } - + // ==================== KEYBOARD MODIFIERS ==================== private boolean isAltKeyDown() { @@ -1872,7 +1883,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { return IndentHelper.getAutoIndentForEnter(currentLine.text, selection.getCursorPosition() - currentLine.start); } // ==================== TEXT FORMATTING ==================== - + private int getTabSize() { return IndentHelper.TAB_SIZE; } @@ -1888,7 +1899,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(result.text); selection.reset(Math.max(0, Math.min(result.cursorPosition, this.text.length()))); } - + // ==================== TAB HANDLING ==================== private void handleTab() { @@ -1986,7 +1997,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - + // ==================== CURSOR MANAGEMENT ==================== private void setCursor(int i, boolean select) { @@ -2014,7 +2025,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public String getSelectionAfterText() { return selection.getTextAfter(text); } - + // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { @@ -2022,21 +2033,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { return; } - + // Check search bar clicks first if (searchBar.isVisible() && searchBar.mouseClicked(xMouse, yMouse, mouseButton)) { return; } - + // If search bar is visible but click was outside it, unfocus the search bar if (searchBar.isVisible()) { searchBar.unfocus(); } - + // Let the overlay consume clicks (it returns true when it handled the event) if (KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton)) return; - + // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { @@ -2067,7 +2078,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (this.clicked && getPaddedLineCount() * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { // We consumed the mouse-down as a scrollbar drag start this.clicked = false; - scroll.startScrollbarDrag(yMouse,this.y,this.height, getPaddedLineCount()); + scroll.startScrollbarDrag(yMouse, this.y, this.height, getPaddedLineCount()); } else { // Handle double/triple click selection counting if (time - this.lastClicked < 300L) { @@ -2111,7 +2122,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { renameHandler.updateCursor(); ++this.cursorCounter; } - + // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { @@ -2140,7 +2151,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.init(text, this.width, this.height); - if (this.enableCodeHighlighting) + if (this.enableCodeHighlighting) this.container.formatCodeText(); // Ensure scroll state stays in bounds after text change @@ -2202,7 +2213,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void clampSelectionBounds() { selection.clamp(text != null ? text.length() : 0); } - + // ==================== INNER CLASSES ==================== public static class UndoData { diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java index 59afbea09..3273f1f6c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java @@ -143,6 +143,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } } } + public void mouseEvent(int i, int j, int k) { } @@ -208,6 +209,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } public int backgroundColor = 0x66000000; + @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { scrollY = (float) lerp(scrollY, nextScrollY, partialTicks); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java index 9a9fc71ca..7350668b8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java @@ -112,7 +112,7 @@ public class GuiUtil { GL11.glEnable(GL11.GL_ALPHA_TEST); GL11.glEnable(GL11.GL_TEXTURE_2D); } - + public static void drawGradientRectHorizontal(int left, int top, int right, int bottom, int startColor, int endColor) { double zLevel = 0; diff --git a/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java index ec186d066..4012c9f73 100644 --- a/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java +++ b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java @@ -9,6 +9,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; public interface IAbilityConfigCallback { /** * Called when an ability has been edited and saved. + * * @param ability The edited ability */ void onAbilitySaved(Ability ability); diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index e8fefd475..7ebc098be 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -79,7 +79,7 @@ public class OverlayKeyPresetViewer { public void drawOverlay(int wheel) { scroll.update(wheel); - + if (hasBorder) { // int borderCol2 = 0xffff00ff,borderCol2 = 0xff00ffff; NEON @@ -133,7 +133,7 @@ public class OverlayKeyPresetViewer { showOverlay = false; return true; } - + // If overlay is visible but no element consumed the key, consider it handled return true; } @@ -155,7 +155,7 @@ public class OverlayKeyPresetViewer { // If overlay is not visible, no other clicks matter if (!showOverlay) return false; - + boolean isAboveOverlay = isMouseAbove(mouseX, mouseY); // In click-to-open mode, clicking outside the overlay (and not the button) should close it if (openOnClick && !isAboveOverlay && !isAboveButton) { @@ -259,7 +259,7 @@ public class OverlayKeyPresetViewer { public void drawBar() { GL11.glPushMatrix(); GL11.glTranslatef(endX - barWidth - 2, startY + yStartSpacing + scrollY * scrollFactor, 1); - GuiUtil.drawRectD(0, 1, barWidth, scrollbarHeight-1, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); + GuiUtil.drawRectD(0, 1, barWidth, scrollbarHeight - 1, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); GL11.glPopMatrix(); } @@ -480,7 +480,7 @@ public class OverlayKeyPresetViewer { int oldMinFilter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); GL11.glDisable(GL11.GL_BLEND); - + GL11.glPushMatrix(); float color = aboveButton ? 1f : 0.4f; GL11.glColor4f(color, color, color, 1); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java index 96fe2a7ba..b7b9ab63a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.ArrayList; import java.util.List; @@ -8,14 +9,15 @@ import java.util.List; * Handles bracket matching, brace span computation, and indent guide generation. */ public class BracketMatcher { - + /** * Find matching bracket at cursor position. + * * @return int[2] with {startPos, endPos} or null if no bracket found */ public static int[] findBracketSpanAt(String text, int pos) { if (text == null || text.isEmpty()) return null; - + // Try at current position if (pos >= 0 && pos < text.length()) { char c = text.charAt(pos); @@ -34,7 +36,10 @@ public class BracketMatcher { int scan = pos - 1; while (scan >= 0) { char sc = text.charAt(scan); - if (sc == ' ' || sc == '\t') { scan--; continue; } + if (sc == ' ' || sc == '\t') { + scan--; + continue; + } if (sc == '\n') break; // Don't cross line boundary int found2 = 0; @@ -73,25 +78,43 @@ public class BracketMatcher { char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0; if (inString) { - if (escape) { escape = false; } - else if (c == '\\') { escape = true; } - else if (c == stringDelimiter) { inString = false; } - else if (c == '\n') { inString = false; } + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == stringDelimiter) { + inString = false; + } else if (c == '\n') { + inString = false; + } continue; } if (inBlockComment) { - if (c == '*' && next == '/') { inBlockComment = false; pos++; } + if (c == '*' && next == '/') { + inBlockComment = false; + pos++; + } continue; } if (inLineComment) { - if (c == '\n') { inLineComment = false; } + if (c == '\n') { + inLineComment = false; + } continue; } - if (c == '/' && next == '/') { inLineComment = true; pos++; continue; } - if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; } + if (c == '/' && next == '/') { + inLineComment = true; + pos++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + pos++; + continue; + } if (c == '"' || c == '\'') { inString = true; @@ -193,29 +216,47 @@ public class BracketMatcher { // String handling if (inString) { - if (escape) { escape = false; } - else if (c == '\\') { escape = true; } - else if (c == stringDelimiter) { inString = false; } - else if (c == '\n') { inString = false; } // Unterminated string + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == stringDelimiter) { + inString = false; + } else if (c == '\n') { + inString = false; + } // Unterminated string continue; } // Block comment handling if (inBlockComment) { - if (c == '*' && next == '/') { inBlockComment = false; pos++; } + if (c == '*' && next == '/') { + inBlockComment = false; + pos++; + } continue; } // Line comment handling if (inLineComment) { - if (c == '\n') { inLineComment = false; } + if (c == '\n') { + inLineComment = false; + } continue; } // Start of comments - if (c == '/' && next == '/') { inLineComment = true; pos++; continue; } - if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; } - + if (c == '/' && next == '/') { + inLineComment = true; + pos++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + pos++; + continue; + } + // Start of string if (c == '"' || c == '\'') { inString = true; @@ -256,11 +297,12 @@ public class BracketMatcher { /** * Find which brace span should be highlighted based on bracket at position + * * @return int[2] {openLine, closeLine} or null */ public static int[] findHighlightedSpan(int bracketPos, List lines, List spans) { if (bracketPos < 0 || lines == null || spans == null) return null; - + // Find line containing bracket int bracketLineIdx = -1; for (int i = 0; i < lines.size(); i++) { @@ -270,7 +312,7 @@ public class BracketMatcher { break; } } - + if (bracketLineIdx >= 0) { for (int[] span : spans) { int openLine = span[1]; @@ -282,13 +324,13 @@ public class BracketMatcher { } return null; } - + /** * Check if a brace at given position has a matching close with same indent */ public static boolean hasMatchingCloseWithSameIndent(String text, int bracePos, List lines, int currentIndent) { if (text == null || lines == null || bracePos < 0) return false; - + // Find line containing the brace int openLineIdx = -1; for (int i = 0; i < lines.size(); i++) { @@ -298,9 +340,9 @@ public class BracketMatcher { break; } } - + if (openLineIdx < 0) return false; - + List spans = computeBraceSpans(text, lines); for (int[] span : spans) { if (span[1] == openLineIdx) { @@ -314,7 +356,7 @@ public class BracketMatcher { } return false; } - + /** * Get leading whitespace count of a line */ @@ -327,4 +369,4 @@ public class BracketMatcher { } return indent; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index e54f5d878..844da165b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -1,6 +1,13 @@ package noppes.npcs.client.gui.util.script; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Helper class for resolving Java classpath imports and determining class types. @@ -21,14 +28,14 @@ public class ClassPathFinder { // Common java.lang classes (auto-imported in Java) public static final Set JAVA_LANG_CLASSES = new HashSet<>(Arrays.asList( - "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", - "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", - "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", - "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", - "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", - "ClassCastException", "IllegalArgumentException", "IllegalStateException", - "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", - "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" + "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", + "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", + "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", + "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", + "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", + "ClassCastException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", + "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" )); /** @@ -193,10 +200,10 @@ public class ClassPathFinder { // Full resolution successful List classSegments = buildClassSegments(segments, classStartIdx, n); - return ResolveResult.found( - new ClassInfo(info.resolvedName, info.type, packagePortion.length()), - packagePortion, validClassPortion, classSegments, classStartIdx - ); + return ResolveResult.found( + new ClassInfo(info.resolvedName, info.type, packagePortion.length()), + packagePortion, validClassPortion, classSegments, classStartIdx + ); } } } @@ -243,7 +250,7 @@ public class ClassPathFinder { // Build the typed package path segment by segment and compare with cache StringBuilder sb = new StringBuilder(); int lastValidIndex = -1; - + for (int i = 0; i < classStartIdx; i++) { if (i > 0) sb.append("."); sb.append(segments[i]); @@ -272,7 +279,7 @@ public class ClassPathFinder { } } } - + // If we found some valid prefix but not all, return where it stopped being valid if (lastValidIndex > 0 && lastValidIndex < classStartIdx) { // Check if segment at lastValidIndex diverges from any known path @@ -282,15 +289,15 @@ public class ClassPathFinder { // There's a known continuation - check if typed segment matches String nextTyped = segments[lastValidIndex]; String cachedRemainder = cached.substring(validPrefix.length() + 1); - String nextCached = cachedRemainder.contains(".") ? - cachedRemainder.substring(0, cachedRemainder.indexOf('.')) : cachedRemainder; + String nextCached = cachedRemainder.contains(".") ? + cachedRemainder.substring(0, cachedRemainder.indexOf('.')) : cachedRemainder; if (!nextTyped.equals(nextCached)) { return lastValidIndex; // Divergence point } } } } - + return -1; // No typo detected } @@ -312,7 +319,7 @@ public class ClassPathFinder { registerValidPackage(packagePath); return true; } - + // Fallback: try to find any class under this package to validate it String[] testClasses = getTestClassesForPackage(packagePath); for (String testClass : testClasses) { @@ -580,7 +587,7 @@ public class ClassPathFinder { default: // For unknown packages, try common class suffixes return new String[]{ - packagePath + ".package-info", // Won't work but harmless + packagePath + ".package-info", // Won't work but harmless }; } } @@ -591,7 +598,7 @@ public class ClassPathFinder { * Parse generic type content and return type names with their positions. * Handles arbitrary nesting depth like "Map>>" * - * @param genericContent The content inside <...> (without the outer angle brackets) + * @param genericContent The content inside <...> (without the outer angle brackets) * @param importedClasses Map of simple name -> full class name for resolution * @return List of TypeOccurrence with position and type info */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java index b5c0f2205..ed586e304 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.List; /** @@ -15,18 +16,18 @@ public class CommentHandler { public final String text; public final int delta; // Change in length (negative if comment removed) public final int nonWsIndex; // Index of first non-whitespace character - + public ToggleResult(String text, int delta, int nonWsIndex) { this.text = text; this.delta = delta; this.nonWsIndex = nonWsIndex; } - + public boolean wasCommented() { return delta < 0; } } - + /** * Toggle line comment on a single line */ @@ -36,11 +37,11 @@ public class CommentHandler { while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) { nonWs++; } - + boolean hasContent = nonWs < lineText.length(); boolean hasComment = hasContent && lineText.startsWith("//", nonWs); String newText; - + if (hasComment) { // Remove comment int cut = Math.min(nonWs + 2, lineText.length()); @@ -52,10 +53,10 @@ public class CommentHandler { // Empty/whitespace line - no change newText = lineText; } - + return new ToggleResult(newText, newText.length() - lineText.length(), nonWs); } - + /** * Result of toggling comments on a selection */ @@ -63,33 +64,33 @@ public class CommentHandler { public final String newText; public final int newStartSelection; public final int newEndSelection; - + public SelectionToggleResult(String text, int start, int end) { this.newText = text; this.newStartSelection = start; this.newEndSelection = end; } } - + /** * Toggle comments on all lines in a selection */ public static SelectionToggleResult toggleCommentSelection( - String text, - List lines, - int startSelection, - int endSelection) { - + String text, + List lines, + int startSelection, + int endSelection) { + StringBuilder newText = new StringBuilder(text.length() + 100); int prevEnd = 0; int newStart = -1; int newEnd = -1; int newIndex = 0; - + for (LineData line : lines) { int safeLineStart = Math.max(0, Math.min(line.start, text.length())); int safeLineEnd = Math.max(0, Math.min(line.end, text.length())); - + // Handle gap between lines (shouldn't happen normally) if (safeLineStart > prevEnd) { if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) { @@ -101,7 +102,7 @@ public class CommentHandler { newText.append(text, prevEnd, safeLineStart); newIndex += safeLineStart - prevEnd; } - + // Check if this line is in selection if (safeLineEnd > startSelection && safeLineStart < endSelection) { String lineText = text.substring(safeLineStart, safeLineEnd); @@ -109,7 +110,7 @@ public class CommentHandler { String newLineText = tr.text; int nonWs = tr.nonWsIndex; int delta = tr.delta; - + // Update start selection position if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { int offsetInOld = startSelection - safeLineStart; @@ -119,7 +120,7 @@ public class CommentHandler { } newStart = newIndex + Math.max(0, newOffset); } - + // Update end selection position if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { int offsetInOld = endSelection - safeLineStart; @@ -129,7 +130,7 @@ public class CommentHandler { } newEnd = newIndex + Math.max(0, newOffset); } - + newText.append(newLineText); newIndex += newLineText.length(); } else { @@ -145,7 +146,7 @@ public class CommentHandler { } prevEnd = safeLineEnd; } - + // Handle remaining text after last line if (prevEnd < text.length()) { if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) { @@ -157,35 +158,35 @@ public class CommentHandler { newText.append(text, prevEnd, text.length()); newIndex += text.length() - prevEnd; } - + // Fallback for positions not found if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length())); if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length())); - + return new SelectionToggleResult(newText.toString(), newStart, newEnd); } - + /** * Toggle comment on a single line at cursor position */ public static SingleLineToggleResult toggleCommentAtCursor( - String text, - List lines, - int cursorPosition) { - + String text, + List lines, + int cursorPosition) { + for (LineData line : lines) { int lineStart = Math.max(0, Math.min(line.start, text.length())); int lineEnd = Math.max(0, Math.min(line.end, text.length())); - + if (cursorPosition >= lineStart && cursorPosition <= lineEnd) { String lineText = text.substring(lineStart, lineEnd); ToggleResult tr = toggleLine(lineText); boolean hadComment = tr.delta < 0; - + String before = text.substring(0, lineStart); String after = lineEnd <= text.length() ? text.substring(lineEnd) : ""; String newText = before + tr.text + after; - + // Calculate new cursor position int cursorDelta = hadComment ? -2 : 2; int newCursor = cursorPosition + cursorDelta; @@ -193,25 +194,25 @@ public class CommentHandler { newCursor = cursorPosition; } newCursor = Math.max(lineStart, newCursor); - + return new SingleLineToggleResult(newText, newCursor); } } - + // No line found at cursor - return unchanged return new SingleLineToggleResult(text, cursorPosition); } - + /** * Result of toggling comment on single line */ public static class SingleLineToggleResult { public final String newText; public final int newCursorPosition; - + public SingleLineToggleResult(String text, int cursor) { this.newText = text; this.newCursorPosition = cursor; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java index 2a39d5bbb..52faebe0c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -9,25 +10,25 @@ import java.util.regex.Pattern; * Handles cursor navigation logic (up, down, left, right, word movement). */ public class CursorNavigation { - + /** * Move cursor up one line, maintaining column position */ public static int cursorUp(int cursorPosition, List lines, String text) { if (lines == null || lines.isEmpty()) return 0; - + for (int i = 0; i < lines.size(); i++) { LineData data = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= data.start && cursorPosition <= data.end) - : (cursorPosition >= data.start && cursorPosition < data.end); - + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + if (isOnLine) { if (i == 0) { return 0; // Already on first line } - + int column = Math.min(cursorPosition - data.start, data.text.length()); LineData target = lines.get(i - 1); int targetColumn = Math.min(column, target.text.length()); @@ -39,26 +40,26 @@ public class CursorNavigation { } return 0; } - + /** * Move cursor down one line, maintaining column position */ public static int cursorDown(int cursorPosition, List lines, String text) { if (lines == null || lines.isEmpty()) return text != null ? text.length() : 0; if (text != null && cursorPosition == text.length()) return cursorPosition; - + for (int i = 0; i < lines.size(); i++) { LineData data = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= data.start && cursorPosition <= data.end) - : (cursorPosition >= data.start && cursorPosition < data.end); - + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + if (isOnLine) { if (i >= lines.size() - 1) { return cursorPosition; // Already on last line } - + int column = Math.min(cursorPosition - data.start, data.text.length()); LineData target = lines.get(i + 1); int targetColumn = Math.min(column, target.text.length()); @@ -70,13 +71,13 @@ public class CursorNavigation { } return text != null ? text.length() : 0; } - + /** * Move cursor left, optionally by word */ public static int cursorLeft(int cursorPosition, String text, Pattern wordPattern, boolean byWord) { if (text == null || cursorPosition <= 0) return 0; - + int moveAmount = 1; if (byWord && wordPattern != null) { Matcher m = wordPattern.matcher(text.substring(0, cursorPosition)); @@ -88,7 +89,7 @@ public class CursorNavigation { } return Math.max(cursorPosition - moveAmount, 0); } - + /** * Move cursor right, optionally by word */ @@ -96,7 +97,7 @@ public class CursorNavigation { if (text == null) return 0; int textLen = text.length(); if (cursorPosition >= textLen) return textLen; - + int moveAmount = 1; if (byWord && wordPattern != null) { Matcher m = wordPattern.matcher(text.substring(cursorPosition)); @@ -108,45 +109,45 @@ public class CursorNavigation { } return Math.min(cursorPosition + moveAmount, textLen); } - + /** * Get cursor position from mouse click coordinates */ public static int getPositionFromMouse( - int mouseX, int mouseY, - int areaX, int areaY, - int gutterWidth, - int lineHeight, - int scrolledLine, - double scrollFracOffset, - List lines, - String text, - FontWidthProvider fontWidth) { - + int mouseX, int mouseY, + int areaX, int areaY, + int gutterWidth, + int lineHeight, + int scrolledLine, + double scrollFracOffset, + List lines, + String text, + FontWidthProvider fontWidth) { + int xMouse = mouseX - (areaX + gutterWidth + 1); int yMouse = mouseY - areaY - 1; - + // Account for fractional scroll offset double fracPixels = scrollFracOffset * lineHeight; yMouse = (int) Math.round(yMouse + fracPixels); - + if (lines == null || lines.isEmpty()) { return text != null ? text.length() : 0; } - + int visibleLines = Math.min(lines.size(), scrolledLine + 50); // Reasonable limit - + for (int i = 0; i < lines.size(); i++) { LineData data = lines.get(i); - + if (i >= scrolledLine && i <= visibleLines) { int yPos = (i - scrolledLine) * lineHeight; - + if (yMouse >= yPos && yMouse < yPos + lineHeight) { // Found the line, now find character position int lineWidth = 0; char[] chars = data.text.toCharArray(); - + for (int j = 1; j <= chars.length; j++) { int w = fontWidth.getWidth(data.text.substring(0, j)); if (xMouse < lineWidth + (w - lineWidth) / 2) { @@ -154,24 +155,24 @@ public class CursorNavigation { } lineWidth = w; } - + // Click past end of line - place cursor at end of text (not on newline) int posAfterChars = data.start + chars.length; return Math.min(posAfterChars, text.length()); } } } - + return text != null ? text.length() : 0; } - + /** * Interface for font width measurement */ public interface FontWidthProvider { int getWidth(String text); } - + /** * Get leading whitespace count of a line */ @@ -184,26 +185,26 @@ public class CursorNavigation { } return indent; } - + /** * Find line index containing given position */ public static int findLineIndex(int position, List lines) { if (lines == null || lines.isEmpty()) return 0; - + for (int i = 0; i < lines.size(); i++) { LineData line = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (position >= line.start && position <= line.end) - : (position >= line.start && position < line.end); + boolean isOnLine = isLastLine + ? (position >= line.start && position <= line.end) + : (position >= line.start && position < line.end); if (isOnLine) { return i; } } return lines.size() - 1; } - + /** * Find line data containing given position */ @@ -214,4 +215,4 @@ public class CursorNavigation { } return null; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java index 5c534cc94..a1eedce0b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -6,7 +6,7 @@ import java.util.regex.Pattern; /** * Configurable code formatter with IntelliJ-like settings. - * + *

* Features: * - Operator spacing (a+b → a + b) * - Whitespace normalization (a b → a b) @@ -15,9 +15,9 @@ import java.util.regex.Pattern; * - Preserves string content and comments */ public class FormatHelper { - + // ==================== SETTINGS ==================== - + /** * Formatting configuration - similar to IntelliJ Code Style settings */ @@ -28,12 +28,12 @@ public class FormatHelper { public boolean spaceAroundComparison = true; // a == b, a != b, a <= b public boolean spaceAroundLogical = true; // a && b || c public boolean spaceAroundBitwise = true; // a & b | c ^ d - + // Spaces within brackets public boolean spaceWithinParens = false; // (a) vs ( a ) public boolean spaceWithinBrackets = false; // [a] vs [ a ] public boolean spaceWithinBraces = true; // {a} vs { a } (for initializers) - + // Spaces before/after public boolean spaceAfterComma = true; // a, b vs a,b public boolean spaceBeforeComma = false; // a, b vs a , b @@ -41,48 +41,48 @@ public class FormatHelper { public boolean spaceBeforeSemicolon = false; public boolean spaceAfterColon = true; // case a: b vs case a:b public boolean spaceBeforeColon = false; - + // Method calls public boolean spaceBeforeMethodParens = false; // foo() vs foo () public boolean spaceAfterTypeCast = true; // (int) x vs (int)x - + // Control statements public boolean spaceBeforeIfParens = true; // if (...) vs if(...) public boolean spaceBeforeWhileParens = true; public boolean spaceBeforeForParens = true; public boolean spaceBeforeSwitchParens = true; public boolean spaceBeforeCatchParens = true; - + // Braces public boolean spaceBeforeOpenBrace = true; // if () { vs if (){ - + // Line length public int maxLineLength = 120; public boolean wrapLongLines = false; // Auto-wrap lines longer than maxLineLength public int wrapIndentSpaces = 4; // Extra indent for wrapped lines public boolean wrapComments = true; // Also wrap long comments - + // Wrap break preferences (where to break long lines) public boolean wrapAfterComma = true; // Prefer breaking after commas public boolean wrapAfterOperator = true; // Prefer breaking after operators public boolean wrapBeforeDot = true; // Break before . for chained calls public boolean wrapMethodArguments = true; // Each arg on new line when wrapping method calls - + // Whitespace normalization public boolean normalizeWhitespace = true; // Multiple spaces → single space public boolean trimTrailingWhitespace = true; - + // Unary operators public boolean spaceAroundUnaryNot = false; // !a vs ! a public boolean spaceAroundUnaryIncDec = false; // ++i vs ++ i - + /** * Create default settings (similar to IntelliJ defaults) */ public static FormatSettings defaults() { return new FormatSettings(); } - + /** * Create compact settings (minimal spacing) */ @@ -95,53 +95,53 @@ public class FormatHelper { return s; } } - + private FormatSettings settings; - + public FormatHelper() { this.settings = FormatSettings.defaults(); } - + public FormatHelper(FormatSettings settings) { this.settings = settings != null ? settings : FormatSettings.defaults(); } - + public void setSettings(FormatSettings settings) { this.settings = settings; } - + public FormatSettings getSettings() { return settings; } - + // ==================== MAIN FORMAT METHOD ==================== - + /** * Format the entire text with all configured options */ public String format(String text) { if (text == null || text.isEmpty()) return text; - + // Process line by line to preserve structure String[] lines = text.split("\n", -1); StringBuilder result = new StringBuilder(); - + for (int i = 0; i < lines.length; i++) { String line = formatLine(lines[i], text, getLineStartOffset(lines, i)); - + if (settings.trimTrailingWhitespace) { line = trimTrailing(line); } - + result.append(line); if (i < lines.length - 1) { result.append("\n"); } } - + return result.toString(); } - + /** * Split multiple statements on one line into separate lines with proper indentation. * E.g., " this.a = 1; this.b = 2;" becomes two lines @@ -150,10 +150,10 @@ public class FormatHelper { private String splitStatementsOntoSeparateLines(String text) { String[] lines = text.split("\n", -1); StringBuilder result = new StringBuilder(); - + for (int lineIdx = 0; lineIdx < lines.length; lineIdx++) { String line = lines[lineIdx]; - + // Skip empty lines or comment-only lines String trimmed = line.trim(); if (trimmed.isEmpty() || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) { @@ -161,7 +161,7 @@ public class FormatHelper { if (lineIdx < lines.length - 1) result.append("\n"); continue; } - + // Extract leading indentation int indent = 0; while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { @@ -169,14 +169,14 @@ public class FormatHelper { } String indentation = line.substring(0, indent); String content = line.substring(indent); - + // Split by semicolons (but preserve those in strings, for loops, etc.) List statements = splitBySemicolons(content); - + for (int si = 0; si < statements.size(); si++) { String stmt = statements.get(si).trim(); if (stmt.isEmpty()) continue; - + // Check if statement opens a scope { and has code after it int braceIdx = stmt.indexOf('{'); if (braceIdx >= 0 && braceIdx < stmt.length() - 1) { @@ -191,20 +191,20 @@ public class FormatHelper { continue; } } - + result.append(indentation).append(stmt); // Add semicolon back if it was removed if (si < statements.size() - 1) { result.append("\n"); } } - + if (lineIdx < lines.length - 1) result.append("\n"); } - + return result.toString(); } - + /** * Split content by semicolons, preserving those in strings and for-loops */ @@ -216,25 +216,25 @@ public class FormatHelper { boolean inChar = false; boolean escaped = false; int forLoopDepth = 0; // Track for(...) depth to preserve semicolons - + for (int i = 0; i < content.length(); i++) { char c = content.charAt(i); char prev = i > 0 ? content.charAt(i - 1) : ' '; - + // Track strings if (c == '\"' && !escaped && !inChar) { inString = !inString; } else if (c == '\'' && !escaped && !inString) { inChar = !inChar; } - + escaped = (c == '\\' && !escaped); - + if (inString || inChar) { current.append(c); continue; } - + // Track for loops if (c == '(' && i >= 3 && content.substring(Math.max(0, i - 3), i + 1).matches(".*\\bfor\\s*\\(")) { forLoopDepth++; @@ -247,7 +247,7 @@ public class FormatHelper { forLoopDepth = 0; } } - + // Semicolon: split if not in for-loop header if (c == ';' && forLoopDepth == 0) { current.append(c); @@ -255,18 +255,18 @@ public class FormatHelper { current = new StringBuilder(); continue; } - + current.append(c); } - + // Add remaining content if (current.length() > 0) { statements.add(current.toString()); } - + return statements; } - + private int getLineStartOffset(String[] lines, int lineIndex) { int offset = 0; for (int i = 0; i < lineIndex; i++) { @@ -274,13 +274,13 @@ public class FormatHelper { } return offset; } - + /** * Format a single line of code */ private String formatLine(String line, String fullText, int lineOffset) { if (line.trim().isEmpty()) return line; - + // Preserve leading indentation int indent = 0; while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { @@ -288,27 +288,27 @@ public class FormatHelper { } String indentation = line.substring(0, indent); String content = line.substring(indent); - + if (content.isEmpty()) return line; - + // Skip formatting for lines that are entirely comments String trimmed = content.trim(); if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) { return line; } - + // Skip formatting for continuation lines (lines that start with operators, dots, etc.) // These are wrapped lines from a previous format and should be preserved as-is if (isContinuationLine(trimmed)) { return line; } - + // Format the content String formatted = formatContent(content, fullText, lineOffset + indent); - + return indentation + formatted; } - + /** * Check if a line appears to be a continuation of a previous line * (i.e., starts with an operator, dot, or other continuation character) @@ -317,7 +317,7 @@ public class FormatHelper { if (trimmedContent.isEmpty()) return false; char firstChar = trimmedContent.charAt(0); // Lines starting with these are likely continuations - if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || firstChar == '*' || firstChar == '/' || firstChar == '%' || firstChar == '&' || firstChar == '|' || firstChar == '^' || firstChar == '?' || firstChar == ':' || firstChar == ',') { @@ -329,53 +329,53 @@ public class FormatHelper { } return false; } - + /** * Format line content, preserving strings and comments */ private String formatContent(String content, String fullText, int offset) { // Find string/comment regions to preserve List preserveRegions = findPreserveRegions(content); - + if (preserveRegions.isEmpty()) { // No strings/comments, format everything return formatCodeSegment(content); } - + // Build result by formatting non-preserved regions StringBuilder result = new StringBuilder(); int lastEnd = 0; - + for (int[] region : preserveRegions) { // Format code before this preserved region if (region[0] > lastEnd) { String codeSegment = content.substring(lastEnd, region[0]); result.append(formatCodeSegment(codeSegment)); } - + // Append preserved region unchanged result.append(content.substring(region[0], region[1])); lastEnd = region[1]; } - + // Format remaining code after last preserved region if (lastEnd < content.length()) { result.append(formatCodeSegment(content.substring(lastEnd))); } - + return result.toString(); } - + /** * Find string literals and comments that should not be formatted */ private List findPreserveRegions(String content) { List regions = new ArrayList<>(); - + int i = 0; while (i < content.length()) { char c = content.charAt(i); - + // String literal if (c == '"') { int end = findStringEnd(content, i, '"'); @@ -383,7 +383,7 @@ public class FormatHelper { i = end; continue; } - + // Char literal if (c == '\'') { int end = findStringEnd(content, i, '\''); @@ -391,13 +391,13 @@ public class FormatHelper { i = end; continue; } - + // Line comment if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '/') { regions.add(new int[]{i, content.length()}); break; } - + // Block comment (could span multiple lines but we process line-by-line) if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '*') { int end = content.indexOf("*/", i + 2); @@ -410,13 +410,13 @@ public class FormatHelper { } continue; } - + i++; } - + return regions; } - + /** * Find end of string/char literal */ @@ -435,37 +435,37 @@ public class FormatHelper { } return content.length(); } - + // ==================== CODE SEGMENT FORMATTING ==================== - + /** * Format a code segment (no strings/comments) */ private String formatCodeSegment(String code) { if (code.isEmpty()) return code; - + String result = code; - + // Normalize whitespace first if (settings.normalizeWhitespace) { result = normalizeWhitespace(result); } - + // Apply operator spacing result = formatOperators(result); - + // Apply bracket spacing result = formatBrackets(result); - + // Apply keyword spacing result = formatKeywords(result); - + // Apply punctuation spacing result = formatPunctuation(result); - + return result; } - + /** * Normalize whitespace (multiple spaces → single space) */ @@ -473,13 +473,13 @@ public class FormatHelper { // Replace multiple spaces with single space, but preserve indentation return code.replaceAll(" +", " "); } - + /** * Format operator spacing */ private String formatOperators(String code) { String result = code; - + // Assignment operators if (settings.spaceAroundAssignment) { // Compound assignments first (longer patterns first) @@ -494,11 +494,11 @@ public class FormatHelper { result = formatBinaryOp(result, "<<=", " <<= "); result = formatBinaryOp(result, ">>=", " >>= "); result = formatBinaryOp(result, ">>>=", " >>>= "); - + // Simple assignment (but not ==, !=, <=, >=) result = formatSimpleAssignment(result); } - + // Comparison operators if (settings.spaceAroundComparison) { result = formatBinaryOp(result, "==", " == "); @@ -508,27 +508,27 @@ public class FormatHelper { // Simple < > (but not generics) result = formatComparisonOperators(result); } - + // Logical operators if (settings.spaceAroundLogical) { result = formatBinaryOp(result, "&&", " && "); result = formatBinaryOp(result, "||", " || "); } - + // Arithmetic operators if (settings.spaceAroundArithmetic) { // Handle ++ and -- first to avoid conflicts result = formatArithmeticOperators(result); } - + // Bitwise operators (but not in generics) if (settings.spaceAroundBitwise) { result = formatBitwiseOperators(result); } - + return result; } - + /** * Format a binary operator with proper spacing */ @@ -537,7 +537,7 @@ public class FormatHelper { String pattern = "\\s*" + Pattern.quote(op) + "\\s*"; return code.replaceAll(pattern, replacement); } - + /** * Format simple assignment = (not ==, !=, <=, >=, etc.) */ @@ -550,13 +550,13 @@ public class FormatHelper { // Check it's not part of ==, !=, <=, >=, +=, etc. char prev = i > 0 ? code.charAt(i - 1) : ' '; char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; - - boolean isCompound = prev == '!' || prev == '<' || prev == '>' || - prev == '+' || prev == '-' || prev == '*' || - prev == '/' || prev == '%' || prev == '&' || - prev == '|' || prev == '^'; + + boolean isCompound = prev == '!' || prev == '<' || prev == '>' || + prev == '+' || prev == '-' || prev == '*' || + prev == '/' || prev == '%' || prev == '&' || + prev == '|' || prev == '^'; boolean isEquality = next == '='; - + if (!isCompound && !isEquality) { // Ensure space before = if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { @@ -577,7 +577,7 @@ public class FormatHelper { } return result.toString(); } - + /** * Format < and > comparison operators (avoiding generics) */ @@ -586,11 +586,11 @@ public class FormatHelper { int i = 0; while (i < code.length()) { char c = code.charAt(i); - + if ((c == '<' || c == '>') && !isLikelyGeneric(code, i, c)) { char prev = i > 0 ? code.charAt(i - 1) : ' '; char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; - + // Skip if part of <<, >>, <=, >= if ((c == '<' && next == '<') || (c == '>' && next == '>') || next == '=' || prev == '<' || prev == '>') { @@ -612,7 +612,7 @@ public class FormatHelper { } return result.toString(); } - + /** * Check if < or > is likely part of a generic type declaration */ @@ -651,7 +651,7 @@ public class FormatHelper { return depth == 0; } } - + /** * Format arithmetic operators (+, -, *, /, %) */ @@ -662,9 +662,9 @@ public class FormatHelper { char c = code.charAt(i); char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; char prev = i > 0 ? code.charAt(i - 1) : ' '; - + boolean isArithOp = c == '+' || c == '-' || c == '*' || c == '/' || c == '%'; - + if (isArithOp) { // Skip ++, --, +=, -=, *=, /=, %=, and unary +/- if ((c == '+' && next == '+') || (c == '-' && next == '-') || @@ -691,41 +691,41 @@ public class FormatHelper { } return result.toString(); } - + /** * Check if position is in a unary context (operator is unary, not binary) */ private boolean isUnaryContext(String code, int pos) { if (pos == 0) return true; - + // Look back for context int i = pos - 1; while (i >= 0 && code.charAt(i) == ' ') { i--; } - + if (i < 0) return true; - + char prev = code.charAt(i); // Unary context after: ( [ { , ; = + - * / % < > & | ^ ? : - return prev == '(' || prev == '[' || prev == '{' || prev == ',' || - prev == ';' || prev == '=' || prev == '+' || prev == '-' || - prev == '*' || prev == '/' || prev == '%' || prev == '<' || - prev == '>' || prev == '&' || prev == '|' || prev == '^' || - prev == '?' || prev == ':' || prev == '!'; + return prev == '(' || prev == '[' || prev == '{' || prev == ',' || + prev == ';' || prev == '=' || prev == '+' || prev == '-' || + prev == '*' || prev == '/' || prev == '%' || prev == '<' || + prev == '>' || prev == '&' || prev == '|' || prev == '^' || + prev == '?' || prev == ':' || prev == '!'; } - + /** * Format bitwise operators */ private String formatBitwiseOperators(String code) { String result = code; - + // Shift operators first result = formatBinaryOp(result, ">>>", " >>> "); result = formatBinaryOp(result, "<<", " << "); result = formatBinaryOp(result, ">>", " >> "); - + // Single & | ^ (not && ||) StringBuilder sb = new StringBuilder(); int i = 0; @@ -733,7 +733,7 @@ public class FormatHelper { char c = result.charAt(i); char next = i + 1 < result.length() ? result.charAt(i + 1) : ' '; char prev = i > 0 ? result.charAt(i - 1) : ' '; - + if ((c == '&' && next != '&' && prev != '&' && next != '=') || (c == '|' && next != '|' && prev != '|' && next != '=') || (c == '^' && next != '=')) { @@ -749,18 +749,18 @@ public class FormatHelper { } i++; } - + return sb.toString(); } - + // ==================== BRACKET FORMATTING ==================== - + /** * Format bracket spacing */ private String formatBrackets(String code) { String result = code; - + // Parentheses if (settings.spaceWithinParens) { result = result.replaceAll("\\(\\s*", "( "); @@ -769,7 +769,7 @@ public class FormatHelper { result = result.replaceAll("\\(\\s+", "("); result = result.replaceAll("\\s+\\)", ")"); } - + // Square brackets if (settings.spaceWithinBrackets) { result = result.replaceAll("\\[\\s*", "[ "); @@ -778,69 +778,69 @@ public class FormatHelper { result = result.replaceAll("\\[\\s+", "["); result = result.replaceAll("\\s+\\]", "]"); } - + // Curly braces (for array initializers inline) // Note: standalone braces for blocks are handled by indentation logic - + return result; } - + // ==================== KEYWORD FORMATTING ==================== - + /** * Format keyword spacing (if, while, for, etc.) */ private String formatKeywords(String code) { String result = code; - + if (settings.spaceBeforeIfParens) { result = result.replaceAll("\\bif\\s*\\(", "if ("); } else { result = result.replaceAll("\\bif\\s+\\(", "if("); } - + if (settings.spaceBeforeWhileParens) { result = result.replaceAll("\\bwhile\\s*\\(", "while ("); } else { result = result.replaceAll("\\bwhile\\s+\\(", "while("); } - + if (settings.spaceBeforeForParens) { result = result.replaceAll("\\bfor\\s*\\(", "for ("); } else { result = result.replaceAll("\\bfor\\s+\\(", "for("); } - + if (settings.spaceBeforeSwitchParens) { result = result.replaceAll("\\bswitch\\s*\\(", "switch ("); } else { result = result.replaceAll("\\bswitch\\s+\\(", "switch("); } - + if (settings.spaceBeforeCatchParens) { result = result.replaceAll("\\bcatch\\s*\\(", "catch ("); } else { result = result.replaceAll("\\bcatch\\s+\\(", "catch("); } - + // Space before opening brace if (settings.spaceBeforeOpenBrace) { result = result.replaceAll("\\)\\s*\\{", ") {"); } else { result = result.replaceAll("\\)\\s+\\{", "){"); } - + return result; } - + // ==================== PUNCTUATION FORMATTING ==================== - + /** * Format punctuation spacing (commas, semicolons, colons) */ private String formatPunctuation(String code) { String result = code; - + // Commas if (settings.spaceAfterComma && !settings.spaceBeforeComma) { result = result.replaceAll("\\s*,\\s*", ", "); @@ -851,17 +851,17 @@ public class FormatHelper { } else { result = result.replaceAll("\\s*,\\s*", ","); } - + // Semicolons (but not in for loops - handle more carefully) // This is tricky because for(;;) should be handled differently // For now, just handle trailing semicolons if (settings.trimTrailingWhitespace) { result = result.replaceAll("\\s+;", ";"); } - + return result; } - + /** * Trim trailing whitespace from a line */ @@ -872,43 +872,43 @@ public class FormatHelper { } return line.substring(0, end); } - + // ==================== STATIC UTILITIES ==================== - + /** * Format code with default settings */ public static String formatCode(String code) { return new FormatHelper().format(code); } - + /** * Format code with custom settings */ public static String formatCode(String code, FormatSettings settings) { return new FormatHelper(settings).format(code); } - + // ==================== LINE WRAPPING ==================== - + /** * Wrap long lines according to settings. * Call this after format() if wrapLongLines is enabled. - * - * @param text The text to wrap + * + * @param text The text to wrap * @param maxWidth Maximum line width (use settings.maxLineLength if 0) * @return Text with wrapped lines */ public String wrapLines(String text, int maxWidth) { if (text == null || text.isEmpty()) return text; - + int width = maxWidth > 0 ? maxWidth : settings.maxLineLength; String[] lines = text.split("\n", -1); StringBuilder result = new StringBuilder(); - + for (int i = 0; i < lines.length; i++) { String line = lines[i]; - + if (getVisualLength(line) <= width) { result.append(line); } else { @@ -922,15 +922,15 @@ public class FormatHelper { result.append(wrapCodeLine(line, width)); } } - + if (i < lines.length - 1) { result.append("\n"); } } - + return result.toString(); } - + /** * Get visual length of a line (tabs count as 4 spaces) */ @@ -945,7 +945,7 @@ public class FormatHelper { } return len; } - + /** * Extract leading indentation from a line */ @@ -956,67 +956,67 @@ public class FormatHelper { } return line.substring(0, i); } - + /** * Wrap a single-line comment (// style) */ private String wrapCommentLine(String line, int maxWidth) { if (!settings.wrapComments) return line; - + String indent = extractIndent(line); String content = line.substring(indent.length()); - + // Must start with // if (!content.startsWith("//")) return line; - + String commentContent = content.substring(2).trim(); String prefix = indent + "// "; int prefixLen = getVisualLength(prefix); int contentWidth = maxWidth - prefixLen; - + if (contentWidth <= 10) return line; // Too narrow to wrap - + StringBuilder result = new StringBuilder(); result.append(indent).append("// "); - + String[] words = commentContent.split("\\s+"); int lineLen = prefixLen; - + for (int i = 0; i < words.length; i++) { String word = words[i]; int wordLen = word.length(); - + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { // Start new line result.append("\n").append(prefix); lineLen = prefixLen; } - + if (lineLen > prefixLen) { result.append(" "); lineLen++; } - + result.append(word); lineLen += wordLen; } - + return result.toString(); } - + /** * Wrap a block comment line (* style) */ private String wrapBlockCommentLine(String line, int maxWidth) { if (!settings.wrapComments) return line; - + String indent = extractIndent(line); String content = line.substring(indent.length()).trim(); - + // Determine prefix for wrapped lines String prefix; String textContent; - + if (content.startsWith("/**")) { prefix = indent + " * "; textContent = content.substring(3).trim(); @@ -1029,20 +1029,20 @@ public class FormatHelper { } else { return line; } - + // Handle closing */ boolean hasClose = textContent.endsWith("*/"); if (hasClose) { textContent = textContent.substring(0, textContent.length() - 2).trim(); } - + int prefixLen = getVisualLength(prefix); int contentWidth = maxWidth - prefixLen; - + if (contentWidth <= 10) return line; - + StringBuilder result = new StringBuilder(); - + // First line keeps original prefix if (content.startsWith("/**")) { result.append(indent).append("/** "); @@ -1051,72 +1051,72 @@ public class FormatHelper { } else { result.append(indent).append(" * "); } - + String[] words = textContent.split("\\s+"); int lineLen = prefixLen; - + for (int i = 0; i < words.length; i++) { String word = words[i]; int wordLen = word.length(); - + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { result.append("\n").append(prefix); lineLen = prefixLen; } - + if (lineLen > prefixLen) { result.append(" "); lineLen++; } - + result.append(word); lineLen += wordLen; } - + if (hasClose) { result.append(" */"); } - + return result.toString(); } - + /** * Wrap a code line at appropriate break points */ private String wrapCodeLine(String line, int maxWidth) { String indent = extractIndent(line); String content = line.substring(indent.length()); - + // Additional indent for wrapped lines String wrapIndent = indent + spaces(settings.wrapIndentSpaces); - + // Find break points - positions where we can break the line List breakPoints = findBreakPoints(content); - + if (breakPoints.isEmpty()) { // No good break points, just return as-is return line; } - + StringBuilder result = new StringBuilder(); result.append(indent); - + int currentLen = getVisualLength(indent); int lastBreak = 0; boolean isFirstLine = true; - + for (BreakPoint bp : breakPoints) { int segmentEnd = bp.position; String segment = content.substring(lastBreak, segmentEnd); int segmentLen = getVisualLength(segment); - + // Check if adding this segment would exceed max width if (currentLen + segmentLen > maxWidth && !isFirstLine) { // Break here result.append("\n").append(wrapIndent); currentLen = getVisualLength(wrapIndent); isFirstLine = false; - + // Trim leading space from continuation if any segment = trimLeading(segment); } else if (currentLen + segmentLen > maxWidth && isFirstLine && lastBreak > 0) { @@ -1125,29 +1125,29 @@ public class FormatHelper { currentLen = getVisualLength(wrapIndent); isFirstLine = false; } - + result.append(segment); currentLen += getVisualLength(segment); lastBreak = segmentEnd; isFirstLine = false; } - + // Append remaining content if (lastBreak < content.length()) { String remaining = content.substring(lastBreak); int remLen = getVisualLength(remaining); - + if (currentLen + remLen > maxWidth && !isFirstLine) { result.append("\n").append(wrapIndent); remaining = trimLeading(remaining); } - + result.append(remaining); } - + return result.toString(); } - + /** * Trim leading whitespace (Java 8 compatible) */ @@ -1158,7 +1158,7 @@ public class FormatHelper { } return s.substring(start); } - + /** * A potential line break point */ @@ -1166,29 +1166,29 @@ public class FormatHelper { int position; // Position in string (after this char) @SuppressWarnings("unused") int priority; // Lower = better place to break (for future sorting) - + BreakPoint(int pos, int prio) { this.position = pos; this.priority = prio; } } - + /** * Find good positions to break a line */ private List findBreakPoints(String content) { List points = new ArrayList<>(); - + int parenDepth = 0; int bracketDepth = 0; int braceDepth = 0; boolean inString = false; char stringChar = 0; - + for (int i = 0; i < content.length(); i++) { char c = content.charAt(i); char prev = i > 0 ? content.charAt(i - 1) : ' '; - + // Track string state if ((c == '"' || c == '\'') && prev != '\\') { if (!inString) { @@ -1198,9 +1198,9 @@ public class FormatHelper { inString = false; } } - + if (inString) continue; - + // Track bracket depth if (c == '(') parenDepth++; else if (c == ')') parenDepth--; @@ -1208,16 +1208,16 @@ public class FormatHelper { else if (c == ']') bracketDepth--; else if (c == '{') braceDepth++; else if (c == '}') braceDepth--; - + // Calculate total nesting depth (prefer breaking at lower depth) int depth = parenDepth + bracketDepth + braceDepth; - + // Find break points if (settings.wrapAfterComma && c == ',') { // After comma is a good break point (better at lower depth) points.add(new BreakPoint(i + 1, 1 + depth)); } - + if (settings.wrapAfterOperator) { // After binary operators if ((c == '+' || c == '-') && i > 0 && i < content.length() - 1) { @@ -1234,24 +1234,24 @@ public class FormatHelper { points.add(new BreakPoint(i + 2, 2 + depth)); } } - + if (settings.wrapBeforeDot && c == '.') { // Before dot (for method chaining) points.add(new BreakPoint(i, 2 + depth)); } - + // After opening brace if (c == '{' && i + 1 < content.length()) { points.add(new BreakPoint(i + 1, 1)); } } - + // Sort by position points.sort((a, b) -> Integer.compare(a.position, b.position)); - + return points; } - + /** * Create a string of n spaces */ @@ -1262,7 +1262,7 @@ public class FormatHelper { } return sb.toString(); } - + /** * Format and wrap code in one step */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index 7a8217493..f054f32a7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -5,7 +5,6 @@ import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.util.ChatAllowedCharacters; import net.minecraft.util.ResourceLocation; -import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; @@ -13,7 +12,7 @@ import org.lwjgl.opengl.GL11; /** * Go To Line:Column dialog for the script editor. * Provides IntelliJ-like "Go to Line" functionality (Ctrl+G). - * + *

* Input format: "line" or "line:column" * Example: "42" goes to line 42, column 1 * Example: "42:15" goes to line 42, column 15 @@ -55,26 +54,35 @@ public class GoToLineDialog { * Callback interface for go-to-line operations */ public interface GoToLineCallback { - /** Get total line count */ + /** + * Get total line count + */ int getLineCount(); - /** Get the column count for a specific line (0-indexed) */ + /** + * Get the column count for a specific line (0-indexed) + */ int getColumnCount(int lineIndex); - /** Navigate to specific line and column (both 1-indexed for user display) */ + /** + * Navigate to specific line and column (both 1-indexed for user display) + */ void goToLineColumn(int line, int column); - /** Called when dialog closes - should restore focus to editor */ + /** + * Called when dialog closes - should restore focus to editor + */ void onDialogClose(); void unfocusMainEditor(); - + void focusMainEditor(); } - + public void setCallback(GoToLineCallback callback) { this.callback = callback; } + /** * Initialize/update position for the dialog */ @@ -163,7 +171,7 @@ public class GoToLineDialog { int fieldY = y + padding + 14 + 5; textFieldWidth = dialogWidth - padding * 2; drawTextField(fieldX, fieldY, textFieldWidth, textFieldHeight, inputText, cursor, - selectionStart, selectionEnd, scrollOffset, focused); + selectionStart, selectionEnd, scrollOffset, focused); if (inputText.isEmpty()) font.drawString("[Line][:Column]", x + padding + 6, y + padding + 23, 0xFF888888); @@ -228,7 +236,7 @@ public class GoToLineDialog { int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), - Math.min(maxSel, visibleText.length())); + Math.min(maxSel, visibleText.length())); int selX = textX + font.getStringWidth(beforeSel); int selW = font.getStringWidth(inSel); Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); @@ -240,7 +248,7 @@ public class GoToLineDialog { // Draw cursor if (isFocused && shouldShowCursor()) { String beforeCursor = adjustedCursor > 0 && adjustedCursor <= visibleText.length() - ? visibleText.substring(0, adjustedCursor) : ""; + ? visibleText.substring(0, adjustedCursor) : ""; int cursorX = textX + font.getStringWidth(beforeCursor); Gui.drawRect(cursorX, textY - 1, cursorX + 1, textY + 9, 0xFFe0e0e0); } @@ -258,13 +266,13 @@ public class GoToLineDialog { int fieldX = x + padding; int fieldY = y + padding + 14; if (mouseX >= fieldX && mouseX < fieldX + textFieldWidth && - mouseY >= fieldY && mouseY < fieldY + textFieldHeight) { + mouseY >= fieldY && mouseY < fieldY + textFieldHeight) { focused = true; if (callback != null) callback.unfocusMainEditor(); // Position cursor based on click int clickX = mouseX - fieldX - 4; String visibleText = scrollOffset > 0 && scrollOffset < inputText.length() - ? inputText.substring(scrollOffset) : inputText; + ? inputText.substring(scrollOffset) : inputText; int newCursor = getCharIndexAtX(visibleText, clickX) + scrollOffset; cursor = Math.max(0, Math.min(newCursor, inputText.length())); selectionStart = cursor; @@ -297,7 +305,7 @@ public class GoToLineDialog { close(); return true; } - + if (!visible || !focused) return false; @@ -617,4 +625,4 @@ public class GoToLineDialog { public int getTotalHeight() { return visible ? dialogHeight + 10 : 0; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java index a978dc724..12a5567ad 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.Arrays; import java.util.List; @@ -8,9 +9,9 @@ import java.util.List; * Helper class for indentation-related operations. */ public class IndentHelper { - + public static final int TAB_SIZE = 4; - + /** * Get leading whitespace count of a line */ @@ -23,7 +24,7 @@ public class IndentHelper { } return leading; } - + /** * Extract leading whitespace as string */ @@ -31,7 +32,7 @@ public class IndentHelper { int indent = getLineIndent(lineText); return lineText.substring(0, indent); } - + /** * Create a string of n spaces */ @@ -41,16 +42,16 @@ public class IndentHelper { Arrays.fill(arr, ' '); return new String(arr); } - + /** * Calculate expected indent for a line based on context */ public static int getExpectedIndent(LineData currentLine, List allLines) { if (currentLine == null || allLines == null) return 0; - + int idx = allLines.indexOf(currentLine); if (idx <= 0) return 0; - + // Find previous non-empty line LineData prevLine = null; for (int i = idx - 1; i >= 0; i--) { @@ -60,40 +61,40 @@ public class IndentHelper { break; } } - + if (prevLine == null) return 0; - + int prevIndent = getLineIndent(prevLine.text); String prevTrimmed = prevLine.text.trim(); String currTrimmed = currentLine.text.trim(); - + // If current line starts with }, expected is one level back if (currTrimmed.startsWith("}")) { return Math.max(0, prevIndent - TAB_SIZE); } - + // If previous line ends with {, expected is one level forward if (prevTrimmed.endsWith("{")) { return prevIndent + TAB_SIZE; } - + return prevIndent; } - + /** * Calculate auto-indent for Enter key */ public static String getAutoIndentForEnter(String lineText, int cursorInLine) { int leading = 0; - while (leading < lineText.length() && - (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { + while (leading < lineText.length() && + (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { leading++; } - + String baseIndent = lineText.substring(0, leading); int relativeCursor = Math.max(0, Math.min(cursorInLine, lineText.length())); String beforeCursor = lineText.substring(0, relativeCursor); - + // Find last non-whitespace before cursor int lastNonWs = -1; for (int i = beforeCursor.length() - 1; i >= 0; i--) { @@ -102,7 +103,7 @@ public class IndentHelper { break; } } - + // Add extra indent if last char is { // Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace) // Check that the last non-whitespace is at or after the line's indent position @@ -110,10 +111,10 @@ public class IndentHelper { if (opensBlock) { return baseIndent + spaces(TAB_SIZE); } - + return baseIndent; } - + /** * Calculate target indent for Tab key */ @@ -136,62 +137,63 @@ public class IndentHelper { } } } - + /** * Calculate target indent for Shift+Tab */ public static int calculateShiftTabTargetIndent(int currentIndent) { return Math.max(0, ((currentIndent - 1) / TAB_SIZE) * TAB_SIZE); } - + /** * Format entire text with proper indentation and operator spacing */ public static FormatResult formatText(String text, int cursorPosition) { return formatText(text, cursorPosition, 0); } - + /** * Format entire text with proper indentation, operator spacing, and optional line wrapping - * @param text The text to format + * + * @param text The text to format * @param cursorPosition Current cursor position - * @param viewportWidth Viewport width for line wrapping (0 = no wrapping) + * @param viewportWidth Viewport width for line wrapping (0 = no wrapping) */ public static FormatResult formatText(String text, int cursorPosition, int viewportWidth) { // First pass: fix indentation String indented = formatIndentation(text, cursorPosition); - + // Second pass: apply operator spacing and whitespace normalization FormatHelper helper = new FormatHelper(); FormatHelper.FormatSettings settings = helper.getSettings(); - + // Enable wrapping if viewport width is provided if (viewportWidth > 0) { settings.wrapLongLines = true; settings.wrapComments = true; // Use 75% of viewport width as max line length for some margin int charWidth = 6; // Approximate character width - settings.maxLineLength = Math.max(60, (int)(viewportWidth * 0.8f / charWidth)); + settings.maxLineLength = Math.max(60, (int) (viewportWidth * 0.8f / charWidth)); } - + String formatted = helper.format(indented); - + // Apply wrapping if enabled if (viewportWidth > 0 && settings.wrapLongLines) { formatted = helper.wrapLines(formatted, settings.maxLineLength); } - + // Recalculate cursor position (try to maintain relative position) int newCursorPos = Math.min(cursorPosition, formatted.length()); - + // Try to find cursor on same line with same content context String[] origLines = text.split("\n", -1); String[] newLines = formatted.split("\n", -1); - + int lineStartPos = 0; int cursorLine = -1; int cursorColInLine = 0; - + for (int li = 0; li < origLines.length; li++) { int lineEndPos = lineStartPos + origLines[li].length(); if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { @@ -201,7 +203,7 @@ public class IndentHelper { } lineStartPos = lineEndPos + 1; } - + if (cursorLine >= 0 && cursorLine < newLines.length) { int newLineStart = 0; for (int li = 0; li < cursorLine; li++) { @@ -210,51 +212,66 @@ public class IndentHelper { // Clamp cursor to new line length newCursorPos = newLineStart + Math.min(cursorColInLine, newLines[cursorLine].length()); } - + return new FormatResult(formatted, newCursorPos); } - + /** * Format indentation only (first pass) */ private static String formatIndentation(String text, int cursorPosition) { String[] linesArr = text.split("\n", -1); StringBuilder out = new StringBuilder(text.length() + 32); - + boolean inString = false; boolean escape = false; boolean inBlockComment = false; int depth = 0; - + for (int li = 0; li < linesArr.length; li++) { String line = linesArr[li]; String trimmedLeading = line.replaceAll("^[ \\t]+", ""); int originalIndent = getLineIndent(line); - + int opens = 0, closes = 0; boolean startsWithClose = false; - + // Parse line for braces for (int idx = 0; idx < line.length(); idx++) { char c = line.charAt(idx); char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0; - + if (inString) { - if (escape) { escape = false; } - else if (c == '\\') { escape = true; } - else if (c == '"') { inString = false; } + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == '"') { + inString = false; + } continue; } - + if (inBlockComment) { - if (c == '*' && next == '/') { inBlockComment = false; idx++; } + if (c == '*' && next == '/') { + inBlockComment = false; + idx++; + } continue; } - + if (c == '/' && next == '/') break; - if (c == '/' && next == '*') { inBlockComment = true; idx++; continue; } - if (c == '"') { inString = true; escape = false; continue; } - + if (c == '/' && next == '*') { + inBlockComment = true; + idx++; + continue; + } + if (c == '"') { + inString = true; + escape = false; + continue; + } + if (c == '{') { opens++; } else if (c == '}') { @@ -264,11 +281,11 @@ public class IndentHelper { } } } - + int indentLevel = depth; if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1); int targetIndent = indentLevel * TAB_SIZE; - + // Check if this is a continuation line: // 1. Line starts with operator/dot (explicit continuation) // 2. Previous line ends with operator (implicit continuation - this line continues it) @@ -279,7 +296,7 @@ public class IndentHelper { String prevLine = linesArr[li - 1].trim(); isContinuation = lineEndsWithContinuationOperator(prevLine); } - + if (isContinuation && originalIndent > targetIndent) { // This is a continuation line - preserve its extra indent out.append(spaces(originalIndent)).append(trimmedLeading); @@ -287,13 +304,13 @@ public class IndentHelper { out.append(spaces(targetIndent)).append(trimmedLeading); } if (li < linesArr.length - 1) out.append('\n'); - + depth = Math.max(0, depth + opens - closes); } - + return out.toString(); } - + /** * Check if a line appears to be a continuation of a previous line * (i.e., starts with an operator, dot, or other continuation character) @@ -302,7 +319,7 @@ public class IndentHelper { if (trimmedContent.isEmpty()) return false; char firstChar = trimmedContent.charAt(0); // Lines starting with these are likely continuations - if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || firstChar == '*' || firstChar == '/' || firstChar == '%' || firstChar == '&' || firstChar == '|' || firstChar == '^' || firstChar == '?' || firstChar == ':' || firstChar == ',') { @@ -314,7 +331,7 @@ public class IndentHelper { } return false; } - + /** * Check if a line ends with an operator that indicates the next line is a continuation */ @@ -326,10 +343,10 @@ public class IndentHelper { trimmedLine = trimmedLine.substring(0, commentIdx).trim(); } if (trimmedLine.isEmpty()) return false; - + // Check if ends with operator char lastChar = trimmedLine.charAt(trimmedLine.length() - 1); - if (lastChar == '+' || lastChar == '-' || lastChar == '*' || + if (lastChar == '+' || lastChar == '-' || lastChar == '*' || lastChar == '/' || lastChar == '%' || lastChar == '=' || lastChar == '&' || lastChar == '|' || lastChar == '^' || lastChar == '?' || lastChar == ':' || lastChar == ',' || @@ -343,17 +360,17 @@ public class IndentHelper { } return false; } - + /** * Result of text formatting */ public static class FormatResult { public final String text; public final int cursorPosition; - + public FormatResult(String text, int cursorPosition) { this.text = text; this.cursorPosition = cursorPosition; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index ccdd9cc44..fe09cb81a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -11,37 +11,38 @@ import java.util.regex.Pattern; public class JavaTextContainer extends TextContainer { public static final Pattern MODIFIER = Pattern.compile( - "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); + "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); public static final Pattern KEYWORD = Pattern.compile( - "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); public static final Pattern CLASS_DECL = Pattern.compile("\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); - public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)");; + public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + ; public static final Pattern METHOD_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type - "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name ); public static final Pattern METHOD_CALL = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); // Class-level global fields public static final Pattern GLOBAL_FIELD_DECL = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); // Local fields inside methods (simplified) public static final Pattern LOCAL_FIELD_DECL = Pattern.compile( - "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); public static final Pattern STRING = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); public static final Pattern COMMENT = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); public static final Pattern NUMBER = Pattern.compile( - "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); + "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); - // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) - // Allows optional whitespace (including newlines) around the dots so wrapped imports match. - // Match imports while allowing optional whitespace/newlines around dots. - public static final Pattern IMPORT = Pattern.compile( - "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*?)?\\s*(?:;|$)"); + // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) + // Allows optional whitespace (including newlines) around the dots so wrapped imports match. + // Match imports while allowing optional whitespace/newlines around dots. + public static final Pattern IMPORT = Pattern.compile( + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*?)?\\s*(?:;|$)"); // ClassPathFinder for resolving imports and determining class types private final ClassPathFinder classPathFinder = new ClassPathFinder(); @@ -73,10 +74,10 @@ public class JavaTextContainer extends TextContainer { return pathStart + orig.length(); } - public void init(String text,int width, int height) { + public void init(String text, int width, int height) { this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); lines.clear(); - String[] split = text.split("\n",-1); + String[] split = text.split("\n", -1); int totalChars = 0; for (String l : split) { @@ -196,32 +197,38 @@ public class JavaTextContainer extends TextContainer { } } } - + /** * Parse and highlight generic type parameters using ClassPathFinder. * Handles arbitrarily nested generics like "Map>>". */ private void highlightGenericTypes(String genericContent, int contentStart, List marks, List excluded) { if (genericContent == null || genericContent.isEmpty()) return; - + List occurrences = classPathFinder.parseGenericTypes(genericContent, importedClasses, importedPackages); - + for (ClassPathFinder.TypeOccurrence occ : occurrences) { int absStart = contentStart + occ.startOffset; int absEnd = contentStart + occ.endOffset; - + if (!isInExcludedRange(absStart, excluded)) { TokenType tokenType; switch (occ.type) { - case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; - case ENUM: tokenType = TokenType.ENUM_DECL; break; - default: tokenType = TokenType.IMPORTED_CLASS; break; + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; + default: + tokenType = TokenType.IMPORTED_CLASS; + break; } marks.add(new Mark(absStart, absEnd, tokenType)); } } } - + private void collectFields() { globalFields.clear(); localFields.clear(); @@ -229,7 +236,7 @@ public class JavaTextContainer extends TextContainer { // Extract method blocks first methodBlocks = MethodBlock.collectMethodBlocks(text); - + // Get excluded ranges (strings and comments) for the entire text List excludedRanges = MethodBlock.getExcludedRanges(text); @@ -238,7 +245,7 @@ public class JavaTextContainer extends TextContainer { while (mGlobal.find()) { String varName = mGlobal.group(2); int varPosition = mGlobal.start(2); - + // Skip if inside a string or comment if (isInExcludedRange(varPosition, excludedRanges)) { continue; @@ -298,7 +305,7 @@ public class JavaTextContainer extends TextContainer { "String", "Object", "Array", "Math", "System", "Integer", "Double", "Float", "Boolean", "Long", "Byte", "Short", "Character", "List", "Map", "Set" )); - + // First pass: handle field accesses (this.field and obj.field patterns) Pattern thisFieldPattern = Pattern.compile("\\bthis\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); Matcher thisFieldMatcher = thisFieldPattern.matcher(text); @@ -312,7 +319,7 @@ public class JavaTextContainer extends TextContainer { marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.UNDEFINED_VAR)); } } - + // Handle identifier.field patterns (e.g., obj.field, global.field) // The field part should be highlighted as GLOBAL_FIELD (light blue) // Also, detect chained accesses like `a.b.c` and treat subsequent segments @@ -378,13 +385,13 @@ public class JavaTextContainer extends TextContainer { marks.add(new Mark(fieldPos, objFieldMatcher.end(2), TokenType.GLOBAL_FIELD)); } } - + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); while (m.find()) { String name = m.group(1); int position = m.start(1); - + // Skip known keywords if (knownIdentifiers.contains(name)) continue; @@ -462,24 +469,24 @@ public class JavaTextContainer extends TextContainer { } return false; } - + /** * Check if the identifier at this position is a field access (preceded by a dot) * e.g., in "obj.field", the "field" part is a field access */ private boolean isFieldAccess(int position) { if (position <= 0) return false; - + // Look backwards from position, skipping any whitespace int i = position - 1; while (i >= 0 && Character.isWhitespace(text.charAt(i))) { i--; } - + // Check if preceded by a dot return i >= 0 && text.charAt(i) == '.'; } - + /** * Check if the identifier at this position is a method call (followed by parenthesis) */ @@ -497,7 +504,7 @@ public class JavaTextContainer extends TextContainer { // Check for opening paren return i < text.length() && text.charAt(i) == '('; } - + /** * Check if this looks like a type reference (e.g., part of a declaration or generic) * But NOT a comparison like "i < container" which uses < as less-than operator @@ -510,7 +517,7 @@ public class JavaTextContainer extends TextContainer { return true; } } - + // Check if preceded by < but make sure it's a generic, not a comparison // Generic: Type tokenStarts = new ArrayList<>(); List tokenEnds = new ArrayList<>(); Matcher tokm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); - while (tokm.find()) { tokens.add(tokm.group()); tokenStarts.add(tokm.start()); tokenEnds.add(tokm.end()); } + while (tokm.find()) { + tokens.add(tokm.group()); + tokenStarts.add(tokm.start()); + tokenEnds.add(tokm.end()); + } int tokenCount = tokenStarts.size(); if (tokenCount > 0) { @@ -713,9 +736,15 @@ public class JavaTextContainer extends TextContainer { TokenType segType = TokenType.IMPORTED_CLASS; if (outerInfo != null) { switch (outerInfo.type) { - case INTERFACE: segType = TokenType.INTERFACE_DECL; break; - case ENUM: segType = TokenType.ENUM_DECL; break; - default: segType = TokenType.IMPORTED_CLASS; break; + case INTERFACE: + segType = TokenType.INTERFACE_DECL; + break; + case ENUM: + segType = TokenType.ENUM_DECL; + break; + default: + segType = TokenType.IMPORTED_CLASS; + break; } } marks.add(new Mark(s, e, segType)); @@ -748,7 +777,6 @@ public class JavaTextContainer extends TextContainer { } } - private void collectImportedClassUsages(List marks) { List excluded = MethodBlock.getExcludedRanges(text); @@ -779,7 +807,7 @@ public class JavaTextContainer extends TextContainer { boolean isShadowed = globalFields.contains(className) || localFields.contains(className); if (block != null) { isShadowed = isShadowed || block.parameters.contains(className) || block.isLocalDeclaredAtPosition( - className, start); + className, start); } if (isImported && !isShadowed) { @@ -788,8 +816,12 @@ public class JavaTextContainer extends TextContainer { TokenType tokenType = TokenType.IMPORTED_CLASS; if (info != null) { switch (info.type) { - case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; - case ENUM: tokenType = TokenType.ENUM_DECL; break; + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; } } marks.add(new Mark(start, end, tokenType)); @@ -819,7 +851,7 @@ public class JavaTextContainer extends TextContainer { } } } - + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); @@ -855,7 +887,7 @@ public class JavaTextContainer extends TextContainer { // Compute indent guides based on matched braces, ignoring strings/comments computeIndentGuides(marks); - + // Step 2: Clear existing tokens for (LineData line : lines) { line.tokens.clear(); @@ -880,7 +912,7 @@ public class JavaTextContainer extends TextContainer { if (cursor < tokenStart) { int end = Math.min(tokenStart, text.length()); line.tokens.add( - new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); + new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); } // token text @@ -910,50 +942,53 @@ public class JavaTextContainer extends TextContainer { private void collectTypeDeclarations(List marks) { List excluded = MethodBlock.getExcludedRanges(text); - + // Pattern to find type declarations: Type<...> variableName or Type variableName // We'll manually parse the generic portion to handle arbitrary nesting Pattern typeStart = Pattern.compile( "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + - "([A-Z][a-zA-Z0-9_]*)\\s*" // Group 1 → type name (must start with uppercase) + "([A-Z][a-zA-Z0-9_]*)\\s*" // Group 1 → type name (must start with uppercase) ); - + for (LineData ld : lines) { int lineStart = ld.start; int lineEnd = ld.end; if (lineStart >= lineEnd) continue; String s = ld.text; - + Matcher m = typeStart.matcher(s); int searchFrom = 0; - + while (m.find(searchFrom)) { int typeNameStart = lineStart + m.start(1); int typeNameEnd = lineStart + m.end(1); - + // Skip if in excluded range boolean skip = false; for (int[] r : excluded) { - if (typeNameStart < r[1] && typeNameEnd > r[0]) { skip = true; break; } + if (typeNameStart < r[1] && typeNameEnd > r[0]) { + skip = true; + break; + } } if (skip) { searchFrom = m.end(); continue; } - + String typeName = m.group(1); int posAfterType = m.end(1); - + // Skip whitespace after type name while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { posAfterType++; } - + // Check for generic parameters String genericContent = null; int genericStart = -1; int genericEnd = -1; - + if (posAfterType < s.length() && s.charAt(posAfterType) == '<') { genericStart = posAfterType; int depth = 1; @@ -970,12 +1005,12 @@ public class JavaTextContainer extends TextContainer { posAfterType = genericEnd; } } - + // Skip whitespace after generics while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { posAfterType++; } - + // Check if this looks like a type declaration: // 1. Has generic content (e.g., List) - always a type // 2. Followed by a variable name (lowercase start or underscore) @@ -983,38 +1018,43 @@ public class JavaTextContainer extends TextContainer { boolean hasGeneric = genericContent != null && !genericContent.isEmpty(); boolean atEndOfLine = posAfterType >= s.length(); boolean followedByVarName = false; - + if (!atEndOfLine) { char nextChar = s.charAt(posAfterType); followedByVarName = Character.isLetter(nextChar) || nextChar == '_'; } - + // Accept as type if: has generics OR followed by variable name // This handles both `List myList` and wrapped lines like `List` if (hasGeneric || followedByVarName) { // This looks like a type declaration - + // Resolve the main type ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses, importedPackages); TokenType tokenType = TokenType.UNDEFINED_VAR; if (info != null) { switch (info.type) { - case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; - case ENUM: tokenType = TokenType.ENUM_DECL; break; - default: tokenType = TokenType.TYPE_DECL; + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; + default: + tokenType = TokenType.TYPE_DECL; } } marks.add(new Mark(typeNameStart, typeNameEnd, tokenType)); - + // Handle generic content if (hasGeneric && genericStart >= 0) { int absGenericStart = lineStart + genericStart; int absGenericEnd = lineStart + genericEnd; - + // Mark < and > as default marks.add(new Mark(absGenericStart, absGenericStart + 1, TokenType.DEFAULT)); marks.add(new Mark(absGenericEnd - 1, absGenericEnd, TokenType.DEFAULT)); - + // Parse and highlight all types in generic content int contentStart = lineStart + genericStart + 1; highlightGenericTypes(genericContent, contentStart, marks, excluded); @@ -1046,7 +1086,7 @@ public class JavaTextContainer extends TextContainer { } } } - + searchFrom = m.end(); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java index ef9567779..6158c85ed 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -11,17 +11,17 @@ public class MethodBlock { // pattern matches returnType + methodName + parentheses public static final Pattern METHOD_PATTERN = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type - "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name ); // Pattern for variable declarations (same as in JavaTextContainer) public static final Pattern LOCAL_VAR_DECL = Pattern.compile( - "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); - + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + // Pattern for method parameters: Type name (with optional ... for varargs) public static final Pattern PARAMETER_PATTERN = Pattern.compile( - "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); public int startOffset; public int endOffset; @@ -37,22 +37,22 @@ public class MethodBlock { extractParameters(); extractLocalVariables(); } - + // Extract method parameters from the method signature private void extractParameters() { parameters.clear(); - + // Find the parameter list between ( and ) int parenStart = text.indexOf('('); int parenEnd = text.indexOf(')'); - + if (parenStart < 0 || parenEnd <= parenStart) return; - + String paramList = text.substring(parenStart + 1, parenEnd); - + // Skip if empty if (paramList.trim().isEmpty()) return; - + // Split by comma (but be careful of generic types like List) Matcher m = PARAMETER_PATTERN.matcher(paramList); while (m.find()) { @@ -67,20 +67,20 @@ public class MethodBlock { private void extractLocalVariables() { localVariables.clear(); localVarPositions.clear(); - + // Get excluded ranges (strings and comments) for this method's text List excludedRanges = getExcludedRanges(text); - + Matcher m = LOCAL_VAR_DECL.matcher(text); while (m.find()) { String varName = m.group(2); int declPosition = m.start(); - + // Skip if inside a string or comment if (isInExcludedRange(declPosition, excludedRanges)) { continue; } - + // Check if this is a "this.field" assignment - if so, skip it // Look backwards from the declaration position to see if preceded by "this." boolean isThisFieldAssignment = false; @@ -91,7 +91,7 @@ public class MethodBlock { isThisFieldAssignment = true; } } - + if (!isThisFieldAssignment && !localVariables.contains(varName)) { localVariables.add(varName); // Store the absolute position in the full text (startOffset + relative position) @@ -99,7 +99,7 @@ public class MethodBlock { } } } - + // Check if a reference position comes before the local variable declaration // This handles cases like: global.field on line 7, then var global = "x" on line 9 // At line 7, the local 'global' doesn't exist yet, so it should refer to the global 'global' @@ -129,10 +129,10 @@ public class MethodBlock { // Matches: returnType methodName(...) { (non-greedy capture until first { ) Pattern methodPattern = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type - "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\([^)]*\\)\\s*" + // method name + parameters - "\\{", // opening brace - Pattern.MULTILINE); + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\([^)]*\\)\\s*" + // method name + parameters + "\\{", // opening brace + Pattern.MULTILINE); Matcher m = methodPattern.matcher(text); while (m.find()) { @@ -160,14 +160,14 @@ public class MethodBlock { Pattern stringPattern = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); Matcher stringMatcher = stringPattern.matcher(text); while (stringMatcher.find()) { - ranges.add(new int[] { stringMatcher.start(), stringMatcher.end() }); + ranges.add(new int[]{stringMatcher.start(), stringMatcher.end()}); } // Find all comments (block and line comments) Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*"); Matcher commentMatcher = commentPattern.matcher(text); while (commentMatcher.find()) { - ranges.add(new int[] { commentMatcher.start(), commentMatcher.end() }); + ranges.add(new int[]{commentMatcher.start(), commentMatcher.end()}); } if (ranges.isEmpty()) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java index 391ea0e14..5dd70e7ce 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java @@ -7,7 +7,6 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 4f36da54b..3d5c9909d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -1,7 +1,6 @@ package noppes.npcs.client.gui.util.script; import net.minecraft.client.gui.GuiScreen; -import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; @@ -12,7 +11,7 @@ import java.util.regex.Pattern; /** * Handles IntelliJ-like rename refactoring with scope-aware renaming. - * + *

* Features: * - Scope detection: local variables rename only within their block, global fields rename everywhere * - Visual feedback: primary occurrence has white border, others just highlighted @@ -33,7 +32,7 @@ public class RenameRefactorHandler { private int primaryOccurrenceEnd = -1; private List allOccurrences = new ArrayList<>(); private ScopeInfo scope = null; - + // For global scope, track positions that have local shadowing (to exclude them) private List localShadowedPositions = new ArrayList<>(); @@ -141,10 +140,10 @@ public class RenameRefactorHandler { initialWord = text.substring(wordBounds[0], wordBounds[1]); // Remember for undo originalWord = initialWord; currentWord = initialWord; - + // Determine scope for this identifier - MUST check if local shadows global scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer()); - + // If global scope, find all positions where local variables shadow this name localShadowedPositions.clear(); if (scope != null && scope.isGlobal) { @@ -158,12 +157,12 @@ public class RenameRefactorHandler { return false; active = true; - + // Set selection to full word (uses GuiScriptTextArea's selection) primaryOccurrenceStart = wordBounds[0]; primaryOccurrenceEnd = wordBounds[1]; callback.getSelectionState().setSelection(wordBounds[0], wordBounds[1]); - // callback.setCursorPosition(wordBounds[1]); + // callback.setCursorPosition(wordBounds[1]); callback.getSelectionState().markActivity(); return true; @@ -207,7 +206,7 @@ public class RenameRefactorHandler { // During live editing we used setTextWithoutUndo to avoid creating intermediate // undo entries; pushing the original snapshot here gives a single predictable // undo step back to the pre-rename state. - if (originalText != null && !originalText.isEmpty()) + if (originalText != null && !originalText.isEmpty()) callback.pushUndoState(originalText, originalCursorPos); // Calculate cursor position relative to primary occurrence @@ -237,6 +236,7 @@ public class RenameRefactorHandler { /** * Handle keyboard input during rename + * * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { @@ -264,7 +264,7 @@ public class RenameRefactorHandler { // Get selection state SelectionState sel = callback.getSelectionState(); boolean hasSelection = sel.hasSelection(); - + // Calculate cursor position within the word int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart; cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length())); @@ -315,7 +315,7 @@ public class RenameRefactorHandler { sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); callback.setCursorPosition(primaryOccurrenceStart); } - + // Left arrow if (keyCode == Keyboard.KEY_LEFT) { if (cursorInWord > 0) { @@ -354,7 +354,7 @@ public class RenameRefactorHandler { // Ctrl+A - select all (select the whole word) if (keyCode == Keyboard.KEY_A && - Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { + Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart + currentWord.length()); callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); sel.markActivity(); @@ -537,22 +537,22 @@ public class RenameRefactorHandler { } } } - + /** * Find all positions where local variables with the same name shadow the global variable. * This populates localShadowedPositions with ranges where local declarations exist. */ private void findLocalShadowedPositions(String text, String varName) { localShadowedPositions.clear(); - + List methods = MethodBlock.collectMethodBlocks(text); List excluded = MethodBlock.getExcludedRanges(text); - + for (MethodBlock method : methods) { // Check if this method has a local variable or parameter with the same name boolean hasLocalDecl = method.localVariables.contains(varName); boolean hasParamDecl = isParameterInMethod(text, varName, method); - + if (hasLocalDecl || hasParamDecl) { // Find where the local/param scope starts int scopeStart; @@ -564,18 +564,18 @@ public class RenameRefactorHandler { scopeStart = findLocalDeclarationPosition(text, varName, method); if (scopeStart < 0) scopeStart = method.startOffset; } - + // Find all occurrences of varName within this method's shadowed range Pattern pattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); Matcher m = pattern.matcher(text); - + while (m.find()) { int pos = m.start(); - + // Skip if in string or comment if (isInExcludedRange(pos, excluded)) continue; - + // Check if within the shadowed scope of this method if (pos >= scopeStart && pos < method.endOffset) { localShadowedPositions.add(new int[]{m.start(), m.end()}); @@ -584,7 +584,7 @@ public class RenameRefactorHandler { } } } - + /** * Check if a position is within a locally shadowed range */ @@ -595,13 +595,13 @@ public class RenameRefactorHandler { } return false; } - + /** * Check if varName is a parameter in the method */ private boolean isParameterInMethod(String text, String varName, MethodBlock method) { String methodHeader = text.substring(method.startOffset, - Math.min(method.startOffset + 500, method.endOffset)); + Math.min(method.startOffset + 500, method.endOffset)); int parenStart = methodHeader.indexOf('('); int parenEnd = methodHeader.indexOf(')'); if (parenStart >= 0 && parenEnd > parenStart) { @@ -687,7 +687,7 @@ public class RenameRefactorHandler { // Cursor is inside a method // Check if it's a method parameter first String methodHeader = text.substring(containingMethod.startOffset, - Math.min(containingMethod.startOffset + 500, containingMethod.endOffset)); + Math.min(containingMethod.startOffset + 500, containingMethod.endOffset)); int parenStart = methodHeader.indexOf('('); int parenEnd = methodHeader.indexOf(')'); if (parenStart >= 0 && parenEnd > parenStart) { @@ -861,9 +861,9 @@ public class RenameRefactorHandler { public boolean isWordFullySelected() { if (callback == null) return false; SelectionState selection = callback.getSelectionState(); - return selection.hasSelection() && - selection.getStartSelection() == primaryOccurrenceStart && - selection.getEndSelection() == primaryOccurrenceEnd; + return selection.hasSelection() && + selection.getStartSelection() == primaryOccurrenceStart && + selection.getEndSelection() == primaryOccurrenceEnd; } /** @@ -884,7 +884,7 @@ public class RenameRefactorHandler { public boolean handleClick(int clickPosInText) { if (!active || callback == null) return false; - + return false; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java index 2649563ff..4d71a6b62 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -21,12 +21,12 @@ public class ScrollState { private int scrollbarDragOffset = 0; // Whether currently dragging the scrollbar private boolean clickScrolling = false; - + // Animation parameters private static final double TAU = 0.1; // Time constant (~55ms feels snappy) private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close private static final double MAX_DT = 0.05; // Max delta time for stability - + /** * Reset scroll state to initial values */ @@ -37,7 +37,7 @@ public class ScrollState { scrolledLine = 0; lastScrollTime = System.currentTimeMillis(); } - + /** * Initialize scroll position if not already initialized */ @@ -47,18 +47,19 @@ public class ScrollState { lastScrollTime = System.currentTimeMillis(); } } - + /** * Update scroll animation. Call once per frame. + * * @param maxScroll Maximum valid scroll position */ public void update(int maxScroll) { long nowMs = System.currentTimeMillis(); double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0); lastScrollTime = nowMs; - + double dist = targetScroll - scrollPos; - + // Snap to target if close enough if (Math.abs(dist) < SNAP_THRESHOLD) { scrollPos = targetScroll; @@ -69,21 +70,21 @@ public class ScrollState { double prev = scrollPos; scrollPos += dist * alpha; scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6); - + // Clamp overshoot if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) { scrollPos = targetScroll; scrollVelocity = 0.0; } } - + // Keep in bounds clampToBounds(maxScroll); - + // Update integer line for rendering scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); } - + /** * Clamp scroll position to valid bounds */ @@ -94,35 +95,37 @@ public class ScrollState { if (targetScroll > maxScroll) targetScroll = maxScroll; scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll)); } - + /** * Apply mouse wheel scroll + * * @param wheelDelta Positive = scroll up, negative = scroll down - * @param maxScroll Maximum valid scroll position + * @param maxScroll Maximum valid scroll position */ public void applyWheelScroll(int wheelDelta, int maxScroll) { double sign = Math.copySign(1, wheelDelta); targetScroll -= sign * 2.0; // 2 lines per wheel tick clampToBounds(maxScroll); } - + /** * Set target scroll directly (for scrollbar dragging) */ public void setTargetScroll(double target, int maxScroll) { targetScroll = Math.max(0, Math.min(target, maxScroll)); } - + /** * Scroll to make a specific line visible - * @param lineIdx Line index to make visible + * + * @param lineIdx Line index to make visible * @param visibleLines Number of visible lines in viewport - * @param maxScroll Maximum scroll position + * @param maxScroll Maximum scroll position */ public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) { int firstVisible = scrolledLine; int lastFullyVisible = scrolledLine + visibleLines; - + if (lineIdx < firstVisible) { // Line is above viewport - scroll up targetScroll = lineIdx; @@ -132,41 +135,66 @@ public class ScrollState { } // If line is visible, don't change scroll } - - // Getters - public double getScrollPos() { return scrollPos; } - public double getTargetScroll() { return targetScroll; } - /** @return First visible line **/ + // Getters + public double getScrollPos() { + return scrollPos; + } + + public double getTargetScroll() { + return targetScroll; + } + + /** + * @return First visible line + **/ public int getScrolledLine() { return scrolledLine; } - public double getScrollVelocity() { return scrollVelocity; } - public boolean isClickScrolling() { return clickScrolling; } - public int getScrollbarDragOffset() { return scrollbarDragOffset; } - + + public double getScrollVelocity() { + return scrollVelocity; + } + + public boolean isClickScrolling() { + return clickScrolling; + } + + public int getScrollbarDragOffset() { + return scrollbarDragOffset; + } + /** * Get fractional offset for sub-pixel rendering */ public double getFractionalOffset() { return scrollPos - scrolledLine; } - + // Setters for scrollbar interaction - public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; } - public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; } - public void setScrolledLine(int line) { this.scrolledLine = line; } + public void setClickScrolling(boolean scrolling) { + this.clickScrolling = scrolling; + } + + public void setScrollbarDragOffset(int offset) { + this.scrollbarDragOffset = offset; + } + + public void setScrolledLine(int line) { + this.scrolledLine = line; + } /** * Handle scrollbar dragging interaction. This encapsulates the logic that was * previously in the GUI class for handling thumb clicks/drags. - * @param yMouse current mouse Y coordinate - * @param areaX GUI area X (unused but kept for parity) - * @param areaY GUI area Y (top of text area) - * @param areaHeight height of the scroll track area + * + * @param yMouse current mouse Y coordinate + * @param areaX GUI area X (unused but kept for parity) + * @param areaY GUI area Y (top of text area) + * @param areaHeight height of the scroll track area * @param visibleLines number of visible lines in viewport - * @param linesCount total number of lines - * @param maxScroll maximum allowed scroll value + * @param linesCount total number of lines + * @param maxScroll maximum allowed scroll value */ public void handleClickScrolling(int yMouse, int areaX, int areaY, int areaHeight, int visibleLines, int linesCount, int maxScroll) { // Keep dragging while mouse button is down @@ -210,4 +238,4 @@ public class ScrollState { int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1; setScrollbarDragOffset(yMouse - thumbTop); } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index ef20e816a..32e835aaf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -5,6 +5,7 @@ import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.util.ChatAllowedCharacters; import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.util.ValueUtil; @@ -16,8 +17,6 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; - /** * Search and Replace bar component for the script editor. * Provides IntelliJ-like search functionality with: @@ -30,10 +29,10 @@ import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; * - All matches highlighting */ public class SearchReplaceBar { - + // ==================== TEXTURE ==================== public static final ResourceLocation TEXTURE = OverlayKeyPresetViewer.TEXTURE; - + // ==================== DIMENSIONS ==================== private int x, y, width; private int barHeight = 24; @@ -43,17 +42,17 @@ public class SearchReplaceBar { private int buttonSize = 16; private int buttonSpacing = 2; private int padding = 4; - + // ==================== STATE ==================== private boolean visible = false; private boolean showReplace = false; private boolean searchFieldFocused = true; private boolean replaceFieldFocused = false; - + // ==================== SEARCH OPTIONS ==================== private boolean matchCase = false; private boolean wholeWord = false; - + // ==================== TEXT FIELDS ==================== private String searchText = ""; private String replaceText = ""; @@ -66,18 +65,18 @@ public class SearchReplaceBar { // Text scroll offsets for when text exceeds field width private int searchScrollOffset = 0; private int replaceScrollOffset = 0; - + // ==================== UNDO/REDO ==================== private List searchUndoList = new ArrayList<>(); private List searchRedoList = new ArrayList<>(); private List replaceUndoList = new ArrayList<>(); private List replaceRedoList = new ArrayList<>(); - + // ==================== MATCHES ==================== private List matches = new ArrayList<>(); private int currentMatchIndex = -1; private List excludedMatches = new ArrayList<>(); - + // ==================== HOVER STATE ==================== private boolean hoverClose = false; private boolean hoverSearchUnwind = false; @@ -88,21 +87,21 @@ public class SearchReplaceBar { private boolean hoverReplace = false; private boolean hoverReplaceAll = false; private boolean hoverExclude = false; - + // ==================== CURSOR BLINK ==================== private int cursorCounter = 0; private long lastInputTime = 0; // For cursor blink pause on activity - + // ==================== DOUBLE/TRIPLE CLICK ==================== private long lastClickTime = 0; private int clickCount = 0; private boolean lastClickWasSearch = true; - + // ==================== CALLBACK ==================== public SearchCallback callback; - + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; - + /** * Callback interface for search/replace operations */ @@ -115,24 +114,31 @@ public class SearchReplaceBar { int getSelectionStart(); int getSelectionEnd(); + void setText(String text); + void scrollToPosition(int position); + void setSelection(int start, int end); + int getGutterWidth(); + void unfocusMainEditor(); + void focusMainEditor(); + void onMatchesUpdated(); // Called when matches change (for undo/redo sync) void resizeEditor(boolean open, int barHeight); } - + public SearchReplaceBar() { } - + public void setCallback(SearchCallback callback) { this.callback = callback; } - + /** * Initialize bar position based on text area bounds. * Preserves existing state (search text, matches, etc.) across initGui calls. @@ -149,8 +155,8 @@ public class SearchReplaceBar { boolean matchHighlight = false; visible = true; if (callback != null) { - // Always notify callback that the search UI is open and provide its current height. - callback.resizeEditor(true, getTotalHeight()); + // Always notify callback that the search UI is open and provide its current height. + callback.resizeEditor(true, getTotalHeight()); callback.unfocusMainEditor(); String highlight = callback.getHighlightedWord(); @@ -179,9 +185,9 @@ public class SearchReplaceBar { searchFieldFocused = true; replaceFieldFocused = false; open(); - + } - + /** * Open search+replace bar (Ctrl+Shift+R) */ @@ -191,7 +197,7 @@ public class SearchReplaceBar { replaceFieldFocused = true; open(); } - + /** * Close the search bar and restore focus to main editor */ @@ -212,6 +218,7 @@ public class SearchReplaceBar { else searchFieldFocused = true; } + /** * Unfocus the search bar (when clicking main editor) */ @@ -219,7 +226,7 @@ public class SearchReplaceBar { searchFieldFocused = false; replaceFieldFocused = false; } - + /** * Save current state to undo list before making changes */ @@ -232,7 +239,7 @@ public class SearchReplaceBar { replaceRedoList.clear(); } } - + /** * Undo last change in focused field */ @@ -256,7 +263,7 @@ public class SearchReplaceBar { updateScrollOffset(false); } } - + /** * Redo last undone change in focused field */ @@ -280,7 +287,7 @@ public class SearchReplaceBar { updateScrollOffset(false); } } - + /** * Update scroll offset for text field to keep cursor visible */ @@ -298,14 +305,14 @@ public class SearchReplaceBar { } else if (cursor < scrollOffset) { scrollOffset = cursor; } - + if (isSearchField) { searchScrollOffset = scrollOffset; } else { replaceScrollOffset = scrollOffset; } } - + /** * Toggle visibility */ @@ -316,7 +323,7 @@ public class SearchReplaceBar { openSearch(); } } - + /** * Toggle replace mode */ @@ -327,23 +334,23 @@ public class SearchReplaceBar { openSearchReplace(); } } - + public boolean isVisible() { return visible; } - + public boolean isSearchFocused() { return visible && searchFieldFocused; } - + public boolean isReplaceFocused() { return visible && replaceFieldFocused; } - + public boolean hasFocus() { return visible && (searchFieldFocused || replaceFieldFocused); } - + /** * Get total height of the bar (for viewport offset) */ @@ -351,143 +358,143 @@ public class SearchReplaceBar { if (!visible) return 0; return showReplace ? barHeight + replaceBarHeight : barHeight; } - + /** * Get all current matches for highlighting */ public List getMatches() { return matches; } - + /** * Get current match index */ public int getCurrentMatchIndex() { return currentMatchIndex; } - + /** * Check if a match is excluded */ public boolean isMatchExcluded(int index) { return excludedMatches.contains(index); } - + /** * Update cursor blink counter */ public void updateCursor() { cursorCounter++; } - + /** * Mark activity to pause cursor blinking */ private void markActivity() { lastInputTime = System.currentTimeMillis(); } - + /** * Check if there was recent input (cursor stays visible during typing) */ private boolean hadRecentInput() { return System.currentTimeMillis() - lastInputTime < 500; } - + /** * Check if cursor should be visible (solid during typing, blinking otherwise) */ private boolean shouldShowCursor() { return hadRecentInput() || (cursorCounter / 10) % 2 == 0; } - + // ==================== DRAWING ==================== - + /** * Draw the search/replace bar */ public void draw(int mouseX, int mouseY) { if (!visible) return; - + updateHoverStates(mouseX, mouseY); - + int totalHeight = getTotalHeight(); Gui.drawRect(x, y, x + width, y + totalHeight, 0xFF2d2d30); Gui.drawRect(x, y + totalHeight - 1, x + width, y + totalHeight, 0xFF414141); - + drawSearchRow(mouseX, mouseY); - + if (showReplace) { drawReplaceRow(mouseX, mouseY); } } - + private void drawSearchRow(int mouseX, int mouseY) { int rowY = y + padding; int currentX = x + padding; - textFieldWidth=100; - drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, - searchText, searchCursor, searchSelectionStart, searchSelectionEnd, - searchFieldFocused, "Search...", searchScrollOffset, true); + textFieldWidth = 100; + drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, + searchText, searchCursor, searchSelectionStart, searchSelectionEnd, + searchFieldFocused, "Search...", searchScrollOffset, true); currentX += textFieldWidth + buttonSpacing + 4; - + // "0 results" in red, otherwise "X/Y" in normal color - String matchInfo = matches.isEmpty() ? "0 results" : - (currentMatchIndex + 1) + "/" + matches.size(); + String matchInfo = matches.isEmpty() ? "0 results" : + (currentMatchIndex + 1) + "/" + matches.size(); int matchInfoWidth = font.getStringWidth(matchInfo); font.drawString(matchInfo, currentX, rowY + 4, - matches.isEmpty() && !searchText.isEmpty() ? 0xFFff6666 : 0xFFcccccc); + matches.isEmpty() && !searchText.isEmpty() ? 0xFFff6666 : 0xFFcccccc); currentX += matchInfoWidth + 8; drawToggleButton(currentX, rowY, "Cc", matchCase, hoverMatchCase, "Match Case"); currentX += buttonSize + buttonSpacing; - + drawToggleButton(currentX, rowY, "W", wholeWord, hoverWholeWord, "Whole Word"); currentX += buttonSize + buttonSpacing + 4; - + boolean prevEnabled = !matches.isEmpty(); drawNavButton(currentX, rowY, "▲", prevEnabled, hoverPrev); currentX += buttonSize + buttonSpacing; - + boolean nextEnabled = !matches.isEmpty(); drawNavButton(currentX, rowY, "▼", nextEnabled, hoverNext); currentX += buttonSize + buttonSpacing; - + int closeX = x + width - buttonSize - padding; drawCloseButton(closeX, rowY, hoverClose); int replaceUnwindButton = closeX - buttonSize - buttonSpacing; drawActionButton(replaceUnwindButton, rowY, showReplace ? "▼" : "◀", hoverSearchUnwind, true); } - + private void drawReplaceRow(int mouseX, int mouseY) { int rowY = y + barHeight + 2; int currentX = x + padding; drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, - replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, - replaceFieldFocused, "Replace...", replaceScrollOffset, false); + replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, + replaceFieldFocused, "Replace...", replaceScrollOffset, false); currentX += textFieldWidth + buttonSpacing + 4; - + drawActionButton(currentX, rowY, "Replace", hoverReplace, !matches.isEmpty()); currentX += font.getStringWidth("Replace") + 12 + buttonSpacing; - + drawActionButton(currentX, rowY, "Replace All", hoverReplaceAll, !matches.isEmpty()); currentX += font.getStringWidth("Replace All") + 12 + buttonSpacing; - + drawActionButton(currentX, rowY, "Exclude", hoverExclude, currentMatchIndex >= 0); } private void drawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, - String text, int cursor, int selStart, int selEnd, - boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { // Draw text field background Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, 0xFF1e1e1e); Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + 1, focused ? 0xFF007acc : 0xFF3c3c3c); Gui.drawRect(fieldX, fieldY + fieldHeight - 1, fieldX + fieldWidth, fieldY + fieldHeight, - focused ? 0xFF007acc : 0xFF3c3c3c); + focused ? 0xFF007acc : 0xFF3c3c3c); Gui.drawRect(fieldX, fieldY, fieldX + 1, fieldY + fieldHeight, focused ? 0xFF007acc : 0xFF3c3c3c); Gui.drawRect(fieldX + fieldWidth - 1, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, - focused ? 0xFF007acc : 0xFF3c3c3c); + focused ? 0xFF007acc : 0xFF3c3c3c); // Calculate visible area int textX = fieldX + 4; @@ -511,7 +518,7 @@ public class SearchReplaceBar { int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), - Math.min(maxSel, visibleText.length())); + Math.min(maxSel, visibleText.length())); int selX = textX + font.getStringWidth(beforeSel); int selW = font.getStringWidth(inSel); Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); @@ -537,7 +544,7 @@ public class SearchReplaceBar { // Draw selection highlight if (selStart != selEnd && focused) { int startX = textX + font.getStringWidth( - text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; + text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); } @@ -555,21 +562,21 @@ public class SearchReplaceBar { int cursorDrawX = textX + cursorPosX - scrollOffset; Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); } - + GL11.glDisable(GL11.GL_SCISSOR_TEST); } - + private void oldDrawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, - String text, int cursor, int selStart, int selEnd, - boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); GL11.glPushMatrix(); GL11.glColor4f(1, 1, 1, 1); float boxScaleX = fieldWidth / 32f, boxScaleY = 1.5f; GL11.glScalef(boxScaleX, boxScaleY, 1); - + if (focused) { GL11.glColor4f(0.28f, 0.45f, 0.7f, 1f); } else { @@ -577,11 +584,11 @@ public class SearchReplaceBar { } GuiUtil.drawTexturedModalRect(fieldX / boxScaleX, (fieldY - 16) / boxScaleY, 32, 20, 0, 492); GL11.glPopMatrix(); - + int textX = fieldX + 4; int textY = fieldY + (fieldHeight - font.FONT_HEIGHT) / 2; int visibleWidth = fieldWidth - 8; - + // Calculate scroll offset to keep cursor visible int cursorPosX = font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); if (cursorPosX - scrollOffset > visibleWidth - 2) { @@ -595,7 +602,7 @@ public class SearchReplaceBar { } else { replaceScrollOffset = scrollOffset; } - + GL11.glEnable(GL11.GL_SCISSOR_TEST); GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); @@ -606,7 +613,7 @@ public class SearchReplaceBar { int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); } - + // Draw text with scroll offset boolean noMatches = !text.isEmpty() && matches.isEmpty(); int col = text.isEmpty() ? 0xFFa0a0a0 : 0xFFe0e0e0; @@ -614,28 +621,28 @@ public class SearchReplaceBar { col = 0xFFff6666; // Red font.drawString(text.isEmpty() ? placeholder : text, textX - scrollOffset, textY, col); - + // Draw cursor (stays visible during typing) if (focused && shouldShowCursor()) { int cursorDrawX = textX + cursorPosX - scrollOffset; Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); } } - + GL11.glDisable(GL11.GL_SCISSOR_TEST); } - + private void drawToggleButton(int btnX, int btnY, String label, boolean active, boolean hovered, String tooltip) { int bgColor = active ? 0xFF4a6da7 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + 1, 0xFF606060); Gui.drawRect(btnX, btnY + buttonSize - 1, btnX + buttonSize, btnY + buttonSize, 0xFF2a2a2a); - + int textColor = active ? 0xFFffffff : (hovered ? 0xFFcccccc : 0xFF999999); int labelWidth = font.getStringWidth(label); font.drawString(label, btnX + (buttonSize - labelWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); } - + private void drawNavButton(int btnX, int btnY, String arrow, boolean enabled, boolean hovered) { int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); @@ -646,57 +653,57 @@ public class SearchReplaceBar { int arrowWidth = font.getStringWidth(arrow); font.drawString(arrow, btnX + (buttonSize - arrowWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2, textColor); } - + private void drawCloseButton(int btnX, int btnY, boolean hovered) { int bgColor = hovered ? 0xFFc75050 : 0xFF3c3c3c; Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); - + int textColor = hovered ? 0xFFffffff : 0xFFaaaaaa; String xChar = "X"; int xWidth = font.getStringWidth(xChar); font.drawString(xChar, btnX + (buttonSize - xWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); } - + private void drawActionButton(int btnX, int btnY, String label, boolean hovered, boolean enabled) { int btnWidth = font.getStringWidth(label) + 10; int btnHeight = 14; - + int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + btnHeight + 1, bgColor); Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + 2, 0xFF555555); Gui.drawRect(btnX, btnY + btnHeight, btnX + btnWidth, btnY + btnHeight + 1, 0xFF2a2a2a); - + int textColor = !enabled ? 0xFF555555 : (hovered ? 0xFFffffff : 0xFFaaaaaa); font.drawString(label, btnX + 5, btnY + (btnHeight - font.FONT_HEIGHT) / 2 + 2, textColor); } - + // ==================== HOVER DETECTION ==================== - + private void updateHoverStates(int mouseX, int mouseY) { int rowY = y + padding; int currentX = x + padding; - + int searchFieldEndX = currentX + textFieldWidth; currentX = searchFieldEndX + buttonSpacing + 4; - + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); currentX += font.getStringWidth(matchInfo) + 8; - + int matchCaseX = currentX; hoverMatchCase = isMouseOver(mouseX, mouseY, matchCaseX, rowY, buttonSize, buttonSize); currentX += buttonSize + buttonSpacing; - + int wholeWordX = currentX; hoverWholeWord = isMouseOver(mouseX, mouseY, wholeWordX, rowY, buttonSize, buttonSize); currentX += buttonSize + buttonSpacing + 4; - + int prevX = currentX; hoverPrev = isMouseOver(mouseX, mouseY, prevX, rowY, buttonSize, buttonSize); currentX += buttonSize + buttonSpacing; - + int nextX = currentX; hoverNext = isMouseOver(mouseX, mouseY, nextX, rowY, buttonSize, buttonSize); - + int closeX = x + width - buttonSize - padding; hoverClose = isMouseOver(mouseX, mouseY, closeX, rowY, buttonSize, buttonSize); @@ -706,50 +713,51 @@ public class SearchReplaceBar { if (showReplace) { int replaceRowY = y + barHeight + 2; currentX = x + padding + textFieldWidth + buttonSpacing + 4; - + int replaceWidth = font.getStringWidth("Replace") + 12; hoverReplace = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceWidth, 14); currentX += replaceWidth + buttonSpacing; - + int replaceAllWidth = font.getStringWidth("Replace All") + 12; hoverReplaceAll = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceAllWidth, 14); currentX += replaceAllWidth + buttonSpacing; - + int excludeWidth = font.getStringWidth("Exclude") + 12; hoverExclude = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, excludeWidth, 14); } else { hoverReplace = hoverReplaceAll = hoverExclude = false; } } - + private boolean isMouseOver(int mouseX, int mouseY, int bx, int by, int bw, int bh) { return mouseX >= bx && mouseX < bx + bw && mouseY >= by && mouseY < by + bh; } - + public void resetSelection() { searchSelectionStart = searchSelectionEnd = searchCursor; replaceSelectionStart = replaceSelectionEnd = replaceCursor; } // ==================== MOUSE INPUT ==================== - + /** * Handle mouse click - supports double-click for word selection, triple-click for all + * * @return true if click was consumed */ public boolean mouseClicked(int mouseX, int mouseY, int button) { if (!visible) return false; if (button != 0) return false; // Only handle left clicks - + int totalHeight = getTotalHeight(); // If click is outside the bar, don't consume it (let main editor handle) if (mouseX < x || mouseX > x + width || mouseY < y || mouseY > y + totalHeight) { resetSelection(); return false; } - + int rowY = y + padding; int currentX = x + padding; - + // Handle click timing for double/triple click long now = System.currentTimeMillis(); boolean isSearchFieldClick = isMouseOver(mouseX, mouseY, currentX, rowY - 2, textFieldWidth, textFieldHeight + 4); @@ -766,11 +774,11 @@ public class SearchReplaceBar { } lastClickTime = now; lastClickWasSearch = true; - + searchFieldFocused = true; replaceFieldFocused = false; markActivity(); - + if (clickCount == 1) { // Single click - position cursor int clickX = mouseX - currentX - 4 + searchScrollOffset; @@ -789,35 +797,35 @@ public class SearchReplaceBar { return true; } currentX += textFieldWidth + buttonSpacing + 4; - + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); currentX += font.getStringWidth(matchInfo) + 8; - + if (hoverMatchCase) { matchCase = !matchCase; updateMatches(); return true; } currentX += buttonSize + buttonSpacing; - + if (hoverWholeWord) { wholeWord = !wholeWord; updateMatches(); return true; } currentX += buttonSize + buttonSpacing + 4; - + if (hoverPrev && !matches.isEmpty()) { goToPreviousMatch(); return true; } currentX += buttonSize + buttonSpacing; - + if (hoverNext && !matches.isEmpty()) { goToNextMatch(); return true; } - + if (hoverClose) { close(); return true; @@ -830,7 +838,7 @@ public class SearchReplaceBar { if (showReplace) { int replaceRowY = y + barHeight + 2; - + if (isReplaceFieldClick || isMouseOver(mouseX, mouseY, x + padding, replaceRowY - 2, textFieldWidth, textFieldHeight + 4)) { // Handle click timing for replace field boolean sameField = !lastClickWasSearch; @@ -841,11 +849,11 @@ public class SearchReplaceBar { } lastClickTime = now; lastClickWasSearch = false; - + searchFieldFocused = false; replaceFieldFocused = true; markActivity(); - + if (clickCount == 1) { int clickX = mouseX - x - padding - 4 + replaceScrollOffset; replaceCursor = getTextCursorPosition(replaceText, clickX); @@ -860,30 +868,30 @@ public class SearchReplaceBar { } return true; } - + if (hoverReplace && !matches.isEmpty()) { replaceCurrent(); return true; } - + if (hoverReplaceAll && !matches.isEmpty()) { replaceAll(); return true; } - + if (hoverExclude && currentMatchIndex >= 0) { excludeCurrentMatch(); return true; } } - + // Click was within bar bounds but not on any control - consume it to prevent MTA from getting focus return true; } - + private int getTextCursorPosition(String text, int clickX) { if (text.isEmpty() || clickX <= 0) return 0; - + for (int i = 1; i <= text.length(); i++) { int w = font.getStringWidth(text.substring(0, i)); if (clickX < w) { @@ -893,17 +901,17 @@ public class SearchReplaceBar { } return text.length(); } - + /** * Select word at cursor position (for double-click) */ private void selectWordAt(String text, int pos, boolean isSearchField) { if (text.isEmpty()) return; - + // Find word boundaries int start = pos; int end = pos; - + // Expand left while (start > 0 && isWordChar(text.charAt(start - 1))) { start--; @@ -912,7 +920,7 @@ public class SearchReplaceBar { while (end < text.length() && isWordChar(text.charAt(end))) { end++; } - + if (isSearchField) { searchSelectionStart = start; searchSelectionEnd = end; @@ -923,15 +931,16 @@ public class SearchReplaceBar { replaceCursor = end; } } - + private boolean isWordChar(char c) { return Character.isLetterOrDigit(c) || c == '_'; } - + // ==================== KEYBOARD INPUT ==================== - + /** * Handle keyboard input + * * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { @@ -941,12 +950,12 @@ public class SearchReplaceBar { } if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; - + // Check modifier keys - don't let them trigger first-match issue boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); boolean alt = Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU); - + // Ignore lone modifier key presses if (keyCode == Keyboard.KEY_LCONTROL || keyCode == Keyboard.KEY_RCONTROL || keyCode == Keyboard.KEY_LSHIFT || keyCode == Keyboard.KEY_RSHIFT || @@ -965,7 +974,7 @@ public class SearchReplaceBar { markActivity(); return true; } - + // Enter/Shift+Enter for next/previous match if (keyCode == Keyboard.KEY_RETURN) { if (shift) { @@ -980,7 +989,7 @@ public class SearchReplaceBar { } return true; } - + // Up/Down arrows cycle through matches when SRB is focused if (keyCode == Keyboard.KEY_UP && !matches.isEmpty()) { goToPreviousMatch(); @@ -990,38 +999,36 @@ public class SearchReplaceBar { goToNextMatch(); return true; } - + if (searchFieldFocused) { handleTextFieldInput(c, keyCode, true); } else if (replaceFieldFocused) { handleTextFieldInput(c, keyCode, false); } - + return true; } - + private void handleTextFieldInput(char c, int keyCode, boolean isSearchField) { String text = isSearchField ? searchText : replaceText; int cursor = isSearchField ? searchCursor : replaceCursor; int selStart = isSearchField ? searchSelectionStart : replaceSelectionStart; int selEnd = isSearchField ? searchSelectionEnd : replaceSelectionEnd; - + boolean hasSelection = selStart != selEnd; boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - + if (ctrl && keyCode == Keyboard.KEY_A) { selStart = 0; selEnd = text.length(); cursor = text.length(); - } - else if (ctrl && keyCode == Keyboard.KEY_C) { + } else if (ctrl && keyCode == Keyboard.KEY_C) { if (hasSelection) { String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); setClipboard(selected); } - } - else if (ctrl && keyCode == Keyboard.KEY_X) { + } else if (ctrl && keyCode == Keyboard.KEY_X) { if (hasSelection) { String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); setClipboard(selected); @@ -1030,8 +1037,7 @@ public class SearchReplaceBar { cursor = Math.min(selStart, selEnd); selStart = selEnd = cursor; } - } - else if (ctrl && keyCode == Keyboard.KEY_V) { + } else if (ctrl && keyCode == Keyboard.KEY_V) { String clipboard = getClipboard(); if (clipboard != null) { clipboard = clipboard.replace("\n", "").replace("\r", ""); @@ -1045,8 +1051,7 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - } - else if (keyCode == Keyboard.KEY_BACK) { + } else if (keyCode == Keyboard.KEY_BACK) { saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); @@ -1056,8 +1061,7 @@ public class SearchReplaceBar { cursor--; } selStart = selEnd = cursor; - } - else if (keyCode == Keyboard.KEY_DELETE) { + } else if (keyCode == Keyboard.KEY_DELETE) { saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); @@ -1066,8 +1070,7 @@ public class SearchReplaceBar { text = text.substring(0, cursor) + text.substring(cursor + 1); } selStart = selEnd = cursor; - } - else if (keyCode == Keyboard.KEY_LEFT) { + } else if (keyCode == Keyboard.KEY_LEFT) { if (shift) { if (cursor > 0) { cursor--; @@ -1088,8 +1091,7 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - } - else if (keyCode == Keyboard.KEY_RIGHT) { + } else if (keyCode == Keyboard.KEY_RIGHT) { if (shift) { if (cursor < text.length()) { cursor++; @@ -1110,8 +1112,7 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - } - else if (keyCode == Keyboard.KEY_HOME) { + } else if (keyCode == Keyboard.KEY_HOME) { if (shift) { selEnd = selStart == selEnd ? cursor : selEnd; selStart = 0; @@ -1119,8 +1120,7 @@ public class SearchReplaceBar { selStart = selEnd = 0; } cursor = 0; - } - else if (keyCode == Keyboard.KEY_END) { + } else if (keyCode == Keyboard.KEY_END) { if (shift) { selStart = selStart == selEnd ? cursor : selStart; selEnd = text.length(); @@ -1128,8 +1128,7 @@ public class SearchReplaceBar { selStart = selEnd = text.length(); } cursor = text.length(); - } - else if (ChatAllowedCharacters.isAllowedCharacter(c)) { + } else if (ChatAllowedCharacters.isAllowedCharacter(c)) { saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + c + text.substring(Math.max(selStart, selEnd)); @@ -1140,16 +1139,16 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - + if (selStart > selEnd) { int temp = selStart; selStart = selEnd; selEnd = temp; } - + // Mark activity on any input to keep cursor visible markActivity(); - + if (isSearchField) { searchText = text; searchCursor = cursor; @@ -1163,14 +1162,15 @@ public class SearchReplaceBar { replaceSelectionEnd = selEnd; } } - + private void setClipboard(String text) { try { java.awt.datatransfer.StringSelection selection = new java.awt.datatransfer.StringSelection(text); java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } - + private String getClipboard() { try { return (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().getData(java.awt.datatransfer.DataFlavor.stringFlavor); @@ -1178,53 +1178,53 @@ public class SearchReplaceBar { return null; } } - + // ==================== SEARCH LOGIC ==================== - + /** * Update matches based on current search text and options */ public void updateMatches() { matches.clear(); excludedMatches.clear(); - + if (callback == null || searchText.isEmpty()) { if (callback != null) callback.onMatchesUpdated(); return; } - + String sourceText = callback.getText(); if (sourceText == null || sourceText.isEmpty()) { callback.onMatchesUpdated(); return; } - + String searchPattern = searchText; - + if (wholeWord) { searchPattern = "\\b" + Pattern.quote(searchPattern) + "\\b"; } else { searchPattern = Pattern.quote(searchPattern); } - + int flags = matchCase ? 0 : Pattern.CASE_INSENSITIVE; - + try { Pattern pattern = Pattern.compile(searchPattern, flags); Matcher matcher = pattern.matcher(sourceText); - + while (matcher.find()) { matches.add(new int[]{matcher.start(), matcher.end()}); } } catch (Exception ignored) { } - + // No need to navigate right now if (!matches.isEmpty()) { // currentMatchIndex = 0; // navigateToCurrentMatch(); } - + currentMatchIndex = ValueUtil.clamp(currentMatchIndex, 0, matches.size() - 1); callback.onMatchesUpdated(); } @@ -1251,7 +1251,7 @@ public class SearchReplaceBar { } catch (Exception ignored) { } } - + /** * Navigate to next match */ @@ -1263,7 +1263,7 @@ public class SearchReplaceBar { navigateToCurrentMatch(); } - + /** * Navigate to previous match */ @@ -1275,20 +1275,20 @@ public class SearchReplaceBar { navigateToCurrentMatch(); } - + /** * Navigate viewport to current match and select it */ private void navigateToCurrentMatch() { if (callback == null || currentMatchIndex < 0 || currentMatchIndex >= matches.size()) return; - + int[] match = matches.get(currentMatchIndex); callback.scrollToPosition(match[0]); callback.setSelection(match[0], match[1]); } - + // ==================== REPLACE LOGIC ==================== - + /** * Replace current match */ @@ -1298,15 +1298,15 @@ public class SearchReplaceBar { goToNextMatch(); return; } - + String sourceText = callback.getText(); int[] match = matches.get(currentMatchIndex); - + String newText = sourceText.substring(0, match[0]) + replaceText + sourceText.substring(match[1]); callback.setText(newText); - + updateMatches(); - + if (!matches.isEmpty()) { int replacementEnd = match[0] + replaceText.length(); for (int i = 0; i < matches.size(); i++) { @@ -1318,17 +1318,17 @@ public class SearchReplaceBar { navigateToCurrentMatch(); } } - + /** * Replace all matches */ public void replaceAll() { if (callback == null || matches.isEmpty()) return; - + String sourceText = callback.getText(); StringBuilder result = new StringBuilder(); int lastEnd = 0; - + for (int i = 0; i < matches.size(); i++) { if (excludedMatches.contains(i)) { continue; @@ -1339,11 +1339,11 @@ public class SearchReplaceBar { lastEnd = match[1]; } result.append(sourceText.substring(lastEnd)); - + callback.setText(result.toString()); updateMatches(); } - + /** * Exclude current match from replacement */ @@ -1359,16 +1359,16 @@ public class SearchReplaceBar { goToNextMatch(); } } - + // ==================== UTILITY ==================== - + /** * Get search text for external highlighting */ public String getSearchText() { return searchText; } - + /** * Check if bar consumes click at position */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java index 242c16a93..89efa34cd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.List; import java.util.regex.Pattern; @@ -12,14 +13,14 @@ public class SelectionState { private int endSelection = 0; private int cursorPosition = 0; private long lastInputTime = 0L; - + /** * Reset all cursor selections to a single position */ public void reset(int position) { startSelection = endSelection = cursorPosition = position; } - + /** * Clamp all selection bounds to valid text range */ @@ -32,13 +33,13 @@ public class SelectionState { endSelection = Math.max(0, Math.min(endSelection, textLength)); cursorPosition = Math.max(0, Math.min(cursorPosition, textLength)); } - + /** * Set cursor position, optionally extending selection */ public void setCursor(int position, int textLength, boolean extendSelection) { position = Math.max(0, Math.min(position, textLength)); - + if (position != cursorPosition) { if (!extendSelection) { endSelection = startSelection = cursorPosition = position; @@ -49,7 +50,7 @@ public class SelectionState { } else if (cursorPosition == endSelection) { endSelection -= diff; } - + // Ensure start <= end if (startSelection > endSelection) { int temp = endSelection; @@ -62,7 +63,7 @@ public class SelectionState { markActivity(); } } - + /** * Select a range of text */ @@ -70,7 +71,7 @@ public class SelectionState { startSelection = Math.min(start, end); endSelection = Math.max(start, end); } - + /** * Select all text */ @@ -78,14 +79,14 @@ public class SelectionState { startSelection = cursorPosition = 0; endSelection = textLength; } - + /** * Check if there's an active selection (not just cursor) */ public boolean hasSelection() { return startSelection != endSelection; } - + /** * Get selected text */ @@ -95,7 +96,7 @@ public class SelectionState { int end = Math.max(0, Math.min(endSelection, text.length())); return text.substring(start, end); } - + /** * Get text before selection/cursor */ @@ -103,7 +104,7 @@ public class SelectionState { if (text == null || startSelection <= 0) return ""; return text.substring(0, Math.min(startSelection, text.length())); } - + /** * Get text after selection/cursor */ @@ -112,37 +113,37 @@ public class SelectionState { int pos = Math.min(endSelection, text.length()); return text.substring(pos); } - + /** * Find which line the cursor is on (0-indexed) */ public int getCursorLineIndex(List lines, int textLength) { if (lines == null || lines.isEmpty()) return 0; - + for (int i = 0; i < lines.size(); i++) { LineData ld = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= ld.start && cursorPosition <= ld.end) - : (cursorPosition >= ld.start && cursorPosition < ld.end); + boolean isOnLine = isLastLine + ? (cursorPosition >= ld.start && cursorPosition <= ld.end) + : (cursorPosition >= ld.start && cursorPosition < ld.end); if (isOnLine) { return i; } } return lines.size() - 1; } - + /** - * Check if cursor is on a specific line, handling the last line specially - * (last line doesn't have trailing newline, so use <= for end boundary) + * Check if cursor is on a specific line, handling the last line specially + * (last line doesn't have trailing newline, so use <= for end boundary) */ public boolean isCursorOnLine(int lineIndex, LineData line, int totalLines) { boolean isLastLine = (lineIndex == totalLines - 1); - return isLastLine - ? (cursorPosition >= line.start && cursorPosition <= line.end) - : (cursorPosition >= line.start && cursorPosition < line.end); + return isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); } - + /** * Find line containing the cursor */ @@ -156,32 +157,51 @@ public class SelectionState { } return null; } - + /** * Mark user activity (for cursor blink timing) */ public void markActivity() { lastInputTime = System.currentTimeMillis(); } - + /** * Check if there was recent input (for cursor blink pause) */ public boolean hadRecentInput() { return System.currentTimeMillis() - lastInputTime < 500; } - + // Getters - public int getStartSelection() { return startSelection; } - public int getEndSelection() { return endSelection; } - public int getCursorPosition() { return cursorPosition; } - public long getLastInputTime() { return lastInputTime; } - + public int getStartSelection() { + return startSelection; + } + + public int getEndSelection() { + return endSelection; + } + + public int getCursorPosition() { + return cursorPosition; + } + + public long getLastInputTime() { + return lastInputTime; + } + // Direct setters (use with caution, prefer higher-level methods) - public void setStartSelection(int pos) { this.startSelection = pos; } - public void setEndSelection(int pos) { this.endSelection = pos; } - public void setCursorPositionDirect(int pos) { this.cursorPosition = pos; } - + public void setStartSelection(int pos) { + this.startSelection = pos; + } + + public void setEndSelection(int pos) { + this.endSelection = pos; + } + + public void setCursorPositionDirect(int pos) { + this.cursorPosition = pos; + } + /** * After text modification, update selection to new position */ @@ -219,4 +239,4 @@ public class SelectionState { } } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java index d7cad5295..c5cfd9f77 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java @@ -7,90 +7,92 @@ import java.util.List; * Manages undo/redo stacks for the text editor. */ public class UndoManager { - + /** * Stores a snapshot of text state for undo/redo */ public static class UndoState { public final String text; public final int cursorPosition; - + public UndoState(String text, int cursorPosition) { this.text = text; this.cursorPosition = cursorPosition; } } - + private final List undoStack = new ArrayList<>(); private final List redoStack = new ArrayList<>(); private boolean isUndoing = false; - + // Optional: limit stack size to prevent memory issues private static final int MAX_STACK_SIZE = 100; - + /** * Record current state before making a change */ public void recordState(String text, int cursorPosition) { if (isUndoing) return; - + undoStack.add(new UndoState(text, cursorPosition)); redoStack.clear(); - + // Trim stack if too large while (undoStack.size() > MAX_STACK_SIZE) { undoStack.remove(0); } } - + /** * Undo to previous state - * @param currentText Current text before undo + * + * @param currentText Current text before undo * @param currentCursor Current cursor position * @return Previous state, or null if nothing to undo */ public UndoState undo(String currentText, int currentCursor) { if (undoStack.isEmpty()) return null; - + isUndoing = true; redoStack.add(new UndoState(currentText, currentCursor)); UndoState state = undoStack.remove(undoStack.size() - 1); isUndoing = false; - + return state; } - + /** * Redo to next state - * @param currentText Current text before redo + * + * @param currentText Current text before redo * @param currentCursor Current cursor position * @return Next state, or null if nothing to redo */ public UndoState redo(String currentText, int currentCursor) { if (redoStack.isEmpty()) return null; - + isUndoing = true; undoStack.add(new UndoState(currentText, currentCursor)); UndoState state = redoStack.remove(redoStack.size() - 1); isUndoing = false; - + return state; } - + /** * Check if undo is available */ public boolean canUndo() { return !undoStack.isEmpty(); } - + /** * Check if redo is available */ public boolean canRedo() { return !redoStack.isEmpty(); } - + /** * Clear all history */ @@ -98,22 +100,27 @@ public class UndoManager { undoStack.clear(); redoStack.clear(); } - + /** * Check if currently performing an undo/redo operation */ public boolean isUndoing() { return isUndoing; } - + /** * Set undoing flag (for external control during setText) */ public void setUndoing(boolean undoing) { this.isUndoing = undoing; } - + // For backward compatibility with existing code - public List getUndoList() { return undoStack; } - public List getRedoList() { return redoStack; } -} \ No newline at end of file + public List getUndoList() { + return undoStack; + } + + public List getRedoList() { + return redoStack; + } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyEvent.java b/src/main/java/noppes/npcs/client/key/KeyEvent.java index 53011f2e8..1948e2f78 100644 --- a/src/main/java/noppes/npcs/client/key/KeyEvent.java +++ b/src/main/java/noppes/npcs/client/key/KeyEvent.java @@ -15,10 +15,27 @@ public class KeyEvent { this.timestamp = System.currentTimeMillis(); } - public boolean is(KeyPreset.PressType t) { return this.type == t; } - public boolean isPress() { return is(KeyPreset.PressType.PRESS); } - public boolean isHold() { return is(KeyPreset.PressType.HOLD); } - public boolean isRelease() { return is(KeyPreset.PressType.RELEASE); } - public boolean isSinglePress() { return is(KeyPreset.PressType.SINGLE_PRESS); } - public boolean isPressRelease() { return is(KeyPreset.PressType.PRESS_RELEASE); } + public boolean is(KeyPreset.PressType t) { + return this.type == t; + } + + public boolean isPress() { + return is(KeyPreset.PressType.PRESS); + } + + public boolean isHold() { + return is(KeyPreset.PressType.HOLD); + } + + public boolean isRelease() { + return is(KeyPreset.PressType.RELEASE); + } + + public boolean isSinglePress() { + return is(KeyPreset.PressType.SINGLE_PRESS); + } + + public boolean isPressRelease() { + return is(KeyPreset.PressType.PRESS_RELEASE); + } } diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 7bae2c58c..e76cf14ff 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -13,8 +13,10 @@ public class KeyPreset { * @field SINGLE_PRESS => on releasing before 5 ticks press time * @field PRESS_RELEASE => on pressing and releasing */ - /** Press types for key events. */ - public enum PressType { PRESS, HOLD, RELEASE, SINGLE_PRESS, PRESS_RELEASE } + /** + * Press types for key events. + */ + public enum PressType {PRESS, HOLD, RELEASE, SINGLE_PRESS, PRESS_RELEASE} public KeyState defaultState = new KeyState(); public KeyState currentState = new KeyState(); @@ -81,7 +83,7 @@ public class KeyPreset { if (throttleInterval <= 0) return false; long currentTime = System.currentTimeMillis(); long intervalToUse = pressType == PressType.HOLD && isFirstAction && firstThrottleInterval > 0 ? - firstThrottleInterval : throttleInterval; + firstThrottleInterval : throttleInterval; if (currentTime - lastActionTime < intervalToUse) return true; lastActionTime = currentTime; isFirstAction = false; @@ -108,7 +110,7 @@ public class KeyPreset { } if (!down && isDown) { // just released - if (getPressTime() <= SHORT_PRESS_MS) + if (getPressTime() <= SHORT_PRESS_MS) onAction(PressType.SINGLE_PRESS); onAction(PressType.RELEASE, PressType.PRESS_RELEASE); pressStartMs = 0; @@ -116,10 +118,10 @@ public class KeyPreset { if (down) // while held onAction(PressType.HOLD); - + this.isDown = down; } - + public long getPressTime() { return pressStartMs > 0 ? (System.currentTimeMillis() - pressStartMs) : 0L; } diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java index 6f973e2b5..1a978cdc7 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -25,7 +25,7 @@ public class KeyPresetManager { } public void tick() { - for (KeyPreset key : keys) + for (KeyPreset key : keys) key.tick(); } diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index f8f262893..4a9e294d0 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -5,8 +5,8 @@ import noppes.npcs.client.key.KeyPresetManager; import org.lwjgl.input.Keyboard; public class ScriptEditorKeys extends KeyPresetManager { - - //////////////////// + + /// ///////////////// // Editor shortcuts (mapped from GuiScriptTextArea.handleShortcutKeys) public final KeyPreset COPY = add("Copy").setDefaultState(Keyboard.KEY_C, true, false, false); public final KeyPreset PASTE = add("Paste").setDefaultState(Keyboard.KEY_V, true, false, false); @@ -14,20 +14,20 @@ public class ScriptEditorKeys extends KeyPresetManager { public final KeyPreset DUPLICATE = add("Duplicate").setDefaultState(Keyboard.KEY_D, true, false, false); public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false); public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false); - + public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, false, true, false); public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false); - + // Search/Replace public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_F, true, false, false); public final KeyPreset SEARCH_REPLACE = add("Replace").setDefaultState(Keyboard.KEY_F, true, false, true); - + // Navigation public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); - + // Refactoring public final KeyPreset RENAME = add("Rename").setDefaultState(Keyboard.KEY_R, true, false, false); - + // View public final KeyPreset FULLSCREEN = add("Toggle Fullscreen").setDefaultState(Keyboard.KEY_F11, false, false, false); diff --git a/src/main/java/noppes/npcs/client/model/ModelPony.java b/src/main/java/noppes/npcs/client/model/ModelPony.java index b0d23b40a..203a1d12b 100644 --- a/src/main/java/noppes/npcs/client/model/ModelPony.java +++ b/src/main/java/noppes/npcs/client/model/ModelPony.java @@ -17,7 +17,7 @@ import noppes.npcs.entity.EntityNpcPony; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java b/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java index 252f0af65..90172ce6f 100644 --- a/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java +++ b/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java @@ -2,7 +2,7 @@ package noppes.npcs.client.renderer; import net.minecraft.client.renderer.ImageBufferDownload; -import java.awt.*; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; diff --git a/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java b/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java index a82ba93a8..b018adf60 100644 --- a/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java +++ b/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java @@ -25,7 +25,7 @@ import noppes.npcs.entity.EntityNpcPony; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java b/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java index 9aa8cdcaa..5be3a9492 100644 --- a/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java +++ b/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java @@ -23,7 +23,7 @@ import noppes.npcs.blocks.tiles.TileScripted.TextPlane; import noppes.npcs.client.TextBlockClient; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Color; import java.util.Random; public class BlockScriptedRenderer extends BlockRendererInterface { diff --git a/src/main/java/noppes/npcs/config/GlyphCache.java b/src/main/java/noppes/npcs/config/GlyphCache.java index 98474c493..86f34d433 100644 --- a/src/main/java/noppes/npcs/config/GlyphCache.java +++ b/src/main/java/noppes/npcs/config/GlyphCache.java @@ -5,7 +5,13 @@ import net.minecraft.client.renderer.GLAllocation; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.RenderingHints; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.Point2D; diff --git a/src/main/java/noppes/npcs/config/StringCache.java b/src/main/java/noppes/npcs/config/StringCache.java index 263b480b9..c40d59f1c 100644 --- a/src/main/java/noppes/npcs/config/StringCache.java +++ b/src/main/java/noppes/npcs/config/StringCache.java @@ -5,7 +5,8 @@ import net.minecraft.client.renderer.Tessellator; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Font; +import java.awt.Point; import java.awt.font.GlyphVector; import java.lang.ref.WeakReference; import java.text.Bidi; diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index f2f614a1e..dcc5b5d95 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -1,13 +1,29 @@ package noppes.npcs.constants; -import noppes.npcs.api.event.*; +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Represents a script context that maps to one or more event interface namespaces. - * + *

* A script context can have multiple namespaces because script editors often * support hooks from multiple event types. For example, Player scripts support: * - IPlayerEvent (core player hooks) @@ -15,17 +31,17 @@ import java.util.concurrent.ConcurrentHashMap; * - IPartyEvent (party hooks) * - ICustomGuiEvent (custom GUI hooks) * - etc. - * + *

* Each context also has a hookContext string used by ScriptHookController for * hook registration and lookup. - * + *

* This is a registerable system - any mod can register their own contexts: - * - * // Register a new context with multiple namespaces: - * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); - * - * // Or add namespaces to an existing context: - * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); + *

+ * // Register a new context with multiple namespaces: + * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); + *

+ * // Or add namespaces to an existing context: + * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); */ public class ScriptContext { @@ -35,42 +51,42 @@ public class ScriptContext { // ==================== BUILT-IN CONTEXTS ==================== public static final ScriptContext NPC = register("NPC", "npc", - INpcEvent.class, - IProjectileEvent.class + INpcEvent.class, + IProjectileEvent.class ); public static final ScriptContext PLAYER = register("PLAYER", "player", - IPlayerEvent.class, - IAnimationEvent.class, - IPartyEvent.class, - IDialogEvent.class, - IQuestEvent.class, - IFactionEvent.class, - ICustomGuiEvent.class + IPlayerEvent.class, + IAnimationEvent.class, + IPartyEvent.class, + IDialogEvent.class, + IQuestEvent.class, + IFactionEvent.class, + ICustomGuiEvent.class ); public static final ScriptContext BLOCK = register("BLOCK", "block", - IBlockEvent.class + IBlockEvent.class ); public static final ScriptContext ITEM = register("ITEM", "item", - IItemEvent.class + IItemEvent.class ); public static final ScriptContext FORGE = register("FORGE", "forge", - IForgeEvent.class + IForgeEvent.class ); public static final ScriptContext LINKED_ITEM = register("LINKED_ITEM", "linked_item", - IItemEvent.class + IItemEvent.class ); public static final ScriptContext RECIPE = register("RECIPE", "recipe", - "Recipe" + "Recipe" ); public static final ScriptContext EFFECT = register("EFFECT", "effect", - "Effect" + "Effect" ); public static final ScriptContext GLOBAL = register("GLOBAL", "", @@ -79,13 +95,19 @@ public class ScriptContext { // ==================== INSTANCE FIELDS ==================== - /** Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") */ + /** + * Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") + */ public final String id; - /** Hook context identifier used by ScriptHookController (e.g., "npc", "player") */ + /** + * Hook context identifier used by ScriptHookController (e.g., "npc", "player") + */ public final String hookContext; - /** The event interface namespaces this context supports */ + /** + * The event interface namespaces this context supports + */ private final List namespaces; // ==================== CONSTRUCTOR ==================== @@ -144,9 +166,9 @@ public class ScriptContext { /** * Register a new script context with a hook context and one or more namespaces. * - * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param id Unique identifier (e.g., "DBC", "CUSTOM") * @param hookContext The hook context string for ScriptHookController (e.g., "dbc") - * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") + * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") * @return The registered ScriptContext */ public static ScriptContext register(String id, String hookContext, String... namespaces) { @@ -158,12 +180,12 @@ public class ScriptContext { /** * Register a new script context using event interface classes. * Automatically extracts the simple name from each class. - * + *

* Example: - * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); + * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); * - * @param id Unique identifier (e.g., "DBC", "CUSTOM") - * @param hookContext The hook context string for ScriptHookController (e.g., "npc") + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "npc") * @param eventClasses The event interface classes (simple names will be extracted) * @return The registered ScriptContext */ diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index 65adfcd3d..e3caddce5 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -231,10 +231,9 @@ public class AnimationController implements IAnimationHandler { } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// // ANIMATION MAP - public File getMapDir() { File dir = CustomNpcs.getWorldSaveDirectory(); if (!dir.exists()) diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java index 9d8c34fa6..864e37ee3 100644 --- a/src/main/java/noppes/npcs/controllers/HookDefinition.java +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -97,7 +97,7 @@ public class HookDefinition implements IHookDefinition { * Create a hook definition from a hook name and event class. * Automatically derives imports from the event class. * - * @param hookName The hook function name + * @param hookName The hook function name * @param eventClass The event class (e.g., INpcEvent.InitEvent.class) * @return A hook definition with auto-derived imports */ @@ -131,7 +131,7 @@ public class HookDefinition implements IHookDefinition { * This provides backward compatibility with the annotation-based hook definition approach. * * @param hookName The hook name (may differ from method name) - * @param method The method to extract metadata from + * @param method The method to extract metadata from * @return A hook definition with extracted metadata */ public static HookDefinition fromMethod(String hookName, Method method) { @@ -165,7 +165,7 @@ public class HookDefinition implements IHookDefinition { } else { // Fallback: use lowercase simple type name names[i] = Character.toLowerCase(params[i].getType().getSimpleName().charAt(0)) - + params[i].getType().getSimpleName().substring(1); + + params[i].getType().getSimpleName().substring(1); } } builder.paramNames(names); diff --git a/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/src/main/java/noppes/npcs/controllers/LinkedItemController.java index 48f19958b..78534a82f 100644 --- a/src/main/java/noppes/npcs/controllers/LinkedItemController.java +++ b/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -94,8 +94,8 @@ public class LinkedItemController { return this.linkedItemsScripts.get(id); } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// public void load() { lastUsedID = 0; @@ -247,10 +247,9 @@ public class LinkedItemController { } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// // LINKED ITEMS MAP - public File getMapDir() { File dir = CustomNpcs.getWorldSaveDirectory(); if (!dir.exists()) diff --git a/src/main/java/noppes/npcs/controllers/MarketRegistry.java b/src/main/java/noppes/npcs/controllers/MarketRegistry.java index 1110f9e5f..55de3a0bf 100644 --- a/src/main/java/noppes/npcs/controllers/MarketRegistry.java +++ b/src/main/java/noppes/npcs/controllers/MarketRegistry.java @@ -15,7 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; * Central registry for linked market synchronization. * Tracks which RoleTrader instances use which markets, * and which players are viewing each market. - * + *

* This enables immediate sync across all traders sharing * the same market when a purchase occurs. */ diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index eb0b8b1c4..738819500 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -270,41 +270,41 @@ public class ScriptContainer implements IScriptUnit { this.evaluated = false; } } - + // ==================== IScriptUnit IMPLEMENTATION ==================== - + @Override public String getScript() { return this.script; } - + @Override public void setScript(String script) { this.script = script; this.evaluated = false; } - + @Override public List getExternalScripts() { return this.scripts; } - + @Override public void setExternalScripts(List scripts) { this.scripts = scripts; this.evaluated = false; } - + @Override public TreeMap getConsole() { return this.console; } - + @Override public void clearConsole() { this.console.clear(); } - + @Override public String getLanguage() { // If a specific language is set for this container, use it @@ -314,7 +314,7 @@ public class ScriptContainer implements IScriptUnit { } return handler != null ? handler.getLanguage() : "ECMAScript"; } - + @Override public void setLanguage(String language) { if (!Objects.equals(this.currentScriptLanguage, language)) { @@ -322,7 +322,7 @@ public class ScriptContainer implements IScriptUnit { this.evaluated = false; } } - + @Override public String generateHookStub(String hookName, Object hookData) { // ECMAScript function template @@ -330,13 +330,14 @@ public class ScriptContainer implements IScriptUnit { } @Override - public void ensureCompiled() {} + public void ensureCompiled() { + } @Override public boolean hasErrored() { return this.errored; } - + @Override public void setErrored(boolean errored) { this.errored = errored; diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index f89177c9e..f3bad8e9d 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,16 +1,43 @@ package noppes.npcs.controllers; -import noppes.npcs.api.event.*; +import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.ICustomNPCsEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.ILinkedItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; +import noppes.npcs.api.event.IRecipeEvent; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.RecipeScript; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import static noppes.npcs.constants.EnumScriptType.*; -import static noppes.npcs.constants.ScriptContext.*; +import static noppes.npcs.constants.ScriptContext.BLOCK; +import static noppes.npcs.constants.ScriptContext.EFFECT; +import static noppes.npcs.constants.ScriptContext.FORGE; +import static noppes.npcs.constants.ScriptContext.ITEM; +import static noppes.npcs.constants.ScriptContext.LINKED_ITEM; +import static noppes.npcs.constants.ScriptContext.NPC; +import static noppes.npcs.constants.ScriptContext.PLAYER; +import static noppes.npcs.constants.ScriptContext.RECIPE; /** * Controller for managing script hooks. @@ -290,8 +317,8 @@ public class ScriptHookController implements IScriptHookHandler { * Register a hook with event class (auto-derives imports). * For addon mods to register custom hooks. * - * @param context Hook context (e.g., "npc", "player") - * @param hookName Function name in scripts + * @param context Hook context (e.g., "npc", "player") + * @param hookName Function name in scripts * @param eventClass The event class (imports auto-derived from enclosing class) */ public void registerHook(String context, String hookName, Class eventClass) { diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index c7d006198..8cf9b3827 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -185,16 +185,26 @@ public class Animation implements IAnimation { } @Override - public boolean hasData(String key) {return dataStore.containsKey(key);} - - @Override - public Object getData(String key) {return dataStore.get(key);} + public boolean hasData(String key) { + return dataStore.containsKey(key); + } @Override - public IAnimation setData(String key, Object v) {dataStore.put(key, v);return this;} + public Object getData(String key) { + return dataStore.get(key); + } @Override - public IAnimation removeData(String key) {dataStore.remove(key);return this;} + public IAnimation setData(String key, Object v) { + dataStore.put(key, v); + return this; + } + + @Override + public IAnimation removeData(String key) { + dataStore.remove(key); + return this; + } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("ID")) { @@ -323,13 +333,13 @@ public class Animation implements IAnimation { this.onAnimationStart = task; return this; } - + @Override public IAnimation onFrame(BiConsumer task) { this.onAnimationFrame = task; return this; } - + @Override public IAnimation onEnd(Consumer task) { this.onAnimationEnd = task; @@ -355,7 +365,7 @@ public class Animation implements IAnimation { } /** - * Only called on setAnimation, to transfer tasks from + * Only called on setAnimation, to transfer tasks from * this global instance stored in AnimationController * to the new local instance on entity's AnimationData, * and clear them from the global instance. @@ -372,6 +382,6 @@ public class Animation implements IAnimation { this.dataStore.putAll(animation.dataStore); animation.dataStore.clear(); } - - + + } diff --git a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java index 9b621234d..7a5db74de 100644 --- a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java +++ b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java @@ -194,7 +194,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EnumScriptType.ON_EFFECT_ADD, event); + script.callScript(EnumScriptType.ON_EFFECT_ADD, event); } EventHooks.onEffectAdded(iPlayer, playerEffect); @@ -214,7 +214,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EnumScriptType.ON_EFFECT_TICK, event); + script.callScript(EnumScriptType.ON_EFFECT_TICK, event); } EventHooks.onEffectTick(iPlayer, playerEffect); @@ -235,7 +235,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); + script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); } EventHooks.onEffectRemove(iPlayer, playerEffect, type); diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index ede4a4c96..74806605f 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -312,6 +312,7 @@ public class DataScript implements IScriptHandlerPacket { public Collection getNPCScripts() { return this.scripts.values(); } + @Override public IScriptUnit createJaninoScriptUnit() { return new EventJaninoScript(ScriptContext.NPC); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index 821e542b4..457eb11d6 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -2,9 +2,7 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.scripted.event.player.PlayerEvent; /** * Script handler for CustomEffect scripts. @@ -12,7 +10,9 @@ import noppes.npcs.scripted.event.player.PlayerEvent; */ public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { - /** The effect ID for packet communication. -1 if not bound to an effect. */ + /** + * The effect ID for packet communication. -1 if not bound to an effect. + */ private int effectId = -1; /** @@ -23,6 +23,7 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP /** * Create an EffectScript bound to a specific effect (for GUI use). + * * @param effectId The ID of the CustomEffect */ public EffectScript(int effectId) { diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index 93892f2a6..812bef6d8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -13,16 +13,16 @@ import java.util.TreeMap; public interface IScriptHandler { default void callScript(EnumScriptType type, Event event) { - if (type != null) + if (type != null) callScript(type.function, event); - + } void callScript(String hookName, Event event); /** * Call a function by name and return its result. - * + *

* Default implementation calls the function on each script unit (in order) and returns * the first non-null value. */ @@ -86,7 +86,7 @@ public interface IScriptHandler { default Map getConsoleText() { Map map = new TreeMap<>(); int tab = 0; - + for (IScriptUnit script : getScripts()) { ++tab; for (Map.Entry entry : script.getConsole().entrySet()) { @@ -97,24 +97,24 @@ public interface IScriptHandler { } default void clearConsole() { - for (IScriptUnit script : getScripts()) + for (IScriptUnit script : getScripts()) script.clearConsole(); - + } - + /** * Create a new Janino (Java) script unit for this handler. * Handlers that don't support Janino scripts should return null. - * + * * @return A new JaninoScript instance, or null if not supported */ default IScriptUnit createJaninoScriptUnit() { return null; // Default: Janino not supported } - + /** * Check if this handler supports Janino (Java) scripts. - * + * * @return true if createJaninoScriptUnit() can create valid Janino scripts */ default boolean supportsJanino() { @@ -133,7 +133,7 @@ public interface IScriptHandler { /** * Get the hook context identifier for this handler. * Used by ScriptHookController to look up available hooks. - * + *

* By default, derives from getContext().hookContext. * Override if a different hook context is needed. * @@ -152,9 +152,9 @@ public interface IScriptHandler { */ default List getHooks() { String context = getHookContext(); - if (context == null || context.isEmpty()) + if (context == null || context.isEmpty()) return Collections.emptyList(); - + return ScriptHookController.Instance.getAllHooks(context); } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java index c307f508d..88ff5fb94 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java @@ -15,9 +15,9 @@ public interface IScriptHandlerPacket extends IScriptHandler { /** * Client-side: send a script container packet to the server. * - * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) + * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) * @param totalCount Total number of scripts for this handler - * @param nbt The script container NBT (tab content or metadata) + * @param nbt The script container NBT (tab content or metadata) */ void sendSavePacket(int index, int totalCount, NBTTagCompound nbt); diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index 258b584b8..b439d7d0a 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -6,7 +6,6 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import java.util.List; -import java.util.Map; import java.util.TreeMap; /** @@ -15,24 +14,24 @@ import java.util.TreeMap; * This allows GuiScriptInterface to work with both script types uniformly. */ public interface IScriptUnit { - + // NBT type identifiers String NBT_TYPE_KEY = "ScriptUnitType"; String TYPE_ECMASCRIPT = "ECMAScript"; String TYPE_JANINO = "Janino"; - + /** * Create an IScriptUnit from NBT data. * Uses the "ScriptUnitType" tag to determine which implementation to create. - * + * * @param compound The NBT data - * @param handler The script handler (used for ScriptContainer) + * @param handler The script handler (used for ScriptContainer) * @return A ScriptContainer or JaninoScript based on the NBT type */ static IScriptUnit createFromNBT(NBTTagCompound compound, IScriptHandler handler) { String type = compound.getString(NBT_TYPE_KEY); IScriptUnit unit; - + if (TYPE_JANINO.equals(type)) { // Create appropriate JaninoScript based on handler unit = handler.createJaninoScriptUnit(); @@ -44,96 +43,97 @@ public interface IScriptUnit { // Default to ScriptContainer for ECMAScript or missing type unit = new ScriptContainer(handler); } - + unit.readFromNBT(compound); return unit; } - + // ==================== SCRIPT CONTENT ==================== - + /** * Get the main script text content. */ String getScript(); - + /** * Set the main script text content. */ void setScript(String script); - + /** * Get the list of external script file names loaded for this unit. */ List getExternalScripts(); - + /** * Set the list of external script file names to load. */ void setExternalScripts(List scripts); - + // ==================== CONSOLE ==================== - + /** * Get the console output map (timestamp -> message). */ TreeMap getConsole(); - + /** * Clear all console output. */ void clearConsole(); - + /** * Append a message to the console. */ void appendConsole(String message); - + // ==================== LANGUAGE ==================== - + /** * Get the scripting language for this unit. * Returns "ECMAScript", "Java", etc. */ String getLanguage(); - + /** * Set the scripting language for this unit. + * * @param language The language name (e.g., "ECMAScript", "Java") */ void setLanguage(String language); - + /** * Check if this script unit uses Janino (Java) compilation. */ default boolean isJanino() { return "Java".equals(getLanguage()); } - + // ==================== HOOK GENERATION ==================== - + /** * Generate a stub/template for the given hook name. * ECMAScript: function hookName(event) { } * Janino: full Java method signature with return type - * + * * @param hookName The hook name to generate a stub for * @param hookData Optional data (e.g., Method object for Janino) * @return The generated stub string */ String generateHookStub(String hookName, Object hookData); - + // ==================== NBT ==================== - + /** * Write this script unit to NBT. */ NBTTagCompound writeToNBT(NBTTagCompound compound); - + /** * Read this script unit from NBT. */ void readFromNBT(NBTTagCompound compound); - + // ==================== STATE ==================== /** @@ -141,43 +141,43 @@ public interface IScriptUnit { * No-op for ScriptContainer. */ void ensureCompiled(); - + /** * Check if this script unit has any code (script or external scripts). */ boolean hasCode(); - + /** * Check if this script unit has encountered an error during execution. */ boolean hasErrored(); - + /** * Set the error state of this script unit. */ void setErrored(boolean errored); - + // ==================== EXECUTION ==================== - + /** * Execute this script unit for the given hook type and event. - * - * @param type The script hook type + * + * @param type The script hook type * @param event The event object */ void run(EnumScriptType type, Event event); - + /** * Execute this script unit for the given hook name and event. * * @param hookName The hook name (e.g., "init", "interact") - * @param event The event object + * @param event The event object */ void run(String hookName, Object event); /** * Call a script function by name and return its result. - * + *

* Default implementation returns null (not supported). */ default Object callFunction(String hookName, Object... args) { diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index fb354e590..cbe1230e0 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -11,13 +11,13 @@ import java.util.function.Supplier; /** * Reusable IScriptHandler implementation for objects that own an optional single JaninoScript unit. - * + *

* This is meant for "function provider" scripts (not EnumScriptType driven). Execution is typically * initiated by the owning system calling script.call(...) directly. - * + *

* NBT storage is kept under the parent compound key "Script" (same convention as JaninoScriptable). */ -public class JaninoScriptHandler> extends ScriptHandler { +public class JaninoScriptHandler> extends ScriptHandler { protected S script; @@ -41,9 +41,9 @@ public class JaninoScriptHandler> extends ScriptHandle } public JaninoScript createScript() { - if (script == null) + if (script == null) script = factory.get(); - + return script; } @@ -96,9 +96,9 @@ public class JaninoScriptHandler> extends ScriptHandle @Override public List getScripts() { - if (script == null) + if (script == null) return Collections.emptyList(); - + return Collections.singletonList(script); } diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java index f1840a625..961c570f9 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java @@ -249,8 +249,8 @@ public class LinkedItem implements ILinkedItem { @Override public void setAttackSpeed(int time) { - if(time <= 0) { - time = 10; + if (time <= 0) { + time = 10; } this.attackSpeed = time; } diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 1ee4537fb..a54c20727 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -9,44 +9,47 @@ import noppes.npcs.constants.ScriptContext; * Manages a single script container for linked item hooks. */ public class LinkedItemScript extends SingleScriptHandler implements IScriptHandlerPacket { - - /** The linked item ID for packet communication. -1 if not bound. */ + + /** + * The linked item ID for packet communication. -1 if not bound. + */ private int linkedItemId = -1; - + /** * Create an unbound LinkedItemScript (for server-side use). */ public LinkedItemScript() { } - + /** * Create a LinkedItemScript bound to a specific linked item (for GUI use). + * * @param linkedItemId The ID of the LinkedItem */ public LinkedItemScript(int linkedItemId) { this.linkedItemId = linkedItemId; } - + @Override public ScriptContext getContext() { return ScriptContext.LINKED_ITEM; } - + @Override public String noticeString() { return "LinkedItem"; } - + @Override public void requestData() { - if (linkedItemId >= 0) + if (linkedItemId >= 0) LinkedItemScriptPacket.Get(linkedItemId); } - + @Override public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { - if (linkedItemId >= 0) + if (linkedItemId >= 0) LinkedItemScriptPacket.Save(linkedItemId, index, totalCount, nbt); - + } } diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java index d175eb3b9..d4b4c027b 100644 --- a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -7,13 +7,11 @@ import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import java.util.ArrayList; -import java.util.Map; -import java.util.TreeMap; /** * Base class for script handlers that manage multiple script containers (tabs). * Provides shared NBT serialization and common script execution patterns. - * + *

* Subclasses should: * - Override getContext() to return the appropriate ScriptContext * - Override getHookContext() if not using ScriptContext's hookContext @@ -21,7 +19,7 @@ import java.util.TreeMap; * - Override isEnabled() if custom enable logic is needed */ public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { - + // ==================== NBT SERIALIZATION ==================== /** diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java index 1271594ba..b2b8c4b57 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -99,6 +99,7 @@ public class PlayerCurrencyData implements IPlayerCurrencyData { /** * Add currency to balance (deposit) + * * @param amount Amount to add * @return true if successful, false if would exceed max balance */ @@ -135,6 +136,7 @@ public class PlayerCurrencyData implements IPlayerCurrencyData { /** * Remove currency from balance (withdraw) + * * @param amount Amount to remove * @return true if successful, false if insufficient funds */ diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index 803a1b30d..97f81df90 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -9,48 +9,53 @@ import noppes.npcs.constants.ScriptContext; * Manages a single script container for recipe hooks (pre, post). */ public class RecipeScript extends SingleScriptHandler implements IScriptHandlerPacket { - - /** The recipe ID for packet communication. -1 if not bound. */ + + /** + * The recipe ID for packet communication. -1 if not bound. + */ private int recipeId = -1; - - /** Whether this is an anvil recipe (vs carpentry). */ + + /** + * Whether this is an anvil recipe (vs carpentry). + */ private boolean anvil = false; - + /** * Create an unbound RecipeScript (for server-side use). */ public RecipeScript() { } - + /** * Create a RecipeScript bound to a specific recipe (for GUI use). + * * @param recipeId The ID of the recipe - * @param anvil true for anvil recipe, false for carpentry recipe + * @param anvil true for anvil recipe, false for carpentry recipe */ public RecipeScript(int recipeId, boolean anvil) { this.recipeId = recipeId; this.anvil = anvil; } - + @Override public ScriptContext getContext() { return ScriptContext.RECIPE; } - + @Override public String noticeString() { return "RecipeScript"; } - + @Override public void requestData() { - if (recipeId >= 0) + if (recipeId >= 0) RecipeScriptPacket.Get(anvil, recipeId); } - + @Override public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { - if (recipeId >= 0) + if (recipeId >= 0) RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, nbt); } @@ -63,4 +68,4 @@ public class RecipeScript extends SingleScriptHandler implements IScriptHandlerP this.function = functionName; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index dd57b05bb..684c9a45b 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -68,6 +68,7 @@ public abstract class ScriptHandler implements IScriptHandler { public void clear() { this.scripts = new ArrayList<>(); } + /** * Reset the lastInited timestamp to force re-initialization on next script call. */ diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java index a047b41b6..00c273bca 100644 --- a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -16,7 +16,7 @@ import java.util.List; /** * Base class for script handlers that manage a single script container. * Provides shared NBT/ByteBuf serialization and packet handling. - * + *

* Subclasses should: * - Store their parent object (e.g., CustomEffect, LinkedItem) * - Override getHookContext() to return the appropriate context string @@ -57,9 +57,9 @@ public abstract class SingleScriptHandler extends ScriptHandler { @Override public List getScripts() { - if(container == null) + if (container == null) return new ArrayList<>(); - + return Collections.singletonList(container); } @@ -112,7 +112,7 @@ public abstract class SingleScriptHandler extends ScriptHandler { /** * Save script data from a ByteBuf packet. * Handles the tab/totalScripts protocol used by script packets. - * + *

* Protocol: * - tab == 0: Script content NBT follows * - tab != 0 (typically -1): Script metadata (language, enabled) follows diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index 68bc96c53..85e76ec02 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -41,7 +41,8 @@ public class TraderStock { /** * Get available stock for a slot - * @param slot The slot index + * + * @param slot The slot index * @param playerName The player name (for per-player mode) * @return Available stock count, or Integer.MAX_VALUE if unlimited */ @@ -73,6 +74,7 @@ public class TraderStock { /** * Consume stock when purchase is made + * * @return true if successful, false if insufficient stock */ public synchronized boolean consumeStock(int slot, String playerName, int amount) { @@ -98,6 +100,7 @@ public class TraderStock { /** * Check if stock should reset based on current time + * * @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type */ public boolean shouldReset(long currentTime) { @@ -113,6 +116,7 @@ public class TraderStock { /** * Reset all stock to max values + * * @param currentTime Current time to record as reset time */ public synchronized void resetStock(long currentTime) { @@ -129,6 +133,7 @@ public class TraderStock { /** * Get reset interval based on reset type + * * @return Interval in ticks (MC time) or milliseconds (real time) */ public long getResetInterval() { @@ -152,6 +157,7 @@ public class TraderStock { /** * Get time until next reset + * * @param currentTime Current time * @return Time remaining until reset, or -1 if no reset scheduled */ @@ -166,6 +172,7 @@ public class TraderStock { /** * Format time until reset for display + * * @param currentTime Current time * @return Formatted string like "2 hours" or "1 day, 5 hours" */ @@ -183,7 +190,7 @@ public class TraderStock { if (days > 0) { return days + " day" + (days != 1 ? "s" : "") + - (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); + (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); } else if (hours > 0) { return hours + " hour" + (hours != 1 ? "s" : ""); } else { @@ -198,7 +205,7 @@ public class TraderStock { if (mcDays > 0) { return mcDays + " MC day" + (mcDays != 1 ? "s" : "") + - (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); + (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); } else { return mcHours + " MC hour" + (mcHours != 1 ? "s" : ""); } @@ -220,6 +227,7 @@ public class TraderStock { /** * Validate and clamp all current stock values to not exceed max stock. * Should be called when trader GUI opens to ensure consistency. + * * @return true if any stock was clamped */ public boolean validateStock() { diff --git a/src/main/java/noppes/npcs/controllers/data/action/Action.java b/src/main/java/noppes/npcs/controllers/data/action/Action.java index 729603019..b7bce1102 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/Action.java +++ b/src/main/java/noppes/npcs/controllers/data/action/Action.java @@ -274,10 +274,9 @@ public class Action implements IAction { // return this; // } - /////////////////////////////////////////////////// - ////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// // Handling - @Override public IAction start() { manager.start(); @@ -405,10 +404,10 @@ public class Action implements IAction { public String toString() { return String.format("%s [queue='%s', scheduled=%s, done=%s, paused=%s, updateEvery=%s, duration=%d/%d, count=%d/%d, threaded=%s]", getIdentifier(), getQueueName(), isScheduled, done, isPaused(), updateEveryXTick, duration, maxDuration, count, maxCount, isThreaded); } - /////////////////////////////////////////////////// - ////////////////////////////////////////////////// - // Thread + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// + // Thread @Override public IAction threadify() { if (!isThreaded) { @@ -469,10 +468,9 @@ public class Action implements IAction { return startAfterTicks > 0; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Sequential - @Override public IAction getNext() { if (queue == null) @@ -606,10 +604,9 @@ public class Action implements IAction { } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Parallels - @Override public IAction parallel(IAction act) { if (unscheduledList != null) @@ -658,10 +655,9 @@ public class Action implements IAction { return parallel(manager.create(name, maxDuration, delay, t)); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Conditionals - @Override public IConditionalAction conditional(IConditionalAction after) { if (unscheduledList != null) diff --git a/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java b/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java index c36aa04a9..9cdaf4e57 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java +++ b/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java @@ -117,10 +117,9 @@ public class ActionManager implements IActionManager { // return false; // } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Creators - @Override public IAction create(String name) { return new Action(this, name); @@ -186,10 +185,9 @@ public class ActionManager implements IActionManager { return new ConditionalAction(this, name, condition, task, terminateWhen, onTermination); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Queues - @Override public IActionQueue createQueue(String name) { return createQueue(name, false); @@ -237,10 +235,9 @@ public class ActionManager implements IActionManager { return true; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Sequential - @Override public IActionQueue getSequentialQueue() { return sequentialQueue; @@ -299,10 +296,9 @@ public class ActionManager implements IActionManager { return sequentialQueue.scheduleActionAt(index, action); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Parallels - @Override public IActionQueue getParallelQueue() { return parallelQueue; @@ -345,7 +341,7 @@ public class ActionManager implements IActionManager { @Override public IAction scheduleParallel(String name, int delay, Consumer task) { - return parallelQueue.schedule(name,delay, task); + return parallelQueue.schedule(name, delay, task); } @Override @@ -353,10 +349,9 @@ public class ActionManager implements IActionManager { return parallelQueue.schedule(name, maxDuration, delay, task); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Conditionals - @Override public IActionQueue getConditionalQueue() { return conditionalQueue; @@ -403,10 +398,9 @@ public class ActionManager implements IActionManager { return schedule(new ConditionalAction(this, name, condition, task, terminateWhen, onTermination)); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Handling - @Override public IActionQueue[] getAllQueues() { ArrayList allQueues = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java b/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java index 0b3d0b483..332adab39 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java +++ b/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java @@ -125,10 +125,9 @@ public class ActionQueue implements IActionQueue { return this; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Schedulers - @Override public IAction schedule(IAction action) { Action act = (Action) action; @@ -203,10 +202,9 @@ public class ActionQueue implements IActionQueue { return action; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Queue Data - @Override public boolean hasActiveTasks() { return !queue.isEmpty(); @@ -276,10 +274,9 @@ public class ActionQueue implements IActionQueue { return false; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Handling - public boolean finish(Action a) { if (a.isDone()) { if (a.onDone != null) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index c30f15f57..20d2f6d5c 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -67,14 +67,13 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraft.world.WorldServer; -import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.event.ServerChatEvent; import noppes.npcs.CustomItems; import noppes.npcs.CustomNpcs; -import noppes.npcs.DataAbilities; import noppes.npcs.DataAI; +import noppes.npcs.DataAbilities; import noppes.npcs.DataAdvanced; import noppes.npcs.DataDisplay; import noppes.npcs.DataInventory; @@ -96,12 +95,12 @@ import noppes.npcs.ai.EntityAIDodgeShoot; import noppes.npcs.ai.EntityAIFindShade; import noppes.npcs.ai.EntityAIFollow; import noppes.npcs.ai.EntityAIJob; +import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAILook; import noppes.npcs.ai.EntityAIMoveIndoors; import noppes.npcs.ai.EntityAIMovingPath; import noppes.npcs.ai.EntityAIOrbitTarget; import noppes.npcs.ai.EntityAIPanic; -import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAIPounceTarget; import noppes.npcs.ai.EntityAIRangedAttack; import noppes.npcs.ai.EntityAIReturn; @@ -155,8 +154,8 @@ import noppes.npcs.roles.JobFollower; import noppes.npcs.roles.JobInterface; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; -import noppes.npcs.roles.RoleMount; import noppes.npcs.roles.RoleInterface; +import noppes.npcs.roles.RoleMount; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; @@ -2150,7 +2149,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti protected boolean handleMountedMovement(float strafe, float forward) { return NPCMountUtil.handleMountedMovement(this, mountState, strafe, forward); } - + public void performMountedMovement(float strafe, float forward, float moveSpeed) { this.moveStrafing = strafe; this.moveForward = forward; diff --git a/src/main/java/noppes/npcs/items/ItemElementalStaff.java b/src/main/java/noppes/npcs/items/ItemElementalStaff.java index 45f45c17f..d786222b1 100644 --- a/src/main/java/noppes/npcs/items/ItemElementalStaff.java +++ b/src/main/java/noppes/npcs/items/ItemElementalStaff.java @@ -9,7 +9,7 @@ import noppes.npcs.CustomItems; import noppes.npcs.CustomNpcs; import noppes.npcs.constants.EnumNpcToolMaterial; -import java.awt.*; +import java.awt.Color; import java.util.List; public class ItemElementalStaff extends ItemStaff { diff --git a/src/main/java/noppes/npcs/items/ItemNpcScripter.java b/src/main/java/noppes/npcs/items/ItemNpcScripter.java index c3a88def0..4d503d97a 100644 --- a/src/main/java/noppes/npcs/items/ItemNpcScripter.java +++ b/src/main/java/noppes/npcs/items/ItemNpcScripter.java @@ -41,7 +41,7 @@ public class ItemNpcScripter extends Item { } return itemStack; } - + CustomNpcs.proxy.openGui(0, 0, 0, EnumGuiType.ScriptEvent, player); return itemStack; } diff --git a/src/main/java/noppes/npcs/items/ItemOrb.java b/src/main/java/noppes/npcs/items/ItemOrb.java index 04df0e16c..b43ccaef4 100644 --- a/src/main/java/noppes/npcs/items/ItemOrb.java +++ b/src/main/java/noppes/npcs/items/ItemOrb.java @@ -5,7 +5,7 @@ import net.minecraft.entity.passive.EntitySheep; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import java.awt.*; +import java.awt.Color; import java.util.List; public class ItemOrb extends ItemNpcInterface { diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index 2ff8b4afb..fc0c63f6d 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -4,7 +4,8 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.HashMap; +import java.util.Map; /** * Resolves script hook methods for compiled script classes. diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 46d0bf164..1800cc895 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -8,13 +8,12 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.config.ConfigScript; -import noppes.npcs.controllers.HookDefinition; -import noppes.npcs.janino.annotations.ParamName; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.janino.annotations.ParamName; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; @@ -23,7 +22,14 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.security.Permissions; import java.security.PrivilegedAction; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -54,7 +60,7 @@ public abstract class JaninoScript implements IScriptUnit { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; this.builder = IScriptBodyBuilder.getBuilder(type, - isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) + isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) .setDefaultImports(this.defaultImports); Permissions permissions = new Permissions(); @@ -67,7 +73,7 @@ public abstract class JaninoScript implements IScriptUnit { this(type, defaultImports, false); } - protected String getHookContext(){ + protected String getHookContext() { return this.context.hookContext; } @@ -326,7 +332,7 @@ public abstract class JaninoScript implements IScriptUnit { public static String generateMethodStub(Method method) { String mods = Modifier.toString(method.getModifiers()); mods = mods.replace("abstract ", "").replace("abstract", "") - .replace("default ", "").replace("default", "").trim(); + .replace("default ", "").replace("default", "").trim(); if (!mods.isEmpty()) mods += " "; String returnTypeStr = getUsableTypeName(method.getReturnType()); @@ -346,7 +352,7 @@ public abstract class JaninoScript implements IScriptUnit { } else { // Fallback to generated name String baseName = Character.toLowerCase(p.getType().getSimpleName().charAt(0)) - + p.getType().getSimpleName().substring(1); + + p.getType().getSimpleName().substring(1); int count = typeCount.getOrDefault(baseName, 0) + 1; typeCount.put(baseName, count); paramName = count == 1 ? baseName : baseName + (count - 1); @@ -393,7 +399,10 @@ public abstract class JaninoScript implements IScriptUnit { // ==================== IScriptUnit ==================== - @Override public String getScript() { return script; } + @Override + public String getScript() { + return script; + } @Override public void setScript(String script) { @@ -402,7 +411,10 @@ public abstract class JaninoScript implements IScriptUnit { hookResolver.clearResolutionCaches(); } - @Override public List getExternalScripts() { return externalScripts; } + @Override + public List getExternalScripts() { + return externalScripts; + } @Override public void setExternalScripts(List scripts) { @@ -411,8 +423,15 @@ public abstract class JaninoScript implements IScriptUnit { hookResolver.clearResolutionCaches(); } - @Override public TreeMap getConsole() { return console; } - @Override public void clearConsole() { console.clear(); } + @Override + public TreeMap getConsole() { + return console; + } + + @Override + public void clearConsole() { + console.clear(); + } @Override public void appendConsole(String message) { @@ -427,16 +446,29 @@ public abstract class JaninoScript implements IScriptUnit { } } - @Override public String getLanguage() { return "Java"; } - @Override public void setLanguage(String language) { } + @Override + public String getLanguage() { + return "Java"; + } + + @Override + public void setLanguage(String language) { + } @Override public boolean hasCode() { return !externalScripts.isEmpty() || (script != null && !script.isEmpty()); } - @Override public boolean hasErrored() { return errored; } - @Override public void setErrored(boolean errored) { this.errored = errored; } + @Override + public boolean hasErrored() { + return errored; + } + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } // ==================== NBT ==================== diff --git a/src/main/java/noppes/npcs/janino/annotations/ParamName.java b/src/main/java/noppes/npcs/janino/annotations/ParamName.java index 6b1e9ec25..deb8ed85c 100644 --- a/src/main/java/noppes/npcs/janino/annotations/ParamName.java +++ b/src/main/java/noppes/npcs/janino/annotations/ParamName.java @@ -12,9 +12,13 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface ParamName { - /** The parameter name (e.g., "event") */ + /** + * The parameter name (e.g., "event") + */ String value(); - - /** The fully qualified type name (e.g., "noppes.npcs.api.event.INpcEvent.InitEvent") */ + + /** + * The fully qualified type name (e.g., "noppes.npcs.api.event.INpcEvent.InitEvent") + */ String type() default ""; } diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 107076a44..91a116efc 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -238,9 +238,9 @@ public class RoleTrader extends RoleInterface { * Consume stock after purchase. * For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise). * - * @param slot The slot purchased from + * @param slot The slot purchased from * @param playerName The player making the purchase - * @param amount Amount to consume + * @param amount Amount to consume * @return true if stock was consumed successfully */ public boolean consumeStock(int slot, String playerName, int amount) { @@ -284,6 +284,7 @@ public class RoleTrader extends RoleInterface { /** * Get time until stock reset in real-time milliseconds. * Converts MC ticks to approximate real-time for client countdown display. + * * @return Remaining time in milliseconds, or -1 if no reset scheduled */ public long getResetTimeRemainingMillis() { diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 3d71e0023..ecf4b8ba0 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.eventhandler.EventBus; import foxz.command.ScriptedCommand; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.block.Block; import net.minecraft.command.CommandHandler; import net.minecraft.entity.Entity; @@ -59,7 +60,22 @@ import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.gui.ICustomGui; -import noppes.npcs.api.handler.*; +import noppes.npcs.api.handler.IAbilityHandler; +import noppes.npcs.api.handler.IActionManager; +import noppes.npcs.api.handler.IAnimationHandler; +import noppes.npcs.api.handler.IAttributeHandler; +import noppes.npcs.api.handler.ICloneHandler; +import noppes.npcs.api.handler.ICustomEffectHandler; +import noppes.npcs.api.handler.IDialogHandler; +import noppes.npcs.api.handler.IFactionHandler; +import noppes.npcs.api.handler.IMagicHandler; +import noppes.npcs.api.handler.INaturalSpawnsHandler; +import noppes.npcs.api.handler.IPartyHandler; +import noppes.npcs.api.handler.IProfileHandler; +import noppes.npcs.api.handler.IQuestHandler; +import noppes.npcs.api.handler.IRecipeHandler; +import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.api.handler.ITransportHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; import noppes.npcs.api.handler.data.IFramePart; @@ -70,7 +86,6 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.containers.ContainerNpcInterface; -import kamkeel.npcs.controllers.data.ability.AbilityController; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.ChunkController; import noppes.npcs.controllers.CustomEffectController; @@ -81,8 +96,8 @@ import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.ScriptEntityData; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.ServerCloneController; import noppes.npcs.controllers.SpawnController; import noppes.npcs.controllers.TransportController; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 85a15041e..f2cfc9a34 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -39,7 +39,6 @@ import noppes.npcs.api.handler.IOverlayHandler; import noppes.npcs.api.handler.data.IAnimationData; import noppes.npcs.api.handler.data.IDialog; import noppes.npcs.api.handler.data.IMagicData; -import noppes.npcs.api.handler.data.IParty; import noppes.npcs.api.handler.data.IPlayerAttributes; import noppes.npcs.api.handler.data.IQuest; import noppes.npcs.api.handler.data.ISound; @@ -882,13 +881,13 @@ public class ScriptPlayer extends ScriptLivingBase @Override - public IPlayer[] getPartyMembers(){ + public IPlayer[] getPartyMembers() { Party party = this.getData().getPlayerParty(); if (party == null) { throw new CustomNPCsException("Player is not in party"); } - List list = new ArrayList<>(); - for(int i = 0; i < party.getPlayerNamesList().size();i++){ + List list = new ArrayList<>(); + for (int i = 0; i < party.getPlayerNamesList().size(); i++) { IPlayer player = NpcAPI.Instance().getPlayer(party.getPlayerNamesList().get(i)); if (player == null) continue; list.add(player); diff --git a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java index 9baf245a2..9bf7719d5 100644 --- a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java @@ -125,7 +125,7 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { private float knockbackUp; public HitEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, - EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { super(npc, ability, target); this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; this.damage = damage; diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 0ca2fa778..e3641570e 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -10,10 +10,10 @@ import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptHandlerPacket; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.CustomNPCsException; import java.util.ArrayList; @@ -279,7 +279,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } public void setAttackSpeed(int speed) { - if(speed <= 0) { + if (speed <= 0) { speed = 10; } this.attackSpeed = speed; diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java index 6303f67b9..9c3cce57f 100644 --- a/src/main/java/noppes/npcs/util/NPCMountUtil.java +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -5,10 +5,10 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.MathHelper; import noppes.npcs.CustomNpcs; +import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.constants.EnumRoleType; import noppes.npcs.roles.RoleMount; public final class NPCMountUtil { diff --git a/src/main/java/noppes/npcs/util/ValueUtil.java b/src/main/java/noppes/npcs/util/ValueUtil.java index a81fb9fef..71846afcc 100644 --- a/src/main/java/noppes/npcs/util/ValueUtil.java +++ b/src/main/java/noppes/npcs/util/ValueUtil.java @@ -9,7 +9,7 @@ public class ValueUtil { public static double lerp(double start, double end, double delta) { return start + delta * (end - start); } - + public static float clamp(float given, float min, float max) { if (given < min) return min; diff --git a/src/main/java/somehussar/janino/AdvancedClassFilter.java b/src/main/java/somehussar/janino/AdvancedClassFilter.java index b48de2c30..7c2d51834 100644 --- a/src/main/java/somehussar/janino/AdvancedClassFilter.java +++ b/src/main/java/somehussar/janino/AdvancedClassFilter.java @@ -24,14 +24,17 @@ public class AdvancedClassFilter implements LoadClassCondition { allowedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); return this; } + public AdvancedClassFilter banRegexes(String... classRegexes) { bannedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); return this; } + public AdvancedClassFilter addClasses(String... classPath) { allowedClasses.addAll(Arrays.asList(classPath)); return this; } + public AdvancedClassFilter banClasses(String... classPath) { bannedClasses.addAll(Arrays.asList(classPath)); return this; diff --git a/src/main/java/somehussar/janino/EntityUnloadListener.java b/src/main/java/somehussar/janino/EntityUnloadListener.java index 592b0750c..b4777e712 100644 --- a/src/main/java/somehussar/janino/EntityUnloadListener.java +++ b/src/main/java/somehussar/janino/EntityUnloadListener.java @@ -47,7 +47,7 @@ public class EntityUnloadListener implements IWorldAccess { EntityNPCInterface npc = (EntityNPCInterface) entity; // TODO: REMOVE SCRIPTED OBJECT AND CLASSLOADER FOR IT } - } + } @Override public void playRecord(String p_72702_1_, int p_72702_2_, int p_72702_3_, int p_72702_4_) {