This guide explains how to create commands, listeners, and GUIs using ExamplePlugin's registration system. All three follow the same pattern: extend a base class (or implement an interface), place the file in the correct package, and the plugin handles the rest automatically at startup.
ExamplePlugin uses a PackageScanner to discover classes at runtime. When the plugin starts, it scans specific packages for concrete (non-abstract) classes and registers them automatically. You never need to edit plugin.yml or manually wire anything up.
| System | Base Class / Interface | Package |
|---|---|---|
| Commands | PluginCommand |
com.example.exampleplugin.commands |
| Listeners | Listener |
com.example.exampleplugin.listeners |
| GUIs | PluginGUI |
com.example.exampleplugin.guis |
Subpackages are also scanned, so you can freely organize classes into folders like commands/game/, listeners/player/, or guis/menus/.
Every command, listener, and GUI class must have one of the following constructors:
| Constructor | When to Use |
|---|---|
| No-arg constructor | When you don't need a reference to the plugin |
Constructor accepting a JavaPlugin |
When you need to access the plugin instance |
The plugin instance is injected automatically when a JavaPlugin constructor is available.
To create a command, extend PluginCommand and place the class anywhere inside the commands package or a subpackage.
By default every command is registered as a sub-command of /exampleplugin (alias /ep). For example, a command with name = "reload" becomes /exampleplugin reload. Set isMainCommand = true to register the command as a standalone top-level command instead.
When a player types /exampleplugin in-game, tab-completion automatically lists all available sub-commands.
Commands are automatically categorised based on their subpackage (folder) inside the commands package. The category is used by the built-in /exampleplugin help command to group commands for display.
| Command Location | Category |
|---|---|
commands/PingCommand.kt |
General |
commands/game/StartCommand.kt |
Game |
commands/admin/BanCommand.kt |
Admin |
The plugin ships with a built-in /exampleplugin help command. It lists every registered command grouped by category, sorted alphabetically within each group, and formatted with colours for readability. Every command should provide a meaningful description so the help output is informative.
| Property | Type | Default | Description |
|---|---|---|---|
name |
String |
(required) | The command name (e.g. "reload" for /exampleplugin reload) |
description |
String |
"" |
A brief description shown in /exampleplugin help — always provide one |
usage |
String |
"/<command>" |
Usage hint shown when the command fails |
aliases |
List<String> |
emptyList() |
Alternative names for the command (applicable to main commands only) |
permission |
String? |
null |
Permission node required to use the command |
isMainCommand |
Boolean |
false |
When true, the command is registered as a standalone top-level command |
| Method | Required | Description |
|---|---|---|
execute |
Yes | Called when a player or console runs the command |
tabComplete |
No | Called when tab-completion is requested |
This command is registered as /exampleplugin ping (the default behavior):
package com.example.exampleplugin.commands
import com.example.exampleplugin.registration.PluginCommand
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class PingCommand : PluginCommand(
name = "ping",
description = "Check your latency",
usage = "/exampleplugin ping",
permission = "exampleplugin.ping"
) {
override fun execute(sender: CommandSender, args: Array<out String>): Boolean {
if (sender !is Player) {
sender.sendMessage("This command can only be used by players.")
return true
}
sender.sendMessage("Pong! Your ping is ${sender.ping}ms.")
return true
}
}This command is registered as /exampleplugin team:
package com.example.exampleplugin.commands.game
import com.example.exampleplugin.registration.PluginCommand
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class TeamCommand : PluginCommand(
name = "team",
description = "Join a team",
usage = "/exampleplugin team <hunters|runners>",
permission = "exampleplugin.team"
) {
private val teams = listOf("hunters", "runners")
override fun execute(sender: CommandSender, args: Array<out String>): Boolean {
if (args.isEmpty() || args[0] !in teams) {
sender.sendMessage("Usage: /exampleplugin team <hunters|runners>")
return false
}
sender.sendMessage("You joined the ${args[0]} team!")
return true
}
override fun tabComplete(sender: CommandSender, args: Array<out String>): List<String> {
if (args.size == 1) {
return teams.filter { it.startsWith(args[0], ignoreCase = true) }
}
return emptyList()
}
}This command is registered as /exampleplugin reload:
package com.example.exampleplugin.commands
import com.example.exampleplugin.registration.PluginCommand
import org.bukkit.command.CommandSender
import org.bukkit.plugin.java.JavaPlugin
class ReloadCommand(private val plugin: JavaPlugin) : PluginCommand(
name = "reload",
description = "Reload the plugin configuration",
permission = "exampleplugin.reload"
) {
override fun execute(sender: CommandSender, args: Array<out String>): Boolean {
plugin.reloadConfig()
sender.sendMessage("Configuration reloaded!")
return true
}
}Set isMainCommand = true to register a standalone top-level command.
This command is registered as /globaltool:
package com.example.exampleplugin.commands
import com.example.exampleplugin.registration.PluginCommand
import org.bukkit.command.CommandSender
class GlobalToolCommand : PluginCommand(
name = "globaltool",
description = "A standalone top-level command",
usage = "/globaltool",
isMainCommand = true
) {
override fun execute(sender: CommandSender, args: Array<out String>): Boolean {
sender.sendMessage("Hello from /globaltool!")
return true
}
}To create a listener, implement Bukkit's Listener interface and place the class anywhere inside the listeners package or a subpackage.
Annotate each event handler method with @EventHandler. The method must accept a single Bukkit event parameter.
package com.example.exampleplugin.listeners
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
class JoinListener : Listener {
@EventHandler
fun onPlayerJoin(event: PlayerJoinEvent) {
event.joinMessage(
net.kyori.adventure.text.Component.text("Welcome, ${event.player.name}!")
)
}
}package com.example.exampleplugin.listeners.player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.plugin.java.JavaPlugin
class DeathListener(private val plugin: JavaPlugin) : Listener {
@EventHandler
fun onPlayerDeath(event: PlayerDeathEvent) {
plugin.logger.info("${event.player.name} has been eliminated!")
}
}To create a GUI (chest-based inventory menu), extend PluginGUI and place the class anywhere inside the guis package or a subpackage.
| Property | Type | Default | Description |
|---|---|---|---|
id |
String |
(required) | Unique identifier used to open the GUI |
title |
String |
(required) | Title displayed at the top of the chest |
rows |
Int |
3 |
Number of rows (1–6, each row = 9 slots) |
| Method | Required | Description |
|---|---|---|
setup |
Yes | Populate the inventory with items before it opens |
onClick |
No | Handle click events (clicks are cancelled by default) |
onClose |
No | Handle cleanup when the GUI is closed |
Use GUIManager.open(player, id) to open a registered GUI for a player:
import com.example.exampleplugin.registration.GUIManager
// Returns true if the GUI was found and opened, false otherwise
GUIManager.open(player, "settings")package com.example.exampleplugin.guis
import com.example.exampleplugin.registration.PluginGUI
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.ItemStack
class SettingsGUI : PluginGUI(
id = "settings",
title = "Settings",
rows = 3
) {
override fun setup(player: Player, inventory: Inventory) {
val compass = ItemStack(Material.COMPASS)
val meta = compass.itemMeta
meta.displayName(net.kyori.adventure.text.Component.text("Tracker"))
compass.itemMeta = meta
inventory.setItem(13, compass)
}
override fun onClick(event: InventoryClickEvent) {
event.isCancelled = true
val player = event.whoClicked as? Player ?: return
if (event.slot == 13) {
player.sendMessage("Tracker selected!")
}
}
}A common pattern is opening a GUI when a player runs a command. This command is registered as /exampleplugin settings:
package com.example.exampleplugin.commands
import com.example.exampleplugin.registration.GUIManager
import com.example.exampleplugin.registration.PluginCommand
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class SettingsCommand : PluginCommand(
name = "settings",
description = "Open the settings menu",
permission = "exampleplugin.settings"
) {
override fun execute(sender: CommandSender, args: Array<out String>): Boolean {
if (sender !is Player) {
sender.sendMessage("This command can only be used by players.")
return true
}
GUIManager.open(sender, "settings")
return true
}
}