From b7aca8cff0e04b1cf35582e1ac509e73162484bc Mon Sep 17 00:00:00 2001 From: demengc Date: Mon, 23 Mar 2026 21:54:07 -0400 Subject: [PATCH] docs: audit menus and item builder pages Fix createCommandHandler() usage to include .build() call. Restructure both pages with reference tables for all API methods, accurate slot indexing documentation, and corrected code examples. Remove redundant glow+HIDE_ENCHANTS pattern. Document all MenuButton.create() overloads, fill methods, copy() shallow semantics, and static material utilities. --- docs/core-features/item-builder.md | 266 ++++++++++------------------- docs/core-features/menus.md | 242 +++++++++++--------------- 2 files changed, 184 insertions(+), 324 deletions(-) diff --git a/docs/core-features/item-builder.md b/docs/core-features/item-builder.md index 0c3684d..5003495 100644 --- a/docs/core-features/item-builder.md +++ b/docs/core-features/item-builder.md @@ -4,234 +4,140 @@ description: Fluent API for creating ItemStacks. # ItemBuilder -## Basic Usage +`ItemBuilder` wraps Bukkit's `ItemStack` and `ItemMeta` manipulation into a chainable builder. Call `.get()` to retrieve the final `ItemStack`. -```java -import dev.demeng.pluginbase.item.ItemBuilder; +## Creating a builder + +| Factory method | Description | +|---|---| +| `ItemBuilder.create(ItemStack stack)` | Clones an existing item stack (null defaults to `STONE`) | +| `ItemBuilder.create(Material material)` | Creates a new stack with amount 1 | +| `ItemBuilder.create(Material material, int amount)` | Creates a new stack with the given amount | +| `ItemBuilder.create(Material material, int amount, short damage)` | Creates a new stack with amount and damage value | -ItemStack item = ItemBuilder.create(Material.DIAMOND_SWORD) +All `Material` parameters default to `STONE` if null. + +```java +ItemBuilder.create(Material.DIAMOND_SWORD) .name("&bLegendary Sword") - .lore("&7Line 1", "&7Line 2") - .amount(1) - .durability((short) 100) + .lore("&7A powerful weapon") .enchant(Enchantment.DAMAGE_ALL, 5) - .enchant(Enchantment.FIRE_ASPECT, 2) - .flags(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES) + .flags(ItemFlag.HIDE_ATTRIBUTES) .unbreakable(true) - .glow(true) // Adds enchantment glow without visible enchants + .glow(true) .get(); ``` -## Player Heads +## Builder methods + +### General + +| Method | Parameters | Description | +|---|---|---| +| `name(String)` | Display name (color codes auto-applied) | Sets the display name | +| `amount(int)` | Stack size | Sets the stack amount | +| `durability(short)` | Durability value | Sets the item durability | +| `lore(String...)` | Lore lines (color codes auto-applied) | Replaces lore; each string is one line | +| `lore(List)` | Lore lines | Same as above, from a list | +| `addLore(String)` | Single line | Appends one line to existing lore | +| `clearLore()` | | Removes all lore | +| `enchant(Enchantment, int)` | Enchantment, level | Adds a safe enchantment | +| `enchant(Enchantment, int, boolean)` | Enchantment, level, safe | Adds an enchantment; `safe=false` allows unsafe levels | +| `enchant(Map)` | Enchantment map | Adds all enchantments from the map | +| `unenchant(Enchantment)` | Enchantment to remove | Removes a specific enchantment | +| `clearEnchants()` | | Removes all enchantments | +| `flags(ItemFlag...)` | Flags to add | Adds item flags | +| `clearFlags()` | | Removes all item flags | +| `unbreakable(boolean)` | true/false | Sets unbreakable state | +| `glow(boolean)` | true/false | Adds Unbreaking I + `HIDE_ENCHANTS` for a visual glow effect | +| `modelData(Integer)` | Custom model data (1.14+, ignored on older versions) | Sets custom model data for resource packs | + +### Item-specific + +| Method | Parameters | Description | +|---|---|---| +| `skullOwner(String)` | Player name | Sets skull owner by name (player heads only) | +| `skullOwner(UUID)` | Player UUID | Sets skull owner by UUID (player heads only) | +| `skullTexture(String)` | Base64-encoded texture | Sets a custom skull texture (player heads only) | +| `armorColor(Color)` | Bukkit `Color` | Sets leather armor color (leather armor only) | + +### Utilities + +| Method | Returns | Description | +|---|---|---| +| `get()` | `ItemStack` | Returns the built item stack | +| `copy()` | `ItemBuilder` | Returns a new builder sharing the same `ItemStack` reference (not a deep clone) | + +## Player heads ```java -// Player head by name -ItemStack head = ItemBuilder.create(Material.PLAYER_HEAD) - .skullOwner(player.getName()) - .name("&e" + player.getName()) +ItemBuilder.create(Material.PLAYER_HEAD) + .skullOwner("Notch") + .name("&eNotch's Head") .get(); -// Player head by UUID -ItemStack head2 = ItemBuilder.create(Material.PLAYER_HEAD) +ItemBuilder.create(Material.PLAYER_HEAD) .skullOwner(player.getUniqueId()) .name("&e" + player.getName()) .get(); -// Custom texture (base64) -ItemStack customHead = ItemBuilder.create(Material.PLAYER_HEAD) - .skullTexture("texture_base64_here") +ItemBuilder.create(Material.PLAYER_HEAD) + .skullTexture("eyJ0ZXh0dXJlcyI6ey...") .name("&6Custom Head") .get(); ``` -## Clone and Modify Existing Item +## Modifying an existing item + +`create(ItemStack)` clones the input, so the original is not modified. ```java ItemStack existing = player.getInventory().getItemInMainHand(); ItemStack modified = ItemBuilder.create(existing) .name("&aModified Item") - .lore("&7Added lore") + .addLore("&7New line") .get(); ``` -## Lore Management +## Leather armor color ```java -ItemBuilder builder = ItemBuilder.create(Material.DIAMOND) - .name("&bDiamond"); - -// Set lore -builder.lore("&7Line 1", "&7Line 2"); - -// Add to lore -builder.addLore("&7Additional line"); - -// Clear lore -builder.clearLore(); - -ItemStack item = builder.get(); -``` - -## Enchantments - -```java -ItemBuilder.create(Material.DIAMOND_SWORD) - .enchant(Enchantment.DAMAGE_ALL, 5) - .enchant(Enchantment.FIRE_ASPECT, 2) - .enchant(Enchantment.KNOCKBACK, 2) +ItemBuilder.create(Material.LEATHER_CHESTPLATE) + .name("&cRed Armor") + .armorColor(Color.RED) .get(); - -// Remove enchantment -builder.unenchant(Enchantment.DAMAGE_ALL); - -// Clear all enchantments -builder.clearEnchants(); ``` -## Item Flags +## Custom model data -```java -// Add multiple flags at once -ItemBuilder.create(Material.DIAMOND_SWORD) - .flags( - ItemFlag.HIDE_ENCHANTS, - ItemFlag.HIDE_ATTRIBUTES, - ItemFlag.HIDE_UNBREAKABLE - ) - .get(); -``` - -## Custom Model Data +Requires Minecraft 1.14+. On older versions, the call is silently ignored. ```java ItemBuilder.create(Material.STICK) - .modelData(1) // For custom resource packs + .modelData(1) .get(); ``` -## Glow Effect +## Glow effect + +`glow(true)` adds Unbreaking I and the `HIDE_ENCHANTS` flag. No need to add the flag separately. Best used for GUI items where the enchantment is purely visual. ```java -// Add glow without visible enchants ItemBuilder.create(Material.DIAMOND) .glow(true) - .flags(ItemFlag.HIDE_ENCHANTS) .get(); ``` -## Leather Armor Color - -```java -ItemBuilder.create(Material.LEATHER_CHESTPLATE) - .name("&cRed Armor") - .armorColor(Color.RED) - .get(); -``` - -## Complete Example - -```java -public class ItemFactory { - - public static ItemStack createSword(int level) { - return ItemBuilder.create(Material.DIAMOND_SWORD) - .name("&b&lLegendary Sword &7(Level " + level + ")") - .lore( - "&7A powerful weapon forged by", - "&7ancient blacksmiths.", - "", - "&eStats:", - "&7▸ Damage: &c+" + (10 * level), - "&7▸ Attack Speed: &a+15%", - "", - "&6&lLEGENDARY" - ) - .enchant(Enchantment.DAMAGE_ALL, level) - .enchant(Enchantment.FIRE_ASPECT, 2) - .enchant(Enchantment.KNOCKBACK, 1) - .flags(ItemFlag.HIDE_ATTRIBUTES) - .unbreakable(true) - .glow(true) - .get(); - } - - public static ItemStack createArmor(Player player) { - return ItemBuilder.create(Material.DIAMOND_CHESTPLATE) - .name("&b" + player.getName() + "'s Armor") - .lore( - "&7Soulbound to " + player.getName(), - "&7Cannot be traded or dropped" - ) - .enchant(Enchantment.PROTECTION_ENVIRONMENTAL, 4) - .enchant(Enchantment.DURABILITY, 3) - .flags(ItemFlag.HIDE_ENCHANTS) - .unbreakable(true) - .get(); - } - - public static ItemStack createToken(int amount) { - return ItemBuilder.create(Material.SUNFLOWER) - .name("&6&lServer Token") - .lore( - "&7A valuable currency", - "&7Amount: &e" + amount - ) - .amount(amount) - .glow(true) - .flags(ItemFlag.HIDE_ENCHANTS) - .get(); - } - - public static ItemStack createMenuButton(String name, Material icon) { - return ItemBuilder.create(icon) - .name(name) - .lore("&eClick to select") - .get(); - } -} - -// Usage -Events.subscribe(PlayerJoinEvent.class) - .handler(e -> { - Player player = e.getPlayer(); - player.getInventory().addItem( - ItemFactory.createSword(1), - ItemFactory.createArmor(player), - ItemFactory.createToken(10) - ); - }) - .bindWith(this); -``` - -## Comparison: Traditional vs ItemBuilder +## Static material utilities -### Traditional +These methods parse a material string (using XMaterial for cross-version support) into an `ItemStack` with no meta: -```java -ItemStack item = new ItemStack(Material.DIAMOND_SWORD); -ItemMeta meta = item.getItemMeta(); -meta.setDisplayName(ChatColor.BLUE + "Legendary Sword"); -List lore = new ArrayList<>(); -lore.add(ChatColor.GRAY + "A powerful weapon"); -meta.setLore(lore); -meta.addEnchant(Enchantment.DAMAGE_ALL, 5, true); -meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); -meta.setUnbreakable(true); -item.setItemMeta(meta); -``` - -### ItemBuilder - -```java -ItemStack item = ItemBuilder.create(Material.DIAMOND_SWORD) - .name("&bLegendary Sword") - .lore("&7A powerful weapon") - .enchant(Enchantment.DAMAGE_ALL, 5) - .flags(ItemFlag.HIDE_ENCHANTS) - .unbreakable(true) - .get(); -``` +| Method | Returns | On invalid input | +|---|---|---| +| `getMaterialSafe(String)` | `Optional` | Returns empty optional | +| `getMaterial(String)` | `ItemStack` | Logs error, returns `STONE` | +| `getMaterialOrDef(String, ItemStack)` | `ItemStack` | Returns the provided default | -## Cross-Version Compatibility +## Cross-version compatibility -PluginBase uses [XSeries](https://github.com/cryptomorin/xseries) for cross-version compatibility. To learn more about cross-version materials (`XMaterial`), refer to XSeries' documentation. +PluginBase uses [XSeries](https://github.com/cryptomorin/xseries) for cross-version material and enchantment support. Refer to XSeries documentation for `XMaterial` mappings. diff --git a/docs/core-features/menus.md b/docs/core-features/menus.md index 33650e7..3c8e5ad 100644 --- a/docs/core-features/menus.md +++ b/docs/core-features/menus.md @@ -4,27 +4,33 @@ description: Interactive inventory GUIs with clickable items. # Menus -## Basic Menu +PluginBase provides `Menu` for single-page inventories and `PagedMenu` for paginated inventories. Both extend `IMenu`, which handles opening and close callbacks. `MenuManager` is registered automatically by `BasePlugin`. + +## Single-page menu + +Extend `Menu`, call `super(size, title)`, and populate buttons in the constructor or a helper method. ```java import dev.demeng.pluginbase.menu.layout.Menu; +import dev.demeng.pluginbase.menu.model.MenuButton; import dev.demeng.pluginbase.item.ItemBuilder; +import dev.demeng.pluginbase.text.Text; +import org.bukkit.Material; +import org.bukkit.entity.Player; public class ShopMenu extends Menu { public ShopMenu() { - super(27, "&6Shop"); // Size (slots), title - populateItems(); + super(27, "&6Shop"); + populate(); } - private void populateItems() { - // Add items + private void populate() { addButton(10, ItemBuilder.create(Material.DIAMOND) .name("&bDiamonds") .lore("&7Price: $100") .get(), click -> { Player player = (Player) click.getWhoClicked(); - // Handle purchase Text.tell(player, "&aPurchased diamonds!"); player.closeInventory(); }); @@ -38,7 +44,6 @@ public class ShopMenu extends Menu { player.closeInventory(); }); - // Fill empty slots with glass setBackground(ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) .name(" ") .get()); @@ -46,18 +51,29 @@ public class ShopMenu extends Menu { } ``` -## Open Menu +Open the menu: ```java -ShopMenu menu = new ShopMenu(); -menu.open(player); +new ShopMenu().open(player); ``` -## Paged Menu +`open(Player... players)` accepts varargs, so you can open for multiple players at once: + +```java +menu.open(player1, player2, player3); +``` + +## Paginated menu + +Extend `PagedMenu` and implement a `Settings` object that controls navigation buttons and available slots. + +Buttons with slot `-1` are auto-assigned across pages. Buttons with a slot `>= 0` are placed on the first page at that fixed position. ```java import dev.demeng.pluginbase.menu.layout.PagedMenu; import dev.demeng.pluginbase.menu.model.MenuButton; +import dev.demeng.pluginbase.item.ItemBuilder; +import java.util.stream.Collectors; import java.util.stream.IntStream; public class PlayerListMenu extends PagedMenu { @@ -65,11 +81,10 @@ public class PlayerListMenu extends PagedMenu { public PlayerListMenu(List players) { super(54, "&bPlayers - Page %current-page%", createSettings()); - // Create buttons for each player List buttons = new ArrayList<>(); for (Player player : players) { buttons.add(MenuButton.create( - -1, // Auto-assign slot + -1, ItemBuilder.create(Material.PLAYER_HEAD) .skullOwner(player.getName()) .name("&e" + player.getName()) @@ -83,7 +98,6 @@ public class PlayerListMenu extends PagedMenu { )); } - // Fill pages with buttons fill(buttons); } @@ -91,51 +105,32 @@ public class PlayerListMenu extends PagedMenu { return new Settings() { @Override public MenuButton getPreviousButton() { - return MenuButton.create( - 45, - ItemBuilder.create(Material.ARROW) - .name("&aPrevious Page") - .get(), - null - ); + return MenuButton.create(45, + ItemBuilder.create(Material.ARROW).name("&aPrevious Page").get(), null); } @Override public MenuButton getDummyPreviousButton() { - return MenuButton.create( - 45, + return MenuButton.create(45, ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) - .name("&cNo previous page") - .get(), - null - ); + .name("&cNo previous page").get(), null); } @Override public MenuButton getNextButton() { - return MenuButton.create( - 53, - ItemBuilder.create(Material.ARROW) - .name("&aNext Page") - .get(), - null - ); + return MenuButton.create(53, + ItemBuilder.create(Material.ARROW).name("&aNext Page").get(), null); } @Override public MenuButton getDummyNextButton() { - return MenuButton.create( - 53, + return MenuButton.create(53, ItemBuilder.create(Material.GRAY_STAINED_GLASS_PANE) - .name("&cNo next page") - .get(), - null - ); + .name("&cNo next page").get(), null); } @Override public List getAvailableSlots() { - // Slots 0-44 (excluding navigation buttons at 45 and 53) return IntStream.range(0, 45).boxed().collect(Collectors.toList()); } }; @@ -143,25 +138,20 @@ public class PlayerListMenu extends PagedMenu { @Override public boolean onClose(InventoryCloseEvent event) { - // Cleanup when menu closes return false; } } ``` -### Simpler PagedMenu Example +### Loading settings from config -You can also load Settings from config: +`Settings.fromConfig(ConfigurationSection)` parses navigation buttons and available slots from YAML. Config slots are 1-indexed and converted automatically. ```java public class SimplePagedMenu extends PagedMenu { public SimplePagedMenu(ConfigurationSection config, List items) { - super( - 54, - "&6Items - Page %current-page%", - Settings.fromConfig(config) // Load from config - ); + super(54, "&6Items - Page %current-page%", Settings.fromConfig(config)); List buttons = items.stream() .map(item -> MenuButton.create(-1, item, null)) @@ -177,15 +167,64 @@ public class SimplePagedMenu extends PagedMenu { } ``` -## Complete Example +### Static buttons on all pages + +Use `addStaticButton(MenuButton)` or `addButton(int slot, ItemStack, Consumer)` after calling `fill()` to place a button on every page. + +## MenuButton factory methods + +`MenuButton.create(...)` has several overloads: + +| Signature | Slot handling | +|---|---| +| `create(int slot, ItemStack stack, Consumer action)` | Uses slot as-is (0-indexed) | +| `create(ConfigurationSection section, Consumer action)` | Reads `slot` from config, subtracts 1 | +| `create(ConfigurationSection section, UnaryOperator translator, Consumer action)` | Same as above, with string translator | +| `create(int slot, ConfigurationSection section, Consumer action)` | Overrides config slot, no subtraction | +| `create(int slot, ConfigurationSection section, UnaryOperator translator, Consumer action)` | Same as above, with string translator | + +## Layout fill methods + +All fill methods only affect empty slots. The item's display name is set to `"&0"` (invisible) automatically. + +| Method | Parameters | Indexing | +|---|---|---| +| `setBackground(ItemStack)` | Fill material | Fills all empty slots | +| `setBorder(ItemStack)` | Fill material | Fills the outer edge (top row, bottom row, left column, right column) | +| `setRow(int row, ItemStack)` | Row number (1-based, top to bottom), fill material | Row 1 = top, row 6 = bottom of a 54-slot inventory | +| `setColumn(int col, ItemStack)` | Column number (1-based, left to right), fill material | Column 1 = leftmost, column 9 = rightmost | +| `applyFillersFromConfig(ConfigurationSection)` | Config section | Reads `background`, `border`, `row`, `column`, `custom` keys | + +## Inventory layout and slot indexing + +A 54-slot (6-row) inventory uses 0-based slot indices in code: + +``` +Row 1: [ 0][ 1][ 2][ 3][ 4][ 5][ 6][ 7][ 8] +Row 2: [ 9][10][11][12][13][14][15][16][17] +Row 3: [18][19][20][21][22][23][24][25][26] +Row 4: [27][28][29][30][31][32][33][34][35] +Row 5: [36][37][38][39][40][41][42][43][44] +Row 6: [45][46][47][48][49][50][51][52][53] +``` + +| Context | Slots | Rows / Columns | +|---|---|---| +| Java code (`addButton`, constructor) | 0-indexed (0-53) | `setRow` and `setColumn` are 1-indexed | +| Config files (`slot` key, `custom` filler slots, `available-slots`) | 1-indexed (1-54), auto-converted | `row` and `column` filler keys are 1-indexed | + +## `onClose` callback + +Override `onClose(InventoryCloseEvent)` in your menu subclass. Return `true` to re-open the menu (cancels close), `false` to allow it to close normally. + +## Full example with command registration ```java public class MyPlugin extends BasePlugin { @Override protected void enable() { - // Command to open shop - Lamp handler = createCommandHandler(); + Lamp handler = createCommandHandler().build(); handler.register(new ShopCommands()); } } @@ -201,38 +240,30 @@ public class ShopCommands { public class ShopMenu extends Menu { public ShopMenu() { - super(36, "&6&lShop"); // 4 rows = 36 slots - populateItems(); + super(36, "&6&lShop"); + populate(); } - private void populateItems() { - // Weapons + private void populate() { addButton(10, ItemBuilder.create(Material.DIAMOND_SWORD) .name("&bDiamond Sword") .lore("&7Price: $500", "", "&eClick to purchase") .get(), this::purchaseWeapon); - // Armor addButton(12, ItemBuilder.create(Material.DIAMOND_CHESTPLATE) .name("&bDiamond Armor") .lore("&7Price: $1000", "", "&eClick to purchase") .get(), this::purchaseArmor); - // Food addButton(14, ItemBuilder.create(Material.COOKED_BEEF) .name("&6Food Pack") .lore("&7Price: $50", "", "&eClick to purchase") .get(), this::purchaseFood); - // Close button addButton(31, ItemBuilder.create(Material.BARRIER) .name("&cClose") - .get(), click -> { - Player player = (Player) click.getWhoClicked(); - player.closeInventory(); - }); + .get(), click -> ((Player) click.getWhoClicked()).closeInventory()); - // Decorative borders setBorder(ItemBuilder.create(Material.BLACK_STAINED_GLASS_PANE) .name(" ") .get()); @@ -240,94 +271,17 @@ public class ShopMenu extends Menu { private void purchaseWeapon(InventoryClickEvent click) { Player player = (Player) click.getWhoClicked(); - if (hasEnoughMoney(player, 500)) { - takeMoney(player, 500); - player.getInventory().addItem(new ItemStack(Material.DIAMOND_SWORD)); - Text.tell(player, "&aPurchased Diamond Sword!"); - player.closeInventory(); - } else { - Text.tell(player, "&cNot enough money!"); - } + player.getInventory().addItem(new ItemStack(Material.DIAMOND_SWORD)); + Text.tell(player, "&aPurchased Diamond Sword!"); + player.closeInventory(); } private void purchaseArmor(InventoryClickEvent click) { - Player player = (Player) click.getWhoClicked(); // Purchase logic } private void purchaseFood(InventoryClickEvent click) { - Player player = (Player) click.getWhoClicked(); // Purchase logic } } ``` - -## Layout Methods - -### Fill Background - -```java -// Fill all empty slots -setBackground(ItemStack item); -``` - -### Fill Border - -```java -// Fill border slots (outer edge) -setBorder(ItemStack item); -``` - -### Fill Row - -```java -// Fill entire row (1-6 for 6-row inventory) -setRow(int row, ItemStack item); -``` - -### Fill Column - -```java -// Fill entire column (1-9) -setColumn(int column, ItemStack item); -``` - -## Menu State - -```java -// Open for multiple players -menu.open(player1, player2, player3); -``` - -## Adding Buttons - -```java -// Add button at specific slot (0-indexed: 0-53 for 54-slot inventory) -addButton(int slot, ItemStack item, Consumer handler); - -// Add button to first empty slot -addButton(ItemStack item, Consumer handler); - -// Add button object -MenuButton button = MenuButton.create(slot, itemStack, clickHandler); -addButton(button); -``` - -## Slot Indexing - -**Important:** Slot indexing differs between code and configuration files: - -* **In Java code:** Slots are **0-indexed** (0-53 for a 54-slot inventory) - - ```java - addButton(0, item, handler); // First slot (top-left) - addButton(53, item, handler); // Last slot (bottom-right in 6-row GUI) - ``` -* **In config files:** Slots are **1-indexed** (1-54 for a 54-slot inventory) - - ```yaml - slot: 1 # First slot (automatically converted to 0 internally) - slot: 54 # Last slot (automatically converted to 53 internally) - ``` - -The framework automatically converts config slots by subtracting 1. This makes config files more user-friendly while keeping code consistent with Bukkit's 0-indexed inventory API.