API
Interface
Forms the player fills in and on-screen elements: boss bars and a sidebar. All of it works for any client, whether or not it has Crafting installed.
Forms
A form has a title, a body text, fields and buttons. You declare it in a file under ui/ and show it wherever you need (a command, an event, when an item is used):
import { give_rubies } from "/lib/rewards"
const SHOP = form("Ruby shop", {
body: "Rubies cost 10 emeralds each.",
fields: [
{ slider: "amount", label: "Rubies", min: 1, max: 16, default: 1 },
{ toggle: "gift", label: "Wrap as a gift" },
{ choice: "pouch", label: "Pouch", options: ["leather", "silk"] },
{ text: "note", label: "Note" }
],
buttons: [{ id: "buy", label: "Buy" }, "cancel"]
})
fun open_shop(p: Player) {
p.show_form(SHOP, fun(r) {
if r.button != "buy" { return } // closed with Esc or cancelled
let n = int(r.number("amount") ?? 1)
if p.inventory.count("minecraft:emerald") < n * 10 { p.send("&cNot enough emeralds"); return }
give_rubies(p, n)
if r.toggle("gift") { p.send("&dWrapped in {r.choice("pouch")}: {r.text("note")}") }
})
}import { open_shop } from "/ui/shop"
command shop describe "Buy rubies" {
if player == null { return "Players only" }
open_shop(player)
}| Field | Written as | Read with |
|---|---|---|
| Text | { text: "note", label: "Note", default: "" } |
r.text("note") |
| Toggle | { toggle: "gift", label: "It's a gift", default: false } |
r.toggle("gift") |
| Slider | { slider: "amount", label: "Amount", min: 1, max: 64, step: 1, default: 8 } |
r.number("amount") |
| Choice | { choice: "color", label: "Color", options: ["red", "blue"], default: "red" } |
r.choice("color") |
- Each field key uses lowercase letters, digits and
_.labelis what the player sees; if it's missing, the key is used. buttonsis a list of{ id, label }or just ids. A form without buttons gets an “OK” button.- In the response,
r.buttonis the id of the button pressed, ornullif the player closed the form (Esc).r.valuesis a map with all the fields. - The response arrives in the function you pass to
show_form; the rest of the server keeps running while the player decides.
In Kotlin
package com.example.rubies.ui
object ShopForm {
private val form = Form("Ruby shop").apply {
body = "Rubies cost 10 emeralds each."
slider("amount", "Rubies", 1.0, 16.0, default = 1.0)
toggle("gift", "Wrap as a gift")
choice("pouch", "Pouch", listOf("leather", "silk"))
text("note", "Note")
button("buy", "Buy")
button("cancel", "Cancel")
}
fun show(player: Player) = player.showForm(form) { r ->
if (r.button != "buy") return@showForm // closed with Esc or cancelled
val n = (r.number("amount") ?: 1.0).toInt()
if (player.inventory.count(Identifier.parse("minecraft:emerald")) < n * 10) { player.send("&cNot enough emeralds"); return@showForm }
player.give(ItemStack.of(Items.RUBY, n))
if (r.toggle("gift") == true) player.send("&dWrapped in ${r.choice("pouch")}: ${r.text("note")}")
}
}Form(title) with body, text(key, label, default, maxLength), toggle(key, label, default), slider(key, label, min, max, step, default), choice(key, label, options, default) and button(id, label). player.showForm(form) { r -> … } receives a FormResponse with button, values and text, toggle, number, choice.
Boss bars
A bar at the top of the screen, with a title, color, progress and style. Use it for a timer, a server event or something's health:
const TIMER = server.boss_bar("rubies:countdown", "&cRuby rush", "red", "segments_10")
let left = 60
on join { TIMER.show(player) }
every 1 second {
left = max(0, left - 1)
TIMER.progress = left / 60
TIMER.title = "&cRuby rush: {left}s"
if left == 0 { TIMER.remove() }
}| Member | What it does |
|---|---|
server.boss_bar(id, title?, color?, style?) |
Creates the bar or returns the existing one with that id. One id per bar: reloading the mod doesn't duplicate it. |
bar.progress |
From 0 to 1. |
bar.title |
The text, with & colors. |
bar.color |
pink, blue, red, green, yellow, purple, white. |
bar.style |
solid, segments_6, segments_10, segments_12, segments_20. |
bar.show(player), bar.hide(player) |
Who sees it. bar.players lists them. |
bar.remove() |
Removes it for everyone and frees the id. |
In Kotlin: Crafting.server?.bossBar("rubies:countdown", "Time", BarColor.RED, BarStyle.SEGMENTS_10), with the same properties in camelCase.
Sidebar
A list of up to 15 lines on the right side of the screen, different for each player. Call it again to update it:
import { visits } from "/lib/storage"
on join {
player.set_sidebar("&6Rubies", [
"&fVisits: &e{visits(player)}",
"&fRubies: &c{player.inventory.count("rubies:ruby")}"
])
}player.set_sidebar(title, lines)shows or updates it; only that player sees it. Colors with&.player.clear_sidebar()removes it.- In Kotlin:
player.setSidebar(title, lines)andplayer.clearSidebar().
Keys of a mod
A mod can add keys of its own. Declare them with key and react with on key, which runs on the server for the player who pressed it. e.key is the id with the mod's namespace (dashmod:dash).
key "dash" { label: "Dash", default: "R" }
key "heal" { label: "Second wind", default: "G" }
on key(e) {
let p = e.player
if e.key == "dashmod:dash" && !p.has_cooldown("minecraft:feather") {
let d = p.direction
p.velocity = vector(d.x * 1.6, 0.35, d.z * 1.6)
p.set_cooldown("minecraft:feather", 2)
}
if e.key == "dashmod:heal" { p.heal(6) }
}The keys show in Options > Controls > Key Binds, in a section named after the mod. Players can change them, and their choice is kept. default is a key name such as "R", "G", "F6" or "LEFT_ALT".
Players need Crafting installed on their game to have the keys. In Kotlin: ContentBuilder.key("dash", label = "Dash", default = "R") and Crafting.events.on<PlayerKeyEvent> { e -> … }.
Good to know
- Forms, bars and the sidebar are visible to any Minecraft Java 26.3 client, with or without Crafting. A server mod can use them without anyone installing anything.
- Titles, the action bar and chat messages are covered in The world and players.
- Custom screens with your own layout (menus, machine inventories) don't exist yet; see the roadmap.