Paper Plugin Guide
File mode0

Java basics

Working with text

Strings, joining text, comparing it properly, and building messages.

Beginner37 min read

Text is everywhere in a plugin: chat messages, command arguments, player names, item names, config values. On this page you will learn how to write, join, search, compare and format Strings, avoid the classic == bug, and build a plugin with a /shout command, an XP bar and a chat filter.

Writing text in code

A String is a piece of text: a sequence of characters, such as letters, digits, spaces and symbols. You write a String value between double quotes. A value written directly in the code like this is called a literal.

String literals
1String welcome = "Welcome to the server!";2String empty = "";
  1. The quotes mark where the text starts and ends. They are not part of the text.
  2. Two quotes with nothing between them is the empty String: text with zero characters. It is a real String, not null.

Special characters: escapes

How do you put a double quote inside a String, when a double quote ends the String? You put a backslash in front of it. A backslash followed by a character is called an escape sequence, and it stands for one special character:

EscapeMeansExample
\nNew line"Line one\nLine two"
\tTab (a wide gap)"Name\tKills"
\"A double quote"Steve said \"hi\""
\\A backslash"plugins\\MyPlugin"
Literals.javaRuns on Java 25
1void main() {2    String welcome = "Welcome to the server!";3    String twoLines = "Line one\nLine two";4    String quote = "Steve said \"hi\" in chat";5    String path = "plugins\\MyPlugin\\config.yml";6    String columns = "Name\tKills";7    String empty = "";8 9    IO.println(welcome);10    IO.println(twoLines);11    IO.println(quote);12    IO.println(path);13    IO.println(columns);14    IO.println("Empty text has length " + empty.length());15}
  1. One String that prints as two lines.
  2. Each \" becomes a plain " in the text.
  3. Each \\ becomes one backslash. Windows paths need this. A single backslash before M would be a compile error ("illegal escape character").
  4. A tab between the two words.
  5. The empty String has length 0.

Text blocks for several lines

For longer text, use a text block: three double quotes, a new line, your text, and three double quotes at the end. Line breaks and quotes inside it need no escapes:

TextBlock.javaRuns on Java 25
1void main() {2    String rules = """3        Server rules:4          1. Be kind in chat.5          2. No griefing.6          3. Have fun!7        """;8    IO.print(rules);9}
  1. A text block starts with """ and the text begins on the next line.
  2. Java removes the indentation that all lines share, so "Server rules:" starts at the left edge.
  3. These lines are indented two spaces more than the first line, and that extra indentation is kept.
  4. The closing quotes. Because they are on their own line, the text ends with a new line, which is why the program uses IO.print instead of println.

Double quotes, not single quotes

Single quotes are for a single char, such as 'A' (see Variables and types). Using them for a String is a compile error, and a confusing one:

CharQuotes.javaRuns on Java 25
1void main() {2    String rank = 'VIP';3}
  1. Three characters in single quotes. Java expected exactly one character, so it reports "unclosed character literal" twice and gives up.

Joining text with +

The + operator glues Strings together. This is called concatenation. When one side of + is a String, the other side is turned into text and glued on, whether it is a number, a boolean or anything else:

Concatenation.javaRuns on Java 25
1void main() {2    String name = "Alex";3    int coins = 250;4 5    IO.println(name + " has " + coins + " coins");6    IO.println("Coins: " + 2 + 3);7    IO.println("Coins: " + (2 + 3));8    IO.println(2 + 3 + " coins");9 10    String message = "Kills: ";11    message += 7;12    message += ", Deaths: " + 2;13    IO.println(message);14}
  1. Text, a number and more text become one String. Notice the spaces inside the quotes; without them you would get "Alexhas250coins".
  2. The trap: Java works left to right. "Coins: " + 2 is the text "Coins: 2", and then + 3 glues on a 3. Result: "Coins: 23".
  3. Parentheses make Java add the numbers first.
  4. Here the numbers come first, so 2 + 3 is real math (5), and then the text is glued on.
  5. += works on Strings too: it glues something onto the end.

You have already seen concatenation in plugin code on earlier pages:

Concatenation in plugin code
1player.sendRichMessage("<gold>Hello, " + player.getName() + "!");2getLogger().info("Loaded " + homeCount + " homes in " + millis + " ms");
  1. The player's name is glued between two pieces of text.
  2. A console line built from text and two numbers.

Useful String methods

A String is an object with many built-in methods. You call them with a dot, like name.length(). Many of them use positions in the text, called indexes. Java counts positions from 0, not from 1:

String name = "Steve"; characters index S 0 t 1 e 2 v 3 e 4 name.substring(1, 4) is "tev" starts at index 1, stops before index 4 name.length() is 5 name.charAt(0) is 'S' name.indexOf("v") is 3 Counting starts at 0, so the last index is always length() - 1. The same 0-based counting is used for command arguments (args[0]) and inventory slots.
Each character has an index, starting at 0. substring(1, 4) takes the characters at indexes 1, 2 and 3.

These are the methods you will use most in plugins:

MethodWhat it gives backExample result
length()Number of characters"Steve".length() is 5
toLowerCase(Locale.ROOT)The text in small letters"HeLLo" becomes "hello"
toUpperCase(Locale.ROOT)The text in capitals"hello" becomes "HELLO"
strip() / trim()The text without spaces at the start and end" hi " becomes "hi"
isEmpty()Is the length 0?"".isEmpty() is true
isBlank()Is it empty or only spaces?" ".isBlank() is true
contains(text)Does the text appear anywhere?"hello world".contains("world") is true
startsWith(text) / endsWith(text)Does it begin or end with this?"/home".startsWith("/") is true
indexOf(text)Index where the text first appears, or -1"Steve".indexOf("v") is 3
substring(start) / substring(start, end)A piece of the text; end is not included"Steve".substring(1, 4) is "tev"
charAt(index)The single character at that index"Steve".charAt(0) is 'S'
replace(old, new)The text with every old replaced"a_b".replace("_", " ") is "a b"
split(separator)The pieces between separators, as an array"a b c".split(" ") gives 3 pieces
repeat(count)The text repeated"=".repeat(5) is "====="
equals(other) / equalsIgnoreCase(other)Is it the same text?See Comparing text

Cleaning up what players type

CleaningText.javaRuns on Java 25
1import java.util.Locale;2 3void main() {4    String typed = "   Hello There   ";5    IO.println("[" + typed + "] has length " + typed.length());6    IO.println("[" + typed.strip() + "]");7    IO.println("[" + typed.toLowerCase(Locale.ROOT) + "]");8    IO.println("[" + typed.toUpperCase(Locale.ROOT) + "]");9 10    String spaces = "    ";11    IO.println("Only spaces, isEmpty: " + spaces.isEmpty());12    IO.println("Only spaces, isBlank: " + spaces.isBlank());13 14    String name = "Steve";15    IO.println("First letter: " + name.charAt(0));16    IO.println("Last letter: " + name.charAt(name.length() - 1));17}
  1. Locale describes a language and region. It lives in java.util, so you import it.
  2. The spaces count as characters: 17 in total.
  3. Removes the spaces at both ends, but keeps the space in the middle. Players often type extra spaces by accident.
  4. Always pass Locale.ROOT. Without it, Java uses the computer's language rules, and on a server set to Turkish, "TITLE".toLowerCase() gives "tıtle" with a dotless i, which breaks comparisons. Locale.ROOT gives the same result everywhere.
  5. Four spaces is not empty: the length is 4.
  6. But it is blank: there is nothing except spaces. Use isBlank() to check whether a player actually typed something.
  7. The first character is at index 0.
  8. The last character is at index length() - 1, because counting started at 0.

Searching and cutting text

SearchingText.javaRuns on Java 25
1void main() {2    String message = "/home add castle";3 4    IO.println("Is a command? " + message.startsWith("/"));5    IO.println("Mentions castle? " + message.contains("castle"));6    IO.println("Ends with .yml? " + message.endsWith(".yml"));7 8    IO.println("Where is the first space? " + message.indexOf(" "));9    IO.println("Where is xyz? " + message.indexOf("xyz"));10 11    IO.println("Without the slash: " + message.substring(1));12    IO.println("Just the command: " + message.substring(1, 5));13    IO.println("Replaced: " + message.replace("castle", "farm"));14}
  1. Checks the beginning of the text.
  2. Checks anywhere in the text. Case matters: "Castle" would not match.
  3. The first space is at index 5 (count the characters: / is 0, h is 1, and so on).
  4. -1 means "not found". Check for -1 before using the index.
  5. Everything from index 1 to the end: the text without the slash.
  6. From index 1 up to, but not including, index 5: "home".
  7. Gives a new String with every "castle" replaced.

Splitting, joining and repeating

SplitJoinRepeat.javaRuns on Java 25
1void main() {2    String typed = "pay Steve 250";3    String[] parts = typed.split(" ");4    IO.println("Words: " + parts.length);5    IO.println("First word: " + parts[0]);6    IO.println("Second word: " + parts[1]);7    IO.println("Third word: " + parts[2]);8 9    String joined = String.join(", ", "Steve", "Alex", "Notch");10    IO.println("Online: " + joined);11 12    IO.println("=".repeat(20));13    IO.println("Hearts: " + "<3 ".repeat(4));14}
  1. Cuts the text at every space and gives back an array: a numbered list of Strings. Lists, sets and maps covers lists properly.
  2. How many pieces there are. For arrays, length has no parentheses.
  3. The first piece. Square brackets with an index pick one item from an array, and the first one is index 0.
  4. The opposite of split: glues pieces together with a separator between them.
  5. 20 equals signs: an instant divider line.

This is exactly how commands work. In a Paper command, the words a player types after the command name arrive already split, as a String[] args array. For /pay Steve 250, args[0] is "Steve" and args[1] is "250". And String.join(" ", args) turns all the words back into one message, which the /shout command below uses.

Strings never change

Strings are immutable: once created, a String can never change. Methods like toUpperCase and replace do not change the original. They build a new String and give it back. If you do not store that new String, it is lost:

Immutable.javaRuns on Java 25
1import java.util.Locale;2 3void main() {4    String name = "steve";5    name.toUpperCase(Locale.ROOT);6    IO.println("After calling toUpperCase: " + name);7 8    name = name.toUpperCase(Locale.ROOT);9    IO.println("After storing the result: " + name);10 11    String original = "Diamond Sword";12    String shouted = original.toUpperCase(Locale.ROOT);13    IO.println(original + " / " + shouted);14}
  1. This line builds "STEVE" and then throws it away, because nothing stores it. name is still "steve". IntelliJ warns you about this with a yellow highlight.
  2. Store the result. Now name holds the new String.
  3. Or store it in a new variable. The original is untouched, so you can use both.

Comparing text: equals, not ==

This is the most famous Java bug. To check whether two Strings contain the same text, use equals. Do not use ==. Run this program; it reads what you type (the recorded run typed reload):

EqualsVsDoubleEquals.javaRuns on Java 25
1void main() {2    String typed = IO.readln("Type a subcommand: ");3    IO.println();4 5    IO.println("You typed: " + typed);6    IO.println("typed == \"reload\": " + (typed == "reload"));7    IO.println("typed.equals(\"reload\"): " + typed.equals("reload"));8    IO.println("typed.equals(\"Reload\"): " + typed.equals("Reload"));9    IO.println("typed.equalsIgnoreCase(\"Reload\"): " + typed.equalsIgnoreCase("Reload"));10}
  1. Waits for the user to type a line and gives it back as a String, just like a player typing a command argument.
  2. == asks "are these the very same String object in memory?". The typed text was created while the program ran, so it is a different object from the "reload" in the code, even though the letters match. The answer is false.
  3. equals asks "do these contain the same characters?". That is the question you actually mean. true.
  4. equals is case sensitive: a capital R makes it different.
  5. Ignores capital and small letters. Perfect for commands, where players may type /home RELOAD.

Sometimes == happens to give true for two Strings written directly in the code, because Java reuses identical literals. That makes the bug worse: code that seems to work in a quick test fails as soon as the text comes from a player, a config file or a command. Never compare Strings with ==.

JavaHas a mistake
1if (args[0] == "reload") {2    plugin.reloadConfig();3}

The fixed version, as a real plugin would write it:

Comparing a command argument
1if (args[0].equalsIgnoreCase("reload")) {2    plugin.reloadConfig();3}
  1. Same letters, any capitalization: reload, Reload and RELOAD all work.

One more trick: if a String variable could be null, put the literal first: "reload".equals(input). A literal is never null, so this cannot crash, and it simply gives false when input is null.

Formatting numbers in text

Gluing a double into text prints every decimal: health 13.4567 shows as "13.4567". String.format lets you write a template with format specifiers, codes starting with %, and fills them in with your values in order:

CodeFills inExampleResult
%sAny value as textformat("%s joined", name)Alex joined
%dA whole numberformat("%d ms", 42)42 ms
%.1fA decimal, rounded to 1 place (%.2f for 2)format("%.1f", 13.4567)13.5
%,dA whole number with thousands separatorsformat("%,d", 1250000)1,250,000
%02dA whole number, padded with zeros to 2 digitsformat("%02d", 7)07
%%A plain percent signformat("%d%%", 75)75%
Formatting.javaRuns on Java 25
1import java.util.Locale;2 3void main() {4    double health = 13.4567;5    int coins = 1250000;6    String name = "Alex";7    int ping = 42;8 9    IO.println("Raw: " + health);10    IO.println(String.format(Locale.ROOT, "Health: %.1f", health));11    IO.println(String.format(Locale.ROOT, "%s has %,d coins", name, coins));12    IO.println(String.format(Locale.ROOT, "Ping: %d ms", ping));13    IO.println(String.format(Locale.ROOT, "Progress: %d%%", 75));14    IO.println(String.format(Locale.ROOT, "Slot %02d", 7));15 16    String line = "%s is at %.2f health".formatted(name, health);17    IO.println(line);18}
  1. The first argument is the locale, the second is the template, and the rest are the values. Locale.ROOT guarantees a dot as the decimal separator; with some computer languages you would get "13,5".
  2. Two specifiers, filled in order: name goes into %s, coins into %,d.
  3. The %% prints one percent sign. A lone % would confuse format.
  4. Handy for timers: %02d:%02d prints 4 minutes 7 seconds as "04:07".
  5. The same thing written the other way round: the template comes first, then .formatted(...). It uses the computer's own locale, so prefer String.format(Locale.ROOT, ...) for decimals in plugins.

Paper's own template code does exactly this to show damage with one decimal:

From a combat listener
1String damage = String.format(Locale.ROOT, "%.1f", event.getFinalDamage());
  1. A double such as 6.666666. Formatted, the player sees "6.7".

Building long text with StringBuilder

Because Strings never change, every + builds a whole new String. For a few pieces that does not matter. When you build text bit by bit, especially inside a loop, use a StringBuilder: a String that can change, which you fill with append and turn into a real String at the end with toString().

BuildingText.javaRuns on Java 25
1void main() {2    StringBuilder message = new StringBuilder();3    message.append("Online: ");4    message.append("Steve");5    message.append(", ");6    message.append("Alex");7    IO.println(message.toString());8 9    StringBuilder countdown = new StringBuilder();10    for (int seconds = 5; seconds >= 1; seconds--) {11        countdown.append(seconds).append("... ");12    }13    countdown.append("Go!");14    IO.println(countdown.toString());15}
  1. Creates an empty builder. new makes a new object; Classes and objects explains it.
  2. Adds text to the end of the builder, changing it in place.
  3. Turns the builder into a normal String.
  4. A loop: it runs the line inside its braces once for each number from 5 down to 1. You will learn loops in Loops.
  5. Appends can be chained, and they accept numbers too.

Text recipes for plugins

A progress bar

repeat makes text bars easy: some filled characters, then some empty ones. Plugins use this for XP, cooldowns, quest progress and boss health in the action bar or scoreboard.

ProgressBar.javaRuns on Java 25
1import java.util.Locale;2 3void main() {4    int percent = 70;5    int barLength = 10;6 7    int filled = percent * barLength / 100;8    int empty = barLength - filled;9 10    String bar = "|".repeat(filled) + ".".repeat(empty);11    IO.println("[" + bar + "] " + percent + "%");12 13    String withFormat = String.format(Locale.ROOT, "Level %d [%s] %d%%", 12, bar, percent);14    IO.println(withFormat);15}
  1. How many of the 10 slots are filled: 70% of 10 is 7. Multiplying first avoids the integer division trap from Operators and math.
  2. The rest of the bar.
  3. 7 bars, followed by 3 dots.
  4. The same bar placed into a format template.

A chat filter

ChatFilter.javaRuns on Java 25
1import java.util.Locale;2 3void main() {4    String message = "THIS SERVER HAS BADWORD LAG";5    String lower = message.toLowerCase(Locale.ROOT);6 7    boolean hasBlockedWord = lower.contains("badword");8    IO.println("Contains a blocked word? " + hasBlockedWord);9 10    String censored = lower.replace("badword", "***");11    IO.println("Censored: " + censored);12 13    boolean isAllCaps = message.length() >= 6 && message.equals(message.toUpperCase(Locale.ROOT));14    IO.println("All caps? " + isAllCaps);15}
  1. Lowercase once, then search in the lowercase copy. That way "BadWord", "BADWORD" and "badword" are all caught.
  2. Is the blocked word anywhere in the message?
  3. Or hide it instead of blocking the whole message.
  4. A message is all caps when turning it into capitals changes nothing.

Reading command input

CommandInput.javaRuns on Java 25
1void main() {2    String typed = "/pay Steve 250";3 4    String withoutSlash = typed.substring(1);5    String[] parts = withoutSlash.split(" ");6 7    String command = parts[0];8    String target = parts[1];9    int amount = Integer.parseInt(parts[2]);10 11    IO.println("Command: " + command);12    IO.println("Target: " + target);13    IO.println("Amount plus tax: " + (amount + amount / 10));14    IO.println("Is it the pay command? " + command.equalsIgnoreCase("PAY"));15}
  1. Removes the leading slash.
  2. Splits into words: pay, Steve, 250. Paper does this step for you in real commands.
  3. The amount is text until you parse it (see Variables and types).
  4. Command names should not care about capitals.

Colored text: Strings vs Components

Everything above is plain text. Chat in Minecraft is richer: colors, bold, hover text, clickable links. Paper represents that rich text with a type called Component, and the easiest way to make one is MiniMessage, a String with tags like <green> and <bold>:

Colored text the modern way
1player.sendRichMessage("<green>Welcome! <gray>You have <gold>" + coins + " coins");
  1. Reads the MiniMessage tags in the String and sends the colored result.
Welcome! You have 250 coins

You will meet older code online that colors text with special characters glued into Strings. It still shows up in tutorials, but the API it relies on is deprecated in Paper, so do not copy it:

JavaOld way
1player.sendMessage(ChatColor.GREEN + "Welcome!");2player.sendMessage("§aWelcome!");

One rule matters right away. When text comes from a player (their chat message, a nickname, a sign line), never glue it straight into a MiniMessage String. A player could type <red> or a click tag, and your plugin would obey it. Insert it as a placeholder instead, with Placeholder.unparsed, which treats it as plain text. The /shout command below shows how. MiniMessage and Text and colors with Adventure cover all of this in depth.

A plugin made of Strings

This plugin has three features: /shout broadcasts a message in capitals, /xpbar draws your XP progress as a colored bar, and a chat listener blocks bad words and calms down all-caps messages.

/shout

ShoutCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-strings-demosrcmainjavacomexamplejavastringsdemoShoutCommand.java

The package com.example.javastringsdemo is the folder path com/example/javastringsdemo 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.

  • java-strings-demo/
    • src/main/
      • java/com/example/javastringsdemo/Package com.example.javastringsdemo
        • ChatFilterListener.javaListener: reacts to events
        • ShoutCommand.javayou are hereCommand (BasicCommand)
        • StringsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • XpBarCommand.javaCommand (BasicCommand)
      • 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.javastringsdemo;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import java.util.Locale;6import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;7import org.bukkit.Bukkit;8import org.bukkit.command.CommandSender;9 10public final class ShoutCommand implements BasicCommand {11 12    private static final int MAX_LENGTH = 100;13 14    @Override15    public void execute(CommandSourceStack source, String[] args) {16        CommandSender sender = source.getSender();17        String message = String.join(" ", args).strip();18 19        if (message.isBlank()) {20            sender.sendRichMessage("<red>Type a message after the command, like /shout hello everyone");21            return;22        }23        if (message.length() > MAX_LENGTH) {24            sender.sendRichMessage("<red>That is too long. Keep it under " + MAX_LENGTH + " characters.");25            return;26        }27 28        String shouted = message.toUpperCase(Locale.ROOT);29        Bukkit.getServer().sendRichMessage("<gold>[Shout] <yellow><name>: <white><message>",30            Placeholder.unparsed("name", sender.getName()),31            Placeholder.unparsed("message", shouted));32    }33}
  1. A limit, so nobody fills the chat with a wall of capitals.
  2. Whoever ran the command: a player or the console. Both have a name and can receive messages.
  3. Paper split the typed words into args. join glues them back into one message with spaces, and strip removes stray spaces at the ends.
  4. Nothing typed (or only spaces): explain how to use the command and stop.
  5. Too long: refuse with a message. The limit is glued into the text with +.
  6. The shouting. Stored in a new variable, because Strings never change.
  7. Sends the message to everyone on the server, plus the console.
  8. Fills the <name> tag in the template with the sender's name, as plain text.
  9. Fills <message> with what the player typed, as plain text. If they typed <rainbow>, everyone sees the literal word, not a rainbow.
[Shout] Steve: WHO WANTS TO GO TO THE NETHER?

/xpbar

XpBarCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-strings-demosrcmainjavacomexamplejavastringsdemoXpBarCommand.java

The package com.example.javastringsdemo is the folder path com/example/javastringsdemo 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.

  • java-strings-demo/
    • src/main/
      • java/com/example/javastringsdemo/Package com.example.javastringsdemo
        • ChatFilterListener.javaListener: reacts to events
        • ShoutCommand.javaCommand (BasicCommand)
        • StringsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • XpBarCommand.javayou are hereCommand (BasicCommand)
      • 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.javastringsdemo;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import java.util.Locale;6import org.bukkit.entity.Player;7 8public final class XpBarCommand implements BasicCommand {9 10    private static final int BAR_LENGTH = 20;11 12    @Override13    public void execute(CommandSourceStack source, String[] args) {14        if (!(source.getExecutor() instanceof Player player)) {15            source.getSender().sendRichMessage("<red>Only players can use /xpbar.");16            return;17        }18 19        int percent = Math.round(player.getExp() * 100);20        int filled = percent * BAR_LENGTH / 100;21        int empty = BAR_LENGTH - filled;22 23        String bar = "<green>" + "|".repeat(filled) + "<dark_gray>" + "|".repeat(empty);24        String text = String.format(Locale.ROOT, "<gray>Level <aqua>%d</aqua> [%s<gray>] <white>%d%%",25            player.getLevel(), bar, percent);26        player.sendRichMessage(text);27    }28}
  1. The bar is 20 characters wide.
  2. getExp() is a float from 0 to 1. Times 100 gives a percentage. Math.round of a float gives an int (of a double it gives a long).
  3. How many of the 20 slots to fill.
  4. Green bars for the filled part, then dark gray bars for the rest. The color tags are our own text, so gluing them in is safe.
  5. The template fills in the level, the bar and the percentage. %% is the percent sign.
  6. MiniMessage turns the tags into colors.
Level 12 [||||||||||||||||||||] 65%

The chat filter

ChatFilterListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-strings-demosrcmainjavacomexamplejavastringsdemoChatFilterListener.java

The package com.example.javastringsdemo is the folder path com/example/javastringsdemo 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.

  • java-strings-demo/
    • src/main/
      • java/com/example/javastringsdemo/Package com.example.javastringsdemo
        • ChatFilterListener.javayou are hereListener: reacts to events
        • ShoutCommand.javaCommand (BasicCommand)
        • StringsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • XpBarCommand.javaCommand (BasicCommand)
      • 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.javastringsdemo;2 3import io.papermc.paper.event.player.AsyncChatEvent;4import java.util.Locale;5import net.kyori.adventure.text.Component;6import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;7import org.bukkit.entity.Player;8import org.bukkit.event.EventHandler;9import org.bukkit.event.Listener;10 11public final class ChatFilterListener implements Listener {12 13    private static final int MIN_CAPS_LENGTH = 6;14 15    @EventHandler(ignoreCancelled = true)16    public void onChat(AsyncChatEvent event) {17        Player player = event.getPlayer();18        String plain = PlainTextComponentSerializer.plainText().serialize(event.message());19        String lower = plain.toLowerCase(Locale.ROOT);20 21        if (lower.contains("badword") || lower.contains("anotherbadword")) {22            event.setCancelled(true);23            player.sendRichMessage("<red>Your message contained a blocked word.");24            return;25        }26 27        boolean hasLetters = !plain.equals(lower);28        boolean isAllCaps = plain.equals(plain.toUpperCase(Locale.ROOT));29        if (plain.length() >= MIN_CAPS_LENGTH && hasLetters && isAllCaps) {30            event.message(Component.text(lower));31            player.sendRichMessage("<gray>Please do not type in all caps. Your message was sent in lowercase.");32        }33    }34}
  1. Fires when a player sends a chat message. It runs off the main server thread, which is fine here because we only read and change text.
  2. The chat message arrives as a Component. This turns it into a plain String so you can use String methods on it.
  3. A lowercase copy for searching.
  4. Either blocked word cancels the message. || means "or".
  5. Nobody sees the message. The player is told why.
  6. If lowercasing changed something, the message has capital letters in it. This stops messages like "123456" from counting as all caps.
  7. Uppercasing changes nothing, so every letter is already a capital.
  8. Replaces the message everyone will see with the lowercase version. Component.text turns a plain String into a Component.
Your message contained a blocked word.
Please do not type in all caps. Your message was sent in lowercase.

The main class

StringsDemoPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-strings-demosrcmainjavacomexamplejavastringsdemoStringsDemoPlugin.java

The package com.example.javastringsdemo is the folder path com/example/javastringsdemo 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.

  • java-strings-demo/
    • src/main/
      • java/com/example/javastringsdemo/Package com.example.javastringsdemo
        • ChatFilterListener.javaListener: reacts to events
        • ShoutCommand.javaCommand (BasicCommand)
        • StringsDemoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • XpBarCommand.javaCommand (BasicCommand)
      • 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.javastringsdemo;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class StringsDemoPlugin extends JavaPlugin {7 8    @Override9    public void onEnable() {10        getServer().getPluginManager().registerEvents(new ChatFilterListener(), this);11        getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {12            event.registrar().register("shout", "Shouts a message to everyone in capital letters", new ShoutCommand());13            event.registrar().register("xpbar", "Shows your XP progress as a bar", new XpBarCommand());14        });15    }16}
  1. Registers the chat filter.
  2. Registers both commands with a name and a description.

Download this plugin as a Gradle project and try /shout, /xpbar and some chat messages on your test server.

Practice

Try it

A shop line

A shop menu shows each item as a line like 3x Golden apple for 1,250 coins. The program below has the item's material id (golden_apple, the way Minecraft names it), the amount and the price. Make it print that exact line.

ShopLineStart.javaRuns on Java 25
1void main() {2    String materialId = "golden_apple";3    int amount = 3;4    int price = 1250;5    IO.println(materialId);6}
Hint 1

First replace the underscore with a space. That gives golden apple.

Hint 2

For the capital G: take the first character with substring(0, 1), make it uppercase, and glue on the rest with substring(1).

Hint 3

For the line itself, use String.format(Locale.ROOT, "%dx %s for %,d coins", ...).

Show the solution

Each String method gives back a new String, so every step stores its result in a variable.

ShopLine.javaRuns on Java 25
1import java.util.Locale;2 3void main() {4    String materialId = "golden_apple";5    int amount = 3;6    int price = 1250;7 8    String spaced = materialId.replace("_", " ");9    String niceName = spaced.substring(0, 1).toUpperCase(Locale.ROOT) + spaced.substring(1);10 11    String line = String.format(Locale.ROOT, "%dx %s for %,d coins", amount, niceName, price);12    IO.println(line);13}
  1. "golden_apple" becomes "golden apple".
  2. "G" + "olden apple".
  3. The amount, the name and the price with a thousands separator.
Try it

Make /shout end with an exclamation mark

Change ShoutCommand so every shout ends with !. If the player already typed one at the end, do not add a second.

Hint 1

Which String method checks the end of a text?

Hint 2

Remember that Strings never change: you have to store the new String back in shouted.

Show the solution

Add these lines right after the line that creates shouted:

ShoutCommand.java (new lines)
1if (!shouted.endsWith("!")) {2    shouted = shouted + "!";3}

!shouted.endsWith("!") reads "does not end with an exclamation mark". Writing only shouted + "!"; without shouted = would not even compile, because Java does not allow a calculation on its own as a statement. And shouted.concat("!"); on its own would compile but change nothing.

Practice more String challenges in the Practice Arena, or paste any message into MiniMessage Studio to see how it would look in game.

Recap

  • Strings are text in double quotes. Escapes like \n, \" and \\ add special characters; text blocks (""") hold several lines.
  • + glues text and values together, working left to right. Use parentheses for math inside text.
  • Indexes start at 0. substring(start, end) stops before end, and indexOf gives -1 when nothing is found.
  • Use toLowerCase(Locale.ROOT) and toUpperCase(Locale.ROOT), and isBlank() to check for empty input.
  • Strings never change. Store the result: name = name.strip();.
  • Compare text with equals or equalsIgnoreCase, never with ==.
  • String.format(Locale.ROOT, "%.1f", value) formats numbers; StringBuilder builds long text efficiently.
  • In plugins, colored text is MiniMessage or Components. Insert player text with Placeholder.unparsed.

Quick quiz

  1. A player runs /kit Starter. Which check correctly matches it no matter how they capitalize it?

  2. What does "Kills: " + 2 + 3 produce?

  3. After String name = " Steve "; name.strip();, what is name?

  4. What is "Diamond".substring(0, 3)?

  5. Why should a plugin insert a player's chat text with Placeholder.unparsed instead of gluing it into a MiniMessage String?

Next steps