6.8 KiB
UI
Built on top of HyUI, a third-party library that wraps Hytale's native Custom UI API in an HTML-like markup (HYUIML). HyUI is bundled into the Staff jar via the shade plugin so you don't need it installed separately.
Reading the source
Each page lives in src/main/java/net/kewwbec/staff/ui/. Pages don't extend the SDK's InteractiveCustomUIPage anymore - they're plain classes that build a PageBuilder and expose an open(store) method. Navigation between pages is new OtherPage(...).open(store).
Page flow
/staffui
|
v
[StaffHubPage]
|
+-> Player Lookup -> [PlayerLookupPage] (text search, click result for detail)
|
v
[PlayerDetailPage]
|
+-> Ban / TempBan / Mute / TempMute / Kick / Warn --> [PunishFormPage]
+-> Unban / Unmute (applied inline, page re-opens)
+-> History --> [HistoryPage]
Every non-hub page has a Back button. ESC always closes (lifetime is CanDismiss).
Page reference
| Page | Purpose |
|---|---|
| StaffHubPage | Top-level menu. Currently just routes to Player Lookup; add buttons here for new sections. |
| PlayerLookupPage | Text input + Search button. Online players are listed first; if you type a name not online, the players_seen table is consulted. Click a result to open detail. |
| PlayerDetailPage | Player info + active ban/mute lines + action buttons. Unban / Unmute fire immediately (no form); the page reopens with refreshed data. |
| PunishFormPage | Reason field, plus duration field for timed types. Validation errors re-open the same page with the entered values and a red error line. |
| HistoryPage | Last 50 entries for the target. Read-only. |
HyUI patterns we use
All pages follow the same skeleton:
public final class SomePage {
private final PlayerRef playerRef;
private final StaffUIContext sCtx;
private final PageBuilder page;
public SomePage(PlayerRef playerRef, StaffUIContext sCtx, ...args) {
this.playerRef = playerRef;
this.sCtx = sCtx;
// Build the HYUIML based on constructor state
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="Title">
<div class="container-contents">
<button id="someBtn">Do thing</button>
</div>
</div>
</div>
""";
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)
.fromHtml(html);
// Wire listeners by element id
page.addEventListener("someBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
// pCtx.getValue("inputId", String.class).orElse("") to read input values
// Navigate by opening a new page instance
});
}
public void open(Store<EntityStore> store) { page.open(store); }
}
Reading input values
Inside an event listener, the second argument is the HyUI page context, which exposes getValue(id, type):
page.addEventListener("submitBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
String reason = pCtx.getValue("reasonInput", String.class).orElse("").trim();
...
});
Re-rendering after state changes
We do not mutate the page in place. Instead, we open a fresh page instance with new state:
new PunishFormPage(playerRef, sCtx, uuid, name, type, durationFromForm, reasonFromForm, "Error: ...").open(store);
This is simpler than HyUI's updatePage(true) re-render dance and keeps each page render side-effect-free. The cost is rebuilding the HTML each navigation - cheap compared to anything else we do.
HYUIML class reference (the bits we use)
<div class="page-overlay">- root, dims background, captures input<div class="container" data-hyui-title="...">- main framed window with header<div class="container-contents">- inner content area<button id="...">Text</button>- clickable button. Don't useclass="back-button"with inline HTML - that class triggers HyUI'sBackButton.uitemplate which expects a nested#HyUIButtonchild that inline buttons don't have, and yourActivatinglistener fails to bind with "Target element... has no compatible Activating event". Plain<button>works for back buttons too; style separately if you need a different look.<input type="text" id="..." value="..." placeholder="...">- text input<p>/<label>- text
The full HYUIML reference is in extras to look at/HyUI-Docs/hyuiml-htmlish-in-hytale.md.
Adding a new page
- Create
src/main/java/net/kewwbec/staff/ui/MyPage.javafollowing the skeleton above. - Use HTMLish in the
fromHtmlblock. Stick to alphanumeric IDs (HyUI sanitizes them, but bugs hide in non-trivial chars). - Wire listeners with
page.addEventListener(id, eventType, handler). - To navigate to it, call
new MyPage(...).open(store)from another page's handler. - To open it from a command, do the same thing in the command's
executemethod.
Things to be aware of
- HyUI must be on the classpath at runtime. This is handled by the shade plugin in pom.xml, which packs HyUI into the Staff jar. Don't drop the shade plugin without a replacement.
- Sanitized IDs. Use plain alphanumeric ids in HYUIML. Use the same string in
addEventListener- HyUI translates internally. - Escape user input when embedding into HTML strings. We have a tiny
escape(String)helper in each page; use it for any value that came from a player. valueattribute is what carries text-field state across re-renders. When you rebuild a form page with an error, setvalue="..."on each input so the user doesn't lose what they typed.- No
<script>, no full CSS. Logic is in Java; layout is via Hytale'sLayoutModeandflex-weightviastyle="..."on<div>s if needed.
What this UI deliberately doesn't do
- Network-wide active punishments view. Could be added as a Hub button if needed.
- Bulk actions. No "ban everyone in this list" - intentional.
- In-place updates. Every state change rebuilds the page. Simpler reasoning, slightly more network chatter.
- Custom themes. We rely on HyUI's default Container/Button styling. Theming via
<style>blocks is possible later.