Paper Plugin Guide
File mode0

Java basics

Variables and types

Store numbers, text and true/false values, and learn which type fits which job.

Beginner35 min read

Plugins are full of values: a player's health, their coins, their name, whether they are flying. On this page you will learn how Java stores values in variables, which type fits which job, how to turn text into numbers, and you will build a plugin with /mystats and /setfood commands.

What a variable is

A variable is a named place that holds one value. You give it a name, put a value in, read the value later, and replace it with a new value whenever you like.

Variables are labeled chests A variable is a labeled chest that holds one value. Four chests hold an int 3, a double 1.5, a boolean true and a String Steve. int lives 3 whole numbers double speed 1.5 numbers with decimals boolean alive true true or false String name "Steve" text int lives = 3; double speed = 1.5; boolean alive = true; String name = "Steve"; The word above a chest is its type: what kind of thing the chest may hold. The label on the lid is the name you use to open the chest. One chest holds one value at a time. Storing a new value replaces the old one.
Each variable is a labeled chest: a type, a name on the sign, and one value inside.

Here is a variable in action. Run it, then read the notes:

FirstVariable.javaRuns on Java 25
1void main() {2    int coins = 50;3    IO.println("You start with " + coins + " coins.");4 5    coins = 80;6    IO.println("After selling wheat: " + coins + " coins.");7 8    coins = coins + 25;9    IO.println("After a quest reward: " + coins + " coins.");10}
  1. Creates a variable. int is the type (a whole number), coins is the name, and 50 is the first value put inside. Creating a variable is called declaring it.
  2. Using the name coins reads whatever is inside the chest right now: 50.
  3. Puts a new value in. The old 50 is gone. No int this time, because the variable already exists; you only write the type once, when you create it.
  4. Read this right to left: take what is in coins (80), add 25, and put the answer (105) back into coins.
  5. Prints the newest value.

Every declaration has the same parts:

PartExampleMeaning
TypeintWhat kind of value fits inside.
NamecoinsThe sign on the chest. camelCase, starting with a small letter.
=="Put this value into the variable." It is not "equals" like in math class.
Value50What goes in. It can be a plain value or a calculation.
;;Ends the statement, as always.

Declaring first, filling later, and copying

You can create a variable without a value and fill it later. And when you put one variable into another, Java copies the value, so the two chests are independent afterwards:

CopyingValues.javaRuns on Java 25
1void main() {2    int kills;3    kills = 3;4 5    int bestKills = kills;6    kills = kills + 1;7 8    IO.println("Kills now: " + kills);9    IO.println("Best before this fight: " + bestKills);10}
  1. Declares an empty int variable. It has no value yet, so you may not read it until you assign one.
  2. The first value goes in. This is called assigning a value.
  3. Copies the 3 into a second chest. The two variables do not stay linked.
  4. Changes only kills, to 4. bestKills still holds 3, as the output shows.

Variables in plugin code

Plugins use variables constantly, mostly to hold answers the server gives you. Each line below asks the server for something and keeps the answer in a variable with the right type:

Inside an event handler
1Player player = event.getPlayer();2String name = player.getName();3double health = player.getHealth();4int foodLevel = player.getFoodLevel();5boolean flying = player.isFlying();
  1. The type here is Player, a Paper type for an online player. Variables can hold game things too, not only numbers and text.
  2. The player's name, as text.
  3. Health is a number with decimals, like 17.5.
  4. The hunger bar is a whole number from 0 to 20.
  5. Flying or not: true or false.

Why store the answer instead of asking again each time? It makes the next lines shorter, it gives the value a clear name, and you know the value will not change halfway through your code.

Types: which chest for which value

Java needs to know each variable's type before the program runs. That lets the compiler catch mistakes early: if you try to put text into a number variable, your code does not even compile. Java has eight basic types built in, called primitive types. They are all written in lowercase:

TypeHoldsRangeMinecraft examples
intWhole numbersabout -2.1 billion to 2.1 billionCoins, food level, XP level, kills, slot numbers, block coordinates
longBig whole numbersabout -9.2 quintillion to 9.2 quintillionTimes in milliseconds, world seeds, huge balances
doubleNumbers with decimalshuge, about 15 to 16 correct digitsHealth, exact coordinates (x, y, z), damage, money with cents
floatNumbers with decimals, less preciseabout 7 correct digitsHead direction (yaw, pitch), saturation, walk speed, XP bar progress
booleantrue or falseonly those twoIs flying, is sneaking, has played before, is enabled
charOne single characterone letter, digit or symbolRarely used in plugins
byte, shortSmall whole numbers-128 to 127, -32,768 to 32,767Rarely used in plugins; you may see byte in saved data

Here are most of them at once, holding a player's stats:

PlayerStats.javaRuns on Java 25
1void main() {2    String name = "Steve";3    double health = 20.0;4    int foodLevel = 20;5    int coins = 1250;6    float walkSpeed = 0.2f;7    boolean isFlying = false;8    long playTimeMillis = 7_200_000L;9    char rankLetter = 'A';10 11    IO.println("Name: " + name);12    IO.println("Health: " + health);13    IO.println("Food: " + foodLevel);14    IO.println("Coins: " + coins);15    IO.println("Walk speed: " + walkSpeed);16    IO.println("Flying: " + isFlying);17    IO.println("Play time (ms): " + playTimeMillis);18    IO.println("Rank: " + rankLetter);19}
  1. Text. String is not a primitive type (notice the capital S); it gets its own section below.
  2. A full health bar is 20.0 health points, which is 10 hearts. Writing .0 makes the value a double.
  3. A full hunger bar.
  4. The f at the end marks the value as a float. Without it, Java treats 0.2 as a double and refuses to squeeze it into a float variable. Paper's player.getWalkSpeed() also returns a float, from -1 to 1.
  5. Only true or false, written without quotes.
  6. Two hours in milliseconds. The L marks a long value. The underscores are just for your eyes, like the commas in 7,200,000; Java ignores them.
  7. A single character goes in single quotes. Double quotes would make it a String.

Want to see the exact limits? Each number type has a MIN_VALUE and MAX_VALUE you can print:

TypeLimits.javaRuns on Java 25
1void main() {2    IO.println("byte:  " + Byte.MIN_VALUE + " to " + Byte.MAX_VALUE);3    IO.println("short: " + Short.MIN_VALUE + " to " + Short.MAX_VALUE);4    IO.println("int:   " + Integer.MIN_VALUE + " to " + Integer.MAX_VALUE);5    IO.println("long:  " + Long.MIN_VALUE + " to " + Long.MAX_VALUE);6    IO.println("double can hold up to about " + Double.MAX_VALUE);7}
  1. The smallest and largest int. Each primitive type has a matching class with a capital letter (Integer for int, Long, Double, and so on) that holds useful constants and helpers.
  2. The E308 means "times 10 to the power 308". You will never run out.

Why health is a double and coins an int

Choose the type by asking: "Can this value have decimals?"

  • Health can. A zombie hit can leave you at 17.5 health, and armor reduces damage by fractions of a point. So health is a double.
  • Coins usually cannot. Nobody owns 12.75 diamonds. Whole numbers are simpler and exact, so coins, levels and item amounts are int.
  • Yes-or-no questions are always boolean. Do not use the numbers 0 and 1 for that.
  • Times in milliseconds are huge numbers, far beyond 2.1 billion, so they need a long.

In plugins, you rarely choose: Paper already decided. Every method tells you what type it gives back, and your variable must match. Hover a method in IntelliJ, or press Ctrl+Q, to see its type. These are the types Paper uses for common player values:

Paper methodReturnsExample value
player.getName()String"Steve"
player.getHealth()double17.5
player.getFoodLevel()int18
player.getSaturation()float3.2
player.getLevel()int12
player.getExp()float0.45 (progress to the next level, 0 to 1)
player.isFlying()booleanfalse
player.getFirstPlayed()longmilliseconds since January 1, 1970
player.getLocation().getX()double104.37
player.getLocation().getBlockX()int104
player.getLocation().getYaw()float-90.0

Notice the naming habit: methods that answer a yes-or-no question start with is or has (isFlying, hasPlayedBefore) and return a boolean. Methods that give a value start with get. Name your own variables the same way: isFlying, hasKit, coins.

String: text

Text is stored in a String (a "string of characters"). A String value is written between double quotes. Unlike the primitive types, String starts with a capital S, because it is a class: a richer type that comes with its own methods, such as length().

TextAndNull.javaRuns on Java 25
1void main() {2    String name = "Alex";3    String greeting = "Welcome back, " + name + "!";4    String nickname = null;5 6    IO.println(greeting);7    IO.println("Letters in the name: " + name.length());8    IO.println("Nickname: " + nickname);9}
  1. A String variable holding the text Alex. The quotes are not part of the text.
  2. You can build a String from other values with +.
  3. null means "this variable holds nothing at all". It is not the same as empty text "".
  4. Because String is a class, a String variable can do things. length() counts the characters.
  5. Gluing null into text prints the word null.

null matters a lot in plugins. Paper returns null when there is nothing to give you, for example player.getKiller() when a player fell to their death, or Bukkit.getPlayerExact("Steve") when Steve is offline. Calling a method on null, such as nickname.length(), crashes with a NullPointerException. You will learn to check for it in Making decisions and Null, exceptions and stack traces. The full toolbox for text is in Working with text.

var: let Java work out the type

Inside a method, you can write var instead of the type, and Java fills in the type from the value. This is called type inference:

VarExample.javaRuns on Java 25
1void main() {2    var coins = 50;3    var health = 20.0;4    var name = "Steve";5    var isFlying = true;6 7    coins = coins + 10;8    IO.println(name + " has " + coins + " coins, " + health + " health, flying: " + isFlying);9}
  1. 50 is a whole number, so coins becomes an int.
  2. 20.0 has a decimal point, so health is a double.
  3. A String.
  4. A boolean.

var does not mean "any type". The type is still fixed forever; you just did not type it out. Putting a decimal into coins later fails:

VarWrongType.javaRuns on Java 25
1void main() {2    var coins = 50;3    coins = 12.5;4}
  1. coins is an int, so it cannot hold 12.5.

While you are learning, write the real type. int coins = 50; tells you, and anyone reading, exactly what the chest holds. With var player = event.getPlayer(); you have to know what getPlayer() returns to understand the line. You will see var in other people's plugins, and it is fine once types feel natural.

final variables and constants

Put final in front of a variable and it can be assigned only once. Trying to change it is a compile error:

FinalVariable.javaRuns on Java 25
1void main() {2    final int maxHomes = 3;3    IO.println("You can set " + maxHomes + " homes.");4    maxHomes = 5;5}
  1. final locks the chest after the first value goes in.
  2. Not allowed. The compiler stops you, which is exactly the point: some values should never change by accident.

Values that are the same for the whole program, like limits and settings, are called constants. You declare them outside any method with static final, and name them in UPPER_SNAKE_CASE so they stand out:

Constants.javaRuns on Java 25
1static final int MAX_HOMES = 3;2static final int TICKS_PER_SECOND = 20;3static final String SERVER_NAME = "Skyblock Island";4 5void main() {6    IO.println("Welcome to " + SERVER_NAME + "!");7    IO.println("Every player may set " + MAX_HOMES + " homes.");8    IO.println("The server runs " + TICKS_PER_SECOND + " ticks per second.");9}
  1. A constant. static means it belongs to the program as a whole, not to one method, and final means it never changes.
  2. A Minecraft server runs 20 ticks per second. Giving the number a name explains what it means wherever it is used.
  3. Constants can be any type, including String.
  4. Every method in the file can read the constant.

Plugins use constants the same way, written at the top of a class. You will see this line in the plugin later on this page:

StatsCommand.java
1private static final int MAX_FOOD_LEVEL = 20;
  1. Only this class may use it. Packages, imports and visibility explains private.

Why bother instead of typing 20? Because a bare 20 in the middle of code could mean 20 food, 20 health, 20 ticks or 20 coins. MAX_FOOD_LEVEL can only mean one thing, and if the value ever changes, you change it in one place. Values that server owners should be able to change go even further, into a config file (see Configuration files).

Converting between types

Between number types: casting

Java converts a smaller number type into a bigger one automatically, because nothing can be lost. Going the other way, from double to int, would throw away the decimals, so Java makes you say so with a cast: the target type in parentheses.

Widening: automatic, nothing is lost int 17 double d = i; double 17.0 A small chest's items always fit a big chest. Narrowing: you must write a cast, decimals are cut off double 7.9 (int) d int 7 The .9 is thrown away, not rounded up to 8. The cast (int) is you telling Java: "I know I will lose the decimals, do it anyway."
int to double happens by itself. double to int needs a cast, and the decimals are cut off, never rounded.
Casting.javaRuns on Java 25
1void main() {2    int foodLevel = 17;3    double asDouble = foodLevel;4    IO.println("int 17 as a double: " + asDouble);5 6    double health = 7.9;7    int wholeHealth = (int) health;8    IO.println("(int) 7.9 = " + wholeHealth);9 10    double damage = -2.7;11    IO.println("(int) -2.7 = " + (int) damage);12 13    double hearts = health / 2;14    int fullHearts = (int) hearts;15    IO.println(health + " health is " + hearts + " hearts, " + fullHearts + " of them full");16}
  1. Automatic: an int fits in a double. 17 becomes 17.0.
  2. A cast. (int) chops off everything after the decimal point, so 7.9 becomes 7, not 8. For proper rounding, use Math.round (see Operators and math).
  3. Chopping works toward zero, so -2.7 becomes -2.
  4. A heart is 2 health points, so dividing by 2 gives hearts: 3.95.
  5. Casting to int keeps only the whole hearts: 3 full hearts.

If you forget the cast, the compiler refuses with "possible lossy conversion". This is one of the errors you will meet most, because Paper gives you a double for health but many things you calculate from it need to be int:

LossyConversion.javaRuns on Java 25
1void main() {2    double health = 19.5;3    int roundedHealth = health;4}
  1. A double cannot go into an int without a cast. Fix it with (int) health if chopping is what you want.

Between text and numbers: parsing

Text that looks like a number is still text. "64" is two characters, not the number 64. To do math with it, parse it into a number. To go the other way, turn a number into text with String.valueOf:

TextToNumbers.javaRuns on Java 25
1void main() {2    String typedAmount = "64";3    int amount = Integer.parseInt(typedAmount);4    IO.println("Doubled: " + (amount * 2));5 6    String typedSpeed = "0.5";7    double speed = Double.parseDouble(typedSpeed);8    IO.println("Speed plus one: " + (speed + 1));9 10    int level = 30;11    String levelText = String.valueOf(level);12    IO.println("As text the level has " + levelText.length() + " characters");13 14    IO.println("Text glued: " + typedAmount + typedAmount);15}
  1. Text, as if a player typed it.
  2. Reads the text and gives back the int 64.
  3. The same for numbers with decimals.
  4. Turns the number 30 into the text "30". You will see this all the time in plugins, because many messages and placeholders want text.
  5. Two Strings glued together: "64" + "64" is "6464", not 128. This is why parsing matters.

If the text is not a number, parsing fails at runtime with a NumberFormatException:

ParseFailure.javaRuns on Java 25
1void main() {2    String typedAmount = "ten";3    int amount = Integer.parseInt(typedAmount);4    IO.println("You asked for " + amount);5}
  1. "ten" is not digits, so Java throws an exception and the program stops here.

This is not just theory. When a player types a command like /setfood 15, your plugin receives the 15 as a String, always. You parse it, and you must be ready for players who type /setfood lots. The plugin below shows how.

A plugin full of variables

This plugin adds two commands. /mystats reads your stats into variables of the right types and prints them. /setfood 15 turns the text 15 into a number and sets your hunger bar. Commands get a whole chapter later (Commands, part 1); here, focus on the variables.

StatsCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-variables-types-statssrcmainjavacomexamplejavavariablestypesstatsStatsCommand.java

The package com.example.javavariablestypesstats is the folder path com/example/javavariablestypesstats 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-variables-types-stats/
    • src/main/
      • java/com/example/javavariablestypesstats/Package com.example.javavariablestypesstats
        • SetFoodCommand.javaCommand (BasicCommand)
        • StatsCommand.javayou are hereCommand (BasicCommand)
        • VariableStatsPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • 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.javavariablestypesstats;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import org.bukkit.entity.Player;6 7public final class StatsCommand implements BasicCommand {8 9    private static final int MAX_FOOD_LEVEL = 20;10 11    @Override12    public void execute(CommandSourceStack source, String[] args) {13        if (!(source.getExecutor() instanceof Player player)) {14            source.getSender().sendRichMessage("<red>Only players can use /mystats.");15            return;16        }17 18        String name = player.getName();19        double health = player.getHealth();20        int foodLevel = player.getFoodLevel();21        float saturation = player.getSaturation();22        int level = player.getLevel();23        boolean flying = player.isFlying();24        long firstPlayed = player.getFirstPlayed();25 26        int fullHearts = (int) (health / 2);27 28        player.sendRichMessage("<gold>Stats for " + name);29        player.sendRichMessage("<gray>Health: <red>" + health + " <gray>(" + fullHearts + " full hearts)");30        player.sendRichMessage("<gray>Food: <green>" + foodLevel + "/" + MAX_FOOD_LEVEL);31        player.sendRichMessage("<gray>Saturation: <yellow>" + saturation);32        player.sendRichMessage("<gray>Level: <aqua>" + level);33        player.sendRichMessage("<gray>Flying: <white>" + flying);34        player.sendRichMessage("<gray>First joined: <white>" + firstPlayed + " <gray>ms after 1970");35    }36}
  1. Makes this class a simple command. Paper calls its execute method every time someone runs the command.
  2. A constant: the hunger bar is always out of 20.
  3. The words typed after the command, as Strings. /mystats does not use them.
  4. Checks that a player ran the command, not the server console, and if so stores that player in the variable player. Making decisions explains this line fully.
  5. Each of the next lines asks the player for one value and keeps it in a variable whose type matches what the method returns.
  6. A double, because health can be 17.5.
  7. Saturation is a float in Paper. Using int here would not compile.
  8. A yes-or-no value.
  9. A huge number of milliseconds, way past the int limit, so it is a long.
  10. Two health points make one heart. The cast keeps only the full hearts, exactly like the Casting program.
  11. Each message glues text and variables together with +. The tags like <gray> are colors, explained in MiniMessage.

Here is what a player sees after typing /mystats:

Stats for Steve
Health: 17.5 (8 full hearts)
Food: 18/20
Saturation: 3.2
Level: 12
Flying: false
First joined: 1790431200000 ms after 1970

The second command turns typed text into a number, keeps it in range, and uses it:

SetFoodCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-variables-types-statssrcmainjavacomexamplejavavariablestypesstatsSetFoodCommand.java

The package com.example.javavariablestypesstats is the folder path com/example/javavariablestypesstats 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-variables-types-stats/
    • src/main/
      • java/com/example/javavariablestypesstats/Package com.example.javavariablestypesstats
        • SetFoodCommand.javayou are hereCommand (BasicCommand)
        • StatsCommand.javaCommand (BasicCommand)
        • VariableStatsPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • 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.javavariablestypesstats;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import org.bukkit.entity.Player;6 7public final class SetFoodCommand implements BasicCommand {8 9    private static final int MAX_FOOD_LEVEL = 20;10 11    @Override12    public void execute(CommandSourceStack source, String[] args) {13        if (!(source.getExecutor() instanceof Player player)) {14            source.getSender().sendRichMessage("<red>Only players can use /setfood.");15            return;16        }17        if (args.length != 1) {18            player.sendRichMessage("<red>Type one number after the command, like /setfood 20.");19            return;20        }21 22        String typed = args[0];23        int amount;24        try {25            amount = Integer.parseInt(typed);26        } catch (NumberFormatException exception) {27            player.sendRichMessage("<red>That is not a whole number. Try /setfood 20.");28            return;29        }30 31        int newFoodLevel = Math.clamp(amount, 0, MAX_FOOD_LEVEL);32        player.setFoodLevel(newFoodLevel);33        player.sendRichMessage("<green>Your food level is now " + newFoodLevel + ".");34    }35 36    @Override37    public String permission() {38        return "variablestats.setfood";39    }40}
  1. Makes sure exactly one word was typed after /setfood. If not, it explains how to use the command and stops with return.
  2. The first word the player typed, as a String. For /setfood 15 this is the text "15". args[0] means "the first item in the list"; lists start counting at 0.
  3. Declared now, assigned on the next lines.
  4. Turns the text into an int.
  5. If the text was not a number, Java jumps here instead of crashing. The player gets a friendly message. Exceptions covers try and catch.
  6. Keeps the number between 0 and 20, so /setfood 500 becomes 20 and /setfood -3 becomes 0.
  7. Gives the number to Paper. setFoodLevel wants an int, and that is exactly what we have.
  8. Only players with this permission (operators, by default) can use the command.
That is not a whole number. Try /setfood 20.
Your food level is now 15.

The main class registers both commands when the plugin starts:

VariableStatsPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-variables-types-statssrcmainjavacomexamplejavavariablestypesstatsVariableStatsPlugin.java

The package com.example.javavariablestypesstats is the folder path com/example/javavariablestypesstats 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-variables-types-stats/
    • src/main/
      • java/com/example/javavariablestypesstats/Package com.example.javavariablestypesstats
        • SetFoodCommand.javaCommand (BasicCommand)
        • StatsCommand.javaCommand (BasicCommand)
        • VariableStatsPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
      • 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.javavariablestypesstats;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class VariableStatsPlugin extends JavaPlugin {7 8    @Override9    public void onEnable() {10        getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {11            event.registrar().register("mystats", "Shows your health, food, level and more", new StatsCommand());12            event.registrar().register("setfood", "Sets your food level from 0 to 20", new SetFoodCommand());13        });14    }15}
  1. Paper's moment for adding commands. Copy this pattern for now; Commands, part 1 explains each piece.
  2. The command name players type, a description, and the object that runs it.
plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livesjava-variables-types-statssrcmainresourcesplugin.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.

  • java-variables-types-stats/
    • src/main/
      • java/com/example/javavariablestypesstats/Package com.example.javavariablestypesstats
        • SetFoodCommand.javaCommand (BasicCommand)
        • StatsCommand.javaCommand (BasicCommand)
        • VariableStatsPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
      • resources/Files copied into the jar as they are
        • 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: VariableStats2version: '1.0.0'3main: com.example.javavariablestypesstats.VariableStatsPlugin4api-version: '26.3'5description: Shows how plugin values fit Java types with /mystats and /setfood.6permissions:7  variablestats.setfood:8    description: Lets a player change their own food level with /setfood.9    default: op
  1. The permission SetFoodCommand asks for.
  2. Operators get it automatically.

Download this plugin as a Gradle project to run it on your test server.

Common mistakes

Reading a variable before it has a value

UnassignedVariable.javaRuns on Java 25
1void main() {2    int coins;3    IO.println("Coins: " + coins);4}
  1. Declared, but nothing is ever put in.
  2. Reading an empty chest is not allowed. Give it a starting value: int coins = 0;.

Going past the limit: overflow

When an int goes past its maximum, it does not stop or crash. It wraps around to the most negative number. This is called overflow, and it has caused real money bugs on servers:

Overflow.javaRuns on Java 25
1void main() {2    int coins = Integer.MAX_VALUE;3    IO.println("Coins: " + coins);4 5    coins = coins + 1;6    IO.println("One more coin: " + coins);7 8    long bigCoins = Integer.MAX_VALUE;9    bigCoins = bigCoins + 1;10    IO.println("With a long: " + bigCoins);11}
  1. The biggest possible int: 2,147,483,647.
  2. One more coin wraps around to -2,147,483,648. A rich player is suddenly deep in debt.
  3. A long has room to spare, so the same math works.

If a number could ever grow past two billion (total blocks mined on a big server, money that players can farm forever, milliseconds), use long.

Comparing decimals exactly

Computers store decimals in binary, so some values are stored very slightly off. The tiny error shows up when you compare with ==:

ComparingDoubles.javaRuns on Java 25
1void main() {2    double total = 0.1 + 0.2;3    IO.println("0.1 + 0.2 = " + total);4    IO.println("Is it exactly 0.3? " + (total == 0.3));5    IO.println("Is it close to 0.3? " + (Math.abs(total - 0.3) < 0.0001));6}
  1. Prints 0.30000000000000004, not 0.3.
  2. So the exact comparison says false.
  3. Instead, ask "are they really close?": is the difference smaller than a tiny amount?

In plugins this bites when you check health or coordinates, such as player.getHealth() == 20.0 after healing. Prefer comparisons like >=, or check whether the values are close. For money, many plugins store whole cents in a long instead of a double, so the math is always exact.

Writing the wrong kind of value

FloatLiteral.javaRuns on Java 25
1void main() {2    float walkSpeed = 0.2;3}
  1. A decimal written plainly is a double. For a float, write 0.2f.

The same rule gives you the plugin version of this error. Paper's getHealth() returns a double, so this line does not compile:

JavaHas a mistake
1int health = player.getHealth();

Either use double health = player.getHealth();, or cast if you really want a whole number: int health = (int) player.getHealth();.

Practice

Try it

A player stats card

Start from this program and turn it into a stats card for a player named Alex who has 15.5 health, 18 food, level 27, 3 billion coins and is not flying. Then give Alex a quest reward: one level and 500 coins, and print the new values.

StatsCardStart.javaRuns on Java 25
1void main() {2    String name = "Alex";3    IO.println("=== " + name + " ===");4}

Requirements:

  • One variable per value, each with the best type.
  • A constant MAX_FOOD_LEVEL for the 20 in "Food: 18/20".
  • Calculate the number of full hearts from the health with a cast.
Hint 1

3 billion is more than an int can hold. Which type has room, and which letter do you need at the end of the number?

Hint 2

Full hearts: int fullHearts = (int) (health / 2);. The parentheses make Java divide first, then cast.

Hint 3

The constant goes above void main(): static final int MAX_FOOD_LEVEL = 20;.

Show the solution

Health is a double because of the .5, the coins need a long with an L, and the quest reward changes the variables with level = level + 1 and coins = coins + 500.

StatsCard.javaRuns on Java 25
1static final int MAX_FOOD_LEVEL = 20;2 3void main() {4    String name = "Alex";5    double health = 15.5;6    int foodLevel = 18;7    int level = 27;8    long coins = 3_000_000_000L;9    boolean isFlying = false;10 11    int fullHearts = (int) (health / 2);12 13    IO.println("=== " + name + " ===");14    IO.println("Health: " + health + " (" + fullHearts + " full hearts)");15    IO.println("Food: " + foodLevel + "/" + MAX_FOOD_LEVEL);16    IO.println("Level: " + level);17    IO.println("Coins: " + coins);18    IO.println("Flying: " + isFlying);19 20    coins = coins + 500;21    level = level + 1;22    IO.println("After the quest: level " + level + ", " + coins + " coins");23}
  1. Without the L, this line fails with "integer number too large".
  2. 15.5 / 2 is 7.75, and the cast keeps 7.
Try it

Show XP progress in /mystats

Add a line to /mystats that shows how far the player is toward their next level, as a whole percentage like "XP to next level: 45%". player.getExp() returns a float from 0 (just leveled up) to 1 (about to level up).

Hint

Multiply the progress by 100 to get a percentage, then cast it to int to drop the decimals.

Show the solution

Add these two lines to StatsCommand, after the other variables and before the messages are sent:

StatsCommand.java (new lines)
1int xpPercent = (int) (player.getExp() * 100);2player.sendRichMessage("<gray>XP to next level: <aqua>" + xpPercent + "%");

With 0.45 progress the player sees "XP to next level: 45%". Without the cast, the line would not compile, because a float times 100 is still a float.

More short challenges are waiting in the Practice Arena.

Recap

  • A variable is a named, typed place for one value. Declare it once (int coins = 50;), then read it by name and change it with =.
  • Pick the type by the value: int for whole numbers, long for huge ones, double for decimals, boolean for yes or no, String for text.
  • In plugins, match the type that Paper's method returns: double for health, int for food and levels, float for saturation and yaw.
  • null means "nothing here". Calling a method on it crashes.
  • var lets Java infer the type, but the type is still fixed.
  • final stops a variable from changing. Constants are static final and named in UPPER_SNAKE_CASE.
  • Cast with (int) to drop decimals. Parse text with Integer.parseInt, and turn numbers into text with String.valueOf. Command arguments always arrive as text.

Quick quiz

  1. Which type should hold a player's health?

  2. What does this print? int coins = 10; coins = coins + 5; IO.println(coins);

  3. What is the value of (int) 9.99?

  4. A player types /setfood 15. What is the type of args[0] in your command?

  5. Why should a plugin that counts total blocks mined on a big server use long instead of int?

Next steps