342 lines
9.1 KiB
Markdown
342 lines
9.1 KiB
Markdown
---
|
|
title: Custom Pages
|
|
type: docs
|
|
weight: 2
|
|
---
|
|
|
|
Custom pages allow plugins to create interactive UI overlays that can receive user input and display dynamic content.
|
|
|
|
**Package:** `com.hypixel.hytale.server.core.entity.entities.player.pages`
|
|
|
|
## Page Lifetimes
|
|
|
|
Control how pages can be closed:
|
|
|
|
| Lifetime | Description |
|
|
|----------|-------------|
|
|
| `CustomPageLifetime.CantClose` | Page cannot be dismissed by the player |
|
|
| `CustomPageLifetime.CanDismiss` | Player can close with Escape key |
|
|
| `CustomPageLifetime.CanDismissOrCloseThroughInteraction` | Can close via Escape or interaction |
|
|
|
|
## Creating a Custom Page
|
|
|
|
Extend `CustomUIPage`:
|
|
|
|
```java
|
|
public class WelcomePage extends CustomUIPage {
|
|
|
|
public WelcomePage(PlayerRef playerRef) {
|
|
super(playerRef, CustomPageLifetime.CanDismiss);
|
|
}
|
|
|
|
@Override
|
|
public void build(Ref<EntityStore> ref, UICommandBuilder builder,
|
|
UIEventBuilder events, Store<EntityStore> store) {
|
|
// Load a UI template from assets (extension .ui is REQUIRED)
|
|
builder.append("Pages/WelcomeScreen.ui");
|
|
|
|
// Set dynamic values
|
|
builder.set("#player_name", playerRef.getUsername());
|
|
builder.set("#server_name", "My Server");
|
|
}
|
|
}
|
|
```
|
|
|
|
## UICommandBuilder
|
|
|
|
Build UI content dynamically:
|
|
|
|
### Append Content
|
|
|
|
```java
|
|
// Append a UI template (extension .ui is REQUIRED)
|
|
builder.append("Pages/TemplatePath.ui");
|
|
|
|
// Append to a specific selector
|
|
builder.append("#container", "Pages/ChildTemplate.ui");
|
|
|
|
// Append inline YAML/JSON
|
|
builder.appendInline("#list", "{ type: text, content: 'Hello' }");
|
|
```
|
|
|
|
### Set Values
|
|
|
|
```java
|
|
// Set text
|
|
builder.set("#title", "Welcome!");
|
|
builder.set("#description", Message.translation("my.key"));
|
|
|
|
// Set numbers
|
|
builder.set("#score", 1500);
|
|
builder.set("#progress", 0.75f);
|
|
|
|
// Set boolean
|
|
builder.set("#visible", true);
|
|
|
|
// Set to null
|
|
builder.setNull("#optional_field");
|
|
```
|
|
|
|
### Set Objects
|
|
|
|
```java
|
|
// Set ItemStack
|
|
ItemStack item = ...;
|
|
builder.setObject("#item_display", item);
|
|
|
|
// Set UI Area
|
|
Area area = new Area(0, 0, 100, 50);
|
|
builder.setObject("#bounds", area);
|
|
|
|
// Set arrays/lists
|
|
builder.set("#items", itemStackArray);
|
|
builder.set("#options", stringList);
|
|
```
|
|
|
|
### Remove Content
|
|
|
|
```java
|
|
// Clear children of an element
|
|
builder.clear("#container");
|
|
|
|
// Remove an element entirely
|
|
builder.remove("#element_id");
|
|
|
|
// Insert before an element
|
|
builder.insertBefore("#existing", "Pages/NewElement.ui");
|
|
```
|
|
|
|
## UIEventBuilder
|
|
|
|
Bind events to UI elements using `addEventBinding`:
|
|
|
|
```java
|
|
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
|
import com.hypixel.hytale.server.core.ui.builder.EventData;
|
|
|
|
@Override
|
|
public void build(Ref<EntityStore> ref, UICommandBuilder builder,
|
|
UIEventBuilder events, Store<EntityStore> store) {
|
|
builder.append("Pages/Menu.ui");
|
|
|
|
// Bind click events using addEventBinding
|
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#button_start",
|
|
EventData.of("Action", "start_game"));
|
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#button_settings",
|
|
EventData.of("Action", "open_settings"));
|
|
}
|
|
```
|
|
|
|
### Event Binding Types
|
|
|
|
| Type | Description |
|
|
|------|-------------|
|
|
| `Activating` | Button click/activation |
|
|
| `RightClicking` | Right-click action |
|
|
| `DoubleClicking` | Double-click action |
|
|
| `MouseEntered` | Mouse hover enter |
|
|
| `MouseExited` | Mouse hover exit |
|
|
| `ValueChanged` | Input value changed |
|
|
| `FocusGained` | Element focused |
|
|
| `FocusLost` | Element lost focus |
|
|
|
|
## Handling Events
|
|
|
|
Override `handleDataEvent` to process user interactions. The `rawData` parameter contains JSON with the event data:
|
|
|
|
```java
|
|
public class MenuPage extends CustomUIPage {
|
|
|
|
public MenuPage(PlayerRef ref) {
|
|
super(ref, CustomPageLifetime.CanDismiss);
|
|
}
|
|
|
|
@Override
|
|
public void build(Ref<EntityStore> ref, UICommandBuilder builder,
|
|
UIEventBuilder events, Store<EntityStore> store) {
|
|
builder.append("Pages/MainMenu.ui");
|
|
|
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#play_button",
|
|
EventData.of("Action", "play"));
|
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#quit_button",
|
|
EventData.of("Action", "quit"));
|
|
}
|
|
|
|
@Override
|
|
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, String rawData) {
|
|
// rawData contains JSON like: {"Action":"play"}
|
|
if (rawData.contains("\"Action\":\"play\"")) {
|
|
startGame();
|
|
} else if (rawData.contains("\"Action\":\"quit\"")) {
|
|
close();
|
|
}
|
|
}
|
|
|
|
private void startGame() {
|
|
// Game logic...
|
|
close(); // Close the page
|
|
}
|
|
}
|
|
```
|
|
|
|
{{< callout type="info" >}}
|
|
**Tip:** For more complex event handling, consider using a JSON parser library or extending `InteractiveCustomUIPage<T>` with a typed data class.
|
|
{{< /callout >}}
|
|
|
|
## Opening Pages
|
|
|
|
Use `PageManager` to open pages:
|
|
|
|
```java
|
|
Player player = ...;
|
|
PageManager pageManager = player.getPageManager();
|
|
|
|
// Open custom page
|
|
pageManager.openCustomPage(
|
|
player.getReference(),
|
|
player.getReference().getStore(),
|
|
new MenuPage(player.getPlayerRef())
|
|
);
|
|
```
|
|
|
|
## Closing Pages
|
|
|
|
```java
|
|
// From within the page
|
|
protected void close() {
|
|
// Inherited method - closes this page
|
|
}
|
|
|
|
// From outside
|
|
pageManager.setPage(ref, store, Page.None);
|
|
```
|
|
|
|
## Updating Pages
|
|
|
|
Update content without rebuilding:
|
|
|
|
```java
|
|
// Send update with new commands
|
|
UICommandBuilder update = new UICommandBuilder();
|
|
update.set("#score", newScore);
|
|
sendUpdate(update);
|
|
|
|
// Full rebuild
|
|
rebuild();
|
|
|
|
// Clear and update
|
|
sendUpdate(update, true); // clear=true
|
|
```
|
|
|
|
## Page with Windows
|
|
|
|
Open pages alongside inventory windows:
|
|
|
|
```java
|
|
// Open page with windows
|
|
pageManager.openCustomPageWithWindows(
|
|
ref, store,
|
|
new CraftingPage(playerRef),
|
|
craftingWindow,
|
|
playerInventoryWindow
|
|
);
|
|
```
|
|
|
|
## Interactive Page Example
|
|
|
|
```java
|
|
public class ShopPage extends CustomUIPage {
|
|
|
|
private int selectedItem = -1;
|
|
private final List<ShopItem> items;
|
|
|
|
public ShopPage(PlayerRef ref, List<ShopItem> items) {
|
|
super(ref, CustomPageLifetime.CanDismiss);
|
|
this.items = items;
|
|
}
|
|
|
|
@Override
|
|
public void build(Ref<EntityStore> ref, UICommandBuilder builder,
|
|
UIEventBuilder events, Store<EntityStore> store) {
|
|
builder.append("Pages/Shop.ui");
|
|
|
|
// Populate items
|
|
for (int i = 0; i < items.size(); i++) {
|
|
ShopItem item = items.get(i);
|
|
builder.set("#item_" + i + "_name", item.getName());
|
|
builder.set("#item_" + i + "_price", item.getPrice());
|
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#item_" + i,
|
|
EventData.of("Action", "select").append("Index", String.valueOf(i)));
|
|
}
|
|
|
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#buy_button",
|
|
EventData.of("Action", "buy"));
|
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#close_button",
|
|
EventData.of("Action", "close"));
|
|
}
|
|
|
|
@Override
|
|
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, String rawData) {
|
|
// Parse JSON event data
|
|
if (rawData.contains("\"Action\":\"select\"")) {
|
|
// Extract index from JSON
|
|
int indexStart = rawData.indexOf("\"Index\":\"") + 9;
|
|
int indexEnd = rawData.indexOf("\"", indexStart);
|
|
selectedItem = Integer.parseInt(rawData.substring(indexStart, indexEnd));
|
|
updateSelection();
|
|
} else if (rawData.contains("\"Action\":\"buy\"") && selectedItem >= 0) {
|
|
purchaseItem(selectedItem);
|
|
} else if (rawData.contains("\"Action\":\"close\"")) {
|
|
close();
|
|
}
|
|
}
|
|
|
|
private void updateSelection() {
|
|
UICommandBuilder update = new UICommandBuilder();
|
|
update.set("#selected_index", selectedItem);
|
|
update.set("#selected_name", items.get(selectedItem).getName());
|
|
sendUpdate(update);
|
|
}
|
|
|
|
private void purchaseItem(int index) {
|
|
// Purchase logic...
|
|
rebuild(); // Refresh UI
|
|
}
|
|
}
|
|
```
|
|
|
|
## Built-in Pages
|
|
|
|
Available page types:
|
|
|
|
| Page | Description |
|
|
|------|-------------|
|
|
| `Page.None` | No page open (close current page) |
|
|
| `Page.Bench` | Crafting/workbench interface |
|
|
| `Page.Inventory` | Player inventory |
|
|
| `Page.ToolsSettings` | Tools settings |
|
|
| `Page.Map` | World map |
|
|
| `Page.MachinimaEditor` | Machinima editor |
|
|
| `Page.ContentCreation` | Content creation tools |
|
|
| `Page.Custom` | Custom UI page |
|
|
|
|
```java
|
|
// Open built-in page
|
|
pageManager.setPage(ref, store, Page.Inventory);
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
{{< callout type="info" >}}
|
|
**Custom Page Guidelines:**
|
|
- Use `CanDismiss` unless the page must be modal
|
|
- Handle the `onDismiss` callback for cleanup
|
|
- Use `sendUpdate()` for small changes, `rebuild()` for major changes
|
|
- Store page state in instance fields
|
|
- Close pages properly using `close()` or `setPage(Page.None)`
|
|
{{< /callout >}}
|
|
|
|
{{< callout type="warning" >}}
|
|
**Important:** Always check if `playerRef.getReference()` is null before accessing the store, as the player may have disconnected.
|
|
{{< /callout >}}
|