Skip to content

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!

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")
}

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 SignaturePurposeGood 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 SignatureReturn 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.

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.

BeginnerPlugin.java
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!");
}
}

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: BeginnerPlugin
api-version: 1.21.4
main: com.learnpaperdev.beginner.BeginnerPlugin
version: 1.0-SNAPSHOT

These 4 entries are all required for the plugin to be loaded.

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):

Terminal window
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!

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:

Terminal window
java -jar paper-1.21.4-222.jar nogui
Starting 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!

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