UI Factory
LDLib2 provides three pre-built factory helpers for the most common use cases. Each factory handles menu registration, client–server routing, and lifecycle validation automatically.
| Factory | Trigger | Use when |
|---|---|---|
BlockUIMenuType |
Player right-clicks a block | Block / Block Entity UI |
HeldItemUIMenuType |
Player right-clicks with an item | Handheld tools, devices, wrenches |
PlayerUIMenuType |
Any server-side call | Commands, keybinds, arbitrary triggers |
KubeJS users can open any of these via the LDLib2UIFactory bindings and register UI builders via the LDLib2UI event group — no Java or Kotlin required.
BlockUIMenuType — Block / Block Entity UI
Use this when a block (or block entity) opens a UI on right-click.
Implement BlockUIMenuType.BlockUI on your Block class:
public class MyBlock extends Block implements BlockUIMenuType.BlockUI {
@Override
public InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
Player player, BlockHitResult hit) {
if (!level.isClientSide) {
BlockUIMenuType.openUI((ServerPlayer) player, pos);
}
return InteractionResult.SUCCESS;
}
@Override
public ModularUI createUI(BlockUIMenuType.BlockUIHolder holder) {
return ModularUI.of(UI.of(
// build your element tree here
), holder.player);
}
}
class MyBlock : Block(...), BlockUIMenuType.BlockUI {
override fun useWithoutItem(state: BlockState, level: Level, pos: BlockPos,
player: Player, hit: BlockHitResult): InteractionResult {
if (!level.isClientSide) {
BlockUIMenuType.openUI(player as ServerPlayer, pos)
}
return InteractionResult.SUCCESS
}
override fun createUI(holder: BlockUIMenuType.BlockUIHolder): ModularUI {
val root = element({ cls = { +"panel_bg" } }) { /* ... */ }
return ModularUI.of(UI.of(root), holder.player)
}
}
Trigger the UI open from a server-side block event:
// server_scripts/main.js
BlockEvents.rightClicked('minecraft:glass', event => {
LDLib2UIFactory.openBlockUI(event.player, event.block.pos, "mymod:my_block_ui");
})
Register the UI builder (must run on both sides — see Script Placement):
The context object passed to createUI / available in the event:
| Field | Type | Description |
|---|---|---|
player |
Player |
The player who opened the UI |
pos |
BlockPos |
The block's position |
blockState |
BlockState |
The block's current state |
HeldItemUIMenuType — Held Item UI
Use this when a held item opens a UI (e.g. a portable device, a wrench, or a scanner).
Implement HeldItemUIMenuType.HeldItemUI on your Item class:
public class MyItem extends Item implements HeldItemUIMenuType.HeldItemUI {
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
if (!level.isClientSide) {
HeldItemUIMenuType.openUI((ServerPlayer) player, hand);
}
return InteractionResultHolder.success(player.getItemInHand(hand));
}
@Override
public ModularUI createUI(HeldItemUIMenuType.HeldItemUIHolder holder) {
return ModularUI.of(UI.of(
// build your element tree here
), holder.player);
}
}
class MyItem : Item(...), HeldItemUIMenuType.HeldItemUI {
override fun use(level: Level, player: Player, hand: InteractionHand): InteractionResultHolder<ItemStack> {
if (!level.isClientSide) {
HeldItemUIMenuType.openUI(player as ServerPlayer, hand)
}
return InteractionResultHolder.success(player.getItemInHand(hand))
}
override fun createUI(holder: HeldItemUIMenuType.HeldItemUIHolder): ModularUI {
val root = element({ cls = { +"panel_bg" } }) { /* ... */ }
return ModularUI.of(UI.of(root), holder.player)
}
}
Trigger the UI open when the player right-clicks with the item:
// server_scripts/main.js
ItemEvents.firstRightClicked('minecraft:stick', event => {
LDLib2UIFactory.openHeldItemUI(event.player, event.hand, "mymod:my_item_ui");
})
Register the UI builder (must run on both sides — see Script Placement):
The context object passed to createUI / available in the event:
| Field | Type | Description |
|---|---|---|
player |
Player |
The player holding the item |
hand |
InteractionHand |
The hand being used |
itemStack |
ItemStack |
The item stack that opened the UI |
PlayerUIMenuType — Player-triggered UI
Use this to open a UI that is not tied to a specific block or item — for example from a command, a keybind, or any arbitrary server-side trigger.
Register a holder factory with a unique ID, then call openUI when needed:
// Register once (e.g. during mod init)
var MY_UI_ID = ResourceLocation.fromNamespaceAndPath("mymod", "my_ui");
PlayerUIMenuType.register(MY_UI_ID, player -> {
return p -> ModularUI.of(UI.of(
// build your element tree here
), p);
});
// Open it later (server side)
PlayerUIMenuType.openUI(serverPlayer, MY_UI_ID);
val MY_UI_ID = ResourceLocation.fromNamespaceAndPath("mymod", "my_ui")
// Register during init
PlayerUIMenuType.register(MY_UI_ID) { player ->
PlayerUIHolder { p ->
val root = element({ cls = { +"panel_bg" } }) { /* ... */ }
ModularUI.of(UI.of(root), p)
}
}
// Open later (server side)
PlayerUIMenuType.openUI(serverPlayer, MY_UI_ID)
Open the UI from any server-side trigger (command, keybind callback, etc.):
Register the UI builder (must run on both sides — see Script Placement):
The context object available in the event:
| Field | Type | Description |
|---|---|---|
player |
Player |
The player for whom the UI is opened |
LDLib2UIFactory.openPlayerUI must be called from the server side. The UI is always considered valid since there is no block or item to validate against.
KubeJS: Script Placement
The LDLib2UI event (.block, .item, .player) fires on both the server and the client:
- The server fires it to build the
ModularUIContainerMenu. - The client fires it to build the
ModularUIContainerScreen.
Both sides must register a handler for the same ID — otherwise the UI will only exist on one side.
Option A: Separate client and server scripts
Register the builder in both client_scripts and server_scripts. You can provide different implementations per side (e.g. a different stylesheet on the client):
// client_scripts/main.js
LDLib2UI.block("mymod:my_block_ui", event => {
event.modularUI = ModularUI.of(UI.of(
new UIElement().addClass("panel_bg")
), event.player);
})
// server_scripts/main.js
LDLib2UI.block("mymod:my_block_ui", event => {
event.modularUI = ModularUI.of(UI.of(
new UIElement().addClass("panel_bg")
), event.player);
})
Option B: Startup script (recommended)
startup_scripts run on both the client and server, so a single registration covers both sides:
// startup_scripts/main.js — executed on both client and server
LDLib2UI.block("mymod:my_block_ui", event => {
event.modularUI = ModularUI.of(UI.of(
new UIElement().addClass("panel_bg")
), event.player);
})
Do not create UI eagerly in startup scripts
Minecraft resources (stylesheets, textures, etc.) are not yet loaded when startup scripts run. Always create the UI inside the event handler lambda, which runs at UI-open time — not at script load time.
// ❌ WRONG — built at startup, before resources are ready
const myUI = ModularUI.of(UI.of(new UIElement()), null);
LDLib2UI.block("mymod:my_block_ui", event => {
event.modularUI = myUI; // stale; resources were not loaded yet
})
// ✅ CORRECT — built lazily when the player opens the UI
LDLib2UI.block("mymod:my_block_ui", event => {
event.modularUI = ModularUI.of(UI.of(new UIElement()), event.player);
})