A Guide to Minecraft Plugin Development with Bukkit | Part 1

Recently, I decided to hold a series of workshops about Java development at the CoderDojo Linz. As lots of kids and teens there enjoy playing Minecraft, it was a straightforward decision to choose Minecraft plugin development as the topic for these workshops. As I never played Minecraft before, I wanted to get to know the game first. My plan to play for 2 or 3 hours to learn the basics escalated rather quickly and I ended up in not being productive for a week. Well, probably I could have anticipated that. Anyway, now I’m back and in the following lines I summarized some basics about Minecraft plugin development with Bukkit.

Prerequisites

Please note that this guide assumes, that you are already familiar with Java. If you are new to Java, I’d recommend getting to know Java first. Here you can find some great resources to learn it.

Further it would be nice if you have a little bit of experience with Gradle or Maven. We are using these build tools to load dependencies (in a nutshell: code from others) into our project and to package the project to a Java Archive (JAR). This guide gives you all the code you need in your build.gradle or pom.xml, but it would be great if you roughly know what they are doing. Note that you need to create a new project with one of the build tools on your own. Here are some nice guides for Gradle and Maven.

Running a local Spigot server

To install Bukkit plugins on your server, you need to run a CraftBukkit server or a fork of it, like Spigot or Paper. You can spin up and connect to a Spigot server with the below described steps.

  1. Download the latest BuildTools from https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar.
  2. Execute the file BuildTools.jar with the command java -jar BuildTools.jar in your terminal.
  3. Copy or move the file spigot-<VERSION>.jar to an empty folder and execute it with java -jar spigot-<VERSION>.jar.
  4. This will generate some files and folders, but won’t spin up the server, as you need to accept Minecraft’s end-user license agreement (EULA) first. To accept it, open the file eula.txt and change the line eula=false to eula=true. Save and close the file.
  5. Execute the spigot-<VERSION>.jar again to spin up the server. Once the line [09:08:39 INFO]: Done (XXXs)! For help, type "help" appears, you are ready to go.
  6. Open Minecraft and select “Multiplayer”. There you can connect to your new server, which is running on localhost.

Creating a basic plugin

Now, that your server is up and running, it’s time to create a basic plugin. To get started, create a new Java project with Gradle or Maven. I went with Gradle and Java 11, but also Maven is described in this section. Further, I used IntelliJ IDEA Community as IDE.

To use Bukkit’s API you need to add the following dependency to your build.gradle and replace [VERSION] with the current version of the Bukkit API, which you can find here. At the time of writing the latest version was 1.15.2-R0.1-SNAPSHOT.

implementation group: 'org.bukkit', name: 'bukkit', version: '[VERSION]'

As this dependency is not stored on any of the standard repositories, you also need to add the following repository.

maven { 
url "https://hub.spigotmc.org/nexus/content/repositories/public/"
}

If you decided for Maven over Gradle, you need to add the following dependency to your pom.xml.

<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>[VERSION]</version>
<type>jar</type>
<scope>provided</scope>
</dependency>

Of course, you need to add the following repository as well.

<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/public/</url>
</repository>

Now it’s time to create the main class of your plugin. Thus, you have to create a new Java class now. You can name it whatever you want, I named the main class for this guide simply BukkitPlugin, which is definitely not a good, descriptive name. When you are developing a real plugin, try to find descriptive names for your classes. Let your new class extend org.bukkit.plugin.java.JavaPlugin, override the method onEnable(...) and you are ready to go. At this moment your full class should look like this:

The onEnable(...) method is always called, when your plugin is enabled on the server.

Next, you need to configure your plugin. To complete this step, create the file plugin.yml in the resources folder of your project.

This file is necessary for Bukkit to load your plugin. You can configure lots of things in this file, but three attributes are mandatory:

  • name: The name of your plugin (must not contain any spaces).
  • version: The version of your plugin.
  • main: The main class of your plugin.

You can find all available attributes in the BukkitWiki. I would recommend, to provide the attribute api-version as well. It describes which API version of Bukkit you intend to use. After setting all of these attributes, your plugin.yml should contain the following attributes:

name: MinecraftPluginGuide_DemoPlugin
main: BukkitPlugin
version: 0.1
api-version: 1.16

Now you created a very basic plugin. It doesn’t do anything, but you’ve set up everything you need for the next steps and you can head on to the next section, to learn how to deploy your plugin to a server.

Deploying a plugin

To deploy your plugin, you first need to generate a JAR file containing all your code and dependencies (if you have some). Next, make sure that your server is not running and copy the JAR to the plugins directory of your server. Now you can run your server (see section 1) and your plugin will be loaded. You can check if it worked by running the command pl in the game or directly in the server window. Your plugin should be contained in the resulting list. Besides that, you'll see a log message when your plugin is loaded.

The following sections describe, how you can package your project to a JAR with Gradle and Maven.

Gradle

If you are only using Bukkit’s dependency, you can simply package your project by executing the following command in the terminal. Please note, that you need to be in your project’s root directory to execute it.

./gradlew jar

After this command succeeded, you can get your project’s jar from the folder build/libs inside your project.

If you are using any other dependencies than Bukkit’s, you need to include them in your JAR and create a so called “fat JAR”. By default they are not included. You could create a fat JAR with the jar task by just overriding it in the following way:

jar { 
manifest {
attributes "Main-Class": "BukkitPlugin"
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
}

This is ok for small projects, but for bigger projects I’d recommend the Shadow plugin. It adds a new gradle task that generates the fat JAR.

To add it, you need to add the following dependency into the plugins block in your build.gradle. Replace the [VERSION] block with your desired version. The latest version at the time of writing is 6.1.0..

id 'com.github.johnrengelman.shadow' version '[VERSION]'

The full plugins block should look like that now.

plugins { 
id 'java'
id 'com.github.johnrengelman.shadow' version '[VERSION]'
}

Now you can create a fat JAR by executing the following command.

./gradlew shadowJar

Maven

If you are only using Bukkit’s dependency, you can simply package your project by executing the corresponding Maven task. If you want to execute it in the console, you need to install Maven on your computer. Therefore I’ll just use IntelliJs built in functionality to execute Maven tasks. To create a JAR open the Maven toolbar in the top right of IntelliJ (orange square in the picture below), then open the Lifecycle dropdown, if it is closed, and click package (green square).

IntelliJ IDEA Community Maven Tasks

After this task succeeded, you can get your project’s JAR from the folder target inside your project.

If you are using any other dependencies than Bukkit’s, you need to include them in your JAR and create a so called “fat JAR”. By default they are not included. To create a fat JAR in maven, you first need to add the line <packaging>jar</packaging> below the version tag in your pom.xml. Now we'll use the Apache Maven Shade Plugin to create a fat JAR. Add the following block to your pom.xml to configure it. Please note, that you probably need to change the main class when copying the configuration below.

Now you can execute the package task again and two JARs will be generated. The one that is suffixed with "-shaded" is the fat JAR.

Logging

Bukkit has a built-in logging mechanism, which works quite good. The following subsections describe how to use it.

Getting a Logger

You can get an instance of the primary logger of a server by calling Bukkit.getLogger() or (if you are inside a JavaPlugin class) getServer().getLogger(). You can use this logger to send messages to the logstream of your server, but I would recommend using the logger of your plugin instead. This way, it is clear that log messages are coming from your plugin because they are prefixed. To get this logger, you need to be inside your JavaPlugin class. There you can simply call getLogger(). I'll use this approach in the following examples.

Log Levels

Bukkit provides 7 predefined log levels, which you should use to differentiate your log messages. Those levels (in descending order) are:

  • SEVERE: indicates a serious failure
  • WARNING: indicates a potential problem
  • INFO: informational messages
  • CONFIG: static configuration messages
  • FINE: tracing information
  • FINER: fairly detailed tracing information
  • FINEST: highly detailed tracing information

By default, all log levels above INFO are printed to the console.

Logging messages

To log a message you can simply execute the following line. Of course the first argument can be replaced with any of the above described log levels. Also the second argument, the message, can be chosen by you.

logger.log(Level.INFO, "Info log message");

The Logger class also provides some methods to directly log to a certain log stream without the need of passing a level. Therefore the same result as with the above statement can be acchieved with the following one.

logger.info("Info log message");

This, of course, works for all log levels.

Setting a custom log prefix

By default, the name you provide in plugin.yml is used as a prefix for log messages, that are sent via the logger of the plugin class. If you prefer using a different prefix, you can set the attribute prefix in the plugin.yml like this:

prefix: custom-log-prefix

Changing the log level and logging to a file

Unfortunately it’s not possible to log anything with a level below INFO to the server's console or standard log files. A moderator of the Bukkit Forum, stated that in this thread. Therefore setting the log level programmatically only makes sense, if you want to set the level to INFO (which is default), WARNING or SEVERE. Setting a log level means, that all messages logged with this level and the levels above this level will be logged. This means, if the log level is INFO all INFO, WARNING and SEVERE messages will be logged. Changing the log level to one of those levels is quite easy with the following command. Of course INFO can be replaced with any other level, but (as explained above) only WARNING or SEVERE make a difference.

logger.setLevel(Level.INFO);

Fortunately there is a possibility to use all log levels. You just have to add a custom handler, which, as the name suggests, handles log messages. The following lines show, how to send all log messages of your plugin’s logger to a file. This can be quite useful as a log file that contains only the log messages of your plugin will be generated. Of course your plugins log messages are still sent to the standard output streams as well.

To add a new handler to your logger, I’d suggest overriding the onLoad() method of your JavaPlugin and adding the following code there. Please read the comments in the code for an explanation of what it's doing.

When the plugin gets disabled, the handler should be closed as well. This can be acchieved with the following code.

You can change the format of the log messages by setting a different formatter when creating the handler. The following code produces log messages in the format [<TIME>][<LEVEL>]<MESSAGE>.

Understanding Minecraft’s coordinate system

A players coordinates represent his position in a dimension. A dimension is an accessible realm inside a world. The center of the coordinate system is the origin point. The spawn points of all players are located close to the origin point.

Minecraft’s coordinate system

Minecraft has a 3 dimensional coordinate system, which means it has 3 axes. All of them are intersecting in the origin point. These are the available axes:

  • X axis: represents how far the player has moved to the east (positive value) or west (negative value) of the origin point.
  • Z axis: represents how far the player has moved to the south (positive value) or north (negative value) of the origin point.
  • Y axis: represents how high (positive value) or low (negative value) the player is compared to the origin point.

This means, a player’s location is always described with 3 values: X, Y and Z. The origin point has the coordinates X: 0, Y: 0, Z: 0. While playing Minecraft, you can simply toggle the debug screen by pressing F3 to see your current coordinates.

One unit on the coordinate system represents one block. This means the location X: 1, Y: 0, Z: 0 would be one block to the east of the origin point.

Adding custom commands

To add custom commands to your server, you need to complete two steps: Registering your command in the plugin.yml of your plugin and adding the command handler to your code. Both steps are described in this section. As an example we'll add two very simple commands. The first commands logs a certain message, while the second command logs the message, the user passes as an argument.

Registering commands

To register your command, you need to add it to the plugin.yml of your plugin. There you can add a list of commands with the key commands. The key elements of the list items are the command names. One command can contain the following attributes:

  • description: A short description of what the command does.
  • aliases: Alternative names for the command.
  • permission: The permission that is required to use the command (more details on that will follow in the second part of this guide).
  • permission-message: The message that is displayed, if somebody without permissions tries to trigger the command.
  • usage: A short description of how to use this command.

As described above we’ll add 2 commands, one that just logs a message and one that logs a message the user passes as an argument. You can see the configuration for the commands below. Note, that you could also add command permissions here, but this will be contained in the second part of this guide. The first command, log-anything can be triggered with 3 names: log-anything, log_anything and loganything because the last two names are in the list of aliases. Please not that adding a command with a capital letter (e.g. logAnything) won't work. The second command log accepts the message to log as an argument. The arguments don't need to be defined here, therefore you have to check in the code if the correct number of arguments is given.

commands:
log-anything:
description: Logs a message to the console
aliases: [loganything, log_anything]
usage: /log-anything
log:
description: Logs the given message to the console
usage: /log [message]

Handling commands

There are two ways to handle commands:

  • Overriding the onCommand(...) method in the main class of your plugin.
  • Adding a custom CommandExecutor and overriding the onCommand(...) method there.

If you just have one or two small commands, I would recommend the first option. In all other cases, I’d prefer the second option as it provides a better separation of concerns. In the following subsections both options are shown.

Overriding onCommand(...) in the main class of your plugin

Probably the simplest possibility to react to custom commands, is overriding the onCommand(...) method in the main class of your plugin. This method is executed every time the user invokes a command, that is registered in the plugin.yml. This method contains the following arguments:

  • CommandSender sender: The origin of the command (see the next subsection for more details on this one).
  • Command command: The command that was executed.
  • String label: The alias that was used.
  • String[] args: The arguments that were passed to the command.

The method returns true, when the triggered command is valid and false otherwise. If false was returned, the usage of the command (defined in plugin.yml) is sent to the player.

If you registered only one command in your plugin.yml, you don't need to check which command was sent in the onCommand(...). As we registered two commands in the previous step, we need to check which command should be executed. This can be done with an if statement or with a switch statement, like in the example below. To check which command was sent, you can use the command's name, which you can get by calling command.getName(). The following code shows how to differentiate the commands.

Let’s take a look at the log command now. It takes the arguments the player provided and logs them. Arguments are always separated by spaces (even if you surround multiple words with quotation marks). Therefore we can't just take the first argument and log it, but need to combine all arguments to log the full phrase the user passed. Please check the comments in the code for further details.

The command log-anything is a bit simpler as it doesn't require any arguments. Therefore it just logs a message and returns true as you can see below.

Your full onCommand(...) method should look like this now:

Adding a custom CommandExecutor

As you can imagine, the main class of your plugin can get quite big if you have lots of commands and use the previously described way. Fortunately, an alternative exists. You can add as many classes that implement CommandExecutor as you want. These classes can handle your commands then. To do so, you need to create a new class and let it implement the CommandExecutor interface. You'll be forced to override the onCommand(...) method and after doing so, you can use it in the exact same way as in the main class of your plugin. If you register a CommandExecutor for just one command, you can skip checking the command name and assume that the command you registered the executor for was triggered.

Let’s start with implementing the CommandExecutor. As said, it's just a class that implements the interface and overrides onCommand(...) again. I won't elaborate further on the code inside onCommand(...) as it's exactly the same code as above. The only thing that's worth noticing in this class, is that I'm expecting a Plugin as an argument. This is necessary to get the plugin's logger when a command is triggered.

After finishing the CommandExecutor it needs to be registered in the onEnable(...) method of the main class of your plugin. This can simply be acchieved with the following code.

Command origin

Commands may be sent either from a player or from the server console. In some cases it may be important to check if the command was sent by a player. You can implement the following check, to make sure the command was sent by a player.

if (sender instanceof Player) { 
// Command was sent from a player
}

Command names

Think carefully about the command names you choose. Choosing command names that are already available in Minecraft or other plugins, may make your plugin incompatible with those plugins.

Communicating with the player

There are many possibilities to communicate with the player. The following subsections describe some ways, you can send information to the player. For all three methods you need an instance of the player you’d like to communicate with. As this is explained in other sections, it’s a prerequisite here that you have a player instance.

Sending a chat message

Sending a chat message works with a single command. You just need to execute the sendMessage(String message) method of the player.

player.sendMessage("This is a chat message");

Displaying a message in the game

Sending a message that is displayed directly in the game, works with sendTitle(...), which expects the following arguments:

  • title: The title to display.
  • subtitle: The subtitle to display.
  • fadeIn: The time in ticks for the titles to fade in (defaults to 10).
  • stay: The time in ticks for the titles to stay (defaults to 70).
  • fadeOut: The time in ticks for the titles to fade out (defaults to 20).
player.sendTitle("This is a title", "And a subtitle", 10, 70, 20);

Playing sounds

Also playing a sound works with a simple method call. The playSound(...) method expects the following arguments:

  • location: Where to play the sound.
  • sound: The sound to play. Unfortunately only the sounds that are already in the game can be played via a plugin. You can access them via the Sound enumeration.
  • volume: The volume of the sound.
  • pitch: The pitch of the sound.
player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1, 1);

Unfortunately, sound can’t be captured in an image, but the below image at least shows method 1 and 2 to communicate with players. I promise it played a sound too ;-).

What’s next?

This guide covered the basics of Minecraft plugin development with Bukkit. I’ve got a few more points on my roadmap, that I’d like to show you. So stay tuned for the continuations of this guide. They’ll follow within the next few weeks and will cover the following topics:

  • Listening to events
  • Adding custom events
  • Adding custom recipes
  • Adding configuration files
  • Handling permissions
  • Persisting data
  • Scheduling tasks
  • Localizing your plugin
  • Debugging your plugin
  • More details on plugin deployment

Resources

The full source code can be found on GitHub.

Version information

Originally published at https://ksick.dev on December 5, 2020.

Creative and detail-oriented software developer. Advanced from mobile to backend development and now getting into full stack solutions.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store