Running a server? This topic in the server admin guide
Commands
Versatia has no command annotation: a plugin declares its commands with the
CommandAPI Kotlin DSL, inside an object whose auto-invoked functions
register the command at start and remove it at stop. CommandAPI stays a soft dependency, so the
plugin runs without it and only the command is missing.
package com.example.shop.command
import com.example.shop.PriceList
import com.example.shop.ShopPlugin
import dev.versatia.ConditionalOnPlugin
import dev.versatia.Phase
import dev.versatia.lifecycle.AutoInvoke
import dev.jorel.commandapi.CommandAPI
import dev.jorel.commandapi.kotlindsl.commandAPICommand
import dev.jorel.commandapi.kotlindsl.playerExecutor
import dev.jorel.commandapi.kotlindsl.stringArgument
import dev.jorel.commandapi.kotlindsl.subcommand
@ConditionalOnPlugin("CommandAPI")
object ShopCommand {
const val NAME = "shop"
const val PRICE_PERMISSION = "versatia.shop.command.price"
@AutoInvoke(Phase.PLUGIN_START_ONLY)
fun register(plugin: ShopPlugin, prices: PriceList) = commandAPICommand(NAME, plugin) {
subcommand("price") {
withPermission(PRICE_PERMISSION)
stringArgument("item")
playerExecutor { player, args -> player.sendMessage(prices.describe(args["item"] as String)) }
}
}
@AutoInvoke(Phase.PLUGIN_STOP)
fun unregister() = CommandAPI.unregister(NAME, true)
}
Where a command lives
- A
commandsubpackage of the feature or plugin the command belongs to, oneobjectper command:com.example.shop.command.ShopCommand, VersatiaCore'sdev.versatia.engine.command.VersatiaCommand. - An
object, not a bean. An object's auto-invoked functions receive any bean as a parameter, so the command needs no instance of its own and nothing injects it. - The CommandAPI types stay inside it. Only this object refers to
dev.jorel.commandapi; a bean or the main class that did would fail to load on a server without CommandAPI.
The build and plugin.yml
CommandAPI itself is a plugin on the server; its Kotlin DSL is a library the plugin ships:
val commandApiVersion = "12.1.0"
dependencies {
compileOnly("dev.jorel:commandapi-paper-core:$commandApiVersion")
implementation("dev.jorel:commandapi-kotlin-paper:$commandApiVersion")
}
// The CommandAPI Kotlin DSL ships inside the plugin JAR
val archives = serviceOf<ArchiveOperations>()
tasks.jar {
from(configurations.runtimeClasspath.map { classpath ->
classpath.filter { it.name.startsWith("commandapi-kotlin-") }.map { archives.zipTree(it) }
})
}
softdepend: makes CommandAPI start first when it is installed. Every permission is declared with
a description and a default (op, true, false or not op), so permission plugins list it
and the default is a decision rather than Bukkit's fallback.
Permissions
A permission is named versatia.<plugin>.command.<command>, where <plugin> is the plugin's
friendly name (or a short lower-case form of its name when
it has none) and <command> the subcommand it guards:
| Plugin | <plugin> | Command | Permission |
|---|---|---|---|
| VersatiaCore | core | /versatia reload | versatia.core.command.reload |
ShopPlugin, @FriendlyName("shop") | shop | /shop price <item> | versatia.shop.command.price |
Keep the names in const vals of the command object, next to the code that checks them.
Command permissions follow one pattern on every Versatia plugin; the server admin guide explains them to the people who grant them.
Registration and reload
| Phase | What happens |
|---|---|
PLUGIN_START_ONLY | the command is registered, once, after every bean of the plugin exists |
| a reload | nothing: the command stays registered as it is |
PLUGIN_STOP | the command and its namespaced form (/shop:shop) are unregistered |
Registering is what the server allows once, so it belongs at
PLUGIN_START_ONLY; a PLUGIN_RELOAD_OR_START
registration would declare the command a second time. The command's shape (its name,
subcommands, arguments and permissions) is therefore fixed for the plugin's life. What it shows
is not: the executor and the suggestions run at each use and read the beans then, so after
/versatia reload shop the same /shop price answers with the new prices. Read the beans'
current state in the lambdas; never copy a value into the command when it is built.
Unregistering at stop keeps a disabled plugin from leaving behind a command that would call into it.
VersatiaCore's /versatia
VersatiaCore declares /versatia reload [plugin] and
/versatia info [plugin] the same way, in dev.versatia.engine.command.VersatiaCommand:
@ConditionalOnPlugin("CommandAPI")
object VersatiaCommand {
const val NAME = "versatia"
const val RELOAD_PERMISSION = "versatia.core.command.reload"
const val INFO_PERMISSION = "versatia.core.command.info"
private var registered = false
@AutoInvoke(Phase.PLUGIN_START_ONLY)
fun register(core: VersatiaCore) = failSoft(core, "registered") {
commandAPICommand(NAME, core) {
subcommand("reload") {
withPermission(RELOAD_PERMISSION)
stringArgument("plugin", optional = true) {
replaceSuggestions(ArgumentSuggestions.stringCollection { core.runtime.registry.friendlyNames })
}
anyExecutor { sender, args -> reload(core, sender, args["plugin"] as? String) }
}
subcommand("info") {
withPermission(INFO_PERMISSION)
stringArgument("plugin", optional = true) {
replaceSuggestions(ArgumentSuggestions.stringCollection { core.runtime.registry.friendlyNames })
}
anyExecutor { sender, args -> info(core, sender, args["plugin"] as? String) }
}
}
registered = true
}
@AutoInvoke(Phase.PLUGIN_STOP)
fun unregister(core: VersatiaCore) {
if (!registered) return
registered = false
failSoft(core, "unregistered") { CommandAPI.unregister(NAME, true) }
}
// failSoft, reload and info are private helpers of the object
}
- The suggestions read the registry at each tab press, so a plugin that started after VersatiaCore is offered without re-registering anything.
failSoftcatches anExceptionor aLinkageErrorand logs one warning naming the installed CommandAPI release, so a release VersatiaCore is not compatible with costs only the command.registeredkeepsunregisterfrom touching CommandAPI when the registration failed.plugin.ymllistsCommandAPIundersoftdepend:and declaresversatia.core.command.reloadandversatia.core.command.info, both withdefault: op.
/versatia info
/versatia info [plugin] lists the running Versatia plugins, and for one plugin its beans by
scope, the exposed beans other plugins use, the plugins its reloads cascade to and its @Value
files. Its output is described in the server admin guide; while
you develop, it is the quickest check that a bean exists and in which scope.
Corner cases
- Without CommandAPI the object is never loaded:
@ConditionalOnPlugindrops its functions, so the DSL classes are never touched and the rest of the plugin starts. See Optional plugins. - CommandAPI installed later does not add the command: the condition is decided at start and a reload never changes it. Restart the server.
- A failed registration fails the start. An exception from the
PLUGIN_START_ONLYfunction fails the plugin's start like any other auto-invoked function; wrap the registration, as VersatiaCore'sfailSoftdoes, when the plugin should start without its command. - A reload does not re-register. A changed command name, alias or permission takes effect after a restart; a changed value the executor reads takes effect at its next use.
- The permission check is CommandAPI's. A sender without the permission does not see the subcommand in tab completion and gets CommandAPI's own message when typing it.