Writing Spigot Plugins with Dependencies

December 12, 2015

Scenario: you’re writing a Minecraft plugin that needs to communicate with another plugin. How do you achieve that?

Some plugins are written with this in mind, and expose their own API. A couple of prominent examples of this are WorldGuard and WorldEdit. Let’s write a plugin that can perform an operation on a player’s WorldEdit selection. Like…a /spongify command that makes every non-air block a sponge.

Maven Remote Dependencies

First thing’s first: we need to add a Maven dependency to our plugin’s pom.xml. Otherwise you won’t be able to access the APIs exposed by the other plugin. For your plugin to compile against the Bukkit API, you’d already have something like this in the file:

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

<dependencies>
    <dependency>
        <groupId>org.bukkit</groupId>
        <artifactId>bukkit</artifactId>
        <version>1.8.7-R0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

That tells Maven to grab the org.bukkit.bukkit package from Spigot’s repository and include it as a dependency when building your plugin.

To add another dependency, you must locate a Maven repository that includes builds of the package you need. In the case of WorldEdit, the author maintains a repository with builds of his plugins. So we would update the pom.xml to look like this:

<repositories>
    <repository>
        <id>spigot-repo</id>
        <url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
    </repository>
    <repository>
        <id>sk89q-repo</id>
        <url>http://maven.sk89q.com/repo/</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>org.bukkit</groupId>
        <artifactId>bukkit</artifactId>
        <version>1.8.7-R0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.sk89q</groupId>
        <artifactId>worldedit</artifactId>
        <version>6.0.0-SNAPSHOT</version>
    </dependency>
</dependencies>

Now when we build our plugin, it will include the com.sk89q.worldedit package, which our plugin can now make use of.

Maven Local Dependencies

But what if a plugin doesn’t have a convenient repository you can just add and have the dependency be automatically resolved?

This is something that I just had to deal with before writing this post, actually. I was making an update to a plugin that depdends on WGCustomFlags, which used to have a remote Maven repository but has since ceased to be online.

The solution is actully very simple. All you have to do is download the plugin’s source, build it with Maven, and then install it to your system’s local repository. Maven caches every package it downloads in an internal repository on your computer, so it doesn’t have to download them every time. When you build a plugin, it quickly checks to see if dependencies on remote repositories are newer, and uses the local copies if the remotes haven’t updated.

So if you download and compile a plugin, compile it, and then run maven install, it will copy the package into your local repository, where other builds can find it. This is exactly what Spigot’s BuildTools.jar does. (Spigot can’t legally distribute completed builds, due to the DMCA concerns, so they distribute source and an automated build tool.)

Basically, I did this:

git clone https://github.com/mewin/WGCustomFlags.git
cd WGCustomFlags
mvn clean package
mvn install

Et voilà. Now my IDE stopped complaining about not being able to resolve the dependency.

The YAML

Before we forget, let’s tell the server that our plugin depends on another one, and that it should be loaded after the other plugin. It’s just a simple addition to plugin.yml, using the plugin’s name (as defined in its own plugin.yml).

depend: [WorldEdit]

Getting the WorldEdit Instance

Now we’re down to the part that’s specific to the plugin in question: actually writing code. The Bukkit API allows us to get a plugin instance through the PluginManager like this:

private WorldEditPlugin getWE() {
    Plugin plugin = getServer().getPluginManager().getPlugin("WorldEdit");
    if (plugin == null || !(plugin instanceof WorldEditPlugin)) {
        return null;
    }
    return (WorldEditPlugin) plugin;
}

With a method like that in place, we can get a WorldEditPlugin instance by calling getWE().

Now if I want to get a WorldEdit Selection object representing a selection the player has made, I can just do this (with player being a standard Bukkit Player object):

Selection sel = getWE().getWE().getSelection(player);

In the case of WorldEdit, sk89q makes thorough documentation of his plugins’ public APIs available in the form of JavaDocs. So you can riddle out how things work between them and the source on GitHub. Other plugins may only give you a GitHub repository and expect you to pick through it the hard way.

Spongify

So about that example use case…let’s make a command that loops through a WorldEdit selection and turns non-air blocks into sponge!

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
    if (cmd.getName().equalsIgnoreCase("spongify")) {

        // Stop console from running the command
        if (!(sender instanceof Player)) {
            sender.sendMessage("Console cannot do that.");
            return true;
        }

        // Get the player and their WE selection
        Player player = (Player) sender;
        Selection sel = getWE().getWE().getSelection(player);

        // Get the min and max coordinates in the cuboid
        Location max = sel.getMaximumPoint();
        Location min = sel.getMinimumPoint();

        // Loop through the blocks
        for (int x = min.getBlockX(); x <= max.getBlockX(); x++) {
            for (int y = min.getBlockY(); y <= max,getBlockY(); y++) {
                for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) {
                    Block block = world.getBlockAt(x, y, z);
                    if (block.getType() != Material.AIR) {
                        block.setType(Material.SPONGE);
                    }
                }
            }
        }

        return true;

    }
    return false;    
}