Project: Welcome kit
Greet new players with a title, a sound and a starter kit, all configurable.
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:
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:
And four items are waiting in their hotbar (the row of slots at the bottom of the screen):
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.
Welcome back, Steve.
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.
| Feature | The piece that does it | Chapter |
|---|---|---|
| Run code when a player joins | PlayerJoinEvent in a listener | Events and listeners |
| Tell new players from returning players | player.hasPlayedBefore() | Working with players |
| Colored messages with the player's name | MiniMessage and a <name> placeholder | MiniMessage |
| Title on the screen | player.showTitle(Title) | Titles, action bars and sounds |
| Sound | player.playSound(...) and the sound registry | Titles, action bars and sounds |
| Starter kit | ItemStack and player.getInventory().addItem(...) | Items and Inventories |
| Everything editable by the server owner | config.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.
Create the project and plugin.yml
The files Paper needs to recognize your plugin, and a first console line.
Greet every player in chat
Your first listener, with the text typed straight into the code.
Move the settings into config.yml
Messages, title, sound and kit become editable.
Add the title and the sound
Show the title and play the configured sound.
Detect the first join
Only new players get the big welcome.
Give the starter kit
Turn config entries into real items in the inventory.
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:
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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.- 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. - 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.
- 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:
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
- java/com/example/projectwelcomestep1/Package com.example.projectwelcomestep1
- 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.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}- Marks this class as the main class of your plugin.
- Paper calls this once, when the plugin starts.
- Tells Paper about our listener. We write that class in the next step. The second argument,
this, is the plugin that owns the listener. - 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.
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
- java/com/example/projectwelcomestep1/Package com.example.projectwelcomestep1
- 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.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}- A listener is a class that promises to be a
Listener. It is notpublic, because only code in the same package needs to see it, andfinalmeans no other class can extend it. - Marks the next method as an event handler: Paper calls it whenever the event in its parameter happens.
- Called every time a player joins. The
eventobject knows who joined. - Saves the player in a variable so the next lines are shorter.
- MiniMessage tags:
<green>and<yellow>color the text, and<name>is a placeholder, a hole that gets filled in below. - 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:
Welcome to the server, Steve!
Check the server console as well. You should see this line, and it is your proof that onEnable ran:
[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:
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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- 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. - A
key and its value. In code you ask for it with the pathmessages.first-join: section name, a dot, key name. - Numbers need no quotes. This one is how long the title stays on screen.
- A yes/no setting. The owner can turn the title off by writing
false. - The sound's name in Minecraft's own naming. Sounds like this are listed on the Minecraft Wiki and in the Names Lookup.
- Each kit item has its own small section. The names
sword,pickaxeand so on are only labels for you and the owner; the plugin reads whatever is there. - The item type, using Bukkit's
Materialnames. - An optional custom name. The
foodandtorchesitems 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.)
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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 }- Declares a record with ten values. Each line inside the parentheses is one value and its type, like a field on a form.
- The kit is a list of
KitEntryobjects. We writeKitEntryin step 6. - 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 finishedWelcomeSettings. - 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 ofnull. - The same idea for a yes/no value.
getIntreads a whole number.- YAML numbers like
1.0come back asdouble. Sound volume and pitch arefloat, so(float)converts one into the other. This is called a cast. - Sound and kit need more checking than a plain text, so they get their own methods (
loadSoundandloadKit). 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.
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.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}- Creates
plugins/WelcomeKit/config.ymlfrom the file inside the jar, but only if it does not exist yet. - Reads the whole config once.
getConfig()is the loaded file, andgetLogger()lets the settings class print warnings. - The listener receives the settings in its
constructor , the special method that runs when you writenew. Now it never needs to touch the config file itself. - 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):
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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 }- One shared MiniMessage object that turns text with tags like
<gold>into a coloredComponent.static finalmeans it is created once and never replaced. - 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. - The constructor. Whoever writes
new WelcomeListener(settings)has to hand over the settings. - Copies the value from the constructor's parameter into the field.
this.settingsis the field; plainsettingsis 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.
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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}- If the owner set
enabled: false, this method does nothing and ends right away withreturn. - Sends the title to this one player.
- 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. - 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.
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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}- If sounds are switched off, there is no sound to look up. Returning
null means "no sound". - Turns text like
entity.player.levelupinto a key object. If the text has characters a key cannot contain, the result isnull. - Asks the registry for the sound with that key. It returns
nullwhen there is no such sound. - The question-mark form is a short
if: "if the key isnull, usenull, otherwise ask the registry". Without it,get(null)would crash. - 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:
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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}- There is no sound when the owner turned it off or mistyped the name, and in both cases we quietly skip it.
- 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:
[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:
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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}- One placeholder object, created once and used for the chat message, the title and the subtitle. A
TagResolveris the type ofPlaceholder.unparsed(...). - The key question.
truemeans this player has been here before. - Returning players get one quiet message, and
returnends the handler right there. This early exit is called aguard clause : it handles the special case first, so the rest of the method only deals with new players. - From here on, only new players run the code. Each job is its own small method, which keeps
onJoinshort 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:
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.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}- One kit item: its type, how many, and an optional custom name (this one can be
null). - Creates a new stack, for example 16 cooked beef.
- Only rename the item if a name was written.
&&means "and": the second check only runs if the first one passed, soisBlank()never touches anull. - Changes the item's extra information (its
ItemMeta). The part after the arrow is what to do with it. - 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:
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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}- An empty list that we fill as we go. Lists are explained in Lists, sets and maps.
- Gets the whole
itemssection. If the owner deleted it, the result isnull, and we return the empty list: a plugin with no kit still works. - The names of the entries inside the section:
sword,pickaxe,food,torches.falsemeans "only the direct children". - A
for-each loop : run the indented block once for every name, with the current name inid. See Loops. - Turns text such as
STONE_SWORDinto theMaterialit names, ornullif there is no such material. It also accepts lowercase. - Some materials, like
WATER, exist as blocks only and cannot be held. They must not end up in a kit. - Skips the rest of this loop round and moves on to the next kit item. One bad entry does not ruin the others.
- 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: 500cannot 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:
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
- java/com/example/projectwelcome/Package com.example.projectwelcome
- 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.
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}- Repeats the block once for every item in the kit.
- 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.
- The map's values are the stacks that did not fit. If there are none, the loop below does not run at all.
- 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
- com/example/projectwelcome/
- resources/
- plugin.ymlName, version, main class and API version
- config.ymlEvery text, the sound and the kit items
- java/
- main/
- src/
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, setsound.enabledtofalse, 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:
[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, noconfig.ymlis created, and every setting silently falls back to its default. - Editing
src/main/resources/config.ymland expecting the running server to change. That file is only the template inside the jar. The live copy is inrun/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
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.)
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
- java/com/example/projectwelcomeexercise/Package com.example.projectwelcomeexercise
- 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.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}- Returning players are skipped, so only first joins are announced.
- Sends the component to everyone who receives server broadcasts, which by default is every player and the console.
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
- java/com/example/projectwelcomeexercise/Package com.example.projectwelcomeexercise
- 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.
1messages:2 announcement: "<gold><name></gold> <gray>joined for the first time. Say hello!"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.
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 reloadthat 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.
PlayerJoinEventruns your code when a player joins;hasPlayedBefore()separates new players from returning ones.saveDefaultConfig()copiesconfig.ymlfrom the jar once;getConfig()reads it. Always givegetString,getIntand 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. addItemreturns the items that did not fit; drop them on the ground so nothing disappears.- Test with a checklist, including the failure cases.
Quick quiz
A player joins for the second time. Which code makes sure they do not get the starter kit again?
hasPlayedBefore()istruefor any player the server has saved data for. An empty inventory proves nothing, because players can empty their inventory at any time, andsaveDefaultConfig()only creates the config file.What does
saveDefaultConfig()do whenplugins/WelcomeKit/config.ymlalready exists?It only copies the file out of the jar when there is none yet. That is why the owner's edits survive plugin updates, and also why you must delete the live copy to see a new template.The owner writes
material: DIAMOND_SWORDDin the kit. What does this plugin do?Material.matchMaterialreturnsnullfor unknown names, andloadKitchecks for that, warns and usescontinueto move on. Checking while loading means the mistake shows up at startup, not when a player joins.Why does the code use
Placeholder.unparsed("name", player.getName())instead of joining the name into the text?Joining text together would let any tags inside the name (or, in other plugins, inside a chat message) change how the message looks. A placeholder keeps that text as harmless plain letters.The inventory is completely full when a new player joins. What happens to the kit?
addItemnever throws items away. It hands back a map of the stacks it could not place, and our code drops each of them withdropItemNaturally.
Next steps
- Project: /heal and /feed: your first project with real commands, permissions and cooldowns.
- Configuration files: everything about
config.yml, lists, sections and saving. - Practice Arena: short challenges that train the skills from this project.