17 KiB
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
- Use InteractiveCustomUIPage - Provides type-safe event handling
- Define event data codec - Create a proper BuilderCodec for your event data
- Use sendUpdate for partial updates - Don't rebuild entire page for small changes
- Extract values with @ prefix - Use
@PropertyNamesyntax to extract element values - Set locksInterface appropriately - Use
falsefor non-blocking events like hover - Clear lists before rebuilding - Always
cmd.clear()before repopulating lists - Handle dismiss - Override
onDismiss()for cleanup - Use Value references - Reference styles from UI documents for consistency