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

554 lines
17 KiB
Markdown

# 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
```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<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<T>
For pages with typed event handling:
```java
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
```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", "<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
```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<T>` class references values either directly or from UI documents.
```java
// 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:
```json
{
"$Document": "Common/Button.ui",
"@Value": "ActiveStyle"
}
```
## UI Data Types
### PatchStyle (9-Patch Styling)
```java
public class PatchStyle {
Value<String> texturePath;
Value<Integer> border;
Value<Integer> horizontalBorder;
Value<Integer> verticalBorder;
Value<String> color;
Value<Area> area;
}
```
### Area
```java
public class Area {
int x, y, width, height;
}
```
### Anchor
```java
public class Anchor {
Value<Integer> left, right, top, bottom;
Value<Integer> width, height;
Value<Integer> minWidth, maxWidth;
Value<Integer> full, horizontal, vertical;
}
```
### ItemGridSlot
```java
public class ItemGridSlot {
ItemStack itemStack;
Value<PatchStyle> 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<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
```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<EntityStore> ref, Store<EntityStore> store, WindowAction action);
public void close();
public void invalidate(); // Mark for update
public void registerCloseEvent(Consumer<Window> 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<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
```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<T>** - 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