# 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` | 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 ```java public enum CustomPageLifetime { CantClose(0), // User cannot close CanDismiss(1), // ESC key to dismiss CanDismissOrCloseThroughInteraction(2) // Dismiss or click to close } ``` ### CustomUIPage Base Class ```java public abstract class CustomUIPage { protected final PlayerRef playerRef; protected CustomPageLifetime lifetime; // Build the UI - called to construct the page public abstract void build(Ref ref, UICommandBuilder cmd, UIEventBuilder events, Store store); // Handle event data from client public void handleDataEvent(Ref ref, Store 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 ref, Store store); } ``` ### InteractiveCustomUIPage For pages with typed event handling: ```java public abstract class InteractiveCustomUIPage extends CustomUIPage { protected final BuilderCodec eventDataCodec; public InteractiveCustomUIPage(PlayerRef playerRef, CustomPageLifetime lifetime, BuilderCodec eventDataCodec) { super(playerRef, lifetime); this.eventDataCodec = eventDataCodec; } // Override this for type-safe event handling public void handleDataEvent(Ref ref, Store 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 ```java 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 ```java 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", ""); 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 ```java 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 ```java 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 ```java // 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` class references values either directly or from UI documents. ```java // Direct value Value count = Value.of(42); Value text = Value.of("Hello"); // Reference from UI document Value style = Value.ref("Common/Button.ui", "ActiveStyle"); Value border = Value.ref("Pages/Dialog.ui", "BorderWidth"); ``` ### Encoding Format References are encoded in JSON as: ```json { "$Document": "Common/Button.ui", "@Value": "ActiveStyle" } ``` ## UI Data Types ### PatchStyle (9-Patch Styling) ```java public class PatchStyle { Value texturePath; Value border; Value horizontalBorder; Value verticalBorder; Value color; Value area; } ``` ### Area ```java public class Area { int x, y, width, height; } ``` ### Anchor ```java public class Anchor { Value left, right, top, bottom; Value width, height; Value minWidth, maxWidth; Value full, horizontal, vertical; } ``` ### ItemGridSlot ```java public class ItemGridSlot { ItemStack itemStack; Value background, overlay, icon; boolean isItemIncompatible; boolean isActivatable; boolean isItemUncraftable; boolean skipItemQualityBackground; String name, description; } ``` ### LocalizableString ```java LocalizableString.fromString("Hello") // Plain string LocalizableString.fromMessageId("key") // Translation key LocalizableString.fromMessageId("key", Map.of("name", "Player")) // With params ``` ## Complete Page Example ```java public class ShopPage extends InteractiveCustomUIPage { private final List items; public ShopPage(PlayerRef playerRef, List items) { super(playerRef, CustomPageLifetime.CanDismiss, ShopEventData.CODEC); this.items = items; } @Override public void build(Ref ref, UICommandBuilder cmd, UIEventBuilder events, Store 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 ref, Store 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 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 ```java 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 ```java 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 ```java public abstract class Window { public abstract JsonObject getData(); protected abstract boolean onOpen0(); protected abstract void onClose0(); public void handleAction(Ref ref, Store store, WindowAction action); public void close(); public void invalidate(); // Mark for update public void registerCloseEvent(Consumer callback); } ``` ### Window Types ```java public enum WindowType { Container(0), PocketCrafting(1), BasicCrafting(2), DiagramCrafting(3), StructuralCrafting(4), Processing(5), Memories(6) } ``` ### WindowManager ```java WindowManager wm = player.getWindowManager(); // Open window OpenWindow packet = wm.openWindow(myWindow); // Open multiple List 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 ```java public class CustomHud implements Packet { boolean clear; CustomUICommand[] commands; } ``` ### HUD Components ```java 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