Content generated with AI — it may contain mistakes.

Interfaces

Input, editors and wizards

Asking a player for something, editing a list of things, and walking somebody through a setup.

Three modules that solve the same shape of problem: getting information out of a player without writing an anvil listener, a chat listener and a state map for each one.

Asking for one thing

PluginInputs inputs = Inputs.of(this);
 
inputs.integer(player, "How many slots?")
      .range(1L, 64L)
      .open()
      .thenAccept(result -> result
          .ifCompleted(this::resize)
          .otherwise(outcome -> {
              if (outcome.byPlayer()) reopenMenu(player);
          }));

Thirteen request types, each a builder:

CallProducesFor
textStringFree-form text
idStringA strict lowercase identifier
slugStringAn identifier derived from display text
integerLongWhole numbers
decimalBigDecimalExact decimals
amountBigDecimalMoney, as a player writes it — 10M
durationDuration30s, 1h30m
flagBooleanA setting or switch
confirmBooleanAn explicit confirmation
choiceTOne of a few options
searchTOne of very many options
iconStringWhat something is drawn as
formFormValuesSeveral things in one window

Every request shares the same modifiers, so they read the same whatever is being asked: timeout(Duration), defaultValue(T), validate(predicate, message), transform(operator), transports(kinds…), and open() or open(consumer).

validate and transform live on the request

Not on the transport. That is what stops case folding from working in chat but not in a dialog.

How it is asked

Five transports, tried in order. The first that can represent the request wins:

OrderTransportChosen when
1DIALOGDialogs are enabled, PacketEvents is loaded and the client is 1.21.6 or newer.
2BEDROCKThe player is on Bedrock and Floodgate is installed.
3ANVIL_SEARCHThe request is a search.
4MENUAn inventory of buttons can express it — choice, confirm, flag.
5CHATAlways.

Chat is the universal fallback: every Java player can answer in chat even with no packet or bridge integration present. Neither PacketEvents nor Floodgate is required, and transports are discovered reflectively so a missing one simply is not offered.

Picking an icon, and the head catalogue

icon asks what something is drawn as, and offers three ways of answering: the item in hand, a search over the server's own materials, and BROWSE — the decorative head catalogue, some eighty-six thousand heads, searched a page at a time and nothing downloaded.

Heads.browse(inputs, player, "{warning}Browse a head")
     .open(head -> arenas.save(arena.withIcon(head.icon())));

browse hands back the SearchInput<Head> rather than opening it, so a timeout, a validation or a page size are still the caller's. A Head carries id(), name(), texture(), category(), icon() — the urlhead-<texture> value a config stores — and item(). The query is matched against names, ids and tag names, so cat finds the cats and flag finds the flags.

One outcome, exactly once

COMPLETED, CANCELLED, TIMED_OUT and the rest arrive exactly once per request, whatever happened — including the player logging out mid-answer. outcome().byPlayer() is the one to branch on when deciding whether to reopen the menu they came from.

Editing a list of things

A rewards list, a loot table, a set of spawn points: the same screen, one engine.

CallEdits
Rewards.of(plugin).editor(rewards)Rewards
Loot.editor(plugin, entries)Loot tables
NamedCommands.editor(plugin, commands)Named console commands
Effects.editor(plugin, effects)Potion effects
Sequences.of(plugin).editor(effects)Effects with odds, conditions and an audience
Editors.of(plugin).items(items)Real items — kits, shop stock
Editors.of(plugin).locations(places)Spawn points, arena corners
Editors.of(plugin).list(descriptor, type, entries)The one only your plugin has

Every list gets pagination, add, edit, delete, copy, paste, save and cancel:

GestureWhat happens
Left click a rowEdit it
Right click a rowDelete it
Shift + leftCopy it
ADDCreate a row and configure it
PASTEAdd whatever is on the clipboard
COPY ALLPut the whole list on the clipboard
SAVE / CANCELKeep everything, or nothing
Nothing is written until save

The list handed in is copied, and every change goes into the copy — so cancel is free and an half-finished row cannot escape into the plugin's state.

A row that is not finished — a command entry with no command — is drawn with a mark rather than hidden. An editor is where a half-configured row gets finished, and one that vanished would take its place in the list with it.

Walking somebody through a setup

A wizard is several questions with branches, a review they can go back from, and nothing applied until they confirm.

static final WizardKey<String> ID    = WizardKey.text("id");
static final WizardKey<Long>   SLOTS = WizardKey.integer("slots");
 
Wizards.of(this)
       .define("create-arena", builder -> builder
               .ask(ID, "Arena id")
               .ask(SLOTS, "How many players?")
               .review()
               .apply(values -> create(values.get(ID), values.get(SLOTS))))
       .start(player);

The one-step shortcuts are what most setup screens actually use:

wizards.askRegion(player, "Capture zone", "Select the area with the wand", accepted, abandoned);
wizards.askLocation(player, "Spawn point", "Stand where players appear", accepted, null);
wizards.askItem(player, "Icon", "Hold the item", accepted, null);

askRegion is the shared block selector: the same wand, the same preview and the same two corners every Exylia plugin selects with — which is why none of them depends on WorldEdit for it.

Abandoning is a first-class outcome

Every flow takes an abandoned callback. A player who backed out leaves the previous configuration untouched, and the screen they came from is reopened rather than left closed.

Something missing on this page? Tell us on Discord