Paper Plugin Guide
File mode0

Start here

Anatomy of a plugin project

Every folder and file in a plugin project, and what each one does.

Beginner24 min read

A plugin project has a dozen files and folders, and only a few of them are yours to edit. On this page you will learn what every one of them does, read the three Gradle files line by line, see how your code and resources end up inside the jar, and rename a plugin without breaking it.

This page uses the project from Your first plugin. If you have it open in IntelliJ, keep it next to this page and click through the folders as you read.

The whole project at a glance

Here is the first-plugin project after you have run the test server and built the jar once. Hover a name to see what it is for.

  • first-plugin/
    • .gradle/Gradle's private cache for this project. Never edit; safe to delete
    • .idea/IntelliJ's settings for this project, such as which JDK it uses. Created when you open the project
    • .run/The run configurations that appear in IntelliJ's run drop-down
      • Build Plugin Jar.run.xmlThe Build Plugin Jar button: runs the Gradle task build
      • Run Paper Server.run.xmlThe Run Paper Server button: runs the Gradle task runServer
    • build/Everything Gradle produces. Deleted and remade freely
      • classes/Your compiled .class files
      • libs/The finished plugin jar
        • first-plugin-1.0.0.jarThe file you put on a server
    • gradle/
      • wrapper/The Gradle wrapper: makes everyone use the same Gradle version
        • gradle-wrapper.jarA tiny program that downloads the right Gradle version
        • gradle-wrapper.propertiesWhich Gradle version to download (9.8.0)
    • run/The test server made by Run Paper Server: worlds, logs, server settings
      • logs/Server log files; latest.log is the current run
      • plugins/Data folders of the plugins on the test server
      • world/The test world
      • server.propertiesSettings of the test server, such as the port
    • src/Your source code: the only folder you work in every day
      • main/
        • java/Java code, in package folders
          • com/
            • example/
              • firstplugin/The package com.example.firstplugin
                • FirstPlugin.javaThe main class
                • JoinListener.javaThe join listener
        • resources/Files copied into the jar unchanged
          • plugin.ymlTells Paper the plugin's name, version and main class
    • .gitignoreTells Git which folders not to save (build, run, .gradle, .idea)
    • build.gradle.ktsThe build recipe: plugins, repositories, dependencies, Java version, test server
    • gradle.propertiesValues the build reads: group, version, Paper version
    • gradlewStarts the Gradle wrapper on macOS and Linux
    • gradlew.batStarts the Gradle wrapper on Windows
    • settings.gradle.ktsThe project's name, and where Gradle may download Java from

That looks like a lot, but it sorts into three groups:

GroupFiles and foldersWhat you do with them
Yourssrc/ (Java code, plugin.yml, config.yml)Edit every day.
Build setupbuild.gradle.kts, settings.gradle.kts, gradle.properties, gradle/wrapper/, gradlew, gradlew.bat, .run/, .gitignoreEdit now and then, on purpose: a new version number, a new library.
Generatedbuild/, run/, .gradle/, .idea/Never edit by hand. Tools make them, and they can be remade.
What Gradle does with your project Gradle reads build.gradle.kts, downloads paper-api, compiles the code in src, adds plugin.yml and writes the plugin jar into build/libs. The API is not packed into the jar because the server already has it. paper-api downloaded from repo.papermc.io build.gradle.kts what to build and with what src/main/java your Java code src/main/resources plugin.yml, config.yml Gradle ./gradlew build 1. get paper-api 2. compile your code 3. pack the jar build/libs/ my-plugin-1.0.0.jar copy this to plugins/ paper-api is not inside the jar: the server already has it. Read left to right: inputs, the build tool, the plugin jar.
How the Gradle files, your source code and the build output fit together

settings.gradle.kts

Gradle reads three files before it builds anything. The first is settings.gradle.kts. The .kts ending means it is written in Kotlin, another programming language. You do not need to learn Kotlin; the build files use a handful of patterns that you can read like a form.

settings.gradle.kts
1plugins {2    id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"3}4 5rootProject.name = "first-plugin"
  1. A list of Gradle plugins, add-ons for Gradle itself. These are not Minecraft plugins; the word is just reused.
  2. Lets Gradle download a Java 25 JDK by itself if your PC does not have one. "foojay" is the name of the website it asks for Java downloads.
  3. The project's name. Gradle uses it to name the jar: first-plugin plus the version becomes first-plugin-1.0.0.jar. It does not change the plugin's name in the game; plugin.yml does that.

gradle.properties

This file holds simple name=value settings that the build file reads. Keeping them here means the values you change most often sit in one short, easy file.

gradle.properties
1group=com.example2version=1.0.03minecraftVersion=26.34paperVersion=26.3.build.142-beta5org.gradle.jvmargs=-Xmx2G -Dfile.encoding=UTF-8
  1. Who made the project, written like a package name. It matters if you publish the project as a library; for a plugin it is just a label.
  2. Your plugin's version. Gradle puts it in the jar's file name. Raise it when you release a new version.
  3. The Minecraft version the test server downloads and starts.
  4. The exact version of the Paper API your code is compiled against. 142 is the Paper build number; beta is part of the official name.
  5. Settings for Gradle's own Java process: up to 2 GB of memory, and UTF-8 text so special characters survive.

build.gradle.kts

This is the build recipe, the most important of the three. Read it top to bottom, one block at a time. Each block is a name followed by braces { } with settings inside.

build.gradle.kts
1plugins {2    java3    id("xyz.jpenilla.run-paper") version "3.1.0"4}5 6val minecraftVersion: String by project7val paperVersion: String by project8 9repositories {10    mavenCentral()11    maven("https://repo.papermc.io/repository/maven-public/")12}13 14dependencies {15    compileOnly("io.papermc.paper:paper-api:$paperVersion")16}17 18java {19    toolchain.languageVersion = JavaLanguageVersion.of(25)20}21 22tasks {23    compileJava {24        options.encoding = Charsets.UTF_8.name()25    }26 27    runServer {28        minecraftVersion(minecraftVersion)29        jvmArgs("-Xms2G", "-Xmx2G", "-Dcom.mojang.eula.agree=true")30    }31}
  1. Turns on Gradle's Java support: compiling src/main/java, copying src/main/resources, and packing a jar.
  2. The run-paper Gradle plugin. It adds the runServer task that downloads Paper and starts your test server.
  3. Reads minecraftVersion from gradle.properties so the lines below can use it.
  4. Reads paperVersion the same way.
  5. Where Gradle downloads libraries from. A repository is an online library shelf.
  6. Maven Central, the biggest public shelf of Java libraries.
  7. PaperMC's own shelf, which holds the Paper API. Without this line Gradle could not find it.
  8. A dependency: a library your code needs. The text is group:name:version, and $paperVersion is replaced with the value from gradle.properties. compileOnly is explained right below.
  9. Compiles with Java 25, whatever Java your PC starts Gradle with. If Gradle cannot find a JDK 25, the foojay line in settings.gradle.kts lets it download one.
  10. Reads your .java files as UTF-8, so characters like accented letters and symbols in messages compile correctly on every PC.
  11. Settings for the test server that Run Paper Server starts.
  12. Which Minecraft version of Paper to download: the value from gradle.properties.
  13. Options for the server's Java process: 2 GB of memory, and accepting the Minecraft EULA for this local test server.

Why compileOnly?

Your code uses classes like JavaPlugin and Player, so the compiler needs the Paper API to check your code. But every Paper server already contains the whole API. If Gradle packed the API into your jar as well, your 5 KB plugin would grow to many megabytes, and the server would find two copies of the same classes, which causes strange errors.

compileOnly means exactly that: "use this library while compiling, but do not put it in the jar". Libraries the server does not have are added differently; the Gradle and Maven reference covers that.

Warnings you may see

With Gradle 9.8, a build may end with the lines "Deprecated Gradle features were used in this build, making it incompatible with Gradle 10". If you add --warning-mode all to the command, Gradle explains that the val ... by project style (the two lines near the top of build.gradle.kts) will be removed in Gradle 10. These are warnings, not errors: the build still says BUILD SUCCESSFUL, and your jar is fine.

The Gradle wrapper

You never installed Gradle, yet it runs. That is the job of the Gradle wrapper: gradlew.bat (Windows), gradlew (macOS and Linux) and the gradle/wrapper folder. The first time you build, the wrapper downloads exactly the Gradle version the project asks for and keeps it in your user folder for later. Everyone who opens the project gets the same Gradle, so "it works on my PC" problems are rare.

gradle/wrapper/gradle-wrapper.properties
1distributionBase=GRADLE_USER_HOME2distributionPath=wrapper/dists3distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip4networkTimeout=100005retries=06retryBackOffMs=5007validateDistributionUrl=true8zipStoreBase=GRADLE_USER_HOME9zipStorePath=wrapper/dists
  1. The only line that matters: which Gradle version to download, here 9.8.0. The backslash before the colon is how properties files write a colon.

When you type .\gradlew.bat build in a terminal, you are asking the wrapper to run Gradle's build task. The Build Plugin Jar button does the same thing for you.

src/main/java and packages

All your Java code lives under src/main/java, sorted into packages. A package is a folder of classes with a dotted name, and each dot in the name is one more folder:

Package nameFolder under src/main/java
com.example.firstplugincom\example\firstplugin
dev.jace.homesdev\jace\homes
dev.jace.homes.commandsdev\jace\homes\commands

The first line of every Java file says which package it belongs to, and it must match the folder the file is in:

src/main/java/com/example/firstplugin/FirstPlugin.java (first line)
1package com.example.firstplugin;

Why does Java bother? Because a class's full name is its package plus its class name: com.example.firstplugin.FirstPlugin. That full name is what you write after main: in plugin.yml, and it is how Paper finds your class inside the jar. Hundreds of plugins have a class called Main or MyListener; the package keeps yours apart from theirs, the way coordinates keep two identical houses apart.

Naming your package

Package names are written as a website address turned around: example.com becomes com.example, then you add your plugin's name. You do not need to own a website. Common choices:

  • dev.yourname.pluginname, for example dev.jace.homes.
  • io.github.yourname.pluginname, if you have a GitHub account with that name.
  • com.example.pluginname, fine for learning and examples (this guide uses it), but pick a real one before you share a plugin.

The rules: all lowercase, letters and digits only, no dashes or spaces, and no part may start with a digit. dev.jace.my-plugin is not allowed; write dev.jace.myplugin. Class names, on the other hand, start with a capital letter: HomesPlugin, JoinListener.

When you want a new class, right-click the package in IntelliJ and choose NewJava Class. IntelliJ puts the file in the right folder and writes the package line for you. For more on packages and the import lines that go with them, see Packages, imports and visibility.

src/main/resources

Everything in src/main/resources is copied into the jar exactly as it is, at the top level of the jar. Two files commonly live here:

  • plugin.yml: required. Paper looks for it at the top of every plugin jar. If it is missing or in the wrong folder, the server ignores your jar.
  • config.yml: optional. The default settings you ship with the plugin, which server owners can change.

To see the journey of a resource, here is a small plugin that ships a config.yml and reads its welcome message from it. First the file in src/main/resources:

config.ymlCompiles on Paper 26.3Compile Lab
Where this file livesproject-anatomy-configsrcmainresourcesconfig.yml

Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.

  • project-anatomy-config/
    • src/main/
      • java/com/example/projectanatomyconfig/Package com.example.projectanatomyconfig
        • ConfigWelcomePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • config.ymlyou are hereDefault settings that server owners can change
        • 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.

1welcome-message: '<gold>Welcome! <gray>This text comes from config.yml.'
  1. A setting name and its value. The quotes keep the MiniMessage text together as one piece of text.

And the main class that uses it:

ConfigWelcomePlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-anatomy-configsrcmainjavacomexampleprojectanatomyconfigConfigWelcomePlugin.java

The package com.example.projectanatomyconfig is the folder path com/example/projectanatomyconfig 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.

  • project-anatomy-config/
    • src/main/
      • java/com/example/projectanatomyconfig/Package com.example.projectanatomyconfig
        • ConfigWelcomePlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • 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.projectanatomyconfig;2 3import org.bukkit.event.EventHandler;4import org.bukkit.event.Listener;5import org.bukkit.event.player.PlayerJoinEvent;6import org.bukkit.plugin.java.JavaPlugin;7 8public final class ConfigWelcomePlugin extends JavaPlugin implements Listener {9 10    @Override11    public void onEnable() {12        saveDefaultConfig();13        getServer().getPluginManager().registerEvents(this, this);14        getLogger().info("Welcome message: " + getConfig().getString("welcome-message"));15    }16 17    @EventHandler18    public void onJoin(PlayerJoinEvent event) {19        String message = getConfig().getString("welcome-message", "<green>Welcome!");20        event.getPlayer().sendRichMessage(message);21    }22}
  1. The main class can be a listener too. That keeps this small example in one file.
  2. Copies config.yml out of the jar into the plugin's own folder on the server, but only if no file is there yet. A server owner's edits are never overwritten.
  3. The first this is the listener (this class), the second is the plugin (also this class).
  4. Prints the value it read, so you can check the console that the file was found.
  5. Reads the setting from the copied file. The second text is a fallback, used if someone deleted the setting.

Build this plugin, open the jar (a jar is a zip file, so any zip tool can open it, or use the jar tool that comes with Java), and you see your classes in their package folders, with the two resource files at the top:

Terminal
 jar tf build\libs\project-anatomy-config-1.0.0.jarMETA-INF/META-INF/MANIFEST.MFcom/com/example/com/example/projectanatomyconfig/com/example/projectanatomyconfig/ConfigWelcomePlugin.classconfig.ymlplugin.yml
Your project first-plugin-1.0.0.jar src/main/java/.../firstplugin/ FirstPlugin.java JoinListener.java src/main/resources/ plugin.yml config.yml com/example/firstplugin/ FirstPlugin.class JoinListener.class top of the jar plugin.yml config.yml META-INF/MANIFEST.MF compiled copied as-is added by Gradle
Gradle compiles the .java files into .class files and copies the resources unchanged. (This picture shows the first plugin with a config.yml added.)

When the server starts the plugin, saveDefaultConfig() creates the plugin's data folder, a folder named after the plugin inside the server's plugins folder, and copies config.yml into it. On the test server that is run\plugins\ConfigWelcome\config.yml. The console confirms it read the file:

Run: Run Paper Server
[14:06:02 INFO]: [ConfigWelcome] Enabling ConfigWelcome v1.0.0[14:06:02 INFO]: [ConfigWelcome] Welcome message: <gold>Welcome! <gray>This text comes from config.yml.
Welcome! This text comes from config.yml.

Notice the two copies: the one in src/main/resources is the default you ship, and the one in the data folder is the server owner's. After the first start, editing the file in src does not change the test server's copy; delete run\plugins\ConfigWelcome\config.yml to get the new default. Configuration files goes much further.

The build and run folders

build/ holds everything Gradle makes: compiled classes, copied resources, reports, and the jar in build/libs. run/ is the test server: Paper itself, the worlds, server.properties, logs, and your plugins' data folders. Run Paper Server loads your jar straight from build/libs, so you will not find it in run/plugins.

Both folders are made by tools from your source files, so they are not part of your project's "real" content:

  • Deleting build/ is always safe. The next build remakes it. The Gradle task clean does exactly that.
  • Deleting run/ resets the test server. Paper is downloaded again (or taken from a cache) and you get a brand-new world. Handy when a test world is a mess; just remember your builds in that world go with it.
  • Neither is saved with Git. When you put your project on GitHub, you share the recipe, not the cake. The .gitignore file lists what Git should skip:
.gitignore
1.gradle/2build/3run/4out/5.idea/6*.iml7.kotlin/
  1. Gradle output.
  2. The test server, with its worlds and logs.
  3. IntelliJ settings that are specific to your PC.
  4. The star means "any name": every file ending in .iml, another kind of IntelliJ settings file.

Git and GitHub for plugin developers explains Git itself.

Gradle or Maven

Gradle is not the only build tool. Many plugins, and the IntelliJ Minecraft Development wizard, use Maven instead. Both do the same job: download the Paper API, compile, and pack a jar. Only the build files differ. This guide uses Gradle everywhere, but you will meet Maven projects online, so here is the same plugin both ways.

  • first-plugin/
    • gradle/wrapper/The wrapper that downloads Gradle 9.8.0
    • build.gradle.ktsThe build recipe
    • gradle.propertiesVersions and other values
    • settings.gradle.ktsThe project name
    • gradlew.batRun Gradle on Windows
    • src/main/java/Your code
    • src/main/resources/plugin.yml and other resources

Build with .\gradlew.bat build or the Build Plugin Jar button. The jar lands in build\libs\first-plugin-1.0.0.jar. The test server comes from the run-paper plugin.

  • first-plugin/
    • pom.xmlThe whole build recipe in one XML file
    • src/main/java/Your code, exactly as in Gradle
    • src/main/resources/plugin.yml and other resources, exactly as in Gradle
pom.xml
1<?xml version="1.0" encoding="UTF-8"?>2<project xmlns="http://maven.apache.org/POM/4.0.0"3         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"4         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">5    <modelVersion>4.0.0</modelVersion>6 7    <groupId>com.example</groupId>8    <artifactId>first-plugin</artifactId>9    <version>1.0.0</version>10    <packaging>jar</packaging>11 12    <properties>13        <java.version>25</java.version>14        <maven.compiler.release>${java.version}</maven.compiler.release>15        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>16    </properties>17 18    <repositories>19        <repository>20            <id>papermc-repo</id>21            <url>https://repo.papermc.io/repository/maven-public/</url>22        </repository>23    </repositories>24 25    <dependencies>26        <dependency>27            <groupId>io.papermc.paper</groupId>28            <artifactId>paper-api</artifactId>29            <version>26.3.build.142-beta</version>30            <scope>provided</scope>31        </dependency>32    </dependencies>33 34    <build>35        <plugins>36            <plugin>37                <groupId>org.apache.maven.plugins</groupId>38                <artifactId>maven-compiler-plugin</artifactId>39                <version>3.15.0</version>40            </plugin>41        </plugins>42    </build>43</project>
  1. The same group as in gradle.properties.
  2. The project name, like rootProject.name. With the version it names the jar.
  3. Compile for Java 25, like the Gradle toolchain line.
  4. The PaperMC repository, like maven(...) in Gradle.
  5. Maven's word for compileOnly: the server provides the Paper API, so it stays out of your jar.
  6. The Maven plugin that compiles Java, pinned to a version that knows Java 25.

Build with mvn package (or the Maven tool window in IntelliJ). The jar lands in target\first-plugin-1.0.0.jar: Maven uses target where Gradle uses build. Maven has no built-in test server, so you copy the jar to a server yourself.

The src folder is identical in both. That is the good news: if you find a Maven plugin online, its Java code works the same way, and you can move it into a Gradle project by copying the src folder and adding its dependencies to build.gradle.kts.

Renaming a plugin safely

Sooner or later you will want a better name than FirstPlugin. A plugin's name appears in several places, and they must stay in step. Here is everything that carries a name, using a rename to Greeter as the example:

PlaceBeforeAfterWhat it controls
name: in plugin.ymlFirstPluginGreeterThe name in the console, /plugins, and the data folder
Main class (file and class)FirstPluginGreeterPluginThe Java class Paper creates
Package (folders and package lines)com.example.firstplugincom.example.greeterWhere the classes live
main: in plugin.ymlcom.example.firstplugin.FirstPlugincom.example.greeter.GreeterPluginHow Paper finds the main class
rootProject.name (optional)first-plugingreeterThe jar's file name

Let IntelliJ do the Java part. Renaming by hand misses places; IntelliJ's rename tool, Shift+F6, changes the name everywhere it is used, renames the file, and moves folders for packages. It does not touch plugin.yml reliably, so you update that file yourself.

Here is what happens when you rename the class inside the file but not the file itself. The class is public, so Java insists that the file name matches:

FirstPlugin.javaRuns on Java 25
1public final class GreeterPlugin {2}
  1. The class was renamed, but the file is still called FirstPlugin.java.
Try it

Rename FirstPlugin to Greeter

In your first-plugin project, rename the plugin to Greeter: plugin name Greeter, main class GreeterPlugin, package com.example.greeter. Then run the test server and check that Greeter appears in plugins in green and the welcome message still works. Before you start, guess what goes wrong if you forget each of the four places.

Hint 1

Rename the class first: open FirstPlugin.java, put the cursor on the name FirstPlugin after class, press Shift+F6, type GreeterPlugin and press Enter.

Hint 2

For the package, right-click com.example.firstplugin in the project tree and choose RefactorRename.... If IntelliJ asks whether to rename the directory or the package, choose to rename the package. Then fix plugin.yml by hand.

Show the solution

After the renames, the files are:

  • src/main/
    • java/com/example/greeter/
      • GreeterPlugin.javaWas FirstPlugin.java
      • JoinListener.javaSame class, new package line
    • resources/
      • plugin.ymlNew name and main lines

The top of GreeterPlugin.java:

GreeterPlugin.java (top)
1package com.example.greeter;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class GreeterPlugin extends JavaPlugin {

And plugin.yml:

plugin.yml
1name: Greeter2version: '1.0.0'3main: com.example.greeter.GreeterPlugin4api-version: '26.3'5description: My first Paper plugin. It greets players when they join.

What breaks if you forget one:

  • Forgot the file name (renamed only the class text): the build fails with class GreeterPlugin is public, should be declared in a file named GreeterPlugin.java, as shown above.
  • Forgot main: in plugin.yml: the build succeeds, but the server says Cannot find main class `com.example.firstplugin.FirstPlugin' and does not load the plugin. This one only shows up when the server starts.
  • Forgot a package line (moved a file by hand): IntelliJ underlines it in red with "Package name does not correspond to the file path". Fix the line so it matches the folder.
  • Forgot name:: nothing breaks, but the plugin is still called FirstPlugin in the console. Note that a new name also means a new data folder: plugins\Greeter instead of plugins\FirstPlugin, so settings saved under the old name are not picked up.

Recap

  • You edit src/ every day, the build files now and then, and never the generated folders (build/, run/, .gradle/, .idea/).
  • settings.gradle.kts names the project; gradle.properties holds the versions; build.gradle.kts is the recipe.
  • The Paper API is compileOnly (Maven: provided) because every server already has it.
  • The Gradle wrapper (gradlew.bat) downloads the right Gradle for you.
  • Packages are folders with dotted, lowercase names, and the package line must match the folder.
  • Resources are copied into the jar unchanged; plugin.yml must be in src/main/resources.
  • To rename a plugin, change the plugin.yml name, the class, the package and the main: line together.

Quick quiz

  1. You changed build/resources/main/plugin.yml, but after the next build your change is gone. Why?

  2. Why is the Paper API added with compileOnly instead of being packed into your jar?

  3. Which package name is valid?

  4. What decides the jar's file name, first-plugin-1.0.0.jar?

  5. You delete the run folder. What happens next time you press Run Paper Server?

Next steps