Skip to content

GUI System

EmakiCoreLib provides a YAML template-driven chest GUI system with support for sound configuration, async rendering, and session handling.

YAML Template Structure

Each GUI is defined by a YAML file with the following basic structure:

yaml
id: "example_menu"
title: "<gradient:gold:yellow>示例菜单</gradient>"
rows: 3

slots:
  decoration_border:
    slots: "0-8,18-26"
    type: DECORATION
    item:
      source: "vanilla-gray_stained_glass_pane"
      name: " "

  info_display:
    slots: "12"
    type: DISPLAY
    item:
      source: "vanilla-book"
      name: "<gold>服务器信息"
      lore:
        - "<gray>在线玩家:<white>%server_online%"
        - "<gray>服务器 TPS:<white>%server_tps%"
        - ""
        - "<yellow>点击刷新"

  confirm_button:
    slots: "14"
    type: BUTTON
    item:
      source: "vanilla-lime_wool"
      name: "<green>确认"
      lore:
        - "<gray>点击确认操作"
    sounds:
      click: "ui.button.click"

  close_button:
    slots: "22"
    type: CLOSE
    item:
      source: "vanilla-barrier"
      name: "<red>关闭"
    sounds:
      click: "block.chest.close"

Top-level Fields

FieldTypeRequiredDescription
idStringYesUnique GUI identifier
titleStringYesGUI title (supports MiniMessage)
rowsintYesNumber of chest rows (1-6)
slotsMapYesSlot definition map

Slot Definition Fields

FieldTypeRequiredDescription
keyStringYesMap key name, used for referencing in code
slotsStringYesSlot indices — supports single "4", range "0-8", or combined "0-8,18-26"
typeStringNoSlot type (can be auto-inferred)
itemMapYesItem configuration
soundsMapNoSound configuration

Item Configuration

yaml
item:
  source: "vanilla-diamond_sword"    # Item source (see Item Source system)
  name: "<gold>传说之剑"              # Custom name (MiniMessage)
  lore:                               # Custom lore
    - "<gray>一把充满力量的剑"
    - ""
    - "<yellow>攻击力:<red>+15"
  amount: 1                           # Amount
  glow: true                          # Enchantment glow effect
  custom_model_data: 10001            # Custom model data

Auto-inferred Slot Types

When the type field is omitted, the system automatically infers the type based on the slot's key name and configuration:

TypeDescriptionAuto-inference Condition
DECORATIONPure decoration, does not respond to clicksKey contains decoration, border, filler
DISPLAYInformation display, clicks have no effectKey contains display, info, status
BUTTONClickable buttonKey contains button, btn, confirm, accept
CLOSEClose GUI buttonKey contains close, exit, back
TOGGLEToggle switchKey contains toggle, switch
INPUTInput slot (allows placing items)Key contains input, slot
OUTPUTOutput slot (allows taking items)Key contains output, result
PAGINATE_PREVPrevious pageKey contains prev, previous
PAGINATE_NEXTNext pageKey contains next

Tip

It is recommended to always explicitly specify type. Auto-inference is only a convenience feature. Explicit declaration avoids behavioral changes caused by key name modifications.

GuiOpenRequest

Open a GUI using GuiOpenRequest:

java
GuiService guiService = EmakiServiceRegistry.get(GuiService.class);

GuiOpenRequest request = GuiOpenRequest.builder()
    .templateId("example_menu")
    .player(player)
    .handler(new MyGuiHandler())
    .placeholder("server_online", String.valueOf(Bukkit.getOnlinePlayers().size()))
    .placeholder("server_tps", String.valueOf(Bukkit.getTPS()[0]))
    .build();

guiService.open(request);
MethodDescription
templateId(String)Specify the GUI template ID
player(Player)Target player
handler(GuiSessionHandler)Session handler
placeholder(String, String)Add a placeholder
placeholders(Map)Add placeholders in bulk
attribute(String, Object)Attach an attribute

GuiSessionHandler Interface

Implement the GuiSessionHandler interface to handle GUI interaction events:

java
public class MyGuiHandler implements GuiSessionHandler {

    @Override
    public void onSlotClick(GuiClickContext ctx) {
        String key = ctx.getSlotKey();

        switch (key) {
            case "confirm_button" -> {
                ctx.getPlayer().sendMessage("已确认!");
                ctx.close();
            }
            case "info_display" -> {
                // Refresh display
                ctx.refreshSlot("info_display");
            }
        }
    }

    @Override
    public void onPlayerInventoryClick(GuiClickContext ctx) {
        // Triggered when the player clicks an item in their own inventory
        ctx.cancel(); // Cancel the operation to prevent item movement
    }

    @Override
    public void onDrag(GuiDragContext ctx) {
        // Triggered when the player drags items
        ctx.cancel();
    }

    @Override
    public void onClose(GuiCloseContext ctx) {
        // Triggered when the GUI is closed
        // Can be used to save data, return items, etc.
        Player player = ctx.getPlayer();
        player.sendMessage("菜单已关闭");
    }
}

Common GuiClickContext Methods

MethodDescription
getPlayer()Get the player who clicked
getSlotKey()Get the key name of the clicked slot
getSlotIndex()Get the index of the clicked slot
getClickType()Get the click type (LEFT, RIGHT, SHIFT_LEFT, etc.)
getCurrentItem()Get the item in the current slot
cancel()Cancel this click event
close()Close the GUI
refreshSlot(String)Refresh the display of a specific slot
refreshAll()Refresh all slots

Sound Configuration

Each slot can have interaction sounds configured:

yaml
sounds:
  click: "ui.button.click"                    # Shorthand format
  hover:                                       # Full format
    sound: "block.note_block.pling"
    volume: 0.8
    pitch: 1.2

Shorthand Format

Directly specify the sound ID, using default volume 1.0 and pitch 1.0:

yaml
sounds:
  click: "ui.button.click"

Full Format

yaml
sounds:
  click:
    sound: "entity.experience_orb.pickup"     # Sound ID
    volume: 1.0                                # Volume (0.0 ~ 2.0)
    pitch: 1.5                                 # Pitch (0.5 ~ 2.0)

Supported Sound Events

EventTrigger
clickWhen the player clicks a slot
hoverWhen the player hovers over a slot (only effective when supported by the client)
openWhen the GUI opens (defined at top level)
closeWhen the GUI closes (defined at top level)

Top-level sound configuration example:

yaml
id: "shop_menu"
title: "<gold>商店"
rows: 6
sounds:
  open: "block.chest.open"
  close: "block.chest.close"
slots:
  # ...

Async Rendering Support

The GUI system supports async rendering, suitable for scenarios that require fetching data from databases or remote APIs:

java
GuiOpenRequest request = GuiOpenRequest.builder()
    .templateId("player_stats")
    .player(player)
    .handler(new StatsHandler())
    .build();

guiService.openAsync(request).thenAccept(session -> {
    // GUI is open, content can be updated asynchronously
    CompletableFuture.supplyAsync(() -> fetchPlayerStats(player))
        .thenAccept(stats -> {
            session.updatePlaceholder("kills", String.valueOf(stats.kills()));
            session.updatePlaceholder("deaths", String.valueOf(stats.deaths()));
            session.refreshAll();
        });
});

Warning

refreshSlot and refreshAll calls during async rendering are automatically dispatched back to the main thread — no manual thread switching is needed. However, make sure to call the refresh method after updating placeholders, otherwise the GUI will not update.

Tip

For simple static GUIs, the synchronous guiService.open(request) is sufficient. Async rendering is primarily intended for complex GUIs that need to load external data.

Released under the GPL-3.0 License