DaisyCommands 3.0 is a cleanup release focused on one thing: a Kotlin-only, Paper-first command API with one obvious DSL. If you are upgrading from 2.x, this guide covers the main breaking changes and the new preferred patterns.
- DaisyCommands is now explicitly Kotlin-only in public API design and documentation.
- Legacy compatibility APIs were removed from the main artifact.
- String-key argument access was removed from execution contexts.
- Flags and options are now first-class alongside positional arguments.
- Message customization moved to
registerCommands { config { ... } }. - The repository branding is now
DaisyCatTs/DaisyCommand. - Package namespace and current published coordinates stay stable for now.
- Replace any legacy command builders with
command(name) { ... }. - Replace string-key getters with typed
ArgumentRef<T>values. - Move registration to
registerCommands(...)orregisterCommands { ... }. - Convert optional values to
optional()ordefault(...). - Move message customization into registration config.
- Adopt flags and options where they remove positional boilerplate.
- Review plugin assumptions for Paper
1.21.11and Java21.
Before:
registerCommands(
command("hello") {
execute {
reply("Hello")
}
},
)After:
registerCommands {
config {
messages {
prefix = "<gray>[<yellow>Example</yellow>]</gray> "
}
}
command("hello") {
execute {
reply("Hello")
}
}
}Before:
command("island") {
sub("invite") {
val target = player("target")
executePlayer {
reply("Invited ${target().name}")
}
}
}After:
command("island") {
sub("invite") {
val target = player("target")
val silent = flag("silent", "s")
executePlayer {
islandService.invite(player, target(), silent())
}
}
}Before:
command("mail") {
val message = text("message").optional()
execute {
reply(message() ?: "Empty")
}
}After:
command("mail") {
val message = text("message").optional()
val mode = choice("mode", "normal", "priority").default("normal")
executePlayer {
reply("${mode()}: ${message() ?: "Empty"}")
}
}Before:
command("ban") {
val target = player("target")
executePlayer { }
}After:
command("ban") {
val target = player("target")
val reason = stringOption("reason", "r").default("No reason")
val silent = flag("silent", "s")
executePlayer {
moderationService.ban(player, target(), reason(), silent())
}
}The two supported registration paths are:
registerCommands(
command("example") {
execute { }
},
)registerCommands {
config {
messages {
prefix = "<gray>[<yellow>Example</yellow>]</gray> "
}
}
command("example") {
execute { }
}
}Use the builder form when you want framework-wide message or theme customization.
3.0 expects you to define refs during DSL construction and resolve them directly inside handlers:
command("warp") {
val target = player("target")
val expires = durationOption("expires", "e").optional()
executePlayer {
reply("Warping ${target().name} for ${expires()}.")
}
}The main pattern is:
- required ref -> non-null typed value
optional()-> nullable typed valuedefault(value)-> non-null fallback value
These are no longer part of the main 3.0 artifact:
- deprecated compatibility builders from older releases
- string-key getters like
getString("name") - broad legacy execution helpers that duplicated the typed-ref flow
The 3.0 API is intentionally narrower so plugin authors have one clear way to write commands.
- Kotlin-only public API direction
- Paper
1.21.11target - legacy compatibility APIs removed from the main artifact
- string-key argument access removed
- flags and options added as first-class syntax
- docs and examples now assume the 3.0 DSL only