Paper Plugin Guide
File mode0

Projects

Project: Welcome kit

Greet new players with a title, a sound and a starter kit, all configurable.

BeginnerProject, about 48 min read

In this project you build a complete plugin from an empty folder: when a new player joins your server, they see a title, hear a sound and receive a starter kit, and every text, sound and item can be changed in a config file without touching the code.

What you will build

Many servers greet new players with a short welcome and a few useful items. You will build exactly that, and you will build it the way real plugins are built: small classes that each do one job, and settings that live in a file the server owner can edit.

When a player joins the server for the very first time, three things happen. They see a message in chat:

Steve joined the game
Welcome to the server, Steve! Your starter kit is in your inventory.

A big title appears in the middle of their screen, and a sound plays:

Welcome! Have fun, Steve

And four items are waiting in their hotbar (the row of slots at the bottom of the screen):

0: stone_sword, 1: stone_pickaxe, 2: cooked_beef, 3: torch

The next time Steve joins, he only gets a quiet "welcome back" line. The title, the sound and the kit are only for first joins, otherwise players would collect a free kit every time they log in.

Steve joined the game
Welcome back, Steve.
What the welcome kit does when a player joins When a player joins, the plugin asks hasPlayedBefore. Returning players only get a short chat message. New players get a chat message, a title, a sound and the starter kit. All texts and items come from config.yml. config.yml texts, sound, kit items read once WelcomeSettings at server start A player joins PlayerJoinEvent hasPlayedBefore() been here before? true false Returning player Short chat message "Welcome back, Steve." New player 1. Chat message 2. Title on screen 3. Sound 4. Starter kit into the inventory Items that do not fit drop at the feet
The whole plugin on one page. The settings are read once when the server starts; every join then takes one of two paths.

The plan

Before writing code, a good developer writes a plan: what must the plugin do, and which piece of the Paper API does each job? Here is ours. Every row links to the chapter that explains that piece in depth, so you can read more whenever something feels new.

FeatureThe piece that does itChapter
Run code when a player joinsPlayerJoinEvent in a listenerEvents and listeners
Tell new players from returning playersplayer.hasPlayedBefore()Working with players
Colored messages with the player's nameMiniMessage and a <name> placeholderMiniMessage
Title on the screenplayer.showTitle(Title)Titles, action bars and sounds
Soundplayer.playSound(...) and the sound registryTitles, action bars and sounds
Starter kitItemStack and player.getInventory().addItem(...)Items and Inventories
Everything editable by the server ownerconfig.yml and getConfig()Configuration files

And here are the steps. Each one gives you something you can run and test before you move on, which is the best habit a beginner can learn: build a little, test a little, and you always know which step broke things.

  1. Create the project and plugin.yml

    The files Paper needs to recognize your plugin, and a first console line.

  2. Greet every player in chat

    Your first listener, with the text typed straight into the code.

  3. Move the settings into config.yml

    Messages, title, sound and kit become editable.

  4. Add the title and the sound

    Show the title and play the configured sound.

  5. Detect the first join

    Only new players get the big welcome.

  6. Give the starter kit

    Turn config entries into real items in the inventory.

  7. Test it properly

    A checklist, including how to become a "new player" again.

The finished project is in the download if you want to compare your work, but try to type the code yourself first. Typing is how it sticks.

Step 1: create the project and plugin.yml

You need a plugin project that builds and starts on a test server. If you already followed Your first plugin, copy that project and rename it. If you have the paper-templates folder on your PC, the quickest way is the helper script, which copies a ready-made project and renames everything for you:

Whichever way you start, the project layout is the same: a build.gradle.kts, and under src/main a java folder for code and a resources folder for files such as plugin.yml and config.yml. This project uses the package com.example.projectwelcome. (The small step-by-step files on this page sit in folders with a number in the package name, like projectwelcomestep1, so they can be downloaded separately. In your own project, use com.example.projectwelcome everywhere.) If you use another package name, that is fine, but the main: line in plugin.yml must always match your main class exactly.

The plugin.yml file is Paper's name tag for your plugin. Without it, Paper does not even look at your code:

plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainresourcesplugin.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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlyou are hereTells 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.

1name: WelcomeKit2version: '1.0.0'3main: com.example.projectwelcome.WelcomeKitPlugin4api-version: '26.3'5description: Greets new players with a title, a sound and a starter kit, all set in config.yml.
  1. The name shown in the plugin list and in log lines like [WelcomeKit]. Paper also uses it as the name of your data folder, plugins/WelcomeKit.
  2. The full name of your main class: package, a dot, then the class name. If you mistype it, Paper prints "Cannot find main class" and refuses to load the plugin.
  3. The Minecraft version this plugin was written for.

The main class is also needed now, even if it does almost nothing yet. This version only prints one line to the console, which proves that Paper loaded the plugin:

WelcomeKitPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcome-step1srcmainjavacomexampleprojectwelcomestep1WelcomeKitPlugin.java

The package com.example.projectwelcomestep1 is the folder path com/example/projectwelcomestep1 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-welcome-step1/
    • src/main/
      • java/com/example/projectwelcomestep1/Package com.example.projectwelcomestep1
        • WelcomeKitPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
      • 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.projectwelcomestep1;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class WelcomeKitPlugin extends JavaPlugin {6 7    @Override8    public void onEnable() {9        getServer().getPluginManager().registerEvents(new WelcomeListener(), this);10        getLogger().info("WelcomeKit is ready.");11    }12}
  1. Marks this class as the main class of your plugin.
  2. Paper calls this once, when the plugin starts.
  3. Tells Paper about our listener. We write that class in the next step. The second argument, this, is the plugin that owns the listener.
  4. Prints a line in the server console. Seeing it tells you the plugin really started.

This file mentions a WelcomeListener that does not exist yet, so IntelliJ shows a red error. That is expected, and the next step fixes it. Learning to read red errors as "the next thing to write" is a big part of programming.

Step 2: greet every player in chat

Time for the first useful code. A listener class waits for the join event and sends a message. This is the same pattern as in Events and listeners, with one new tool: a placeholder.

WelcomeListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcome-step1srcmainjavacomexampleprojectwelcomestep1WelcomeListener.java

The package com.example.projectwelcomestep1 is the folder path com/example/projectwelcomestep1 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-welcome-step1/
    • src/main/
      • java/com/example/projectwelcomestep1/Package com.example.projectwelcomestep1
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javayou are hereListener: reacts to events
      • 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.projectwelcomestep1;2 3import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;4import org.bukkit.entity.Player;5import org.bukkit.event.EventHandler;6import org.bukkit.event.Listener;7import org.bukkit.event.player.PlayerJoinEvent;8 9final class WelcomeListener implements Listener {10 11    @EventHandler12    public void onJoin(PlayerJoinEvent event) {13        Player player = event.getPlayer();14        player.sendRichMessage(15            "<green>Welcome to the server, <yellow><name></yellow>!",16            Placeholder.unparsed("name", player.getName()));17    }18}
  1. A listener is a class that promises to be a Listener. It is not public, because only code in the same package needs to see it, and final means no other class can extend it.
  2. Marks the next method as an event handler: Paper calls it whenever the event in its parameter happens.
  3. Called every time a player joins. The event object knows who joined.
  4. Saves the player in a variable so the next lines are shorter.
  5. MiniMessage tags: <green> and <yellow> color the text, and <name> is a placeholder, a hole that gets filled in below.
  6. Fills the <name> hole with the player's real name. "Unparsed" means the name is inserted as plain text, so any <...> tags inside it are shown as letters and never change how the message looks.

Build the plugin and run the server (see Build, install and test), join, and you see:

Steve joined the game
Welcome to the server, Steve!

Check the server console as well. You should see this line, and it is your proof that onEnable ran:

Server console
[14:02:11 INFO]: [WelcomeKit] Enabling WelcomeKit v1.0.0[14:02:11 INFO]: [WelcomeKit] WelcomeKit is ready.

Step 3: move the settings into config.yml

Right now, to change "Welcome to the server" you must edit the code, rebuild the plugin and restart. A server owner who just downloaded your plugin cannot do that. The solution is a config file: a text file next to the plugin that the owner can edit with Notepad. Paper's name for this file is config.yml, and it is written in a format called YAML.

Think of it like the options screen of a game: the game's code stays the same, but you change the values. Here is our whole config:

config.ymlCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainresourcesconfig.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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.

1messages:2  first-join: "<green>Welcome to the server, <yellow><name></yellow>! Your starter kit is in your inventory."3  returning: "<gray>Welcome back, <yellow><name></yellow>."4 5title:6  enabled: true7  main: "<gold><bold>Welcome!"8  sub: "<gray>Have fun, <name>"9  stay-seconds: 310 11sound:12  enabled: true13  name: "entity.player.levelup"14  volume: 1.015  pitch: 1.016 17kit:18  items:19    sword:20      material: STONE_SWORD21      amount: 122      name: "<aqua>Starter Sword"23    pickaxe:24      material: STONE_PICKAXE25      amount: 126      name: "<aqua>Starter Pickaxe"27    food:28      material: COOKED_BEEF29      amount: 1630    torches:31      material: TORCH32      amount: 32
  1. A section: a heading that groups related settings. The lines under it are indented by two spaces, and the indentation is how YAML knows they belong to it. Always use spaces, never the Tab key.
  2. A key and its value. In code you ask for it with the path messages.first-join: section name, a dot, key name.
  3. Numbers need no quotes. This one is how long the title stays on screen.
  4. A yes/no setting. The owner can turn the title off by writing false.
  5. The sound's name in Minecraft's own naming. Sounds like this are listed on the Minecraft Wiki and in the Names Lookup.
  6. Each kit item has its own small section. The names sword, pickaxe and so on are only labels for you and the owner; the plugin reads whatever is there.
  7. The item type, using Bukkit's Material names.
  8. An optional custom name. The food and torches items have no name, so they keep their normal one.

Texts are wrapped in quotation marks. They are not always required, but they keep characters that YAML treats as special, like : and #, from breaking the file. Wrap every text in quotes and you never have to think about it.

Turning the file into Java objects

Reading getConfig().getString("title.main") in ten different places gets messy fast. Instead we read the file once, when the plugin starts, and keep the results in one small object. A Java record is made for exactly this: a class that only holds values. You list the values in parentheses and Java builds the class for you, including methods like titleText() that read each value back. (Records are explained in Enums, records and modern Java.)

WelcomeSettings.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeSettings.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
        • WelcomeSettings.javayou are hereRecord: a small data class
      • 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.

12record WelcomeSettings(13    String firstJoinMessage,14    String returningMessage,15    boolean titleEnabled,16    String titleText,17    String subtitleText,18    int titleStaySeconds,19    Sound sound,20    float soundVolume,21    float soundPitch,22    List<KitEntry> kit) {23 24    static WelcomeSettings from(ConfigurationSection config, Logger logger) {25        return new WelcomeSettings(26            config.getString("messages.first-join", "<green>Welcome, <name>!"),27            config.getString("messages.returning", "<gray>Welcome back, <name>."),28            config.getBoolean("title.enabled", true),29            config.getString("title.main", "<gold>Welcome!"),30            config.getString("title.sub", "<gray>Have fun, <name>"),31            config.getInt("title.stay-seconds", 3),32            loadSound(config, logger),33            (float) config.getDouble("sound.volume", 1.0),34            (float) config.getDouble("sound.pitch", 1.0),35            loadKit(config, logger));36    }
  1. Declares a record with ten values. Each line inside the parentheses is one value and its type, like a field on a form.
  2. The kit is a list of KitEntry objects. We write KitEntry in step 6.
  3. A static method belongs to the class itself, so you can call it without having an object yet. It reads the config and returns a finished WelcomeSettings.
  4. The second argument is a default value. If the owner deletes this line from the file, the plugin still has something sensible to say instead of null.
  5. The same idea for a yes/no value.
  6. getInt reads a whole number.
  7. YAML numbers like 1.0 come back as double. Sound volume and pitch are float, so (float) converts one into the other. This is called a cast.
  8. Sound and kit need more checking than a plain text, so they get their own methods (loadSound and loadKit). We write them in steps 4 and 6; the rest of the file is in those steps.

The main class now loads the settings and hands them to the listener. It also calls saveDefaultConfig(), which copies config.yml out of the plugin jar into plugins/WelcomeKit/ the first time the plugin runs. If the file is already there, it is left alone, so the owner's edits are never overwritten.

WelcomeKitPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeKitPlugin.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.projectwelcome;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class WelcomeKitPlugin extends JavaPlugin {6 7    @Override8    public void onEnable() {9        saveDefaultConfig();10        WelcomeSettings settings = WelcomeSettings.from(getConfig(), getLogger());11        getServer().getPluginManager().registerEvents(new WelcomeListener(settings), this);12        getLogger().info("WelcomeKit is ready with " + settings.kit().size() + " kit items.");13    }14}
  1. Creates plugins/WelcomeKit/config.yml from the file inside the jar, but only if it does not exist yet.
  2. Reads the whole config once. getConfig() is the loaded file, and getLogger() lets the settings class print warnings.
  3. The listener receives the settings in its constructor, the special method that runs when you write new. Now it never needs to touch the config file itself.
  4. A record's values are read with methods named after them. This prints how many kit items were understood, which is handy when you test.

The listener from step 2 must now receive these settings. Change the top of WelcomeListener so it keeps them in a field, and replace each typed-in text with a value from the settings (you will see the finished onJoin in step 5):

WelcomeListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeListener.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javayou are hereListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.

15final class WelcomeListener implements Listener {16 17    private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();18 19    private final WelcomeSettings settings;20 21    WelcomeListener(WelcomeSettings settings) {22        this.settings = settings;23    }
  1. One shared MiniMessage object that turns text with tags like <gold> into a colored Component. static final means it is created once and never replaced.
  2. A field: a variable that belongs to the object and lives as long as the listener does. Every method in the class can read it.
  3. The constructor. Whoever writes new WelcomeListener(settings) has to hand over the settings.
  4. Copies the value from the constructor's parameter into the field. this.settings is the field; plain settings is the parameter.

Step 4: add the title and the sound

A title is a Title object made of two pieces of text and three timings. Titles, action bars, boss bars and sounds explains each part; here is how our listener uses it. The method receives the name placeholder, so the title can greet the player too.

WelcomeListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeListener.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javayou are hereListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.

41private void showTitle(Player player, TagResolver name) {42    if (!settings.titleEnabled()) {43        return;44    }45    player.showTitle(Title.title(46        MINI_MESSAGE.deserialize(settings.titleText(), name),47        MINI_MESSAGE.deserialize(settings.subtitleText(), name),48        Title.Times.times(49            Duration.ofMillis(500),50            Duration.ofSeconds(settings.titleStaySeconds()),51            Duration.ofSeconds(1))));52}
  1. If the owner set enabled: false, this method does nothing and ends right away with return.
  2. Sends the title to this one player.
  3. Turns the MiniMessage text from the config into a colored Component, filling in the <name> placeholder. The subtitle on the next line works the same way.
  4. Fade in for half a second, stay for the configured seconds, fade out for one second.

Playing a sound safely

Sounds are the one setting a server owner can easily get wrong: one typo in entity.player.levelup and there is no sound at all. We cannot stop typos, but we can notice them. When the settings load, loadSound looks the name up in the sound registry, the server's list of every sound that exists. If the name is not in the list, we print a warning once, at startup, instead of failing silently on every join.

WelcomeSettings.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeSettings.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
        • WelcomeSettings.javayou are hereRecord: a small data class
      • 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.

38private static Sound loadSound(ConfigurationSection config, Logger logger) {39    if (!config.getBoolean("sound.enabled", true)) {40        return null;41    }42    String name = config.getString("sound.name", "entity.player.levelup");43    NamespacedKey key = NamespacedKey.fromString(name);44    Sound sound = key == null ? null : Registry.SOUNDS.get(key);45    if (sound == null) {46        logger.warning("Unknown sound '" + name + "' in config.yml, so no sound will play.");47    }48    return sound;49}
  1. If sounds are switched off, there is no sound to look up. Returning null means "no sound".
  2. Turns text like entity.player.levelup into a key object. If the text has characters a key cannot contain, the result is null.
  3. Asks the registry for the sound with that key. It returns null when there is no such sound.
  4. The question-mark form is a short if: "if the key is null, use null, otherwise ask the registry". Without it, get(null) would crash.
  5. Prints a yellow warning in the console, with the name the owner typed, so they can find the mistake.

Playing the sound is then simple. The listener only checks that a sound exists:

WelcomeListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeListener.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javayou are hereListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.

54private void playSound(Player player) {55    if (settings.sound() == null) {56        return;57    }58    player.playSound(player.getLocation(), settings.sound(), settings.soundVolume(), settings.soundPitch());59}
  1. There is no sound when the owner turned it off or mistyped the name, and in both cases we quietly skip it.
  2. Plays the sound at the player's own position, so they hear it as if it came from themselves. Volume and pitch come from the config; pitch 1.0 is normal, higher is squeakier.

If the owner mistypes the sound, this is what the console shows when the server starts:

Server console
[14:02:11 WARN]: [WelcomeKit] Unknown sound 'entity.player.levelop' in config.yml, so no sound will play.

Step 5: detect the first join

Now the heart of the plugin: who is new? Every player object has a method hasPlayedBefore(). It answers true if the server remembers this player from an earlier visit, and false during their very first join. Here is the whole join handler:

WelcomeListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeListener.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javayou are hereListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.

25@EventHandler26public void onJoin(PlayerJoinEvent event) {27    Player player = event.getPlayer();28    TagResolver name = Placeholder.unparsed("name", player.getName());29 30    if (player.hasPlayedBefore()) {31        player.sendRichMessage(settings.returningMessage(), name);32        return;33    }34 35    player.sendRichMessage(settings.firstJoinMessage(), name);36    showTitle(player, name);37    playSound(player);38    giveKit(player);39}
  1. One placeholder object, created once and used for the chat message, the title and the subtitle. A TagResolver is the type of Placeholder.unparsed(...).
  2. The key question. true means this player has been here before.
  3. Returning players get one quiet message, and return ends the handler right there. This early exit is called a guard clause: it handles the special case first, so the rest of the method only deals with new players.
  4. From here on, only new players run the code. Each job is its own small method, which keeps onJoin short enough to read in one glance.

Step 6: give the starter kit

The config describes each kit item with a material, an amount and an optional name. We need one object per item, so here is the small record for it, plus the method that turns it into a real ItemStack, the stack of items you hold in a slot:

KitEntry.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeKitEntry.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javayou are hereRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.projectwelcome;2 3import net.kyori.adventure.text.minimessage.MiniMessage;4import org.bukkit.Material;5import org.bukkit.inventory.ItemStack;6 7record KitEntry(Material material, int amount, String name) {8 9    private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();10 11    ItemStack createItem() {12        ItemStack item = ItemStack.of(material, amount);13        if (name != null && !name.isBlank()) {14            item.editMeta(meta -> meta.itemName(MINI_MESSAGE.deserialize(name)));15        }16        return item;17    }18}
  1. One kit item: its type, how many, and an optional custom name (this one can be null).
  2. Creates a new stack, for example 16 cooked beef.
  3. Only rename the item if a name was written. && means "and": the second check only runs if the first one passed, so isBlank() never touches a null.
  4. Changes the item's extra information (its ItemMeta). The part after the arrow is what to do with it.
  5. Sets the item's name. Unlike displayName, an item name is not italic, so it looks like a normal item name in the game. Items explains the difference.

Next, loadKit walks through the kit.items section of the config and builds one KitEntry per item. It is the longest method in the plugin, so read it in pieces:

WelcomeSettings.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeSettings.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javaListener: reacts to events
        • WelcomeSettings.javayou are hereRecord: a small data class
      • 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.

51private static List<KitEntry> loadKit(ConfigurationSection config, Logger logger) {52    List<KitEntry> kit = new ArrayList<>();53    ConfigurationSection items = config.getConfigurationSection("kit.items");54    if (items == null) {55        return kit;56    }57    for (String id : items.getKeys(false)) {58        ConfigurationSection entry = items.getConfigurationSection(id);59        if (entry == null) {60            continue;61        }62        Material material = Material.matchMaterial(entry.getString("material", ""));63        if (material == null || !material.isItem()) {64            logger.warning("Kit item '" + id + "' has an unknown material, so it is skipped.");65            continue;66        }67        int amount = Math.clamp(entry.getInt("amount", 1), 1, material.getMaxStackSize());68        kit.add(new KitEntry(material, amount, entry.getString("name")));69    }70    return kit;71}
  1. An empty list that we fill as we go. Lists are explained in Lists, sets and maps.
  2. Gets the whole items section. If the owner deleted it, the result is null, and we return the empty list: a plugin with no kit still works.
  3. The names of the entries inside the section: sword, pickaxe, food, torches. false means "only the direct children".
  4. A for-each loop: run the indented block once for every name, with the current name in id. See Loops.
  5. Turns text such as STONE_SWORD into the Material it names, or null if there is no such material. It also accepts lowercase.
  6. Some materials, like WATER, exist as blocks only and cannot be held. They must not end up in a kit.
  7. Skips the rest of this loop round and moves on to the next kit item. One bad entry does not ruin the others.
  8. Forces the amount into a safe range: at least 1, and at most the material's stack size (64 for torches, 1 for swords, 16 for ender pearls). Someone who writes amount: 500 cannot crash the plugin.

The last piece gives the items to the player. addItem puts each stack into the first free slots, filling the hotbar first. If the inventory is full, it cannot place everything, and it returns the leftovers in a map instead of throwing them away. We drop those on the ground so nothing is lost:

WelcomeListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcomesrcmainjavacomexampleprojectwelcomeWelcomeListener.java

The package com.example.projectwelcome is the folder path com/example/projectwelcome 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-welcome/
    • src/main/
      • java/com/example/projectwelcome/Package com.example.projectwelcome
        • KitEntry.javaRecord: a small data class
        • WelcomeKitPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • WelcomeListener.javayou are hereListener: reacts to events
        • WelcomeSettings.javaRecord: a small data class
      • 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.

61private void giveKit(Player player) {62    for (KitEntry entry : settings.kit()) {63        Map<Integer, ItemStack> leftovers = player.getInventory().addItem(entry.createItem());64        for (ItemStack leftover : leftovers.values()) {65            player.getWorld().dropItemNaturally(player.getLocation(), leftover);66        }67    }68}
  1. Repeats the block once for every item in the kit.
  2. Tries to put the new stack into the inventory. The return value is a map of everything that did not fit; it is empty when all went well.
  3. The map's values are the stacks that did not fit. If there are none, the loop below does not run at all.
  4. Drops the item on the ground at the player's feet, with a small random toss like a broken block would give.

That finishes the code. The finished project has five files, and none of them is longer than a screen or two:

  • project-welcome/
    • src/
      • main/
        • java/
          • com/example/projectwelcome/
            • WelcomeKitPlugin.javaThe main class: loads the config and registers the listener
            • WelcomeSettings.javaReads config.yml once into one object, and checks sound and kit entries
            • KitEntry.javaOne kit item, and how to make a real ItemStack from it
            • WelcomeListener.javaHandles the join: chat, title, sound and kit for new players
        • resources/
          • plugin.ymlName, version, main class and API version
          • config.ymlEvery text, the sound and the kit items

Step 7: test it properly

"It compiled" is not the same as "it works". Players find the bugs you did not test for, so go through a checklist like this one every time you finish a plugin. Start the server with Run Paper Server and tick off each line:

  • The console shows WelcomeKit is ready with 4 kit items. and no warnings or red errors.
  • A first join shows the chat message, the title and the sound, and the four items land in the hotbar.
  • Leave and join again: only the "welcome back" line appears, and no new items.
  • Fill your inventory completely, become new again, and join: the kit items fall on the ground at your feet.
  • Edit run/plugins/WelcomeKit/config.yml: change a message, set sound.enabled to false, and remove one kit item. Restart and check that each change took effect.
  • Break the config on purpose: mistype a material name and a sound name. The console should warn about both, and nothing else should break.

How to become a new player again

The first-join code only runs once per player, so to test it again you must make the server forget you. Stop the server first. Then delete your player file, which is named after your UUID (your player's unique id), from the world's player data folder. In Paper 26.x that folder is run/world/players/data; on older versions it was called playerdata. Start the server again and join: you are new.

Here is what the console should look like when everything works, including the two warnings from the "break it on purpose" test:

Server console
[14:20:03 WARN]: [WelcomeKit] Unknown sound 'entity.player.levelop' in config.yml, so no sound will play.[14:20:03 WARN]: [WelcomeKit] Kit item 'torches' has an unknown material, so it is skipped.[14:20:03 INFO]: [WelcomeKit] WelcomeKit is ready with 3 kit items.

Mistakes to avoid

  • Tabs in the YAML file. YAML only allows spaces for indentation. A Tab character gives a loading error that names the exact line.
  • Forgetting saveDefaultConfig(). Without it, no config.yml is created, and every setting silently falls back to its default.
  • Editing src/main/resources/config.yml and expecting the running server to change. That file is only the template inside the jar. The live copy is in run/plugins/WelcomeKit/config.yml, and it is never overwritten once it exists. Delete the live copy to get the new template.
  • Gluing a player's name into the message text. Always use a placeholder, so text that comes from players or files can never add MiniMessage tags to your message. Minecraft names rarely contain such characters, but chat text and nicknames can, and the habit costs nothing.
  • Trusting the owner's input. Mistyped materials, sounds and amounts happen. Check values when loading and warn, instead of letting the plugin crash on the first join.

Exercises

Try it

Announce newcomers to everyone

Add a setting messages.announcement to the config. When a new player joins, everyone online should see it, for example "Steve joined for the first time. Say hello!" Returning players should not trigger it.

Hint 1

You already know how to send a message to one player. To send one to everybody there is Bukkit.broadcast(Component).

Hint 2

You only want this for new players, so use the same hasPlayedBefore() check, and return early for returning players.

Show the solution

A second, tiny listener does the job. Its text comes from the config through the constructor, exactly like the settings in the main project. (To keep this exercise small, it reads the one text directly instead of using the settings record.)

NewPlayerAnnouncer.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcome-exercisesrcmainjavacomexampleprojectwelcomeexerciseNewPlayerAnnouncer.java

The package com.example.projectwelcomeexercise is the folder path com/example/projectwelcomeexercise 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-welcome-exercise/
    • src/main/
      • java/com/example/projectwelcomeexercise/Package com.example.projectwelcomeexercise
        • AnnouncePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • NewPlayerAnnouncer.javayou are hereListener: reacts to events
      • 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.projectwelcomeexercise;2 3import net.kyori.adventure.text.minimessage.MiniMessage;4import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;5import org.bukkit.Bukkit;6import org.bukkit.entity.Player;7import org.bukkit.event.EventHandler;8import org.bukkit.event.Listener;9import org.bukkit.event.player.PlayerJoinEvent;10 11final class NewPlayerAnnouncer implements Listener {12 13    private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();14 15    private final String announcement;16 17    NewPlayerAnnouncer(String announcement) {18        this.announcement = announcement;19    }20 21    @EventHandler22    public void onJoin(PlayerJoinEvent event) {23        Player player = event.getPlayer();24        if (player.hasPlayedBefore()) {25            return;26        }27        Bukkit.broadcast(MINI_MESSAGE.deserialize(announcement, Placeholder.unparsed("name", player.getName())));28    }29}
  1. Returning players are skipped, so only first joins are announced.
  2. Sends the component to everyone who receives server broadcasts, which by default is every player and the console.
config.ymlCompiles on Paper 26.3Compile Lab
Where this file livesproject-welcome-exercisesrcmainresourcesconfig.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-welcome-exercise/
    • src/main/
      • java/com/example/projectwelcomeexercise/Package com.example.projectwelcomeexercise
        • AnnouncePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • NewPlayerAnnouncer.javaListener: reacts to events
      • 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.

1messages:2  announcement: "<gold><name></gold> <gray>joined for the first time. Say hello!"
Try it

Add a golden apple and a rename, with no code

Change only config.yml: add a fifth kit item, three golden apples with the custom name "Emergency Snack" in gold. Then answer a question: what happens if you set the pickaxe's amount to 5?

Hint

Copy one of the existing items and change its label, material and amount. Golden apples are GOLDEN_APPLE.

Show the solution

The new entry goes under kit.items, with the same indentation as the others. A pickaxe cannot stack, so Math.clamp lowers 5 to 1 and the player receives one pickaxe.

config.yml (new entry)
1snack:2  material: GOLDEN_APPLE3  amount: 34  name: "<gold>Emergency Snack"

Extension ideas

When you finish the main project, these are good next steps. Each one uses something from a chapter you can read at any time:

  • A reload command. Add /welcomekit reload that rebuilds the settings without a restart (you need to keep the listener's settings in a place that can change). Builds on Brigadier commands, and the next project, /heal and /feed, shows a real command class.
  • Armor in the kit. Equip leather armor straight onto the player with player.getInventory().setHelmet(...). See Inventories.
  • A kit per permission. Give donors a bigger kit using player.hasPermission(...). See Permissions.
  • A delayed welcome. Show the title two seconds after joining, so the player has finished loading in. See The scheduler.
  • A custom join message. Replace the yellow "joined the game" line with your own using event.joinMessage(...), as in Events and listeners.

You can also open the finished plugin in the Compile Lab, change it and compile it right in your browser.

Recap

  • A plugin is built from small classes that each do one job: a main class, a settings class, a listener and a record for one kit item.
  • PlayerJoinEvent runs your code when a player joins; hasPlayedBefore() separates new players from returning ones.
  • saveDefaultConfig() copies config.yml from the jar once; getConfig() reads it. Always give getString, getInt and the others a default value.
  • Read the config once at startup into a record, and check every value that the owner can get wrong. Warn in the console instead of crashing.
  • Placeholders such as <name> insert player names into MiniMessage text safely.
  • addItem returns the items that did not fit; drop them on the ground so nothing disappears.
  • Test with a checklist, including the failure cases.

Quick quiz

  1. A player joins for the second time. Which code makes sure they do not get the starter kit again?

  2. What does saveDefaultConfig() do when plugins/WelcomeKit/config.yml already exists?

  3. The owner writes material: DIAMOND_SWORDD in the kit. What does this plugin do?

  4. Why does the code use Placeholder.unparsed("name", player.getName()) instead of joining the name into the text?

  5. The inventory is completely full when a new player joins. What happens to the kit?

Next steps