Files
hytale-server/docs/15-ui-system.md

17 KiB

UI System

The Hytale server implements a server-authoritative UI system with two main subsystems: Pages (full-screen UI) and Windows (inventory-style containers).

Architecture Overview

UI System
├── Pages System - Full-screen UI (dialogs, menus, custom UIs)
│   ├── CustomUIPage - Base page class
│   ├── UICommandBuilder - DOM manipulation
│   └── UIEventBuilder - Event bindings
└── Windows System - Inventory-style containers
    ├── Window - Base window class
    └── WindowManager - Window lifecycle

Page System

Core Classes

Class Package Description
CustomUIPage com.hypixel.hytale.server.core.entity.entities.player.pages Abstract base for all custom pages
BasicCustomUIPage Same Simple pages without event data parsing
InteractiveCustomUIPage<T> Same Pages with typed event handling
PageManager Same Manages page state for a player
UICommandBuilder com.hypixel.hytale.server.core.ui.builder Builds UI DOM commands
UIEventBuilder Same Builds event bindings

Page Lifetime

public enum CustomPageLifetime {
    CantClose(0),                           // User cannot close
    CanDismiss(1),                          // ESC key to dismiss
    CanDismissOrCloseThroughInteraction(2)  // Dismiss or click to close
}

CustomUIPage Base Class

public abstract class CustomUIPage {
    protected final PlayerRef playerRef;
    protected CustomPageLifetime lifetime;
    
    // Build the UI - called to construct the page
    public abstract void build(Ref<EntityStore> ref, UICommandBuilder cmd, 
                              UIEventBuilder events, Store<EntityStore> store);
    
    // Handle event data from client
    public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, String rawData);
    
    // Rebuild entire page
    protected void rebuild();
    
    // Send partial update
    protected void sendUpdate(UICommandBuilder commandBuilder, boolean clear);
    
    // Close the page
    protected void close();
    
    // Called when page is dismissed by user
    public void onDismiss(Ref<EntityStore> ref, Store<EntityStore> store);
}

InteractiveCustomUIPage

For pages with typed event handling:

public abstract class InteractiveCustomUIPage<T> extends CustomUIPage {
    protected final BuilderCodec<T> eventDataCodec;
    
    public InteractiveCustomUIPage(PlayerRef playerRef, CustomPageLifetime lifetime, 
                                   BuilderCodec<T> eventDataCodec) {
        super(playerRef, lifetime);
        this.eventDataCodec = eventDataCodec;
    }
    
    // Override this for type-safe event handling
    public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, T data);
    
    // Send update with new event bindings
    protected void sendUpdate(UICommandBuilder cmd, UIEventBuilder events, boolean clear);
}

UICommandBuilder

Builds commands to manipulate the UI DOM structure.

Command Types

public enum CustomUICommandType {
    Append(0),           // Append UI document
    AppendInline(1),     // Append inline XML
    InsertBefore(2),     // Insert before element
    InsertBeforeInline(3),
    Remove(4),           // Remove element
    Set(5),              // Set property value
    Clear(6)             // Clear children
}

Methods

UICommandBuilder cmd = new UICommandBuilder();

// Load UI documents
cmd.append("Pages/MyPage.ui");                    // Load to root
cmd.append("#Container", "Common/Button.ui");    // Append to selector

// Clear and remove
cmd.clear("#ItemList");                          // Clear children
cmd.remove("#OldElement");                       // Remove element

// Set property values
cmd.set("#Title.Text", "Hello World");           // String
cmd.set("#Counter.Value", 42);                   // Integer
cmd.set("#Progress.Value", 0.75f);               // Float
cmd.set("#Panel.Visible", true);                 // Boolean
cmd.set("#Label.Text", Message.translation("key")); // Localized

// Set with Value references
cmd.set("#Button.Style", Value.ref("Common/Button.ui", "ActiveStyle"));

// Set complex objects
cmd.setObject("#ItemSlot", itemGridSlot);        // Single object
cmd.set("#SlotList", itemGridSlotArray);         // Array
cmd.set("#DataList", itemList);                  // List

// Inline XML
cmd.appendInline("#Container", "<Panel Id='NewPanel'/>");
cmd.insertBefore("#Element", "Pages/Header.ui");

Selector Syntax

  • #ElementId - Select by ID
  • #Parent #Child - Descendant selector
  • #List[0] - Index accessor (for lists)
  • #List[0] #Button - Combined
  • #Element.Property - Property accessor

UIEventBuilder

Builds event bindings that trigger server callbacks.

Event Binding Types

public enum CustomUIEventBindingType {
    Activating(0),           // Click/press
    RightClicking(1),        // Right-click
    DoubleClicking(2),       // Double-click
    MouseEntered(3),         // Mouse enter
    MouseExited(4),          // Mouse leave
    ValueChanged(5),         // Input value changed
    ElementReordered(6),     // Drag reorder
    Validating(7),           // Input validation
    Dismissing(8),           // Page dismiss
    FocusGained(9),          // Element focused
    FocusLost(10),           // Element unfocused
    KeyDown(11),             // Key pressed
    MouseButtonReleased(12), // Mouse released
    SlotClicking(13),        // Inventory slot click
    SlotDoubleClicking(14),  // Slot double-click
    SlotMouseEntered(15),    // Slot hover enter
    SlotMouseExited(16),     // Slot hover exit
    DragCancelled(17),       // Drag cancelled
    Dropped(18),             // Item dropped
    SlotMouseDragCompleted(19),
    SlotMouseDragExited(20),
    SlotClickReleaseWhileDragging(21),
    SlotClickPressWhileDragging(22),
    SelectedTabChanged(23)   // Tab selection
}

Methods

UIEventBuilder events = new UIEventBuilder();

// Simple binding
events.addEventBinding(CustomUIEventBindingType.Activating, "#CloseButton");

// With event data
events.addEventBinding(CustomUIEventBindingType.Activating, "#SubmitButton",
    EventData.of("Action", "Submit"));

// Without interface lock (for non-blocking events)
events.addEventBinding(CustomUIEventBindingType.MouseEntered, "#HoverArea",
    EventData.of("Action", "Hover"), false);

// With value extraction from element
events.addEventBinding(CustomUIEventBindingType.ValueChanged, "#SearchInput",
    EventData.of("@Query", "#SearchInput.Value"), false);

EventData

// Simple key-value
EventData.of("Action", "Submit")

// Multiple values
new EventData()
    .append("Type", "Update")
    .append("Index", "5")

// Value extraction (@ prefix)
EventData.of("@Value", "#InputField.Value")     // Extract from element
EventData.of("@Selected", "#Dropdown.Selected") // Extract selection

Value References

The Value<T> class references values either directly or from UI documents.

// Direct value
Value<Integer> count = Value.of(42);
Value<String> text = Value.of("Hello");

// Reference from UI document
Value<String> style = Value.ref("Common/Button.ui", "ActiveStyle");
Value<Integer> border = Value.ref("Pages/Dialog.ui", "BorderWidth");

Encoding Format

References are encoded in JSON as:

{
    "$Document": "Common/Button.ui",
    "@Value": "ActiveStyle"
}

UI Data Types

PatchStyle (9-Patch Styling)

public class PatchStyle {
    Value<String> texturePath;
    Value<Integer> border;
    Value<Integer> horizontalBorder;
    Value<Integer> verticalBorder;
    Value<String> color;
    Value<Area> area;
}

Area

public class Area {
    int x, y, width, height;
}

Anchor

public class Anchor {
    Value<Integer> left, right, top, bottom;
    Value<Integer> width, height;
    Value<Integer> minWidth, maxWidth;
    Value<Integer> full, horizontal, vertical;
}

ItemGridSlot

public class ItemGridSlot {
    ItemStack itemStack;
    Value<PatchStyle> background, overlay, icon;
    boolean isItemIncompatible;
    boolean isActivatable;
    boolean isItemUncraftable;
    boolean skipItemQualityBackground;
    String name, description;
}

LocalizableString

LocalizableString.fromString("Hello")              // Plain string
LocalizableString.fromMessageId("key")             // Translation key
LocalizableString.fromMessageId("key", Map.of("name", "Player"))  // With params

Complete Page Example

public class ShopPage extends InteractiveCustomUIPage<ShopPage.ShopEventData> {
    
    private final List<ShopItem> items;
    
    public ShopPage(PlayerRef playerRef, List<ShopItem> items) {
        super(playerRef, CustomPageLifetime.CanDismiss, ShopEventData.CODEC);
        this.items = items;
    }
    
    @Override
    public void build(Ref<EntityStore> ref, UICommandBuilder cmd, 
                     UIEventBuilder events, Store<EntityStore> store) {
        // Load main layout
        cmd.append("Pages/ShopPage.ui");
        
        // Set title
        cmd.set("#Title.Text", Message.translation("shop.title"));
        
        // Build item list
        cmd.clear("#ItemList");
        for (int i = 0; i < items.size(); i++) {
            ShopItem item = items.get(i);
            String selector = "#ItemList[" + i + "]";
            
            cmd.append("#ItemList", "Pages/ShopElementButton.ui");
            cmd.set(selector + " #Name.Text", item.getName());
            cmd.set(selector + " #Price.Text", String.valueOf(item.getPrice()));
            cmd.setObject(selector + " #Icon", item.toItemGridSlot());
            
            events.addEventBinding(
                CustomUIEventBindingType.Activating,
                selector,
                EventData.of("Action", "Buy").append("Index", String.valueOf(i)),
                false
            );
        }
        
        // Close button
        events.addEventBinding(CustomUIEventBindingType.Activating, "#CloseButton",
            EventData.of("Action", "Close"));
        
        // Search input
        events.addEventBinding(CustomUIEventBindingType.ValueChanged, "#SearchInput",
            EventData.of("Action", "Search").append("@Query", "#SearchInput.Value"),
            false);
    }
    
    @Override
    public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, 
                               ShopEventData data) {
        switch (data.action) {
            case "Close" -> close();
            case "Buy" -> {
                if (data.index >= 0 && data.index < items.size()) {
                    buyItem(ref, store, items.get(data.index));
                    // Update UI
                    UICommandBuilder cmd = new UICommandBuilder();
                    cmd.set("#Balance.Text", getPlayerBalance(ref, store));
                    sendUpdate(cmd, false);
                }
            }
            case "Search" -> {
                filterItems(data.query);
                rebuild();
            }
        }
    }
    
    public static class ShopEventData {
        public static final BuilderCodec<ShopEventData> CODEC = BuilderCodec
            .builder(ShopEventData.class, ShopEventData::new)
            .addField(new KeyedCodec<>("Action", Codec.STRING), 
                (d, v) -> d.action = v, d -> d.action)
            .addField(new KeyedCodec<>("Index", Codec.INTEGER), 
                (d, v) -> d.index = v, d -> d.index)
            .addField(new KeyedCodec<>("Query", Codec.STRING), 
                (d, v) -> d.query = v, d -> d.query)
            .build();
        
        String action;
        int index = -1;
        String query;
    }
}

PageManager

Player player = ...;
PageManager pageManager = player.getPageManager();

// Open custom page
pageManager.openCustomPage(ref, store, new ShopPage(playerRef, items));

// Open built-in page
pageManager.setPage(ref, store, Page.Inventory);
pageManager.setPage(ref, store, Page.None);  // Close

// Open page with windows
pageManager.openCustomPageWithWindows(ref, store, customPage, window1, window2);

// Get current custom page
CustomUIPage current = pageManager.getCustomPage();

Built-in Pages

public enum Page {
    None(0),            // No page
    Bench(1),           // Crafting bench
    Inventory(2),       // Player inventory
    ToolsSettings(3),   // Builder tools
    Map(4),             // World map
    MachinimaEditor(5), // Machinima
    ContentCreation(6), // Content creation
    Custom(7)           // Custom server page
}

Window System

Window Base Class

public abstract class Window {
    public abstract JsonObject getData();
    protected abstract boolean onOpen0();
    protected abstract void onClose0();
    
    public void handleAction(Ref<EntityStore> ref, Store<EntityStore> store, WindowAction action);
    public void close();
    public void invalidate();  // Mark for update
    public void registerCloseEvent(Consumer<Window> callback);
}

Window Types

public enum WindowType {
    Container(0),
    PocketCrafting(1),
    BasicCrafting(2),
    DiagramCrafting(3),
    StructuralCrafting(4),
    Processing(5),
    Memories(6)
}

WindowManager

WindowManager wm = player.getWindowManager();

// Open window
OpenWindow packet = wm.openWindow(myWindow);

// Open multiple
List<OpenWindow> packets = wm.openWindows(window1, window2);

// Close
wm.closeWindow(windowId);
wm.closeAllWindows();

// Update
wm.updateWindow(window);
wm.updateWindows();  // Update all dirty

// Get window
Window w = wm.getWindow(windowId);

HUD System

The HUD is a persistent UI layer separate from pages.

CustomHud Packet

public class CustomHud implements Packet {
    boolean clear;
    CustomUICommand[] commands;
}

HUD Components

public enum HudComponent {
    Hotbar(0), StatusIcons(1), Reticle(2), Chat(3),
    Requests(4), Notifications(5), KillFeed(6), InputBindings(7),
    PlayerList(8), EventTitle(9), Compass(10), ObjectivePanel(11),
    PortalPanel(12), BuilderToolsLegend(13), Speedometer(14),
    UtilitySlotSelector(15), BlockVariantSelector(16),
    BuilderToolsMaterialSlotSelector(17), Stamina(18),
    AmmoIndicator(19), Health(20), Mana(21), Oxygen(22), Sleep(23)
}

Protocol Packets

Page Packets

Packet ID Direction Description
SetPage 216 S->C Set built-in page
CustomHud 217 S->C Update HUD
CustomPage 218 S->C Send custom page
CustomPageEvent 219 C->S Page event

Window Packets

Packet ID Direction Description
OpenWindow 200 S->C Open window
UpdateWindow 201 S->C Update window
CloseWindow 202 S->C Close window
ClientOpenWindow 203 C->S Request window
SendWindowAction 204 C->S Window action

Known UI Documents

Path Purpose
Pages/DialogPage.ui Dialog/conversation
Pages/BarterPage.ui Trading interface
Pages/ShopPage.ui Shop interface
Pages/RespawnPage.ui Death/respawn
Pages/ChangeModelPage.ui Model selection
Pages/WarpListPage.ui Teleport list
Pages/CommandListPage.ui Command help
Pages/PluginListPage.ui Plugin list
Common/TextButton.ui Reusable button

Best Practices

  1. Use InteractiveCustomUIPage - Provides type-safe event handling
  2. Define event data codec - Create a proper BuilderCodec for your event data
  3. Use sendUpdate for partial updates - Don't rebuild entire page for small changes
  4. Extract values with @ prefix - Use @PropertyName syntax to extract element values
  5. Set locksInterface appropriately - Use false for non-blocking events like hover
  6. Clear lists before rebuilding - Always cmd.clear() before repopulating lists
  7. Handle dismiss - Override onDismiss() for cleanup
  8. Use Value references - Reference styles from UI documents for consistency