-
Notifications
You must be signed in to change notification settings - Fork 0
Menus
Menus in PrisonCore are described declaratively, the same way commands are. You build a MenuDescriptor, register slot contents and click handlers on it, and hand it to MenuService.open. The platform handles inventory creation, click dispatch, view tracking, and cleanup.
The api package is intentionally Bukkit-free. ItemProvider returns Object and the Bukkit-side service casts to ItemStack.
com.github.frosxt.prisoncore.menu.api.MenuService
public interface MenuService {
void open(UUID playerId, MenuDescriptor descriptor);
void open(UUID playerId, PaginatedMenuDescriptor descriptor, int page);
void close(UUID playerId);
boolean hasOpenMenu(UUID playerId);
void refresh(UUID playerId);
}Resolved from the service container:
final MenuService menuService = context.services().resolve(MenuService.class);open(UUID, MenuDescriptor) opens a static menu. open(UUID, PaginatedMenuDescriptor, int) opens a paginated menu at the given zero-based page; the platform handles flipping when the player clicks the previous/next buttons.
close shuts the active menu for that player. hasOpenMenu checks if one is open. refresh re-renders the currently open menu in place, which is useful when its data has changed and you want to update it without re-opening the inventory.
com.github.frosxt.prisoncore.menu.api.MenuDescriptor
Built with MenuDescriptor.builder(id, title, rows).
final MenuDescriptor menu = MenuDescriptor.builder("hello:greeting", "&aGreetings", 3)
.slot(13, SlotDescriptor.of(
() -> BukkitItemBuilder.of(Material.PAPER).name("&fHello!").build(),
ctx -> sendMessage(ctx.viewerId(), "Hello!")))
.border(SlotDescriptor.displayOnly(
() -> BukkitItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build()))
.build();
menuService.open(player.getUniqueId(), menu);The title runs through ColorTranslator.colorize automatically when the menu is rendered, so ampersand, hex & rgb color codes work in titles, item names, and lore without you having to call colorize yourself.
Builder methods:
-
slot(int index, SlotDescriptor descriptor)— set a single slot. Latest call wins. -
fill(SlotDescriptor descriptor)— fill every empty slot. Useful as a background after you've placed your content. -
border(SlotDescriptor descriptor)— fill the outer border (top row, bottom row, leftmost column, rightmost column). -
row(int row, SlotDescriptor descriptor)— fill an entire row. -
column(int col, SlotDescriptor descriptor)— fill an entire column. -
applyPattern(MenuLayoutPattern pattern)— apply a pre-baked layout pattern.
fill, border, row, and column use putIfAbsent semantics. They will not overwrite slots you set explicitly with slot(...). The order doesn't strictly matter, but placing your content first and your background second reads more naturally.
com.github.frosxt.prisoncore.menu.api.layout.SlotDescriptor
A slot has two parts: how to render it, and what happens when it's clicked.
public static SlotDescriptor of(ItemProvider item, ClickHandler handler);
public static SlotDescriptor displayOnly(ItemProvider item);of(...) is the standard form. displayOnly(...) is a convenience for items with no click behavior (borders, info displays, decorative slots).
final SlotDescriptor button = SlotDescriptor.of(
() -> BukkitItemBuilder.of(Material.EMERALD)
.name("&aBuy")
.lore(List.of("&7Click to purchase"))
.build(),
ctx -> handlePurchase(ctx.viewerId())
);SlotDescriptor.ItemProvider is a functional interface returning Object. The Bukkit menu service casts to ItemStack. In practice you'll always return an ItemStack, usually built with BukkitItemBuilder.
The provider is called every time the slot is rendered, including on refresh. If your item depends on dynamic data, capture that dependency in the lambda and the slot will reflect the latest state when you call refresh.
com.github.frosxt.prisoncore.menu.api.click.ClickHandler
@FunctionalInterface
public interface ClickHandler {
void handle(MenuClickContext context);
static ClickHandler closing(Runnable action);
static ClickHandler of(Map<ClickType, ClickHandler> handlers);
static ClickHandler leftClick(ClickHandler handler);
static ClickHandler rightClick(ClickHandler handler);
static ClickHandler empty();
}The static helpers cover the common cases:
-
leftClick(handler)runs only on left or shift-left clicks. -
rightClick(handler)runs only on right or shift-right clicks. -
of(map)dispatches by exactClickType. -
empty()does nothing. -
closing(action)runs the action and closes the menu.
You can compose them too. A click handler is just a Consumer<MenuClickContext> underneath.
com.github.frosxt.prisoncore.menu.api.click.MenuClickContext
public UUID viewerId();
public int slot();
public ClickType clickType();
public boolean isLeftClick(); // LEFT or SHIFT_LEFT
public boolean isRightClick(); // RIGHT or SHIFT_RIGHT
public boolean isShiftClick(); // SHIFT_LEFT or SHIFT_RIGHTviewerId() is the player who clicked. slot() is the inventory slot index of the click.
The boolean helpers are there for the common case where your handler doesn't care which side button was used.
com.github.frosxt.prisoncore.menu.api.click.ClickType — LEFT, RIGHT, SHIFT_LEFT, SHIFT_RIGHT, plus a few more.
com.github.frosxt.prisoncore.menu.api.PaginatedMenuDescriptor
For lists that don't fit in one inventory.
final PaginatedMenuDescriptor descriptor = PaginatedMenuDescriptor.builder()
.id("hello:players")
.title("&aOnline Players")
.rows(6)
.contentRange(0, 44) // first five rows
.previousButton(SlotDescriptor.of(
() -> BukkitItemBuilder.of(Material.ARROW).name("&7Previous").build(),
ctx -> {} // navigation is wired automatically
))
.nextButton(SlotDescriptor.of(
() -> BukkitItemBuilder.of(Material.ARROW).name("&7Next").build(),
ctx -> {}
))
.previousButtonSlot(48) // optional; defaults to bottom-left
.nextButtonSlot(50) // optional; defaults to bottom-right
.items(buildPlayerEntries())
.build();
menuService.open(player.getUniqueId(), descriptor, 0);Builder methods:
-
id(String)/title(String)/rows(int)— same asMenuDescriptor. -
contentRange(int start, int end)— inclusive slot range that holds page content. -
previousButton(SlotDescriptor)/nextButton(SlotDescriptor)— navigation buttons. The previous button only renders on pages > 0, the next button only renders if more pages exist. -
previousButtonSlot(int)/nextButtonSlot(int)— override where the buttons render. Both are optional; if you don't set them, the previous button defaults to the bottom-left slot (rows * 9 - 9) and the next button defaults to the bottom-right slot (rows * 9 - 1). -
items(List<SlotDescriptor>)— set the full item list. -
addItem(SlotDescriptor)— append to the item list.
buildPage(int) produces a flat MenuDescriptor for a specific page if you want to inspect or test page rendering.
totalPages() returns the number of pages, computed from the item count and content range.
The descriptor validates button placement at build time and logs a warning if either button lands somewhere problematic:
- Out of bounds (slot index is negative or beyond the inventory size). The button will silently fail to render.
- Inside the
contentRange. The button will overwrite a content item on any page that fills that slot. - Both buttons on the same slot. The next button will overwrite the previous button on every page after the first.
Pick slots outside the content range whenever you can. A common pattern is to leave the bottom row out of the content range and reserve it for navigation and meta items:
PaginatedMenuDescriptor.builder()
.rows(6)
.contentRange(0, 44) // first five rows for content
.previousButtonSlot(45)
.nextButtonSlot(53)
.items(entries)
.build();public final class GreetingMenu {
private final MenuService menuService;
public GreetingMenu(final MenuService menuService) {
this.menuService = menuService;
}
public void open(final Player viewer) {
final MenuDescriptor descriptor = MenuDescriptor.builder("hello:greeting", "&aGreeting", 3)
.border(border())
.slot(13, hello(viewer))
.build();
menuService.open(viewer.getUniqueId(), descriptor);
}
private SlotDescriptor border() {
return SlotDescriptor.displayOnly(() ->
BukkitItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build());
}
private SlotDescriptor hello(final Player viewer) {
return SlotDescriptor.of(
() -> BukkitItemBuilder.of(Material.PAPER)
.name("&fHello, &a" + viewer.getName())
.lore(List.of("&7Click to wave"))
.build(),
ctx -> {
final Player player = Bukkit.getPlayer(ctx.viewerId());
if (player != null) {
player.sendMessage("waves");
}
});
}
}Open menus are automatically tracked per player. When the player closes the inventory, the platform clears the tracking entry for you. If you want to react to the close event, hook the appropriate event through the event bus instead of trying to attach to the menu lifecycle directly.
This wiki documents the public API surface module authors are expected to touch. Kernel internals are intentionally omitted. Spot something wrong? Open an issue.
Start here
Architecture
Subsystems
- Commands
- Menus
- Messages and Placeholders
- Scheduler
- Storage
- Events and Listeners
- Configuration
- Player Profiles
Utilities