Update script to write to vendor/hytale-server
This commit is contained in:
551
docs/15-ui-system.md
Normal file
551
docs/15-ui-system.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user