Skip to content

Custom GUIs (menus)

A menu is a server-authoritative chest-style GUI owned by the plugin. Every plugin has Menus (plugin.Menus) to create one: Chest(rows, title) for one to six rows of nine slots, Hopper(title) for five slots and Dispenser(title) for a 3 x 3 grid. The menu is a display: a click never moves an item in or out unless your handler allows it.

Type What it is for
Menus creates menus owned by your plugin: Chest, Hopper, Dispenser, Create(type, title, rows)
Menu the GUI: Slot(slot, icon, onClick), FillEmpty, OnClick, OnClose, Open(player), Close(player), Viewers, Title, Refresh(), Dispose()
MenuClick one click: Player, Slot, Type (a MenuClickType), Clicked, Cursor, Allow(), Cancel(), Close()
MenuCloseReason why a menu closed: Player, Plugin, Unload, Quit, Replaced, Invalid
ItemStack an immutable item: new ItemStack("minecraft:diamond_sword", 1), with WithName, WithLore, WithEnchantment, WithCount and more

Rules:

  • A click on one of the menu’s own slots is cancelled by default. MenuClick.Allow() lets it through, so the player can take the stack out or put one in, just as in a chest. Cancel() denies it for good.
  • A click that only touches the player’s own inventory passes unless a handler cancels it. Set Menu.LockPlayerInventory to deny those too.
  • Handlers run on the tick thread, like every other callback.
  • A menu is closed for all viewers when its plugin unloads or reloads, and its handlers are dropped.
  • ItemStack is a value: WithName(...) returns a new stack.

A menu with one icon per warp. Each icon’s click handler teleports the player.

Warps.cs
using Mc.Scripting.Inventories;
using Mc.Scripting.Text;
namespace Warps;
public sealed class PluginMain : Plugin
{
private Menu _menu = null!;
protected override void OnEnable()
{
_menu = Menus.Chest(3, Component.Parse("<dark_green>Warps"))
.FillEmpty(new ItemStack("minecraft:gray_stained_glass_pane").WithName(Component.Text(" ")))
.Slot(11, new ItemStack("minecraft:grass_block").WithName(Component.Legacy("&aSpawn")),
click => Go(click.Player, "minecraft:overworld", 0, 80, 0))
.Slot(13, new ItemStack("minecraft:netherrack").WithName(Component.Legacy("&cNether hub")),
click => Go(click.Player, "minecraft:the_nether", 0, 70, 0))
.Slot(15, new ItemStack("minecraft:end_stone").WithName(Component.Legacy("&5The End")),
click => Go(click.Player, "minecraft:the_end", 100, 50, 0))
.OnClose(player => Log.Debug($"{player.Name} closed the warp menu"));
Commands.Register(Command.Literal("warps")
.Executes(ctx => { _menu.Open(ctx.RequirePlayer()); return 1; }),
description: "Open the warp menu");
}
private static void Go(Player player, string world, double x, double y, double z)
{
if (Server.GetWorld(world) is not { } target) return; // the dimension is not loaded
player.Teleport(new Location(target, x, y, z));
}
}

The menu does not depend on the command: call Open from any event handler and it works the same way.

Open returns false when a handler cancelled the opening or the player is offline. Close(player) closes the menu for one viewer, and Close() closes it for everybody.

  • OnClick(handler) runs for every click in the menu, before the slot’s own handler. Use it for a rule that covers the whole menu, such as logging clicks or cancelling every click when the player is not allowed to use it.
  • A slot’s handler runs only for a click the menu did not cancel. Its MenuClick.Clicked and Cursor are the stacks before the click.
  • OnClose has two forms: OnClose(player => ...) for the closing player, and OnClose((player, reason) => ...) to also get the MenuCloseReason. The cursor stack has already been given back when it runs.
  • Menu.Title can be changed at any time. The viewers’ screens are reopened with the same contents.

The engine also raises MenuClickEvent and MenuCloseEvent for every menu, not only yours. They carry the menu’s native id instead of a Menu object, so use them to observe menus you do not own.

  • Items you put in a menu are ordinary stacks: Get, Set, AddItem and RemoveItem on the menu read and write them.
  • Shared state between plugins: inter-plugin services.
  • Plain events and commands: first plugin.