Creating a Plugin
In the previous page we have setup our development environment and created a very basic, empty project structure using Gradle. On this page, we will create our first plugin logic and successfully run it on a Paper server!
Where we left off
Section titled “Where we left off”These are the files that should be present in your project directory. Furthermore, your build.gradle.kts
should have the following content.
File structure
DirectoryBeginnerPlugin
Directorygradle/
- …
Directorysrc
Directorymain
Directoryjava
Directoryyour
Directorypackage
Directoryname
- BeginnerPlugin.java
- build.gradle.kts
- gradlew
- gradlew.bat
- settings.gradle.kts
build.gradle.kts
plugins { id("java")}
group = "your.package.name"version = "1.0-SNAPSHOT"
repositories { maven("https://repo.papermc.io/repository/maven-public/")}
dependencies { compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")}
The JavaPlugin
class
Section titled “The class”First, we will fill up our BeginnerPlugin.java
file with a bit of logic. Every Paper plugin needs to have a class with extends the JavaPlugin
class.
It is furthermore considered good practice to make your plugin’s main class (this is what you call the class that extends
JavaPlugin
) final
to avoid any intended or unintended
extending.
You then have a few methods you can override. They are listed in the following table:
Method Signature | Purpose | Good to know |
---|---|---|
void onLoad() | First method that is called when the server loads the plugin into memory. | Most Paper API is not available yet, so their usage is not advised. |
void onEnable() | Called right before the server starts to tick. | A lot of Paper API isn’t available yet either, but you can register event listeners here. |
void onDisable() | Called before any plugin is unloaded. | You should use this to close database connections and save data to disk. |
Your plugin main class also inherits a few useful methods. Under those, these are the most important ones:
Method Signature | Return value purpose |
---|---|
ComponentLogger getComponentLogger() | You can use the ComponentLogger to log information into the server console. |
Path getDataPath() | This points to the file path where you should store your plugin configs and data (/plugins/YourPluginName/ ). |
File getDataFile() | Same as getDataPath() , except as a File object instead. |
PluginMeta getPluginMeta() | Provides useful information about your plugin, like the plugin version or the author. |
Setting up our main class
Section titled “Setting up our main class”For the sake of simplicity, we will execute some very simple logging in each phase of our plugins lifetime just to get acquainted with the methods.
package com.learnpaperdev.beginner;
import org.bukkit.plugin.java.JavaPlugin;
public final class BeginnerPlugin extends JavaPlugin {
@Override public void onLoad() { getComponentLogger().info("Our plugin has loaded!"); }
@Override public void onEnable() { getComponentLogger().warn("Our plugin has been enabled!"); }
@Override public void onDisable() { getComponentLogger().error("Our plugin has been disabled!"); }}
paper-plugin.yml
Section titled “paper-plugin.yml”With our main class sorted out, there is still one thing left to do: Adding a paper-plugin.yml
. Each Paper plugin needs to have either a plugin.yml
or paper-plugin.yml
file which declares the plugin’s name, version, where the main class is located, and other metadata.
For this, first create a new folder, src/main/resources
, and then create a new file, src/main/resources/paper-plugin.yml
.
Directorymain/java
Directorycom/learnpaperdev/beginner
- BeginnerPlugin.java
Directoryresources
- paper-plugin.yml
You will then want to fill the paper-plugin.yml
with the following:
name: BeginnerPluginapi-version: 1.21.4main: com.learnpaperdev.beginner.BeginnerPluginversion: 1.0-SNAPSHOT
These 4 entries are all required for the plugin to be loaded.
Running a test server
Section titled “Running a test server”First, you will want to head over to the Paper downloads page: https://papermc.io/downloads/paper. Download the latest build. Once you have done this, place the jar somewhere in a folder. It doesn’t matter where, just make sure you can find it again.
Now, you can open a new terminal session in that folder and run the following command (Your filename will be a bit different, as Paper may have released new builds by the time you read this):
java -jar paper-1.21.4-222.jar nogui
If it runs successfully, it will download the Vanilla jar, apply its patches, and start the server. It should quickly stop itself though, as you need to agree to the EULA. Head over to eula.txt
and set the value to true
.
eula=true
Rerun the server start command again. It should now successfully start the server. Close it with stop
.
And there you have your test server!
Adding our plugin
Section titled “Adding our plugin”Go back to our project and compile the project. Take the compiled jar (BeginnerPlugin-1.0-SNAPSHOT.jar
) and drop it into our test server’s plugins
folder.
When you now start the server, you should notice our logging take place:
java -jar paper-1.21.4-222.jar noguiStarting org.bukkit.craftbukkit.Main[22:08:43 INFO]: [bootstrap] Running Java 21 (Java HotSpot(TM) 64-Bit Server VM 21.0.6+8-LTS-188; Oracle Corporation null) on Windows 11 10.0 (amd64)[22:08:43 INFO]: [bootstrap] Loading Paper 1.21.4-222-main@9b1798d (2025-03-27T13:35:40Z) for Minecraft 1.21.4[22:08:43 INFO]: [PluginInitializerManager] Initializing plugins...[22:08:44 INFO]: [PluginInitializerManager] Initialized 1 plugin[22:08:44 INFO]: [PluginInitializerManager] Paper plugins (1): - BeginnerPlugin (1.0-SNAPSHOT)19 collapsed lines
[22:08:47 INFO]: Environment: Environment[sessionHost=https://sessionserver.mojang.com, servicesHost=https://api.minecraftservices.com, name=PROD][22:08:49 INFO]: Loaded 1370 recipes[22:08:49 INFO]: Loaded 1481 advancements[22:08:49 INFO]: [MCTypeRegistry] Initialising converters for DataConverter...[22:08:49 INFO]: [MCTypeRegistry] Finished initialising converters for DataConverter in 134,8ms[22:08:49 INFO]: Starting minecraft server version 1.21.4[22:08:49 INFO]: Loading properties[22:08:49 INFO]: This server is running Paper version 1.21.4-222-main@9b1798d (2025-03-27T13:35:40Z) (Implementing API version 1.21.4-R0.1-SNAPSHOT)[22:08:49 INFO]: [spark] This server bundles the spark profiler. For more information please visit https://docs.papermc.io/paper/profiling[22:08:49 INFO]: Server Ping Player Sample Count: 12[22:08:49 INFO]: Using 4 threads for Netty based IO[22:08:49 INFO]: [MoonriseCommon] Paper is using 5 worker threads, 1 I/O threads[22:08:49 INFO]: [ChunkTaskScheduler] Chunk system is using population gen parallelism: true[22:08:50 INFO]: Default game type: SURVIVAL[22:08:50 INFO]: Generating keypair[22:08:50 INFO]: Starting Minecraft server on *:25565[22:08:50 INFO]: Using default channel type[22:08:50 INFO]: Paper: Using Java compression from Velocity.[22:08:50 INFO]: Paper: Using Java cipher from Velocity.[22:08:50 INFO]: [BeginnerPlugin] Loading server plugin BeginnerPlugin v1.0-SNAPSHOT[22:08:50 INFO]: [BeginnerPlugin] Our plugin has loaded!12 collapsed lines
[22:08:50 INFO]: Server permissions file permissions.yml is empty, ignoring it[22:08:50 INFO]: Preparing level "world"[22:08:50 INFO]: Preparing start region for dimension minecraft:overworld[22:08:50 INFO]: Preparing spawn area: 0%[22:08:51 INFO]: Preparing spawn area: 2%[22:08:51 INFO]: Time elapsed: 692 ms[22:08:51 INFO]: Preparing start region for dimension minecraft:the_nether[22:08:51 INFO]: Preparing spawn area: 0%[22:08:51 INFO]: Time elapsed: 82 ms[22:08:51 INFO]: Preparing start region for dimension minecraft:the_end[22:08:51 INFO]: Preparing spawn area: 0%[22:08:51 INFO]: Time elapsed: 103 ms[22:08:51 INFO]: [BeginnerPlugin] Enabling BeginnerPlugin v1.0-SNAPSHOT[22:08:51 WARN]: [BeginnerPlugin] Our plugin has been enabled!5 collapsed lines
[22:08:51 INFO]: [spark] Starting background profiler...[22:08:51 INFO]: [spark] The async-profiler engine is not supported for your os/arch (windows11/amd64), so the built-in Java engine will be used instead.[22:08:51 INFO]: Done preparing level "world" (1.472s)[22:08:51 INFO]: Running delayed init tasks[22:08:51 INFO]: Done (8.831s)! For help, type "help"> stop[22:08:56 INFO]: Stopping the server[22:08:56 INFO]: Stopping server[22:08:56 INFO]: [BeginnerPlugin] Disabling BeginnerPlugin v1.0-SNAPSHOT[22:08:56 ERROR]: [BeginnerPlugin] Our plugin has been disabled!32 collapsed lines
[22:08:56 INFO]: Saving players[22:08:56 INFO]: Saving worlds[22:08:56 INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:overworld[22:08:56 INFO]: [ChunkHolderManager] Waiting 60s for chunk system to halt for world 'world'[22:08:56 INFO]: [ChunkHolderManager] Halted chunk system for world 'world'[22:08:56 INFO]: [ChunkHolderManager] Saving all chunkholders for world 'world'[22:08:56 INFO]: [ChunkHolderManager] Saved 49 block chunks, 49 entity chunks, 0 poi chunks in world 'world' in 0,23s[22:08:56 INFO]: [ChunkHolderManager] Waiting 60s for chunk I/O to halt for world 'world'[22:08:56 INFO]: [ChunkHolderManager] Halted I/O scheduler for world 'world'[22:08:56 INFO]: Saving chunks for level 'ServerLevel[world_nether]'/minecraft:the_nether[22:08:56 INFO]: [ChunkHolderManager] Waiting 60s for chunk system to halt for world 'world_nether'[22:08:56 INFO]: [ChunkHolderManager] Halted chunk system for world 'world_nether'[22:08:56 INFO]: [ChunkHolderManager] Saving all chunkholders for world 'world_nether'[22:08:56 INFO]: [ChunkHolderManager] Saved 49 block chunks, 49 entity chunks, 0 poi chunks in world 'world_nether' in 0,04s[22:08:56 INFO]: [ChunkHolderManager] Waiting 60s for chunk I/O to halt for world 'world_nether'[22:08:56 INFO]: [ChunkHolderManager] Halted I/O scheduler for world 'world_nether'[22:08:56 INFO]: Saving chunks for level 'ServerLevel[world_the_end]'/minecraft:the_end[22:08:56 INFO]: [ChunkHolderManager] Waiting 60s for chunk system to halt for world 'world_the_end'[22:08:56 INFO]: [ChunkHolderManager] Halted chunk system for world 'world_the_end'[22:08:56 INFO]: [ChunkHolderManager] Saving all chunkholders for world 'world_the_end'[22:08:56 INFO]: [ChunkHolderManager] Saved 49 block chunks, 49 entity chunks, 0 poi chunks in world 'world_the_end' in 0,05s[22:08:56 INFO]: [ChunkHolderManager] Waiting 60s for chunk I/O to halt for world 'world_the_end'[22:08:56 INFO]: [ChunkHolderManager] Halted I/O scheduler for world 'world_the_end'[22:08:56 INFO]: ThreadedAnvilChunkStorage (world): All chunks are saved[22:08:56 INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved[22:08:56 INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved[22:08:56 INFO]: ThreadedAnvilChunkStorage: All dimensions are saved[22:08:56 INFO]: Waiting for I/O tasks to complete...[22:08:56 INFO]: All I/O tasks to complete[22:08:56 INFO]: [MoonriseCommon] Awaiting termination of worker pool for up to 60s...> 2025-04-04T20:08:56.867222200Z Log4j2-AsyncAppenderEventDispatcher-1-Async WARN Advanced terminal features are not available in this environment[22:08:56 INFO]: [MoonriseCommon] Awaiting termination of I/O pool for up to 60s...
And that is all!
Run-Task Gradle plugin
Section titled “Run-Task Gradle plugin”Having to move over the build plugin jar each time you do a change is annoying. For this, the run-task Gradle plugin exists. I will be using
that one for the remainder of the Guide, so don’t wonder about the extra lines in my build.gradle.kts
file. You can find a small guide on how to set it up
here.
Learn Paper Dev is licensed under CC BY-NC-SA 4.0