DSL for Paper Dialog API
Find a file
2026-08-01 15:52:07 +04:00
examples add example and fix some issues 2026-08-01 15:52:07 +04:00
gradle/wrapper initial commit 2026-08-01 14:49:44 +04:00
src/main add example and fix some issues 2026-08-01 15:52:07 +04:00
.gitignore initial commit 2026-08-01 14:49:44 +04:00
build.gradle.kts en comments 2026-08-01 14:58:17 +04:00
gradlew initial commit 2026-08-01 14:49:44 +04:00
gradlew.bat initial commit 2026-08-01 14:49:44 +04:00
README.md initial commit 2026-08-01 14:49:44 +04:00
settings.gradle.kts initial commit 2026-08-01 14:49:44 +04:00

⚠️ 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:

  1. onClick { ... } — a lambda callback (DialogAction.customClick(callback, options)). Every call registers a callback on the server for the duration of lifetime. 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 to uses(1) + lifetime = 5 minutes to avoid leaking memory on frequently-reopened dialogs (e.g. pagination). Override this via the options parameter if you need different behavior.

  2. onClick(Key) — event-based (DialogAction.customClick(key, null)), handled through a global @EventHandler on PlayerCustomClickEvent. 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.kt is the least certain part: the RegistryEvents.DIALOG signatures and the Dialog.base()/Dialog.type() getters are not guaranteed for your exact Paper version.
  • view.getBoolean(...) for the bool input was inferred by analogy with getText/getFloat but the method name itself was not cross-checked separately.