Paper Plugin Guide
File mode0

Reference

Gradle and Maven reference

Common build tasks, adding dependencies, shading libraries and run-paper options.

IntermediateReference

This is the page to keep open while you work on your build files. It lists the commands you will run, shows how to add a library to your plugin, how to pack that library into your jar, how to tune the test server, and how to fix the build errors beginners meet most often. Every snippet is shown for Gradle and for Maven.

You do not have to read it from top to bottom. Use the headings like a menu. If a word is new, hover it: the build tools use a lot of their own vocabulary. For a slower tour of the files themselves, read Anatomy of a plugin project first.

What a build tool does

A build tool turns your folder of source files into a plugin jar you can drop on a server. It does four jobs, always in the same order:

  1. Find the libraries your code needs (above all the Paper API) and download them once.
  2. Compile your Java files into class files.
  3. Process resources: copy plugin.yml and other files, filling in values such as the version.
  4. Package everything into one jar.

Everything on this page is a way of controlling one of those four jobs.

From source file to a running plugin You write MyPlugin.java, javac turns it into MyPlugin.class files, Gradle packs them into a .jar, you put the jar in the server's plugins folder, and Paper loads it. You write MyPlugin.java source code javac the Java compiler checks and translates Compiled files MyPlugin.class code the JVM can run Gradle packs a jar the build tool ./gradlew build Plugin jar my-plugin-1.0.0.jar in build/libs Server folder plugins/ you copy the jar here Paper loads it on start Gradle runs javac for you, so you rarely call it by hand.
The build pipeline: source code goes in, a plugin jar comes out.

Commands you will use

You run a build tool by typing a command in a terminal. In IntelliJ, open the terminal tool window at the bottom (Alt+F12) and it already sits inside your project folder. You can also click the same tasks in the Gradle tool window, as the next picture shows.

The Gradle tool window lists every task. Double-click one to run it, which is the same as typing it in the terminal.

A Gradle command starts with .\gradlew.bat. That is the Gradle wrapper: a small script inside your project that downloads the exact Gradle version the project wants, so you never have to install Gradle yourself. Always use the wrapper, never a Gradle you installed some other way.

CommandWhat it doesWhen to use it
.\gradlew.bat buildCompiles, packs the jar and runs any tests. The jar lands in build\libs.Every time you want a jar to install.
.\gradlew.bat runServerBuilds your plugin, downloads Paper if needed and starts a test server in the run folder.Testing while you code.
.\gradlew.bat jarPacks only the plain jar, without the other build steps.Rarely; build is easier to remember.
.\gradlew.bat cleanDeletes the build folder.When old output confuses you. Use clean build for a fresh jar.
.\gradlew.bat tasksLists every task this project understands.When you wonder what else you can run.
.\gradlew.bat dependenciesPrints the tree of libraries your project uses.To see what ended up where (see below).
.\gradlew.bat build --refresh-dependenciesIgnores the download cache and asks the repositories again.When a library should exist but Gradle claims it does not, or a library was updated on the repository after Gradle saved its copy.
.\gradlew.bat wrapper --gradle-version 9.8.0Changes which Gradle version the wrapper downloads.To upgrade Gradle. Run any task afterward so it downloads.
.\gradlew.bat cleanPaperCacheDeletes the Paper server jars that run-paper downloaded.When the test server jar seems broken.

A task is one named job. build is a task, and it quietly runs several smaller tasks first (compile, process resources, jar). Gradle prints each task name as it runs it, so you can follow along:

PowerShell
 .\gradlew.bat build> Task :compileJava> Task :processResources> Task :classes> Task :jar> Task :assemble> Task :compileTestJava NO-SOURCE> Task :processTestResources NO-SOURCE> Task :testClasses UP-TO-DATE> Task :test NO-SOURCE> Task :check UP-TO-DATE> Task :buildBUILD SUCCESSFUL in 4s

NO-SOURCE only means the task had nothing to do, because your project has no test files. UP-TO-DATE means nothing changed since the last run, so Gradle skipped the work. Both are good news, not errors.

CommandWhat it doesWhen to use it
mvn packageCompiles, copies resources and packs the jar. The jar lands in target.Every time you want a jar to install.
mvn clean packageDeletes target first, then does a full build.When old output confuses you.
mvn compileOnly compiles. No jar.Quick check that the code compiles.
mvn dependency:treePrints the tree of libraries your project uses.To see what ended up where.
mvn -U packageForces Maven to check the repositories for updates.When a library should exist but Maven claims it does not.

Maven works in phases that always run in the same order: compile, then test, then package. Asking for package runs every phase before it too. Maven has no test server and no wrapper by default: you install Maven yourself (IntelliJ ships its own copy and uses it behind the scenes) and you copy the jar to a server by hand. See Build, install and test for that.

PowerShell
 mvn package[INFO] --- compiler:3.15.0:compile (default-compile) @ my-plugin ---[INFO] --- jar:3.4.1:jar (default-jar) @ my-plugin ---[INFO] BUILD SUCCESS

Repositories and dependencies

Your plugin uses code you did not write: the Paper API, and maybe a library for caching, JSON or databases. In build files, such a library is a dependency. A dependency has two parts: where to download it from (a repository) and which library you want (its coordinates).

Coordinates are three pieces of text that together name one exact file, like a postal address with street, house number and apartment:

  • Group: who made it, written like a reversed web address. Example: io.papermc.paper.
  • Name: the library itself. Example: paper-api.
  • Version: which release. Example: 26.3.build.142-beta.

Maven calls them groupId, artifactId and version. You will see the whole thing written with colons: io.papermc.paper:paper-api:26.3.build.142-beta.

To add a library you do two things: make sure its repository is listed, then add one line to the dependencies. Most libraries live on Maven Central, the huge public repository that Gradle and Maven both know. Paper's own files live on the PaperMC repository, and many plugins publish their API on their own repository (the plugin's documentation tells you the address).

build.gradle.kts (relevant parts)
1repositories {2    mavenCentral()3    maven("https://repo.papermc.io/repository/maven-public/")4}5 6dependencies {7    compileOnly("io.papermc.paper:paper-api:$paperVersion")8    implementation("com.github.ben-manes.caffeine:caffeine:3.3.0")9}
  1. Adds Maven Central. The shortcut exists because nearly every library is there.
  2. Adds any other repository by its web address. Add as many as you need.
  3. Use this library to compile, but do not pack it into the jar, because the server already has it.
  4. Reads the value from gradle.properties, so the version lives in one place.
  5. Use this library to compile and also pack it into the jar. Which one you pick is the big decision, see the table below.
pom.xml (relevant parts)
1<repositories>2    <repository>3        <id>papermc-repo</id>4        <url>https://repo.papermc.io/repository/maven-public/</url>5    </repository>6</repositories>7 8<dependencies>9    <dependency>10        <groupId>io.papermc.paper</groupId>11        <artifactId>paper-api</artifactId>12        <version>26.3.build.142-beta</version>13        <scope>provided</scope>14    </dependency>15    <dependency>16        <groupId>com.github.ben-manes.caffeine</groupId>17        <artifactId>caffeine</artifactId>18        <version>3.3.0</version>19    </dependency>20</dependencies>
  1. Maven Central is built in, so you only list extra repositories.
  2. Use this library to compile but do not pack it into the jar, because the server already has it.
  3. No scope line means the default scope, compile: use it and pack it.

compileOnly or implementation?

This single choice causes more beginner errors than any other line in a build file. Ask one question: will the server already have this code when my plugin runs?

Gradle wordMaven wordMeaningUse it for
compileOnlyprovidedAvailable while compiling. Not packed into your jar.The Paper API and the API of another plugin that is installed on the server (Vault, LuckPerms, PlaceholderAPI).
implementationcompile (the default)Available while compiling. Packed into your jar when you shade it (next section).A normal library the server does not have.

A plugin API such as Vault is also compileOnly, but you must additionally tell Paper to load Vault first, with depend or softdepend in plugin.yml. That story is in Working with other plugins.

Seeing what you actually depend on

Libraries often need other libraries, which need others again. These are transitive dependencies. Print the tree to see them. The classpath is the list of libraries Java can use. Asking for runtimeClasspath shows the list your plugin needs while it runs, and notice that the Paper API is missing from it because it is compileOnly:

PowerShell
 .\gradlew.bat dependencies --configuration runtimeClasspathruntimeClasspath - Runtime classpath of source set 'main'.\--- com.github.ben-manes.caffeine:caffeine:3.3.0     +--- org.jspecify:jspecify:1.0.1     \--- com.google.errorprone:error_prone_annotations:2.50.0

With Maven the same question is mvn dependency:tree.

Shading: packing a library into your jar

Here is the trap. You added Caffeine with implementation, your code compiles, you copy the jar to the server, and the plugin crashes at startup with NoClassDefFoundError. The server has the Paper API, but nobody put Caffeine on the server, and a plain jar contains only your own classes.

Shading fixes this: the build tool copies the library's classes into your jar, so your plugin carries everything it needs. Gradle does it with the Shadow plugin and Maven with the Maven Shade plugin.

Relocation: avoiding clashes

One more problem. Imagine two plugins both shade Caffeine, but different versions. On a server, both would use the same class names, and one plugin would end up running the other's version. Relocation prevents this: while packing, the build tool renames the library's package. Caffeine's com.github.benmanes.caffeine becomes com.example.myplugin.libs.caffeine inside your jar, and it also rewrites your own code to match. Now only your plugin sees that copy.

Your classes com.example.myplugin Library (implementation) com.github.benmanes Paper API (compileOnly) stays out of the jar shadowJar copy and rename not packed One plugin jar Your classes unchanged The library, renamed com.example.myplugin .libs.caffeine no clash with others
Shading with relocation: your classes stay as they are, the library is copied in under a new package name, and the Paper API stays out.

Always relocate to a package inside your own, so the new name is unique to you.

build.gradle.kts (relevant parts)
1plugins {2    java3    id("xyz.jpenilla.run-paper") version "3.1.0"4    id("com.gradleup.shadow") version "9.6.1"5}6 7dependencies {8    compileOnly("io.papermc.paper:paper-api:$paperVersion")9    implementation("com.github.ben-manes.caffeine:caffeine:3.3.0")10}11 12tasks {13    shadowJar {14        archiveClassifier = ""15        relocate("com.github.benmanes.caffeine", "com.example.myplugin.libs.caffeine")16    }17 18    build {19        dependsOn(shadowJar)20    }21}
  1. Adds the Shadow plugin. It brings a new task called shadowJar.
  2. The shaded jar is normally named my-plugin-1.0.0-all.jar. An empty classifier gives it the plain name my-plugin-1.0.0.jar, so there is only one jar to pick up.
  3. First the library's package, then the new name. Use a package inside your own.
  4. Makes build also run shadowJar, so the jar in build\libs is always the shaded one.

Build, then look inside the jar to be sure. Only the shaded names should exist, and your classes should sit beside them:

PowerShell
 .\gradlew.bat build jar tf build\libs\my-plugin-1.0.0.jarcom/example/myplugin/MyPlugin.classcom/example/myplugin/libs/caffeine/cache/Caffeine.classplugin.yml

Good to know: when the Shadow plugin is present, run-paper starts the test server with the shaded jar automatically, so runServer tests the same jar you will ship.

pom.xml (inside build > plugins)
1<plugin>2    <groupId>org.apache.maven.plugins</groupId>3    <artifactId>maven-shade-plugin</artifactId>4    <version>3.6.2</version>5    <executions>6        <execution>7            <phase>package</phase>8            <goals>9                <goal>shade</goal>10            </goals>11            <configuration>12                <relocations>13                    <relocation>14                        <pattern>com.github.benmanes.caffeine</pattern>15                        <shadedPattern>com.example.myplugin.libs.caffeine</shadedPattern>16                    </relocation>17                </relocations>18                <createDependencyReducedPom>false</createDependencyReducedPom>19            </configuration>20        </execution>21    </executions>22</plugin>
  1. Run the shade step as part of package, right after the jar is made.
  2. The library's package, exactly as it is now.
  3. The new name. Use a package inside your own.
  4. Stops Maven from writing an extra pom file you do not need.

After mvn package, the jar in target is the shaded one, and an untouched copy is kept as original-my-plugin-1.0.0.jar. Install the one without original- in its name.

Filling in plugin.yml automatically

Changing your version in three places is how bugs happen. Build tools can copy the version into plugin.yml for you. You write a placeholder in the file and the build replaces it while copying resources. This is what the paper-templates projects do.

src/main/resources/plugin.yml
1name: BuildInfo2version: '${version}'3main: com.example.refgradlemavenversion.BuildInfoPlugin4api-version: '${apiVersion}'
  1. A placeholder. The build swaps it for the real version.
  2. Same here, for the Minecraft version.
build.gradle.kts (inside tasks)
1processResources {2    val properties = mapOf("version" to project.version, "apiVersion" to minecraftVersion)3    inputs.properties(properties)4    filteringCharset = Charsets.UTF_8.name()5    filesMatching(listOf("plugin.yml", "paper-plugin.yml")) {6        expand(properties)7    }8}
  1. The names on the left are what you can write between ${ and } in the file. The values come from gradle.properties.
  2. Only these two files are filled in. Other resources are copied untouched, which matters because a stray $ in a config file would break the expansion.
  3. Does the swap.
src/main/resources/plugin.yml
1name: BuildInfo2version: '${project.version}'3main: com.example.refgradlemavenversion.BuildInfoPlugin4api-version: '${minecraft.version}'
  1. Maven's built-in name for the version line of the pom.
  2. A property you define yourself in the pom, see below.
pom.xml (relevant parts)
1<properties>2    <minecraft.version>26.3</minecraft.version>3</properties>4 5<build>6    <resources>7        <resource>8            <directory>src/main/resources</directory>9            <filtering>true</filtering>10        </resource>11    </resources>12</build>
  1. Your own property. Any name works; it must match the placeholder.
  2. Turns on placeholder replacement for every file in the resources folder.

To prove it works, here is a tiny plugin that prints what ended up in plugin.yml. getPluginMeta() gives you the data Paper read from the file, so you can check the build did its job without opening the jar:

BuildInfoPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesref-gradle-maven-versionsrcmainjavacomexamplerefgradlemavenversionBuildInfoPlugin.java

The package com.example.refgradlemavenversion is the folder path com/example/refgradlemavenversion inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • ref-gradle-maven-version/
    • src/main/
      • java/com/example/refgradlemavenversion/Package com.example.refgradlemavenversion
        • BuildInfoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.refgradlemavenversion;2 3import io.papermc.paper.plugin.configuration.PluginMeta;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class BuildInfoPlugin extends JavaPlugin {7 8    @Override9    public void onEnable() {10        PluginMeta meta = getPluginMeta();11        getLogger().info("Running " + meta.getName() + " " + meta.getVersion());12        getLogger().info("Written for Paper API " + meta.getAPIVersion());13    }14}
  1. Returns the information Paper read from plugin.yml: name, version, API version and more.
  2. If the placeholder was never replaced, this would print the literal text ${version}. That is the sign that resource processing is not working.
  3. The api-version line from plugin.yml.
Server console
[14:02:11 INFO]: [BuildInfo] Enabling BuildInfo v1.0.0[14:02:11 INFO]: [BuildInfo] Running BuildInfo 1.0.0[14:02:11 INFO]: [BuildInfo] Written for Paper API 26.3

Test server options (run-paper)

The runServer task comes from a Gradle plugin called run-paper. You configure it inside tasks { runServer { ... } }. Maven has no equivalent, but you can run a normal Paper server in a folder and copy your jar into its plugins folder after each mvn package.

build.gradle.kts (inside tasks)
1runServer {2    minecraftVersion(minecraftVersion)3    jvmArgs("-Xms2G", "-Xmx2G", "-Dcom.mojang.eula.agree=true")4    runDirectory.set(layout.projectDirectory.dir("test-server"))5    downloadPlugins {6        modrinth("luckperms", "v5.5.71-bukkit")7    }8}
  1. Which Paper version to download and run. The value comes from gradle.properties.
  2. Options for the Java process. -Xms2G -Xmx2G gives the server 2 GB of memory. The last one accepts Minecraft's EULA for you, so only use it once you have read the EULA.
  3. Where the server's world and config files go. The default is a folder called run. Change it to keep several test worlds apart.
  4. Downloads other plugins into the test server for you. Here LuckPerms from Modrinth. The second text is the version number shown on the plugin's Modrinth page.
OptionWhat it does
minecraftVersion("26.3")Paper version to run. Required.
jvmArgs(...)Extra Java options: memory, system properties, debug flags.
runDirectoryFolder the server runs in. Default run.
downloadPlugins { ... }Adds plugins from modrinth(id, version), hangar(name, version), github(owner, repo, tag, fileName) or url(address).
pluginJars(...)Adds jar files you already have on disk as extra plugins.
cleanPaperCache (a task)Deletes the downloaded Paper jars so they are fetched again.

The test server section explains what happens when you press Run, and the Test Server tool shows what a running server looks like.

Common build errors and fixes

When a build fails, Gradle prints FAILURE: Build failed with an exception and then, under What went wrong, the reason. Read that part first, then find your message below. Maven prints lines starting with [ERROR]. More error messages, with plugin startup and runtime errors, are in the Error encyclopedia, and you can paste any of them into the Error Doctor.

Could not find paper-api

PowerShell
* What went wrong:Execution failed for task ':compileJava'.> Could not resolve all files for configuration ':compileClasspath'.   > Could not find io.papermc.paper:paper-api:26.3.build.999-beta.     Searched in the following locations:       - https://repo.maven.apache.org/maven2/io/papermc/paper/paper-api/26.3.build.999-beta/paper-api-26.3.build.999-beta.pom       - https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/26.3.build.999-beta/paper-api-26.3.build.999-beta.pom

Gradle looked in every repository you listed and found no such file. Check three things, in this order: the version number has a typo (the real one is 26.3.build.142-beta at the time of writing, and the update-paper-version.ps1 script in paper-templates picks the newest), the PaperMC repository line is missing from repositories, or you have no internet connection. Run again with --refresh-dependencies after fixing.

Cannot get non-null property

PowerShell
* Where:Build file 'C:\Users\you\IdeaProjects\my-plugin\build.gradle.kts' line: 8* What went wrong:Cannot get non-null property 'paperVersion' on root project 'my-plugin' as it does not exist

The build file asks for a value that gradle.properties does not contain. Open gradle.properties and add the missing line (paperVersion=26.3.build.142-beta), or the file may have been deleted or saved in the wrong folder.

Plugin was not found

PowerShell
* What went wrong:Plugin [id: 'com.gradleup.shadow', version: '9.99.9'] was not found in any of the following sources:

The plugin id or its version is wrong. Copy both exactly from the plugin's page on plugins.gradle.org. Old tutorials that say com.github.johnrengelman.shadow are the usual cause.

The wrong Java version

PowerShell
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile (default-compile) on project my-plugin: Fatal error compiling: error: release version 25 not supported

Paper 26.3 needs Java 25, and the build is running on an older Java. With Gradle the java.toolchain line downloads a matching JDK for you, so this error is rarer there. With Maven, or when IntelliJ uses the wrong JDK, open FileProject Structure and pick a JDK 25 under SDK, then check that the terminal's java -version says 25. A similar message, Unsupported class file major version 69, means a tool is too old to read Java 25 class files: update that tool. The toolbox page shows how to install the JDK.

NoClassDefFoundError after the build succeeded

Server console
[14:02:11 ERROR]: Error occurred while enabling MyPlugin v1.0.0 (Is it up to date?)java.lang.NoClassDefFoundError: com/github/benmanes/caffeine/cache/Caffeine

The build was fine, but the server cannot find a library's class. You used implementation but did not shade, or you shaded and the jar you installed was the plain one. Build again, then open the jar (jar tf, shown above) and look for the library's classes. If you installed a jar named original-... or ...-all by mistake, install the other one.

Other quick fixes

  • IntelliJ shows red code but the terminal build works. IntelliJ has not reloaded the project. Click the Gradle reload button.
  • The jar is huge (megabytes of Paper classes). The Paper API is declared with implementation or compile. Change it to compileOnly or provided.
  • The plugin shows version ${version} in /plugins. Resource processing is not replacing the placeholder: check processResources (Gradle) or filtering (Maven).
  • The build seems stuck on an old file. Run clean build (Gradle) or clean package (Maven).
  • Weird characters in text after building. Set the encoding to UTF-8 as in the starter build file (options.encoding for Gradle, project.build.sourceEncoding for Maven).

Practice

Try it

Shade a library and check your work

Add the Caffeine cache library to a plugin project, make sure it is packed into the jar under your own package name, and prove it by listing the jar contents.

Hint 1

Caffeine's coordinates are com.github.ben-manes.caffeine:caffeine:3.3.0. It is not on the server, so it must be implementation.

Hint 2

You need the Shadow plugin in plugins, a relocate call with Caffeine's package com.github.benmanes.caffeine, and build must depend on shadowJar.

Show the solution

The three changes from the shading section, together. Gradle first:

build.gradle.kts (changes)
1plugins {2    id("com.gradleup.shadow") version "9.6.1"3}4 5dependencies {6    implementation("com.github.ben-manes.caffeine:caffeine:3.3.0")7}8 9tasks {10    shadowJar {11        archiveClassifier = ""12        relocate("com.github.benmanes.caffeine", "com.example.myplugin.libs.caffeine")13    }14 15    build {16        dependsOn(shadowJar)17    }18}

Then run .\gradlew.bat build and jar tf build\libs\my-plugin-1.0.0.jar. You should see lines starting with com/example/myplugin/libs/caffeine/ and none starting with com/github/benmanes. In your Java code you still write the normal import, import com.github.benmanes.caffeine.cache.Caffeine;, because the relocation happens afterward, inside the jar.

Try it

Bump the version once

Put the BuildInfo plugin from above into a project that uses the placeholders (any paper-templates project does), then change the plugin version to 1.1.0 by editing only one line in one file. Rebuild, and confirm that both the jar name and the console message show the new version.

Hint

In Gradle the version lives in gradle.properties. In Maven it is the version line of the pom. plugin.yml is filled in automatically, so it must not be edited.

Show the solution

Change version=1.0.0 to version=1.1.0 in gradle.properties (Maven: <version>1.1.0</version> in pom.xml). Run the build again. The jar is now named after your project and the new version, for example build\libs\build-info-1.1.0.jar (Maven: target\build-info-1.1.0.jar) and the server prints:

Server console
[14:09:40 INFO]: [BuildInfo] Running BuildInfo 1.1.0

One edit, three places updated: the jar name, the version inside plugin.yml, and what getVersion() returns.

Recap

  • A build tool finds libraries, compiles, processes resources and packs the jar. Gradle uses .\gradlew.bat build, Maven uses mvn package.
  • A dependency is a repository plus coordinates: group, name and version.
  • compileOnly (provided) is for code the server already has: the Paper API and other plugins. implementation is for libraries you ship.
  • Shading copies a library into your jar, and relocation renames its package so plugins cannot clash. Use the Shadow plugin com.gradleup.shadow or the Maven Shade plugin.
  • Placeholders in plugin.yml are filled in by processResources (Gradle) or resource filtering (Maven), so the version lives in one place.
  • The run-paper plugin gives you runServer, with options for the Minecraft version, memory, run folder and downloaded plugins.
  • When a build fails, read the What went wrong part first.

Quick quiz

  1. You add the Paper API with implementation instead of compileOnly. What goes wrong?

  2. Your plugin uses a small library from Maven Central. The build succeeds but the server prints NoClassDefFoundError for the library's class. What is the likely fix?

  3. What does relocation do?

  4. Where does Maven put the jar, and where does Gradle?

  5. The server lists your plugin with the version ${version} written literally. What is wrong?

Next steps