Skip to content

Custom Pages System

The Custom Pages System allows you to create fully customizable GUI interfaces for players. This includes interactive dialogs, settings pages, choice menus, shop interfaces, and more.

PageManager (Per-Player)
├── Standard Pages (Page enum)
│ └── None, Bench, Inventory, ToolsSettings, Map, etc.
└── Custom Pages (CustomUIPage hierarchy)
├── BasicCustomUIPage - Simple display-only pages
├── InteractiveCustomUIPage<T> - Pages with event handling
└── ChoiceBasePage - Choice/dialog pages

The PageManager handles opening, closing, and updating pages for each player. Access it through the Player component.

MethodDescription
setPage(ref, store, page)Set a standard page (from Page enum)
setPage(ref, store, page, canCloseThroughInteraction)Set page with close-through-interaction option
setPageWithWindows(ref, store, page, canCloseThroughInteraction, windows...)Set page with inventory windows
openCustomPage(ref, store, customPage)Open a custom UI page
openCustomPageWithWindows(ref, store, page, windows...)Open custom page with inventory windows
getCustomPage()Get the currently open custom page
init(playerRef, windowManager)Initialize the PageManager (called automatically)
ValueDescription
NoneNo page open (closes current page)
BenchCrafting bench interface
InventoryPlayer inventory
ToolsSettingsTool settings interface
MapWorld map
MachinimaEditorMachinima editing tools
ContentCreationContent creation tools
CustomCustom page (used internally)
Player playerComponent = store.getComponent(ref, Player.getComponentType());
PageManager pageManager = playerComponent.getPageManager();
// Open standard page
pageManager.setPage(ref, store, Page.Inventory);
// Open page that can be closed by clicking elsewhere
pageManager.setPage(ref, store, Page.Bench, true);
// Close any open page
pageManager.setPage(ref, store, Page.None);
public abstract class CustomUIPage {
protected final PlayerRef playerRef;
protected CustomPageLifetime lifetime;
// Must implement - builds the initial page UI
public abstract void build(
Ref<EntityStore> ref,
UICommandBuilder commandBuilder,
UIEventBuilder eventBuilder,
Store<EntityStore> store
);
// Override for cleanup when page is dismissed
public void onDismiss(Ref<EntityStore> ref, Store<EntityStore> store);
// Rebuild the entire page UI
protected void rebuild();
// Send partial updates to the page (multiple overloads)
protected void sendUpdate(); // Sends an update with no commands (does not rebuild)
protected void sendUpdate(@Nullable UICommandBuilder commandBuilder);
protected void sendUpdate(@Nullable UICommandBuilder commandBuilder, boolean clear);
// Get/set the page lifetime
public CustomPageLifetime getLifetime();
public void setLifetime(CustomPageLifetime lifetime);
// Close this page
protected void close();
}
ValueDescription
CantClosePlayer cannot close the page (e.g., death screen)
CanDismissPlayer can dismiss with escape key
CanDismissOrCloseThroughInteractionCan dismiss or close by clicking outside

For simple pages that don’t need event handling:

public class WelcomePage extends BasicCustomUIPage {
public WelcomePage(PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss);
}
@Override
public void build(UICommandBuilder commandBuilder) {
commandBuilder.append("Pages/WelcomePage.ui");
commandBuilder.set("#Title.Text", "Welcome!");
commandBuilder.set("#PlayerName.Text", playerRef.getUsername());
}
}

For pages that handle user interactions. This class extends CustomUIPage with typed event handling and has an additional sendUpdate signature:

// Additional sendUpdate signature for interactive pages
protected void sendUpdate(@Nullable UICommandBuilder commandBuilder,
@Nullable UIEventBuilder eventBuilder,
boolean clear);
public class SettingsPage extends InteractiveCustomUIPage<SettingsEventData> {
public SettingsPage(PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss, SettingsEventData.CODEC);
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder commands,
UIEventBuilder events, Store<EntityStore> store) {
commands.append("Pages/SettingsPage.ui");
// Bind save button (note: keys that start with letters must be uppercase)
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#SaveButton",
EventData.of("Action", "save")
);
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store,
SettingsEventData data) {
if ("save".equals(data.action)) {
// Handle save
close();
}
}
}

Create a data class with codec for receiving events:

public static class SettingsEventData {
public static final BuilderCodec<SettingsEventData> CODEC =
BuilderCodec.builder(SettingsEventData.class, SettingsEventData::new)
.append(new KeyedCodec<>("Action", Codec.STRING),
(d, v) -> d.action = v, d -> d.action)
.add()
.append(new KeyedCodec<>("@Value", Codec.INTEGER),
(d, v) -> d.value = v, d -> d.value)
.add()
.build();
public String action;
public Integer value;
}

Important: KeyedCodec requires that if a key starts with a letter, it must be uppercase. @-prefixed reference keys are allowed.

  • Static keys (e.g., "Action") - Sent as literal values, and if they start with a letter, it must be uppercase
  • Reference keys (prefixed with @, e.g., "@Value") - Reference UI element values at event time

EventData only supports String and Enum values. Numbers must be converted to strings:

// Static factory method - keys that start with letters must be uppercase
EventData.of("Action", "save")
// Append methods (returns self for chaining)
.append("ItemId", "sword_01")
.append("State", MyEnum.VALUE)
// For integers, convert to string
EventData.of("Action", "buy").append("Index", Integer.toString(i))

The UIEventBuilder creates event bindings for UI elements:

// Basic event binding
events.addEventBinding(CustomUIEventBindingType.Activating, "#Button");
// With event data (keys that start with letters must be uppercase)
events.addEventBinding(CustomUIEventBindingType.Activating, "#Button", EventData.of("Action", "click"));
// With locksInterface parameter (default is true)
events.addEventBinding(CustomUIEventBindingType.Activating, "#Button", EventData.of("Action", "click"), false);
TypeDescription
ActivatingElement clicked/activated
RightClickingRight mouse button click
DoubleClickingDouble click
MouseEnteredMouse enters element
MouseExitedMouse exits element
ValueChangedInput value changed
ElementReorderedElement reordered in list
ValidatingInput validation
DismissingPage being dismissed
FocusGainedElement gained focus
FocusLostElement lost focus
KeyDownKey pressed
MouseButtonReleasedMouse button released
SlotClickingInventory slot clicked
SlotDoubleClickingInventory slot double-clicked
SlotMouseEnteredMouse enters slot
SlotMouseExitedMouse exits slot
DragCancelledDrag operation cancelled
DroppedElement dropped
SlotMouseDragCompletedSlot drag completed
SlotMouseDragExitedDrag exited slot
SlotClickReleaseWhileDraggingClick released while dragging
SlotClickPressWhileDraggingClick pressed while dragging
SelectedTabChangedTab selection changed

The UICommandBuilder creates UI update commands:

MethodDescription
append(documentPath)Append UI document at root
append(selector, documentPath)Append UI document to element
appendInline(selector, document)Append inline UI definition
insertBefore(selector, documentPath)Insert UI document before element
insertBeforeInline(selector, document)Insert inline UI before element
clear(selector)Clear element’s children
remove(selector)Remove element from DOM
MethodDescription
set(selector, String)Set string value
set(selector, boolean)Set boolean value
set(selector, int)Set integer value
set(selector, float)Set float value
set(selector, double)Set double value
set(selector, Message)Set localized message
set(selector, Value<T>)Set reference value
set(selector, T[])Set array of values
set(selector, List<T>)Set list of values
setNull(selector)Set null value
setObject(selector, Object)Set compatible object (Area, ItemGridSlot, ItemStack, etc.)
public class ShopPage extends InteractiveCustomUIPage<ShopPage.ShopEventData> {
private final List<ShopItem> items;
private int playerCoins;
public ShopPage(PlayerRef playerRef, List<ShopItem> items, int coins) {
super(playerRef, CustomPageLifetime.CanDismiss, ShopEventData.CODEC);
this.items = items;
this.playerCoins = coins;
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder commands,
UIEventBuilder events, Store<EntityStore> store) {
commands.append("Pages/ShopPage.ui");
commands.set("#CoinsLabel.Text", playerCoins + " coins");
for (int i = 0; i < items.size(); i++) {
ShopItem item = items.get(i);
commands.append("#ItemList", "Components/ShopItem.ui");
commands.set("#Item" + i + ".Name", item.getName());
commands.set("#Item" + i + ".Price", item.getPrice() + "c");
// Note: KeyedCodec keys that start with letters must be uppercase
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#BuyBtn" + i,
EventData.of("Action", "buy").append("Index", Integer.toString(i))
);
}
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#CloseBtn",
EventData.of("Action", "close")
);
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store,
ShopEventData data) {
switch (data.action) {
case "buy":
ShopItem item = items.get(data.index);
if (playerCoins >= item.getPrice()) {
playerCoins -= item.getPrice();
// Give item to player...
// Update UI
UICommandBuilder update = new UICommandBuilder();
update.set("#CoinsLabel.Text", playerCoins + " coins");
sendUpdate(update);
}
break;
case "close":
close();
break;
}
}
public static class ShopEventData {
public static final BuilderCodec<ShopEventData> CODEC =
BuilderCodec.builder(ShopEventData.class, ShopEventData::new)
.append(new KeyedCodec<>("Action", Codec.STRING),
(d, v) -> d.action = v, d -> d.action).add()
.append(new KeyedCodec<>("Index", Codec.INTEGER),
(d, v) -> d.index = v, d -> d.index).add()
.build();
public String action;
public Integer index;
}
}
private void updateScore(int newScore) {
UICommandBuilder builder = new UICommandBuilder();
builder.set("#ScoreLabel.Text", String.valueOf(newScore));
sendUpdate(builder);
}
// Completely rebuild the page
rebuild();

A specialized page for presenting choices/dialogs to players. Extends InteractiveCustomUIPage<ChoicePageEventData>:

public abstract class ChoiceBasePage extends InteractiveCustomUIPage<ChoicePageEventData> {
public ChoiceBasePage(PlayerRef playerRef, ChoiceElement[] elements, String pageLayout) {
super(playerRef, CustomPageLifetime.CanDismiss, ChoicePageEventData.CODEC);
// ...
}
protected ChoiceElement[] getElements();
protected String getPageLayout();
}

The page automatically:

  • Appends the page layout
  • Clears #ElementList
  • Adds buttons for each ChoiceElement with Activating event bindings
  • Handles element selection and runs associated ChoiceInteractions

For fully custom page layouts with images and custom styling, create .ui files in your plugin’s asset pack.

src/main/resources/
├── manifest.json # Set "IncludesAssetPack": true
└── Common/
└── UI/
└── Custom/
├── MyStatusPage.ui # Your custom .ui file
└── MyBackground.png # Images

Create src/main/resources/Common/UI/Custom/MyStatusPage.ui:

// Include Common.ui to access built-in styles
$Common = "Common.ui";
// Define texture (path relative to this .ui file)
@MyTex = PatchStyle(TexturePath: "MyBackground.png");
Group {
LayoutMode: Center;
Group #MyPanel {
Background: @MyTex;
Anchor: (Width: 400, Height: 300);
LayoutMode: Top;
// Dynamic text (set from Java)
Label #WelcomeText {
Style: $Common.@DefaultLabelStyle;
Anchor: (Bottom: 8);
}
Label #StatusText {
Style: (FontSize: 12, TextColor: #cccccc, Wrap: true, HorizontalAlignment: Center);
}
}
}
public class MyStatusPage extends InteractiveCustomUIPage<MyStatusPage.EventData> {
// Reference your .ui file from Custom/ directory
private static final String PAGE_LAYOUT = "Custom/MyStatusPage.ui";
public MyStatusPage(PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC);
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder commands,
UIEventBuilder events, Store<EntityStore> store) {
// Load the custom .ui page
commands.append(PAGE_LAYOUT);
// Set dynamic text content using element IDs from the .ui file
commands.set("#WelcomeText.Text", "Welcome, " + playerRef.getUsername() + "!");
commands.set("#StatusText.Text", "Your current status information here.");
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, EventData data) {
// Handle events or close page
}
public static class EventData {
public static final BuilderCodec<EventData> CODEC = BuilderCodec
.builder(EventData.class, EventData::new)
.build();
}
}
  1. Import Common.ui - Use $Common = "Common.ui"; to access built-in styles
  2. Reference styles - Use Style: $Common.@DefaultInputFieldStyle; for consistent styling
  3. Texture paths are relative - Put images in the same folder and reference by filename
  4. PatchStyle for images - Define with @MyTex = PatchStyle(TexturePath: "file.png"); and apply with Background: @MyTex;
  5. Textures auto-stretch - Images automatically stretch to fit the element size

The game includes built-in UI files for common pages:

FilePurpose
Pages/DialogPage.uiNPC conversation dialogs
Pages/ShopPage.uiShop interfaces
Pages/BarterPage.uiTrading interfaces
Pages/RespawnPage.uiDeath/respawn screen
Pages/WarpListPage.uiTeleportation lists
Pages/CommandListPage.uiCommand browser
Pages/PluginListPage.uiPlugin management
  1. Use appropriate lifetime - CantClose for important dialogs, CanDismiss for menus
  2. Handle all events - Always have a way to close the page
  3. Validate event data - Clients can send unexpected values
  4. Batch updates - Combine multiple changes in one sendUpdate() call
  5. Clean up in onDismiss - Release resources when page closes