HyCodeYourTale

UI Events

UI Events

Detailní dokumentace k event handlingu v UI systému Hytale.

---

Přehled

UI eventy umožňují interakci mezi klientem a serverem. Klient posílá eventy při interakci s UI a server je zpracovává.

Klient                         Server
│ │
│ Klik na tlačítko │
├─────────────────────────────>│
│ │ handleDataEvent()
│ │
│ Aktualizace UI │
│<─────────────────────────────┤
│ │

---

InteractiveCustomUIPage

Pro stránky s event handlingem:

public abstract class InteractiveCustomUIPage extends CustomUIPage {
@Nonnull
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();

// Codec pro deserializaci event dat
@Nonnull
protected final BuilderCodec eventDataCodec;

public InteractiveCustomUIPage(
@Nonnull PlayerRef playerRef,
@Nonnull CustomPageLifetime lifetime,
@Nonnull BuilderCodec eventDataCodec
) {
super(playerRef, lifetime);
this.eventDataCodec = eventDataCodec;
}

// Přepsat pro zpracování eventů (typované)
public void handleDataEvent(
@Nonnull Ref ref,
@Nonnull Store store,
@Nonnull T data
) {
}

// Interní - deserializuje raw JSON a volá typovanou metodu
@Override
public void handleDataEvent(
@Nonnull Ref ref,
@Nonnull Store store,
String rawData
) {
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();

T data;
try {
data = this.eventDataCodec.decodeJson(new RawJsonReader(rawData.toCharArray()), extraInfo);
} catch (IOException e) {
throw new RuntimeException(e);
}

extraInfo.getValidationResults().logOrThrowValidatorExceptions(LOGGER);
this.handleDataEvent(ref, store, data);
}

// Aktualizace s event bindingem
protected void sendUpdate(
@Nullable UICommandBuilder commandBuilder,
@Nullable UIEventBuilder eventBuilder,
boolean clear
) {
Ref ref = this.playerRef.getReference();
if (ref != null) {
Store store = ref.getStore();
World world = store.getExternalData().getWorld();

// Důležité: Musí běžet na world threadu
world.execute(() -> {
if (ref.isValid()) {
Player playerComponent = store.getComponent(ref, Player.getComponentType());
playerComponent.getPageManager().updateCustomPage(
new CustomPage(
this.getClass().getName(),
false,
clear,
this.lifetime,
commandBuilder != null ? commandBuilder.getCommands() : UICommandBuilder.EMPTY_COMMAND_ARRAY,
eventBuilder != null ? eventBuilder.getEvents() : UIEventBuilder.EMPTY_EVENT_BINDING_ARRAY
)
);
}
});
}
}
}

---

UIEventBuilder

Builder pro event bindings:

public class UIEventBuilder {
public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
public static final CustomUIEventBinding[] EMPTY_EVENT_BINDING_ARRAY = new CustomUIEventBinding[0];

@Nonnull
private final List events = new ObjectArrayList();

// Základní binding bez dat
@Nonnull
public UIEventBuilder addEventBinding(CustomUIEventBindingType type, String selector) {
return this.addEventBinding(type, selector, null);
}

// Binding s kontrolou zámku interface
@Nonnull
public UIEventBuilder addEventBinding(CustomUIEventBindingType type, String selector, boolean locksInterface) {
return this.addEventBinding(type, selector, null, locksInterface);
}

// Binding s daty
@Nonnull
public UIEventBuilder addEventBinding(CustomUIEventBindingType type, String selector, EventData data) {
return this.addEventBinding(type, selector, data, true);
}

// Plný binding
@Nonnull
public UIEventBuilder addEventBinding(
CustomUIEventBindingType type,
String selector,
@Nullable EventData data,
boolean locksInterface
) {
String dataString = null;
if (data != null) {
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
dataString = MapCodec.STRING_HASH_MAP_CODEC.encode(data.events(), extraInfo).asDocument().toJson();
extraInfo.getValidationResults().logOrThrowValidatorExceptions(LOGGER);
}

this.events.add(new CustomUIEventBinding(type, selector, dataString, locksInterface));
return this;
}

@Nonnull
public CustomUIEventBinding[] getEvents() {
return this.events.toArray(CustomUIEventBinding[]::new);
}
}

---

CustomUIEventBindingType

Typy UI eventů:

public enum CustomUIEventBindingType {
Activating, // Aktivace (klik, enter)
ValueChanged, // Změna hodnoty (input, slider)
FocusGained, // Získání focusu
FocusLost, // Ztráta focusu
HoverEnter, // Najetí myší
HoverLeave // Odjetí myší
}

---

EventData

Pomocná třída pro předání dat v eventu:

public record EventData(Map events) {

// Jednoduchá hodnota
public static EventData of(String key, String value) {
return new EventData(Map.of(key, value));
}

// Více hodnot
public static EventData of(String key1, String value1, String key2, String value2) {
return new EventData(Map.of(key1, value1, key2, value2));
}

// Reference na UI hodnotu
// @key = reference, #Element.Property = zdroj
public static EventData reference(String key, String uiReference) {
return of("@" + key, uiReference);
}
}

Speciální Syntaxe

| Syntaxe | Význam |
|---------|--------|
| "Value" | Statická hodnota |
| "#Element.Value" | Hodnota z UI elementu |
| "@Key" | Reference key v event datech |

---

Příklad: Kompletní Interaktivní Stránka

public class ItemShopPage extends InteractiveCustomUIPage {

private final String shopId;
private final List items;
private int selectedIndex = -1;

public ItemShopPage(@Nonnull PlayerRef playerRef, String shopId, List items) {
super(playerRef, CustomPageLifetime.CanDismiss, ShopEventData.CODEC);
this.shopId = shopId;
this.items = items;
}

@Override
public void build(
@Nonnull Ref ref,
@Nonnull UICommandBuilder commandBuilder,
@Nonnull UIEventBuilder eventBuilder,
@Nonnull Store store
) {
// Hlavní UI
commandBuilder.append("Pages/ShopPage.ui");
commandBuilder.set("#ShopTitle.Text", "Shop: " + shopId);

// Seznam položek
buildItemList(commandBuilder, eventBuilder);

// Tlačítko koupit
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
"#BuyButton",
EventData.of("Action", "buy"),
true // Zamkne interface během zpracování
);

// Tlačítko zavřít
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
"#CloseButton",
EventData.of("Action", "close"),
false
);
}

private void buildItemList(UICommandBuilder commandBuilder, UIEventBuilder eventBuilder) {
commandBuilder.clear("#ItemList");

for (int i = 0; i < items.size(); i++) {
ShopItem item = items.get(i);
String selector = "#ItemList[" + i + "]";

// Přidej položku
commandBuilder.append("#ItemList", "Pages/ShopItemEntry.ui");

// Nastav hodnoty
commandBuilder.set(selector + " #ItemName.Text", item.getName());
commandBuilder.set(selector + " #ItemPrice.Text", String.valueOf(item.getPrice()));
commandBuilder.set(selector + " #ItemIcon.Texture", item.getIconPath());

// Zvýraznění vybrané položky
boolean isSelected = (i == selectedIndex);
commandBuilder.set(selector + ".Selected", isSelected);

// Event pro výběr
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
selector,
EventData.of("Action", "select", "Index", String.valueOf(i)),
false
);
}

// Aktualizuj tlačítko koupit
commandBuilder.set("#BuyButton.Enabled", selectedIndex >= 0);
}

@Override
public void handleDataEvent(
@Nonnull Ref ref,
@Nonnull Store store,
@Nonnull ShopEventData data
) {
String action = data.getAction();

switch (action) {
case "select" -> {
// Změna výběru
this.selectedIndex = data.getIndex();

// Částečná aktualizace
UICommandBuilder cmd = new UICommandBuilder();
UIEventBuilder evt = new UIEventBuilder();
buildItemList(cmd, evt);
sendUpdate(cmd, evt, false);
}

case "buy" -> {
if (selectedIndex >= 0 && selectedIndex < items.size()) {
ShopItem item = items.get(selectedIndex);

// Zpracování nákupu
if (processPurchase(ref, store, item)) {
// Úspěch - aktualizuj UI
UICommandBuilder cmd = new UICommandBuilder();
cmd.set("#StatusText.Text", "Purchased: " + item.getName());
sendUpdate(cmd, null, false);
} else {
// Chyba
UICommandBuilder cmd = new UICommandBuilder();
cmd.set("#StatusText.Text", "Not enough money!");
sendUpdate(cmd, null, false);
}
}
}

case "close" -> {
close();
}
}
}

private boolean processPurchase(Ref ref, Store store, ShopItem item) {
// Implementace nákupu...
return true;
}

// Event data class
public static class ShopEventData {
@Nonnull
public static final BuilderCodec CODEC = BuilderCodec.builder(
ShopEventData.class, ShopEventData::new
)
.append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action).add()
.append(new KeyedCodec<>("Index", Codec.INTEGER), (e, i) -> e.index = i, e -> e.index).defaultValue(-1).add()
.build();

private String action;
private int index = -1;

public ShopEventData() {}

public String getAction() { return action; }
public int getIndex() { return index; }
}
}

---

Event Data Codec

Definice codecu pro event data:

public static class MyEventData {
@Nonnull
public static final BuilderCodec CODEC = BuilderCodec.builder(
MyEventData.class, MyEventData::new
)
// Povinné pole
.append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action)
.add()

// Volitelné pole s default hodnotou
.append(new KeyedCodec<>("Value", Codec.INTEGER), (e, i) -> e.value = i, e -> e.value)
.defaultValue(0)
.add()

// String pole
.append(new KeyedCodec<>("Name", Codec.STRING), (e, s) -> e.name = s, e -> e.name)
.add()

// Reference na UI hodnotu (s @ prefixem)
.append(new KeyedCodec<>("@InputValue", Codec.STRING), (e, s) -> e.inputValue = s, e -> e.inputValue)
.add()

.build();

private String action;
private int value = 0;
private String name;
private String inputValue;

public MyEventData() {}

// Gettery...
}

---

Příklad: Vyhledávání (Real-time)

Z WarpListPage - vyhledávání s ValueChanged eventem:

@Override
public void build(...) {
commandBuilder.append("Pages/WarpListPage.ui");

// Event pro změnu hodnoty v search inputu
eventBuilder.addEventBinding(
CustomUIEventBindingType.ValueChanged,
"#SearchInput",
EventData.of("@SearchQuery", "#SearchInput.Value") // Reference na hodnotu
);

this.buildWarpList(commandBuilder, eventBuilder);
}

@Override
public void handleDataEvent(..., WarpListPageEventData eventData) {
if (eventData.getSearchQuery() != null) {
// Aktualizuj vyhledávací dotaz
this.searchQuery = eventData.getSearchQuery().trim().toLowerCase();

// Rebuild seznamu
UICommandBuilder commandBuilder = new UICommandBuilder();
UIEventBuilder eventBuilder = new UIEventBuilder();
this.buildWarpList(commandBuilder, eventBuilder);
this.sendUpdate(commandBuilder, eventBuilder, false);
}
}

---

Page Events

PageManager zpracovává tyto event typy:

public void handleEvent(Ref ref, Store store, CustomPageEvent event) {
switch (event.type) {
case Dismiss:
// Hráč zavřel stránku (ESC)
if (this.customPage != null) {
this.customPage.onDismiss(ref, store);
this.customPage = null;
}
break;

case Data:
// Data event od klienta
if (this.customPage != null) {
this.customPage.handleDataEvent(ref, store, event.data);
}
break;

case Acknowledge:
// Klient potvrdil přijetí aktualizace
this.customPageRequiredAcknowledgments.decrementAndGet();
break;
}
}

onDismiss Callback

public class MyPage extends CustomUIPage {

@Override
public void onDismiss(@Nonnull Ref ref, @Nonnull Store store) {
// Cleanup při zavření stránky
cleanupResources();

// Notifikace
Player player = store.getComponent(ref, Player.getComponentType());
player.sendMessage(Message.raw("Page closed"));
}
}

---

Best Practices

1. Validace Eventů

@Override
public void handleDataEvent(..., MyEventData data) {
// Vždy validuj data
if (data.getAction() == null) {
return;
}

// Kontrola rozsahu
int index = data.getIndex();
if (index < 0 || index >= items.size()) {
return;
}

// Zpracování...
}

2. Minimální Aktualizace

// Špatně - rebuild celé stránky
this.rebuild();

// Správně - aktualizuj pouze změněné části
UICommandBuilder cmd = new UICommandBuilder();
cmd.set("#Counter.Text", String.valueOf(counter));
sendUpdate(cmd, null, false);

3. Thread Safety

// sendUpdate v InteractiveCustomUIPage automaticky
// používá world.execute() pro thread safety

// Pro vlastní operace:
world.execute(() -> {
// Thread-safe operace
store.setComponent(ref, componentType, component);
});

---

Shrnutí

| Třída | Účel |
|-------|------|
| InteractiveCustomUIPage | Interaktivní stránka s typovanými eventy |
| UIEventBuilder | Builder pro event bindings |
| EventData | Helper pro event data |
| CustomUIEventBindingType | Typy eventů |

| Event Typ | Kdy |
|-----------|-----|
| Activating | Klik, Enter |
| ValueChanged | Změna hodnoty inputu |
| FocusGained | Získání focusu |
| FocusLost | Ztráta focusu |
| HoverEnter | Najetí myší |
| HoverLeave | Odjetí myší |

| EventData Syntaxe | Význam |
|-------------------|--------|
| "staticValue" | Statická hodnota |
| "#Element.Value" | Reference na UI element |
| "@Key" | Key v event datech |

Last updated: 20. ledna 2026