- Kotlin 100%
| examples | ||
| gradle/wrapper | ||
| src/main | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
⚠️ Made by AI (Claude Sonnet 5), experimental. This code was written in a conversation with Claude and has not been compiled or run on a real Paper server (only cross-checked against the official javadoc/documentation piece by piece). Build and test it yourself before using it in production — especially
DialogRegistration.kt(bootstrapper registration), where the API signatures were not verified as thoroughly as the main DSL.
Dialogus
A Kotlin DSL on top of the Paper Dialog API (1.21.7+). Method names mirror the original
API (chainable builders, Component instead of String), just without the "builder
inside a builder" nesting.
Why
The native Paper Dialog API works fine but is verbose: to show a simple confirmation
dialog you have to nest Dialog.create { } → DialogBase.builder() →
DialogType.confirmation() → ActionButton.builder() → DialogAction.customClick().
Dialogus flattens this into a single DSL block while keeping the same methods and types.
Requirements
- Paper 1.21.7+ (this is where the Dialog API was introduced)
- Kotlin 2.0+
- The underlying API is marked Experimental by Paper — signatures may change in future versions
Adding it to your plugin
plugin.yml of the dependent plugin:
depend: [Dialogus]
Import:
import ai.claude.dialogus.showDialog
Examples
Notice
player.showDialog {
title(Component.text("Info"))
body(Component.text("Kick history was cleared."))
notice(Component.text("OK"))
}
Confirmation
player.showDialog {
title(Component.text("Reset"))
bodyItem(ItemStack(Material.SKELETON_SKULL))
body(Component.text("Are you sure you want to kill yourself?"))
canCloseWithEscape(false)
confirmation(
yesLabel = Component.text("Yes", NamedTextColor.GREEN),
noLabel = Component.text("No", NamedTextColor.RED),
yesButton = {
tooltip(Component.text("Click to reset", NamedTextColor.GRAY))
onClick {
player?.health = 0.0
player?.closeDialog()
}
},
)
}
MultiAction
player.showDialog {
title(Component.text("Select a kit"))
bodyItem(ItemStack(Material.SHULKER_BOX))
body(Component.text("Select a kit you starting with!"))
multiAction(columns = 3) {
button(Component.text("Redstowner", NamedTextColor.RED)) {
onClick(Key.key("kit:redstowner")) // event-based, handle it in @EventHandler on PlayerCustomClickEvent
}
button(Component.text("Crystal PVPer", NamedTextColor.LIGHT_PURPLE)) {
onClick(Key.key("kit:cpvper"))
}
exitAction(Component.text("Close", NamedTextColor.GRAY))
}
}
All 4 input types
player.showDialog {
title(Component.text("Ban ${target.name}"))
input {
text("reason", Component.text("Reason")) { initial("griefing"); maxLength(100) }
bool("notify", Component.text("Notify player"), initial = true)
numberRange("duration_days", Component.text("Duration"), 1f, 30f) { step(1f); initial(7f) }
singleOption("severity", Component.text("Severity"), listOf("minor", "major", "critical"))
}
confirmation(
yesLabel = Component.text("Ban"),
noLabel = Component.text("Cancel"),
yesButton = {
onClick {
val reason = view.getText("reason")
val days = view.getFloat("duration_days").toInt()
banPlayer(target, reason, days)
}
},
)
}
DialogList (a list of other dialogs)
player.showDialog {
title(Component.text("Choose category"))
dialogList(dialogs = someRegistrySet, columns = 2, buttonWidth = 150)
}
Registering through the bootstrapper (avoid recreating the Dialog every time)
// inside PluginBootstrap.bootstrap(context)
context.registerDialog(Key.key("myplugin", "kit_select")) {
title(Component.text("Select a kit"))
multiAction(columns = 3) { /* ... */ }
}
See details and the warning in DialogRegistration.kt.
About DialogAction.customClick and callback leaks
Two ways to handle a click:
-
onClick { ... }— a lambda callback (DialogAction.customClick(callback, options)). Every call registers a callback on the server for the duration oflifetime. By default the vanilla API sets this to 12 hours, and the callback is not automatically removed when the dialog is closed or the player disconnects — see PaperMC/Paper#13236. Dialogus defaults touses(1)+lifetime = 5 minutesto avoid leaking memory on frequently-reopened dialogs (e.g. pagination). Override this via theoptionsparameter if you need different behavior. -
onClick(Key)— event-based (DialogAction.customClick(key, null)), handled through a global@EventHandleronPlayerCustomClickEvent. Best suited for dialogs that are registered once and reused many times.
Module structure
dialogus/
├── build.gradle.kts
├── settings.gradle.kts
├── src/main/kotlin/ai/claude/dialogus/
│ ├── DialogusPlugin.kt — plugin entry point (does nothing by itself)
│ ├── Dialogs.kt — the main DSL
│ └── DialogRegistration.kt — bootstrapper registration helper
├── src/main/resources/plugin.yml
└── examples/ResetCommand.kt
Known limitations / not verified against a live server
- Nothing in this repository has been compiled or run on an actual Paper server — it was only checked line by line against the official javadoc.
DialogRegistration.ktis the least certain part: theRegistryEvents.DIALOGsignatures and theDialog.base()/Dialog.type()getters are not guaranteed for your exact Paper version.view.getBoolean(...)for theboolinput was inferred by analogy withgetText/getFloatbut the method name itself was not cross-checked separately.