diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md index ba7786d0a..4236e0e63 100644 --- a/1.11_Market_Update.md +++ b/1.11_Market_Update.md @@ -313,6 +313,8 @@ All actions are validated server-side: | Auction expiry check | `placeBid()/buyout()` | Double-check inside write lock | | Currency validation | All actions | Verify balance before operations | | Claim ownership | `claimItem()/claimCurrency()` | `claim.isForPlayer(playerUUID)` check | +| Blacklist check | `createListing()` | `AuctionBlacklist.isBlacklistedForPlayer()` check | +| Bound item check | `createListing()` | Auto-blocks Soulbound/Profile-Slotbound items | ### 3.7 Snipe Protection @@ -325,6 +327,53 @@ Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 --- +### 3.8 Item Blacklist System + +Prevents specific items from being listed on the auction house. + +#### Blacklist Types + +| Type | Description | Example | +|------|-------------|---------| +| **Items** | Block specific items by registry name | `minecraft:diamond_sword` | +| **Wildcards** | Block items matching pattern | `minecraft:diamond*` | +| **Mods** | Block all items from a mod | `customnpcs` | +| **NBT Tags** | Block items with specific NBT | `ench` (enchanted items) | + +#### Auto-Blocked Items (Always Blocked) + +These items are **always** blocked regardless of blacklist settings: +- **Soulbound Items** - Items with `cnpc_soulbind` requirement +- **Profile-Slotbound Items** - Items with `cnpc_profile_slot` requirement + +The system checks for these under `RPGCore/Requirements` in the item's NBT. + +#### Bypass Permission + +Players with OP status or the `customnpcs.auction.blacklist.bypass` permission can list blacklisted items. + +#### Configuration + +``` +Market.Auction.Blacklist: + Enabled: true + BlacklistedItems: ["minecraft:barrier", "minecraft:command_block*"] + BlacklistedMods: [] + BlacklistedNBTTags: [] +``` + +#### Commands + +| Command | Description | +|---------|-------------| +| `/cnpc auction blacklist add ` | Add to blacklist (runtime only) | +| `/cnpc auction blacklist remove ` | Remove from blacklist (runtime only) | +| `/cnpc auction blacklist list [item\|mod\|nbt]` | List blacklisted entries | +| `/cnpc auction blacklist reload` | Reload blacklist from config | +| `/cnpc auction blacklist check` | Check if held item is blacklisted | + +--- + ## 4. AuctionController Performance & Thread Safety ### 4.1 Threading Model @@ -418,6 +467,7 @@ controllers/data/ AuctionListing.java ✅ AuctionClaim.java ✅ AuctionFilter.java ✅ + AuctionBlacklist.java ✅ (item blacklist checking + bypass) PlayerTradeData.java ✅ (currency + claims storage per player) roles/ @@ -457,7 +507,9 @@ network/packets/player/ ``` command/ - AuctionCommand.java ✅ /auction command with list, view, open subcommands + AuctionCommand.java ✅ /cnpc auction with list, view, open, blacklist + command/auction/ + BlacklistSubCommand.java ✅ /cnpc auction blacklist subcommands ``` ### Files Modified @@ -518,43 +570,77 @@ public static boolean LogAuctionSold = true; public static boolean LogAuctionExpired = true; public static boolean LogAuctionCancelled = true; public static boolean LogAuctionClaimed = true; + +// ========================================= +// Auction Blacklist (Market.Auction.Blacklist) +// ========================================= +public static boolean BlacklistEnabled = true; +public static String[] BlacklistedItems = {}; // Registry names (supports wildcards) +public static String[] BlacklistedMods = {}; // Mod IDs to block entirely +public static String[] BlacklistedNBTTags = {}; // NBT tag keys to block ``` -### Trade Slot Permissions - -Players can have more than the default trade slots via permissions: +### Auction Permissions | Permission | Description | |------------|-------------| | `customnpcs.auction.trades.*` | Unlimited trade slots (up to 45) | | `customnpcs.auction.trades.X` | Exactly X trade slots (where X is 1-45) | +| `customnpcs.auction.blacklist.bypass` | Bypass item blacklist restrictions | -**How it works:** +**Trade Slot Permissions:** - Default is 8 trade slots - Permissions only **increase** the limit (permissions below 8 are ignored) - Maximum is capped at 45 (9x5 grid) - Trade slots = Active Listings + Active Bids + Pending Claims +**Blacklist Bypass:** +- OP players or those with `customnpcs.auction.blacklist.bypass` can list blacklisted items +- Soulbound and Profile-Slotbound items are **always** blocked (no bypass) + --- ## 7. Commands -### `/auction` Command +### `/cnpc auction` Command Allows interaction with the Auction House without needing an Auctioneer NPC. | Subcommand | Usage | Permission | Description | |------------|-------|------------|-------------| -| `list` | `/auction list [page]` | Default | View active auctions in chat | -| `view` | `/auction view ` | Default | View a player's auction activity | -| `open` | `/auction open` | Player only | Opens the Auction House GUI | +| `list` | `/cnpc auction list [page]` | OP | View active auctions in chat | +| `view` | `/cnpc auction view ` | OP | View a player's auction activity | +| `open` | `/cnpc auction open` | Any | Opens the Auction House GUI | **Examples:** ``` -/auction list - Show first page of active auctions -/auction list 2 - Show page 2 of auctions -/auction view Steve - Show Steve's listings, bids, and claims -/auction open - Open the full Auction House GUI +/cnpc auction list - Show first page of active auctions +/cnpc auction list 2 - Show page 2 of auctions +/cnpc auction view Steve - Show Steve's listings, bids, and claims +/cnpc auction open - Open the full Auction House GUI +``` + +### `/cnpc auction blacklist` Command + +Manage the auction item blacklist (runtime changes, not persisted to config). + +| Subcommand | Usage | Permission | Description | +|------------|-------|------------|-------------| +| `add` | `/cnpc auction blacklist add ` | OP | Add to blacklist | +| `remove` | `/cnpc auction blacklist remove ` | OP | Remove from blacklist | +| `list` | `/cnpc auction blacklist list [item\|mod\|nbt]` | OP | List blacklisted entries | +| `reload` | `/cnpc auction blacklist reload` | OP | Reload from config | +| `check` | `/cnpc auction blacklist check` | Any | Check if held item is blacklisted | + +**Examples:** +``` +/cnpc auction blacklist add item minecraft:diamond_sword +/cnpc auction blacklist add item minecraft:diamond* (wildcard) +/cnpc auction blacklist add mod industrialcraft2 +/cnpc auction blacklist add nbt ench +/cnpc auction blacklist remove item minecraft:diamond_sword +/cnpc auction blacklist list item +/cnpc auction blacklist check (checks held item) ``` --- @@ -607,6 +693,11 @@ Allows interaction with the Auction House without needing an Auctioneer NPC. - [x] **30-second intervals** - Combined process/save cycle for simplicity - [x] **Async offline player claims** - Claims added to offline players via CNPC+ thread - [x] **Claim migration** - Old claims in auction.dat automatically migrated to PlayerData +- [x] **Item Blacklist System** - Block items by registry name, mod, NBT tag, or wildcard pattern + - Auto-blocks Soulbound and Profile-Slotbound items (checks `RPGCore/Requirements`) + - Per-player bypass via `customnpcs.auction.blacklist.bypass` permission + - Runtime add/remove commands (not persisted to config) + - Check command to test held item --- @@ -622,13 +713,14 @@ Allows interaction with the Auction House without needing an Auctioneer NPC. | Async Saves | CNPC+ Thread | Doesn't block server tick | | Offline Claims | Load PlayerData async, add claim, save | Claims can be added to offline players without blocking | | Config Sync | LoginPacket → AuctionClientConfig | Client never accesses server config directly | +| Item Blacklist | Static utility class | Simple, fast checks without instantiation; auto-blocks bound items | +| Bound Item Detection | Check `RPGCore/Requirements` NBT path | Uses AttributeItemUtil constants for consistency | --- ## 10. Future Considerations (Not in 1.11) - Category-based filtering (by item type, mod, etc.) -- Advanced blacklist system (items, mods, NBT patterns) - Auction history/statistics tracking - Auction house tax sink (currency removed from economy) - Reserve prices (hidden minimum price) diff --git a/Auction_Item_Blacklist_Plan.md b/Auction_Item_Blacklist_Plan.md deleted file mode 100644 index d9dfd9075..000000000 --- a/Auction_Item_Blacklist_Plan.md +++ /dev/null @@ -1,218 +0,0 @@ -# Auction Item Blacklist Feature Plan - -## Overview - -Add the ability to prevent certain items from being listed on the Auction House via a configurable blacklist system. - ---- - -## 1. Blacklist Types - -### 1.1 Item ID Blacklist -- Block specific items by registry name (e.g., `minecraft:diamond_sword`, `customnpcs:npcSoulstoneFilled`) -- Supports wildcard patterns (e.g., `minecraft:*_sword` for all swords) - -### 1.2 Mod Blacklist -- Block all items from a specific mod (e.g., `appliedenergistics2`, `thaumcraft`) -- Useful for blocking items from tech/magic mods that could disrupt economy - -### 1.3 NBT Pattern Blacklist (Advanced) -- Block items with specific NBT tags (e.g., items with "Soulbound" enchant, creative-only items) -- Pattern matching on NBT paths - ---- - -## 2. Configuration - -### 2.1 Config File Location -``` -config/CustomNPC+/auction_blacklist.cfg -``` - -### 2.2 Config Structure -```java -// In ConfigMarket.java or separate ConfigAuctionBlacklist.java -public static String[] BlacklistedItems = new String[]{}; -public static String[] BlacklistedMods = new String[]{}; -public static String[] BlacklistedNBTPatterns = new String[]{}; -public static boolean EnableBlacklist = true; -public static boolean ShowBlacklistReason = true; -``` - -### 2.3 Example Configuration -```cfg -# Items that cannot be listed on the Auction House -S:BlacklistedItems < - minecraft:bedrock - minecraft:command_block - minecraft:barrier - customnpcs:npcSoulstoneFilled - customnpcs:npcMoney* - > - -# Mods whose items are completely blocked -S:BlacklistedMods < - projecte - equivalentexchange3 - > - -# NBT patterns that block items (advanced) -S:BlacklistedNBTPatterns < - tag.ench[*].id=34 - tag.Unbreakable=1 - > -``` - ---- - -## 3. Implementation - -### 3.1 New Files -``` -controllers/data/AuctionBlacklist.java - Blacklist checking logic -config/ConfigAuctionBlacklist.java - Configuration handling -``` - -### 3.2 AuctionBlacklist.java -```java -public class AuctionBlacklist { - private static Set blacklistedItems = new HashSet<>(); - private static Set blacklistedMods = new HashSet<>(); - private static List blacklistedNBT = new ArrayList<>(); - - public static boolean isBlacklisted(ItemStack item) { - if (item == null) return true; - - // Check item registry name - String registryName = GameRegistry.findUniqueIdentifierFor(item.getItem()).toString(); - if (matchesPattern(registryName, blacklistedItems)) return true; - - // Check mod - String modId = GameRegistry.findUniqueIdentifierFor(item.getItem()).modId; - if (blacklistedMods.contains(modId.toLowerCase())) return true; - - // Check NBT patterns - if (item.hasTagCompound()) { - for (NBTPattern pattern : blacklistedNBT) { - if (pattern.matches(item.getTagCompound())) return true; - } - } - - return false; - } - - public static String getBlacklistReason(ItemStack item) { - // Returns human-readable reason why item is blocked - } - - public static void reload() { - // Reload from config - } -} -``` - -### 3.3 Integration Points - -#### AuctionController.createListing() -```java -// Add at start of createListing method: -if (AuctionBlacklist.isBlacklisted(item)) { - if (ConfigMarket.ShowBlacklistReason) { - return "This item cannot be listed: " + AuctionBlacklist.getBlacklistReason(item); - } - return "This item cannot be listed on the Auction House."; -} -``` - -#### GuiAuctionSell (optional visual feedback) -- Gray out confirm button if item is blacklisted -- Show tooltip explaining why item can't be listed - ---- - -## 4. Commands - -### 4.1 Admin Commands -``` -/auction blacklist add -/auction blacklist remove -/auction blacklist list -/auction blacklist reload -/auction blacklist check -``` - -### 4.2 Examples -``` -/auction blacklist add item minecraft:diamond_block -/auction blacklist add mod projecte -/auction blacklist add nbt tag.ench[*].id=34 -/auction blacklist check held -``` - ---- - -## 5. Default Blacklist (Recommended) - -### 5.1 Vanilla Items -- `minecraft:command_block*` -- `minecraft:barrier` -- `minecraft:bedrock` -- `minecraft:structure_block` -- `minecraft:structure_void` - -### 5.2 CustomNPC+ Items -- `customnpcs:npcSoulstoneFilled` (contains NPC data) -- `customnpcs:npcWand` (admin tool) -- `customnpcs:npcMobCloner` (admin tool) -- `customnpcs:npcScripter` (admin tool) - ---- - -## 6. Future Considerations - -### 6.1 Whitelist Mode -- Invert the blacklist to only allow specific items -- Useful for strict economy servers - -### 6.2 Category-Based Blocking -- Block by item category (weapons, tools, armor, etc.) -- Integration with Minecraft's creative tabs - -### 6.3 Price Restrictions -- Set minimum/maximum prices for specific items -- Prevent price manipulation on valuable items - -### 6.4 Per-Player Overrides -- Permission to bypass blacklist: `customnpcs.auction.blacklist.bypass` -- Admin listing capability - ---- - -## 7. Files to Create/Modify - -### New Files -``` -config/ConfigAuctionBlacklist.java -controllers/data/AuctionBlacklist.java -``` - -### Modified Files -``` -AuctionController.java - Add blacklist check in createListing -AuctionCommand.java - Add blacklist subcommands -en_US.lang - Add blacklist messages -1.11_Market_Update.md - Document feature -``` - ---- - -## 8. Estimated Scope - -| Component | Complexity | Priority | -|-----------|------------|----------| -| Basic item blacklist | Low | High | -| Mod blacklist | Low | High | -| NBT pattern matching | Medium | Medium | -| Admin commands | Low | Medium | -| GUI feedback | Low | Low | -| Whitelist mode | Low | Low | diff --git a/src/main/java/foxz/command/CommandNoppes.java b/src/main/java/foxz/command/CommandNoppes.java index 8c051aa85..f5b5ccd73 100644 --- a/src/main/java/foxz/command/CommandNoppes.java +++ b/src/main/java/foxz/command/CommandNoppes.java @@ -2,9 +2,16 @@ package foxz.command; import net.minecraft.command.CommandBase; import net.minecraft.command.ICommandSender; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; import java.util.List; +/** + * @deprecated The /noppes command is deprecated and no longer supported. + * Please use the /kam command instead for all CNPC+ operations. + */ +@Deprecated public class CommandNoppes extends CommandBase { public CmdNoppes noppes = new CmdNoppes(this); @@ -21,6 +28,15 @@ public class CommandNoppes extends CommandBase { @Override public void processCommand(ICommandSender var1, String[] var2) { + // Send deprecation warning + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[DEPRECATED] " + + EnumChatFormatting.YELLOW + "The /noppes command is deprecated and no longer supported.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "Please use " + EnumChatFormatting.GREEN + "/kam" + EnumChatFormatting.YELLOW + " commands instead.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + + "Type " + EnumChatFormatting.WHITE + "/kam help" + EnumChatFormatting.GRAY + " for available commands.")); + + // Still execute the command for backwards compatibility noppes.processCommand(var1, var2); } diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java index a6e05a401..87d7e4385 100644 --- a/src/main/java/kamkeel/npcs/command/AuctionCommand.java +++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java @@ -1,9 +1,9 @@ package kamkeel.npcs.command; +import kamkeel.npcs.command.auction.BlacklistSubCommand; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; import noppes.npcs.NoppesUtilServer; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumGuiType; @@ -21,6 +21,11 @@ import static kamkeel.npcs.util.ColorUtil.sendResult; public class AuctionCommand extends CommandKamkeelBase { + public AuctionCommand() { + super(); + registerNestedCommand(new BlacklistSubCommand()); + } + @Override public String getCommandName() { return "auction"; @@ -158,10 +163,17 @@ public class AuctionCommand extends CommandKamkeelBase { } @Override - public List addTabCompletionOptions(ICommandSender par1, String[] args) { + public List addTabCompletionOptions(ICommandSender sender, String[] args) { if (args.length == 1) { - return getListOfStringsMatchingLastWord(args, new String[]{"list", "view", "open"}); + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); } + + // Delegate to nested commands for deeper tab completion + List nestedCompletions = getNestedTabCompletions(sender, args); + if (nestedCompletions != null) { + return nestedCompletions; + } + return null; } } diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java index af2366f89..df4d34998 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java @@ -11,13 +11,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public abstract class CommandKamkeelBase extends CommandBase { public Map subcommands = new HashMap(); + public Map nestedCommands = new HashMap(); public CommandKamkeelBase() { for (Method m : this.getClass().getDeclaredMethods()) { @@ -31,6 +29,14 @@ public abstract class CommandKamkeelBase extends CommandBase { } } + /** + * Register a nested command class as a subcommand. + * The nested command's getCommandName() becomes the subcommand name. + */ + public void registerNestedCommand(CommandKamkeelBase command) { + nestedCommands.put(command.getCommandName().toLowerCase(), command); + } + @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { @@ -51,7 +57,7 @@ public abstract class CommandKamkeelBase extends CommandBase { } public boolean runSubCommands() { - return !subcommands.isEmpty(); + return !subcommands.isEmpty() || !nestedCommands.isEmpty(); } @Retention(value = RetentionPolicy.RUNTIME) @@ -71,6 +77,22 @@ public abstract class CommandKamkeelBase extends CommandBase { } public void processSubCommand(ICommandSender sender, String command, String[] args) throws CommandException { + // Check nested commands first + CommandKamkeelBase nested = nestedCommands.get(command.toLowerCase()); + if (nested != null) { + if (!canSendNestedCommand(sender, nested)) { + throw new CommandException("You are not allowed to use this command: " + command); + } + + if (args.length == 0 || !nested.runSubCommands()) { + nested.processCommand(sender, args); + } else { + nested.processSubCommand(sender, args[0], Arrays.copyOfRange(args, 1, args.length)); + } + return; + } + + // Fall back to method subcommands Method m = subcommands.get(command.toLowerCase()); if (m == null) throw new CommandException("Unknown subcommand " + command); @@ -132,4 +154,55 @@ public abstract class CommandKamkeelBase extends CommandBase { return false; } + + public boolean canSendNestedCommand(ICommandSender sender, CommandKamkeelBase nested) { + int permission = nested.getRequiredPermissionLevel(); + String subCommand = nested.getCommandName(); + + if (sender.canCommandSenderUseCommand(permission, getSubUniversalPermission())) { + return true; + } + + if (sender.canCommandSenderUseCommand(permission, getSubCommandPermission(subCommand))) { + return true; + } + + if (sender instanceof EntityPlayer) { + return CustomNpcsPermissions.hasCustomPermission((EntityPlayer) sender, getSubCommandPermission(subCommand)); + } + + return false; + } + + // ========================================= + // Tab Completion Helpers + // ========================================= + + /** + * Get all subcommand names (both method-based and nested) + */ + public String[] getAllSubCommandNames() { + Set names = new HashSet<>(); + names.addAll(subcommands.keySet()); + names.addAll(nestedCommands.keySet()); + return names.toArray(new String[0]); + } + + /** + * Get tab completions for nested command at given depth + */ + public List getNestedTabCompletions(ICommandSender sender, String[] args) { + if (args.length == 0) return null; + + String subCmd = args[0].toLowerCase(); + CommandKamkeelBase nested = nestedCommands.get(subCmd); + + if (nested != null && args.length > 1) { + // Delegate to nested command + String[] nestedArgs = Arrays.copyOfRange(args, 1, args.length); + return nested.addTabCompletionOptions(sender, nestedArgs); + } + + return null; + } } diff --git a/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java new file mode 100644 index 000000000..a9902a890 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java @@ -0,0 +1,226 @@ +package kamkeel.npcs.command.auction; + +import kamkeel.npcs.command.CommandKamkeelBase; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.data.AuctionBlacklist; + +import java.util.List; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendMessage; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class BlacklistSubCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "blacklist"; + } + + @Override + public String getDescription() { + return "Manage auction item blacklist"; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) throws CommandException { + // Show help when called with no subcommand + sendHelp(sender); + } + + private void sendHelp(ICommandSender sender) { + sendMessage(sender, "\u00A76=== Auction Blacklist Commands ==="); + sendMessage(sender, "\u00A7e/cnpc auction blacklist add "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist remove "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist list [item|mod|nbt]"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist reload"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist check \u00A77(checks held item)"); + } + + @SubCommand( + desc = "Add item, mod, or NBT tag to blacklist", + usage = " " + ) + public void add(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist add "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.addItem(value)) { + sendResult(sender, "Added item to blacklist: " + value); + } else { + sendError(sender, "Failed to add item to blacklist"); + } + break; + case "mod": + if (AuctionBlacklist.addMod(value)) { + sendResult(sender, "Added mod to blacklist: " + value); + } else { + sendError(sender, "Failed to add mod to blacklist"); + } + break; + case "nbt": + if (AuctionBlacklist.addNBTTag(value)) { + sendResult(sender, "Added NBT tag to blacklist: " + value); + } else { + sendError(sender, "Failed to add NBT tag to blacklist"); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "Remove item, mod, or NBT tag from blacklist", + usage = " " + ) + public void remove(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist remove "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.removeItem(value)) { + sendResult(sender, "Removed item from blacklist: " + value); + } else { + sendError(sender, "Item not found in blacklist: " + value); + } + break; + case "mod": + if (AuctionBlacklist.removeMod(value)) { + sendResult(sender, "Removed mod from blacklist: " + value); + } else { + sendError(sender, "Mod not found in blacklist: " + value); + } + break; + case "nbt": + if (AuctionBlacklist.removeNBTTag(value)) { + sendResult(sender, "Removed NBT tag from blacklist: " + value); + } else { + sendError(sender, "NBT tag not found in blacklist: " + value); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "List blacklisted items, mods, or NBT tags", + usage = "[item|mod|nbt]" + ) + public void list(ICommandSender sender, String[] args) throws CommandException { + String filter = args.length > 0 ? args[0].toLowerCase() : "all"; + + if (filter.equals("all") || filter.equals("item")) { + List items = AuctionBlacklist.getBlacklistedItems(); + sendMessage(sender, "\u00A76=== Blacklisted Items (" + items.size() + ") ==="); + if (items.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String item : items) { + sendMessage(sender, "\u00A7e " + item); + } + } + } + + if (filter.equals("all") || filter.equals("mod")) { + List mods = AuctionBlacklist.getBlacklistedMods(); + sendMessage(sender, "\u00A76=== Blacklisted Mods (" + mods.size() + ") ==="); + if (mods.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String mod : mods) { + sendMessage(sender, "\u00A7e " + mod); + } + } + } + + if (filter.equals("all") || filter.equals("nbt")) { + List tags = AuctionBlacklist.getBlacklistedNBTTags(); + sendMessage(sender, "\u00A76=== Blacklisted NBT Tags (" + tags.size() + ") ==="); + if (tags.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String tag : tags) { + sendMessage(sender, "\u00A7e " + tag); + } + } + } + + sendMessage(sender, "\u00A77Note: Soulbound and Profile-Slotbound items are always blocked."); + } + + @SubCommand( + desc = "Reload blacklist from config" + ) + public void reload(ICommandSender sender, String[] args) throws CommandException { + AuctionBlacklist.reload(); + sendResult(sender, "Blacklist reloaded from config"); + } + + @SubCommand( + desc = "Check if held item is blacklisted", + permission = 0 + ) + public void check(ICommandSender sender, String[] args) throws CommandException { + if (!(sender instanceof EntityPlayer)) { + sendError(sender, "This command can only be used by players"); + return; + } + + EntityPlayer player = (EntityPlayer) sender; + ItemStack heldItem = player.getHeldItem(); + + if (heldItem == null) { + sendError(sender, "You are not holding an item"); + return; + } + + String registryName = AuctionBlacklist.getRegistryName(heldItem); + sendMessage(sender, "\u00A76=== Blacklist Check ==="); + sendMessage(sender, "\u00A77Item: \u00A7e" + heldItem.getDisplayName()); + sendMessage(sender, "\u00A77Registry: \u00A7e" + (registryName != null ? registryName : "unknown")); + + boolean isBlacklisted = AuctionBlacklist.isBlacklisted(heldItem); + boolean canBypass = AuctionBlacklist.canBypass(player); + + if (isBlacklisted) { + sendMessage(sender, "\u00A7cStatus: BLACKLISTED"); + if (canBypass) { + sendMessage(sender, "\u00A7aYou have bypass permission"); + } + } else { + sendMessage(sender, "\u00A7aStatus: ALLOWED"); + } + } + + @Override + public List addTabCompletionOptions(ICommandSender sender, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); + } + if (args.length == 2) { + String subCmd = args[0].toLowerCase(); + if (subCmd.equals("add") || subCmd.equals("remove") || subCmd.equals("list")) { + return getListOfStringsMatchingLastWord(args, new String[]{"item", "mod", "nbt"}); + } + } + return null; + } +} diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 2ba7c7c92..e14861033 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -13,6 +13,7 @@ public class ConfigMarket { public final static String CURRENCY = "Market.Currency"; public final static String AUCTION = "Market.Auction"; + public final static String AUCTION_BLACKLIST = "Market.Auction.Blacklist"; public final static String AUCTION_LOGGING = "Market.Auction.Logging"; // Max trades grid = 9x5 = 45 slots @@ -41,6 +42,14 @@ public class ConfigMarket { public static double MinBidIncrementPercent = 0.05; public static double CancellationPenaltyPercent = 0.10; + // ========================================= + // Auction Blacklist Settings + // ========================================= + public static boolean BlacklistEnabled = true; + public static String[] BlacklistedItems = new String[]{}; + public static String[] BlacklistedMods = new String[]{}; + public static String[] BlacklistedNBTTags = new String[]{}; + // ========================================= // Auction Logging Settings // ========================================= @@ -113,6 +122,45 @@ public class ConfigMarket { CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10, "Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10); + // ========================================= + // Auction Blacklist Settings + // ========================================= + config.setCategoryComment(AUCTION_BLACKLIST, + "Item Blacklist settings for the Auction House.\n" + + "Prevents specific items, mods, or items with certain NBT tags from being listed.\n\n" + + "ITEM FORMAT: Use 'modid:itemname' format (e.g., 'minecraft:bedrock', 'customnpcs:npcWand')\n" + + "WILDCARDS: Use * for wildcards (e.g., 'customnpcs:npc*' blocks all items starting with 'npc')\n" + + "MOD FORMAT: Use just the mod ID (e.g., 'projecte' blocks all items from that mod)\n" + + "NBT FORMAT: Use the NBT tag key name (e.g., 'AdminOnly' blocks items with that tag)"); + + BlacklistEnabled = config.get(AUCTION_BLACKLIST, "Enable Blacklist", true, + "Enable item blacklist checking when creating listings").getBoolean(true); + + BlacklistedItems = config.get(AUCTION_BLACKLIST, "Blacklisted Items", new String[]{ + "minecraft:bedrock", + "minecraft:command_block", + "customnpcs:npcWand", + "customnpcs:npcMobCloner", + "customnpcs:npcScripter", + "customnpcs:npcMovingPath", + "customnpcs:npcMounter", + "customnpcs:npcTeleporter", + "customnpcs:npcTool", + "customnpcs:npcSoulstoneFilled" + }, + "Items that cannot be listed on the Auction House.\n" + + "Format: modid:itemname (supports * wildcards)\n" + + "Example: 'minecraft:diamond_sword' or 'customnpcs:npc*'").getStringList(); + + BlacklistedMods = config.get(AUCTION_BLACKLIST, "Blacklisted Mods", new String[]{}, + "All items from these mods are blocked from the Auction House.\n" + + "Format: modid (e.g., 'projecte', 'thaumcraft')").getStringList(); + + BlacklistedNBTTags = config.get(AUCTION_BLACKLIST, "Blacklisted NBT Tags", new String[]{}, + "Items containing any of these NBT tag keys are blocked.\n" + + "Checks the root level of the item's NBT compound.\n" + + "Example: 'AdminOnly', 'CreativeMode'").getStringList(); + // ========================================= // Auction Logging Settings // ========================================= @@ -163,6 +211,13 @@ public class ConfigMarket { "Cancellation Penalty Percent" ))); + config.setCategoryPropertyOrder(AUCTION_BLACKLIST, new ArrayList<>(Arrays.asList( + "Enable Blacklist", + "Blacklisted Items", + "Blacklisted Mods", + "Blacklisted NBT Tags" + ))); + config.setCategoryPropertyOrder(AUCTION_LOGGING, new ArrayList<>(Arrays.asList( "Enable Auction Logging", "Log Created", diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 375767deb..e6ca0c6cf 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -21,6 +21,7 @@ import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.constants.EnumNotificationType; +import noppes.npcs.controllers.data.AuctionBlacklist; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; @@ -86,6 +87,7 @@ public class AuctionController implements IAuctionHandler { public AuctionController() { Instance = this; + AuctionBlacklist.reload(); load(); } @@ -241,6 +243,11 @@ public class AuctionController implements IAuctionHandler { return "Auction is disabled."; } + // Check blacklist (with bypass for admins) + if (AuctionBlacklist.isBlacklistedForPlayer(item, player)) { + return "This item cannot be listed on the Auction House."; + } + // Validate minimum price if (startingPrice < ConfigMarket.MinimumListingPrice) { return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; @@ -1414,4 +1421,18 @@ public class AuctionController implements IAuctionHandler { AuctionListing listing = listings.get(listingId); return listing != null && listing.isSeller(playerUUID); } + + /** + * Reload the item blacklist from config. + */ + public void reloadBlacklist() { + AuctionBlacklist.reload(); + } + + /** + * Check if an item is blacklisted. + */ + public boolean isItemBlacklisted(ItemStack item) { + return AuctionBlacklist.isBlacklisted(item); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java new file mode 100644 index 000000000..75a1a714a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java @@ -0,0 +1,237 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.registry.GameRegistry; +import kamkeel.npcs.util.AttributeItemUtil; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public class AuctionBlacklist { + private static Set blacklistedItems = new HashSet<>(); + private static Set wildcardPatterns = new HashSet<>(); + private static Set blacklistedMods = new HashSet<>(); + private static Set blacklistedNBTTags = new HashSet<>(); + + // Built-in bound item requirement keys (always blocked) + private static final String SOULBIND_KEY = "cnpc_soulbind"; + private static final String PROFILE_SLOT_KEY = "cnpc_profile_slot"; + + // Bypass permission + private static final String BYPASS_PERMISSION = "customnpcs.auction.blacklist.bypass"; + + public static void reload() { + blacklistedItems.clear(); + wildcardPatterns.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + + // Load blacklisted items + for (String item : ConfigMarket.BlacklistedItems) { + if (item == null || item.isEmpty()) continue; + item = item.toLowerCase().trim(); + + if (item.contains("*")) { + String regex = "^" + item.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(item); + } + } + + // Load blacklisted mods + for (String mod : ConfigMarket.BlacklistedMods) { + if (mod == null || mod.isEmpty()) continue; + blacklistedMods.add(mod.toLowerCase().trim()); + } + + // Load blacklisted NBT tags + for (String tag : ConfigMarket.BlacklistedNBTTags) { + if (tag == null || tag.isEmpty()) continue; + blacklistedNBTTags.add(tag.trim()); + } + } + + /** + * Check if player can bypass the blacklist (OP or has permission) + */ + public static boolean canBypass(EntityPlayer player) { + if (player == null) return false; + + // Check OP status + if (player.canCommandSenderUseCommand(2, BYPASS_PERMISSION)) { + return true; + } + + // Check custom permission + return CustomNpcsPermissions.hasCustomPermission(player, BYPASS_PERMISSION); + } + + /** + * Check if item is blacklisted (without bypass check) + */ + public static boolean isBlacklisted(ItemStack item) { + if (!ConfigMarket.BlacklistEnabled) return false; + if (item == null || item.getItem() == null) return true; + + // Check bound items (always blocked, even if blacklist is "disabled") + if (hasBoundRequirement(item)) { + return true; + } + + // Get registry name + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return true; + + String registryName = (uid.modId + ":" + uid.name).toLowerCase(); + String modId = uid.modId.toLowerCase(); + + // Check exact item match + if (blacklistedItems.contains(registryName)) { + return true; + } + + // Check wildcard patterns + for (Pattern pattern : wildcardPatterns) { + if (pattern.matcher(registryName).matches()) { + return true; + } + } + + // Check mod blacklist + if (blacklistedMods.contains(modId)) { + return true; + } + + // Check NBT tags + if (item.hasTagCompound() && !blacklistedNBTTags.isEmpty()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtKey : blacklistedNBTTags) { + if (tag.hasKey(nbtKey)) { + return true; + } + } + } + + return false; + } + + /** + * Check if item is blacklisted for a specific player (with bypass check) + */ + public static boolean isBlacklistedForPlayer(ItemStack item, EntityPlayer player) { + if (canBypass(player)) return false; + return isBlacklisted(item); + } + + /** + * Get the registry name of an item + */ + public static String getRegistryName(ItemStack item) { + if (item == null || item.getItem() == null) return null; + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return null; + return uid.modId + ":" + uid.name; + } + + // ========================================= + // Bound Item Checking (RPGCore/Requirements) + // ========================================= + + /** + * Check if item has soulbind or profile slot requirement. + * Requirements are stored under: item.stackTagCompound/RPGCore/Requirements + */ + private static boolean hasBoundRequirement(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + + NBTTagCompound root = item.getTagCompound(); + if (!root.hasKey(AttributeItemUtil.TAG_RPGCORE)) return false; + + NBTTagCompound rpgCore = root.getCompoundTag(AttributeItemUtil.TAG_RPGCORE); + if (!rpgCore.hasKey(AttributeItemUtil.TAG_REQUIREMENTS)) return false; + + NBTTagCompound requirements = rpgCore.getCompoundTag(AttributeItemUtil.TAG_REQUIREMENTS); + return requirements.hasKey(SOULBIND_KEY) || requirements.hasKey(PROFILE_SLOT_KEY); + } + + // ========================================= + // Runtime Blacklist Management + // ========================================= + + public static boolean addItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(registryName); + } + return true; + } + + public static boolean removeItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + return wildcardPatterns.removeIf(p -> p.pattern().equals(regex)); + } else { + return blacklistedItems.remove(registryName); + } + } + + public static boolean addMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.add(modId.toLowerCase().trim()); + } + + public static boolean removeMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.remove(modId.toLowerCase().trim()); + } + + public static boolean addNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.add(tag.trim()); + } + + public static boolean removeNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.remove(tag.trim()); + } + + // ========================================= + // Getters for listing + // ========================================= + + public static List getBlacklistedItems() { + List result = new ArrayList<>(blacklistedItems); + for (Pattern p : wildcardPatterns) { + // Convert regex back to wildcard format + String pattern = p.pattern(); + pattern = pattern.substring(1, pattern.length() - 1); // Remove ^ and $ + pattern = pattern.replace("\\.", ".").replace(".*", "*"); + result.add(pattern); + } + return result; + } + + public static List getBlacklistedMods() { + return new ArrayList<>(blacklistedMods); + } + + public static List getBlacklistedNBTTags() { + return new ArrayList<>(blacklistedNBTTags); + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index dc2757916..7f0618786 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1769,6 +1769,8 @@ auction.bid.buyout=Buyout Now # Errors auction.error.notEnoughCurrency=Not enough currency! auction.error.bidTooLow=Bid must be at least %s. +auction.error.blacklisted=This item cannot be listed on the Auction House. +auction.error.boundItem=Bound items cannot be listed on the Auction House. # Navigation auction.nav.listings=Browse Auctions