
DialogAPI is a developer-focused API for easily testing and extending Minecraft's new native dialogs
DialogAPI is a developer-focused API for easily testing and extending Minecraft's new native dialogs (1.21.6), specifically leveraging the ServerboundCustomClickActionPacket. It offers a full Kotlin-based wrapper for creating rich, interactive dialogs with buttons, inputs, and custom actions for Paper plugins.
❗ Note: This API is primarily intended as a developer utility. While functional, ongoing maintenance for future Minecraft versions is not strictly guaranteed. Feel free to fork, adapt, and build your own features on top of it.
⚠️ Important Consideration: PaperMC is developing its own official Dialog API. While DialogAPI is useful now, developers should keep an eye on Paper's native solution for long-term projects, as it will likely become the standard.
MultiAction, List, Links, Notice.ServerboundCustomClickActionPacket system.To add DialogAPI to your project, include the following in your build.gradle.kts (or equivalent for your build system like Maven or Gradle Groovy):
repositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.AlepandoCR:DialogAPI:v1.1.1")
}
💡 Tip: Always check JitPack or the GitHub Releases page for the latest version number.
Call this function in your plugin's onEnable method to initialize the API internals and register the required packet listeners. This enables features like CustomActions and InputReaders, which rely on packet-level data.
⚠️ Crucial Step: If you don't call
DialogApi.initialize(this), dialogs will still open and render correctly, but essential features like button actions (CustomAction) and input field processing (InputReader) will not function.
// In your main plugin class
override fun onEnable() {
// Initialize DialogAPI, passing your plugin instance
DialogApi.initialize(this)
// Your other onEnable logic...
logger.info("MyPlugin has been enabled and DialogAPI is initialized!")
}
This section guides you through creating a basic dialog with a title, body, and a button.
First, define the core data for your dialog using DialogDataBuilder:
val dialogData = DialogDataBuilder()
.title(Component.text("Test Menu")) // The title displayed at the top of the dialog window
.externalTitle(Component.text("Menu Test")) // Title shown in server logs when the dialog is opened (useful for debugging)
.canCloseWithEscape(true) // Allows the player to close the dialog using the ESC key
.afterAction(DialogAction.CLOSE) // Action after dialog closes or button is clicked (default: CLOSE, can be KEEP_OPEN)
.addBody(
PlainMessageDialogBody( // A simple text body part
100, // Height of this body part
Component.text("Hello from DialogAPI! This is a basic dialog example.")
)
)
// You can add more body parts, like ItemDialogBody, if needed
.build()
💡 New in DialogDataBuilder:
.afterAction(DialogAction.KEEP_OPEN): Instructs the dialog to attempt to remain open after a button'sCustomActionis executed. The default isDialogAction.CLOSE. This is useful for dialogs that update themselves or have multiple steps.
Dialogs can contain different types of body content. You use DialogDataBuilder().addBody(...) to add them.
This is a simple text body, already shown in previous examples.
.addBody(PlainMessageDialogBody(100, Component.text("Your message here.")))
ItemDialogBody)You can display a Minecraft item within your dialog, complete with its name, lore, enchantments, and an optional description.
// --- Example: Using ItemDialogBody ---
// First, prepare your ItemStack
val diamondSword = ItemStack(Material.DIAMOND_SWORD)
// Now, create the ItemDialogBody
val itemBody = ItemDialogBodyBuilder()
.width(200) // Width allocated for this body part in the dialog layout
.height(100) // Height allocated for this body part
.item(diamondSword) // The ItemStack to display
.showDecorations(true) // Shows enchant glint, etc. (default: true)
.showTooltip(true) // Shows item name, lore, enchants on hover (default: true)
.description("This is a special item found deep within the Obsidian Caves. It is said to possess immense power.", 180) // Optional text description displayed near the item, and width for this description.
.build()
// Add it to your DialogData
val itemShowcaseData = DialogDataBuilder()
.title(Component.text("Item Showcase"))
.addBody(itemBody) // Add the ItemDialogBody
.addBody(PlainMessageDialogBody(50, Component.text("What will you do with it?"))) // You can mix body types
// ... add buttons or other inputs ...
.build()
// Then, use this DialogData with a dialog builder (e.g., MultiActionDialogBuilder)
val keyedAction = KeyedAction(ResourceLocation("key", "close_dialog_action")) // You can also add an optional DataContainer
val exitButton = Button(ButtonDataBuilder().label(Component.text("Close")).width(80).build(), Optional.of(keyedAction))
val itemShowcaseDialog = MultiActionDialogBuilder()
.data(itemShowcaseData)
.addButton(exitButton)
.columns(1)
.build()
player.openDialog(itemShowcaseDialog)
Next, create buttons and associate actions with them. Actions are triggered when a button is clicked. KeyedAction links a button to a registered CustomAction.
// Define a custom action key (namespace and path)
val customActionNamespace = "key" // Can be your plugin's name or whatever key
val customActionPath = "custom_action"
val resourceLocation = ResourceLocation(customActionNamespace, customActionPath)
// Create a button with an associated action
val testButton = Button(
ButtonDataBuilder()
.label(Component.text("Click Me!"))
.width(100) // Width of the button
.build(),
Optional.of(KeyedAction(resourceLocation)) // Associates the button with the custom action
)
ℹ️ Note: For this button to work, you'll need to register a
CustomActionwith theresourceLocationdefined above. See the Registering an Action section for details.
Now, assemble the dialog using a specific dialog type builder (e.g., MultiActionDialogBuilder for dialogs with multiple buttons).
// Assuming 'exitButton' is defined elsewhere (e.g., a button to close the dialog)
// val exitButton = ...
val dialog = MultiActionDialogBuilder() // Use the appropriate builder for your dialog type
.data(dialogData) // Set the core dialog data
.columns(1) // Number of columns for button layout
.exitButton(exitButton) // Optional: A dedicated exit button
.addButton(testButton) // Add your custom button
.build()
💡 Tip: DialogAPI supports various dialog types like
ListDialog,LinksDialog, andNoticeDialog. Choose the builder that best fits your needs.
The method to open a dialog depends on whether you're using Kotlin or Java:
If you're coding in Kotlin, use the provided extension function:
player.openDialog(dialog)
If you're using Java, use the utility method:
PlayerOpener.INSTANCE.openDialog(player, dialog);
ℹ️ Both methods work identically under the hood — use the one that fits your language of choice.
DialogAPI offers several specialized dialog types beyond the flexible MultiActionDialog.
A NoticeDialog is a simple dialog used to display a message with a single acknowledgment button.
// --- Example: Creating a Notice Dialog ---
val noticeData = DialogDataBuilder()
.title(Component.text("Important Notice!"))
.addBody(PlainMessageDialogBody(100, Component.text("Server restarting in 5 minutes.")))
.build()
val noticeOkButton = Button(
ButtonDataBuilder().label(Component.text("OK")).width(80).build(),
Optional.of(KeyedAction(ResourceLocation("key", "action_path")))
)
val noticeDialog = NoticeDialogBuilder()
.data(noticeData)
.button(noticeOkButton)
.build()
player.openDialog(noticeDialog)
A ConfirmationDialog presents the user with a binary choice, typically "Yes" and "No" buttons, for confirming an action.
// --- Example: Creating a Confirmation Dialog ---
val confirmData = DialogDataBuilder()
.title(Component.text("Confirm Purchase"))
.addBody(PlainMessageDialogBody(120, Component.text("Are you sure you want to buy the Legendary Sword for 1000 Gems?")))
.externalTitle(Component.text("Player Purchase Confirmation"))
.build()
val yesButton = Button(
ButtonDataBuilder().label(Component.text("Yes, Buy It!")).width(100).build(),
Optional.of(KeyedAction(ResourceLocation("key", "confirm_action_path")))
)
val noButton = Button(
ButtonDataBuilder().label(Component.text("No, Cancel")).width(100).build(),
Optional.of(KeyedAction(ResourceLocation("key", "cancel_action_path")))
)
val confirmationDialog = ConfirmationDialogBuilder()
.data(confirmData)
.yesButton(yesButton)
.noButton(noButton)
.build()
player.openDialog(confirmationDialog)
DialogAPI also supports more specialized dialog types for advanced use cases:
LinksDialog:
ListDialog:
Dialog.Custom actions are the heart of interactive dialogs. They allow you to execute specific server-side logic when a player interacts with a dialog element (e.g., clicks a button).
First, you need to register your custom action with a unique ResourceLocation key. This key is crucial as it links the client-side dialog interaction (like a button click) to your server-side CustomAction implementation.
val killPlayerNamespace = "dialog" // Example namespace
val killPlayerPath = "damage_player" // Example path
val killPlayerKey = ResourceLocation(killPlayerNamespace, killPlayerPath)
try {
CustomKeyRegistry.register(
killPlayerKey, // The unique key for this action
KillPlayerAction, // Your CustomAction implementation (see below)
PlayerReturnValueReader // Your InputReader implementation, you can also register an action without the Reader, although you might not be able to register the reader to the same key later on
)
} catch (e: IllegalStateException) {
// Handle cases where the key might already be registered
// This message is good for debugging during development
player.sendMessage("Note: Kill player key was already registered, perhaps by another part of your plugin or a different plugin: ${e.message}")
}
💡 Best Practice: Register all your custom keys during your plugin's
onEnablephase to ensure they are available when needed and to handle any registration conflicts early.
Create a class (or object for singletons) that extends CustomAction and implement the task method. This method contains the server-side logic that will be executed when the action is triggered.
object KillPlayerAction : CustomAction() {
override fun task(player: Player, plugin: Plugin) {
// Optional: Start a dynamic listener if needed for this action
dynamicListener?.start()
// Optional: Stop the dynamic listener after a delay or when the action is complete
dynamicListener?.stopListenerAfter(20L) // Time is based ticks
player.damage(5.0) // Example action: damage the player
}
// Optional: Define a Bukkit event listener specific to this action
// Custom listeners are not required for CustomActions, but it's an option.
// Listener also includes the player that triggered the action
override fun listener(dialogPlayer: Player): Listener {
return object : Listener {
@EventHandler
fun onPlayerDeath(event: PlayerDeathEvent) {
if (event.player == dialogPlayer) {
dialogPlayer.sendMessage("you died during your dialog")
}
}
}
}
}
ℹ️ The
plugin: Pluginparameter intaskrefers to your main plugin instance, allowing you to access plugin-specific resources or schedulers, this plugin instance is the same as the one you use to initialize DialogAPI
Input readers (InputReader) are essential when your dialog includes input fields (like text boxes, number inputs, etc.). They are responsible for processing the data submitted by the player from these fields. Each CustomAction that handles a dialog submission with inputs can have an associated InputReader.
object PlayerReturnValueReader : InputReader {
// InputValueList offers a getter based on keys.
// Useful for getting specific values with the key set on Input creation (see below).
override fun task(player: Player, values: InputValueList) {
for (input in values.list) {
player.sendMessage("Received input - Key: ${input.key}, Value: ${input.value}") // Corrected to show input.value
}
}
}
DialogAPI provides builders for various input field types, allowing you to collect different kinds of data from players. Each input field should be given a unique key (String) so you can identify its value in the InputReader.
NumberRangeInputBuilder)val numberRangeInput = NumberRangeInputBuilder()
.label(Component.text("Enter a Number (1-10)"))
.key("number_input") // Unique key for this input field
.width(150)
.rangeInfo(RangeInfo(1.0f, 10.0f)) // Define min and max values
.labelFormat("") // Optional: Custom format for the label
.build()
TextInputBuilder)val stringInput = TextInputBuilder()
.label(Component.text("Enter Feedback (max 300 chars)"))
.width(256) // Width of the input field
.key("feedback_text") // Unique key for this input
.initial("It was great!") // Optional: Pre-fills the input field
.labelVisible(true) // Whether the label is shown above the input
.maxLength(300) // Maximum allowed characters
// For multiline: MultilineOptions(visibleLines, maxCharactersPerLineForWrapping)
.multiline(MultilineOptions(5, 40)) // Makes it a multiline input with 5 visible lines, wrapping at 40 chars
.build()
// Note: .multiline() must be called with a valid MultilineOptions object before .build() for TextInput.
NumberRangeInputBuilder)val numberRangeInput = NumberRangeInputBuilder()
.label(Component.text("Enter a Number (1.0-10.0, step 0.5)"))
.key("quantity_input") // Unique key for this input field
.width(150)
// labelFormat allows specifying how the number is displayed, using printf-style formatters.
.labelFormat("Selected: %.1f items") // Example: "Selected: 5.0 items"
// RangeInfo(min, max, step, initialValue)
.rangeInfo(RangeInfo(1.0f, 10.0f, 0.5f, 2.5f)) // Define min, max, step, and initial values
.build()
// Note: .rangeInfo() must be called with a valid RangeInfo object before .build() for NumberRangeInput.
SingleOptionInputBuilder - Dropdown/Radio style)val singleOptionInput = SingleOptionInputBuilder()
.label(Component.text("Choose Your Class"))
.key("player_class_choice")
.width(150) // Affects how the options are displayed if they are too long
.labelVisible(true)
.addEntry( // Add each choice as an entry
EntryBuilder()
//ℹ️ There should always be an initial entry
.initial(true) // Sets this as the default selected option (only one 'initial(true)' allowed per input)
.id("warrior_class") // This ID string is the actual value returned when this option is selected
.display(Component.text("Warrior 💪").color(NamedTextColor.RED)) // Text displayed to the player
.build()
)
.addEntry(
EntryBuilder() // 'initial' is false by default
.id("mage_class") // This ID string is the value, the value InputReader will read from the player_class_choice key
.display(Component.text("Mage 🧙").color(NamedTextColor.BLUE))
.build()
)
.addEntry(
EntryBuilder()
.id("archer_class")
.display(Component.text("Archer 🏹").color(NamedTextColor.GREEN))
.build()
)
.build()
ℹ️ Important for
SingleOptionInput: Theidstring you provide inEntryBuilder().id("your_id_here")is the value that yourInputReaderwill receive for thisSingleOptionInputfield when that particular option is chosen by the player.
BooleanInputBuilder - Checkbox/Toggle style)val booleanInput = BooleanInputBuilder()
.label(Component.text("Enable Super Powers?"))
.key("boolean_super_powers") // Unique key
.initial(false) // Initial state: false (unchecked), true (checked)
.build()
ℹ️ Key Reminder: To retrieve values from these input fields, you must:
- Assign a unique
keyto each input field during its creation.- Ensure the
CustomActionthat handles the dialog submission is registered with anInputReader(likePlayerReturnValueReadershown previously).- In your
InputReader'staskmethod, usevalues.getValue("your_input_key")or iterate throughvalues.listto access the submitted data using these keys.
Check out the official companion project: DialogApiTest 🧪.
It’s a fully functional sample plugin that demonstrates how to use DialogAPI in a real Paper server environment (compatible with the Minecraft versions mentioned at the top). It includes practical examples of:
Perfect for learning, testing, or kickstarting your own dialog plugin! You can clone the repository and run it on your test server.
DialogAPI is designed with extensibility in mind. You can enhance its capabilities by:
CustomAction Implementations: Define new actions to handle various interactive elements.InputReaders: If you have complex input processing logic, create readers tailored to specific data structures.The core builder and registry patterns (e.g., CustomKeyRegistry) are your main tools for adding functionality.
Contributors are welcome! If you'd like to help improve DialogAPI, please follow these steps:
git clone https://github.com/YourUsername/DialogAPI.git (Replace YourUsername)git checkout -b feature/YourAmazingFeature or fix/IssueBeingFixed. Descriptive branch names are helpful!git commit -m "feat: Add support for ItemDialogBody tooltips"git commit -m "fix: Correctly handle null inputs in PlayerReturnValueReader"git push origin feature/YourAmazingFeature.master or main branch.Your contributions, big or small, are highly appreciated and help make DialogAPI better for everyone!
Q: My dialog opens, but buttons with CustomAction or input fields don't work. What's wrong?
A: ⚠️ The most common reason is forgetting to call DialogApi.initialize(this) in your plugin's onEnable() method. This step is crucial for registering the necessary packet listeners that handle custom actions and input data. Without it, the API won't process clicks or input submissions from the client.
Q: I'm getting an IllegalStateException: Key [your_key] is already registered when trying to register a CustomKey.
A: This error means the ResourceLocation (the key, composed of a namespace and path, e.g., yourplugin:some_action) you're trying to use for your CustomAction is already in use. This could be by another part of your plugin, or rarely, a different plugin (if namespaces aren't unique).
ResourceLocation is unique.myplugin).open_main_menu, submit_player_report).Q: How do I choose a good ResourceLocation for my custom actions?
A: A ResourceLocation has two string parts: namespace and path.
mycoolplugin, superutils). This helps prevent conflicts with other plugins. Avoid generic namespaces like minecraft, dialog, or custom.snake_case (e.g., open_shop_menu, confirm_teleport_request, process_player_settings_form).ResourceLocation("myplugin", "open_level_selector")Q: Is DialogAPI compatible with Java-based plugins?
A: ✅ Yes, absolutely! DialogAPI is written in Kotlin but is designed to be fully interoperable with Java. You can use all its builders, classes, and methods from your Java plugin code without any issues. The README provides Java examples for crucial parts like opening dialogs, and the Kotlin builder syntax (method chaining) translates very directly and naturally to Java.
Q: Where can I find the most up-to-date version number for the dependency?
A: 🔗 The best places to check for the latest version are:
Q: My input fields (text, number, etc.) are not returning the values I expect in my InputReader. What should I check?
A: 🕵️♀️ Double-check these common points:
key string (e.g., TextInputBuilder().key("player_name_input"), NumberRangeInputBuilder().key("item_quantity")). This key is how you identify the input's value.InputReader Registration: Verify that the CustomAction responsible for handling the dialog submission is correctly registered with the appropriate InputReader instance in the CustomKeyRegistry.register() call.InputReader's task(player: Player, values: InputValueList) method, make sure you are using the correct key to retrieve the value: values.getValue("your_exact_input_key"). You can also iterate through values.list and check input.key and input.value for debugging.NumberRangeInput will provide a number, a BooleanInput a boolean).