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:
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
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique GUI identifier |
title | String | Yes | GUI title (supports MiniMessage) |
rows | int | Yes | Number of chest rows (1-6) |
slots | Map | Yes | Slot definition map |
Slot Definition Fields
| Field | Type | Required | Description |
|---|---|---|---|
key | String | Yes | Map key name, used for referencing in code |
slots | String | Yes | Slot indices — supports single "4", range "0-8", or combined "0-8,18-26" |
type | String | No | Slot type (can be auto-inferred) |
item | Map | Yes | Item configuration |
sounds | Map | No | Sound configuration |
Item Configuration
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 dataAuto-inferred Slot Types
When the type field is omitted, the system automatically infers the type based on the slot's key name and configuration:
| Type | Description | Auto-inference Condition |
|---|---|---|
DECORATION | Pure decoration, does not respond to clicks | Key contains decoration, border, filler |
DISPLAY | Information display, clicks have no effect | Key contains display, info, status |
BUTTON | Clickable button | Key contains button, btn, confirm, accept |
CLOSE | Close GUI button | Key contains close, exit, back |
TOGGLE | Toggle switch | Key contains toggle, switch |
INPUT | Input slot (allows placing items) | Key contains input, slot |
OUTPUT | Output slot (allows taking items) | Key contains output, result |
PAGINATE_PREV | Previous page | Key contains prev, previous |
PAGINATE_NEXT | Next page | Key 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:
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);| Method | Description |
|---|---|
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:
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
| Method | Description |
|---|---|
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:
sounds:
click: "ui.button.click" # Shorthand format
hover: # Full format
sound: "block.note_block.pling"
volume: 0.8
pitch: 1.2Shorthand Format
Directly specify the sound ID, using default volume 1.0 and pitch 1.0:
sounds:
click: "ui.button.click"Full Format
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
| Event | Trigger |
|---|---|
click | When the player clicks a slot |
hover | When the player hovers over a slot (only effective when supported by the client) |
open | When the GUI opens (defined at top level) |
close | When the GUI closes (defined at top level) |
Top-level sound configuration example:
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:
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.