Anatomy of a plugin project
Every folder and file in a plugin project, and what each one does.
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)
- wrapper/The Gradle wrapper: makes everyone use the same Gradle version
- 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
- firstplugin/The package com.example.firstplugin
- example/
- com/
- resources/Files copied into the jar unchanged
- plugin.ymlTells Paper the plugin's name, version and main class
- java/Java code, in package folders
- main/
- .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:
| Group | Files and folders | What you do with them |
|---|---|---|
| Yours | src/ (Java code, plugin.yml, config.yml) | Edit every day. |
| Build setup | build.gradle.kts, settings.gradle.kts, gradle.properties, gradle/wrapper/, gradlew, gradlew.bat, .run/, .gitignore | Edit now and then, on purpose: a new version number, a new library. |
| Generated | build/, run/, .gradle/, .idea/ | Never edit by hand. Tools make them, and they can be remade. |
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.
1plugins {2 id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"3}4 5rootProject.name = "first-plugin"- A list of Gradle plugins, add-ons for Gradle itself. These are not Minecraft plugins; the word is just reused.
- 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. - The project's name. Gradle uses it to name the jar:
first-pluginplus the version becomesfirst-plugin-1.0.0.jar. It does not change the plugin's name in the game;plugin.ymldoes 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.
1group=com.example2version=1.0.03minecraftVersion=26.34paperVersion=26.3.build.142-beta5org.gradle.jvmargs=-Xmx2G -Dfile.encoding=UTF-8- 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.
- Your plugin's version. Gradle puts it in the jar's file name. Raise it when you release a new version.
- The Minecraft version the test server downloads and starts.
- The exact version of the Paper API your code is compiled against.
142is the Paper build number;betais part of the official name. - 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.
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}- Turns on Gradle's Java support: compiling
src/main/java, copyingsrc/main/resources, and packing a jar. - The run-paper Gradle plugin. It adds the
runServertask that downloads Paper and starts your test server. - Reads
minecraftVersionfromgradle.propertiesso the lines below can use it. - Reads
paperVersionthe same way. - Where Gradle downloads libraries from. A
repository is an online library shelf. - Maven Central, the biggest public shelf of Java libraries.
- PaperMC's own shelf, which holds the Paper API. Without this line Gradle could not find it.
- A
dependency : a library your code needs. The text isgroup:name:version, and$paperVersionis replaced with the value fromgradle.properties.compileOnlyis explained right below. - Compiles with Java 25, whatever Java your PC starts Gradle with. If Gradle cannot find a JDK 25, the foojay line in
settings.gradle.ktslets it download one. - Reads your
.javafiles as UTF-8, so characters like accented letters and symbols in messages compile correctly on every PC. - Settings for the test server that Run Paper Server starts.
- Which Minecraft version of Paper to download: the value from
gradle.properties. - 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.
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- 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 name | Folder under src/main/java |
|---|---|
com.example.firstplugin | com\example\firstplugin |
dev.jace.homes | dev\jace\homes |
dev.jace.homes.commands | dev\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:
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 exampledev.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 . 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:
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
- java/com/example/projectanatomyconfig/Package com.example.projectanatomyconfig
- 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
- src/main/
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.'- 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:
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
- java/com/example/projectanatomyconfig/Package com.example.projectanatomyconfig
- 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
- src/main/
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}- The main class can be a listener too. That keeps this small example in one file.
- Copies
config.ymlout 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. - The first
thisis the listener (this class), the second is the plugin (also this class). - Prints the value it read, so you can check the console that the file was found.
- 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:
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.ymlWhen 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:
[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.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 taskcleandoes 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
.gitignorefile lists what Git should skip:
1.gradle/2build/3run/4out/5.idea/6*.iml7.kotlin/- Gradle output.
- The test server, with its worlds and logs.
- IntelliJ settings that are specific to your PC.
- 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
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>- The same
groupas ingradle.properties. - The project name, like
rootProject.name. With the version it names the jar. - Compile for Java 25, like the Gradle toolchain line.
- The PaperMC repository, like
maven(...)in Gradle. - Maven's word for
compileOnly: the server provides the Paper API, so it stays out of your jar. - 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:
| Place | Before | After | What it controls |
|---|---|---|---|
name: in plugin.yml | FirstPlugin | Greeter | The name in the console, /plugins, and the data folder |
| Main class (file and class) | FirstPlugin | GreeterPlugin | The Java class Paper creates |
Package (folders and package lines) | com.example.firstplugin | com.example.greeter | Where the classes live |
main: in plugin.yml | com.example.firstplugin.FirstPlugin | com.example.greeter.GreeterPlugin | How Paper finds the main class |
rootProject.name (optional) | first-plugin | greeter | The 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:
1public final class GreeterPlugin {2}- The class was renamed, but the file is still called
FirstPlugin.java.
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 . 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
- java/com/example/greeter/
The top of GreeterPlugin.java:
1package com.example.greeter;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class GreeterPlugin extends JavaPlugin {And 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 saysCannot find main class `com.example.firstplugin.FirstPlugin'and does not load the plugin. This one only shows up when the server starts. - Forgot a
packageline (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\Greeterinstead ofplugins\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.ktsnames the project;gradle.propertiesholds the versions;build.gradle.ktsis 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
packageline must match the folder. - Resources are copied into the jar unchanged;
plugin.ymlmust be insrc/main/resources. - To rename a plugin, change the plugin.yml name, the class, the package and the
main:line together.
Quick quiz
You changed
build/resources/main/plugin.yml, but after the next build your change is gone. Why?Gradle copiessrc/main/resources/plugin.ymlintobuild/each time it builds. Edit the source copy undersrc/main.Why is the Paper API added with
compileOnlyinstead of being packed into your jar?Your code needs the API to compile, and the server supplies it at runtime. Packing it in would make the jar huge and give the server two copies of the same classes.Which package name is valid?
Package names are lowercase letters and digits, no dashes, and no part may start with a digit.What decides the jar's file name,
first-plugin-1.0.0.jar?Gradle names the jar after the project and its version. The plugin's in-game name comes from plugin.yml and can be different.You delete the
runfolder. What happens next time you press Run Paper Server?run/only holds the test server. Your code is insrc/, and the build does not needrun/at all.
Next steps
- Build, install and test: build the jar, install it on a real server and read the console.
- plugin.yml explained: every field the file can have.
- Gradle and Maven reference: more tasks, libraries and settings.