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.
Architecture
Section titled “Architecture”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 pagesPageManager
Section titled “PageManager”The PageManager handles opening, closing, and updating pages for each player. Access it through the Player component.
Key Methods
Section titled “Key Methods”| Method | Description |
|---|---|
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) |
Standard Page Enum
Section titled “Standard Page Enum”| Value | Description |
|---|---|
None | No page open (closes current page) |
Bench | Crafting bench interface |
Inventory | Player inventory |
ToolsSettings | Tool settings interface |
Map | World map |
MachinimaEditor | Machinima editing tools |
ContentCreation | Content creation tools |
Custom | Custom page (used internally) |
Opening Pages
Section titled “Opening Pages”Player playerComponent = store.getComponent(ref, Player.getComponentType());PageManager pageManager = playerComponent.getPageManager();
// Open standard pagepageManager.setPage(ref, store, Page.Inventory);
// Open page that can be closed by clicking elsewherepageManager.setPage(ref, store, Page.Bench, true);
// Close any open pagepageManager.setPage(ref, store, Page.None);CustomUIPage Hierarchy
Section titled “CustomUIPage Hierarchy”CustomUIPage (Base Class)
Section titled “CustomUIPage (Base Class)”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();}CustomPageLifetime Enum
Section titled “CustomPageLifetime Enum”| Value | Description |
|---|---|
CantClose | Player cannot close the page (e.g., death screen) |
CanDismiss | Player can dismiss with escape key |
CanDismissOrCloseThroughInteraction | Can dismiss or close by clicking outside |
BasicCustomUIPage
Section titled “BasicCustomUIPage”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()); }}InteractiveCustomUIPage
Section titled “InteractiveCustomUIPage”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 pagesprotected 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(); } }}Event Data Class
Section titled “Event Data Class”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;}Event Data Keys
Section titled “Event Data Keys”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 Methods
Section titled “EventData Methods”EventData only supports String and Enum values. Numbers must be converted to strings:
// Static factory method - keys that start with letters must be uppercaseEventData.of("Action", "save")
// Append methods (returns self for chaining).append("ItemId", "sword_01").append("State", MyEnum.VALUE)
// For integers, convert to stringEventData.of("Action", "buy").append("Index", Integer.toString(i))UIEventBuilder
Section titled “UIEventBuilder”The UIEventBuilder creates event bindings for UI elements:
// Basic event bindingevents.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);CustomUIEventBindingType
Section titled “CustomUIEventBindingType”| Type | Description |
|---|---|
Activating | Element clicked/activated |
RightClicking | Right mouse button click |
DoubleClicking | Double click |
MouseEntered | Mouse enters element |
MouseExited | Mouse exits element |
ValueChanged | Input value changed |
ElementReordered | Element reordered in list |
Validating | Input validation |
Dismissing | Page being dismissed |
FocusGained | Element gained focus |
FocusLost | Element lost focus |
KeyDown | Key pressed |
MouseButtonReleased | Mouse button released |
SlotClicking | Inventory slot clicked |
SlotDoubleClicking | Inventory slot double-clicked |
SlotMouseEntered | Mouse enters slot |
SlotMouseExited | Mouse exits slot |
DragCancelled | Drag operation cancelled |
Dropped | Element dropped |
SlotMouseDragCompleted | Slot drag completed |
SlotMouseDragExited | Drag exited slot |
SlotClickReleaseWhileDragging | Click released while dragging |
SlotClickPressWhileDragging | Click pressed while dragging |
SelectedTabChanged | Tab selection changed |
UICommandBuilder
Section titled “UICommandBuilder”The UICommandBuilder creates UI update commands:
Layout Commands
Section titled “Layout Commands”| Method | Description |
|---|---|
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 |
Value Setting Commands
Section titled “Value Setting Commands”| Method | Description |
|---|---|
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.) |
Complete Interactive Example
Section titled “Complete Interactive Example”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; }}Updating Pages Dynamically
Section titled “Updating Pages Dynamically”Partial Updates
Section titled “Partial Updates”private void updateScore(int newScore) { UICommandBuilder builder = new UICommandBuilder(); builder.set("#ScoreLabel.Text", String.valueOf(newScore)); sendUpdate(builder);}Full Rebuild
Section titled “Full Rebuild”// Completely rebuild the pagerebuild();ChoiceBasePage
Section titled “ChoiceBasePage”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
ChoiceElementwithActivatingevent bindings - Handles element selection and runs associated
ChoiceInteractions
Creating Custom .ui Files
Section titled “Creating Custom .ui Files”For fully custom page layouts with images and custom styling, create .ui files in your plugin’s asset pack.
Directory Structure
Section titled “Directory Structure”src/main/resources/├── manifest.json # Set "IncludesAssetPack": true└── Common/ └── UI/ └── Custom/ ├── MyStatusPage.ui # Your custom .ui file └── MyBackground.png # ImagesExample Custom Page (.ui file)
Section titled “Example Custom Page (.ui file)”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); } }}Java Page Class
Section titled “Java Page Class”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(); }}Key Points for Custom .ui Files
Section titled “Key Points for Custom .ui Files”- Import Common.ui - Use
$Common = "Common.ui";to access built-in styles - Reference styles - Use
Style: $Common.@DefaultInputFieldStyle;for consistent styling - Texture paths are relative - Put images in the same folder and reference by filename
- PatchStyle for images - Define with
@MyTex = PatchStyle(TexturePath: "file.png");and apply withBackground: @MyTex; - Textures auto-stretch - Images automatically stretch to fit the element size
Built-in UI Files
Section titled “Built-in UI Files”The game includes built-in UI files for common pages:
| File | Purpose |
|---|---|
Pages/DialogPage.ui | NPC conversation dialogs |
Pages/ShopPage.ui | Shop interfaces |
Pages/BarterPage.ui | Trading interfaces |
Pages/RespawnPage.ui | Death/respawn screen |
Pages/WarpListPage.ui | Teleportation lists |
Pages/CommandListPage.ui | Command browser |
Pages/PluginListPage.ui | Plugin management |
Best Practices
Section titled “Best Practices”- Use appropriate lifetime -
CantClosefor important dialogs,CanDismissfor menus - Handle all events - Always have a way to close the page
- Validate event data - Clients can send unexpected values
- Batch updates - Combine multiple changes in one
sendUpdate()call - Clean up in onDismiss - Release resources when page closes