Paper Plugin Guide
File mode0

Java basics

Loops: doing things many times

for, for-each and while loops, plus break and continue.

Beginner44 min read

A loop runs the same code many times: once for every online player, every inventory slot, every block in a wall. On this page you will learn Java's loops and use them to build plugin commands that give everyone a gift, count diamonds, build a pillar and a platform, and run a countdown without freezing the server.

Why loops?

Imagine a /gift command that gives every online player five bread. You cannot write one line per player, because you do not know who will be online: maybe 2 players, maybe 200. You need a way to say "do this for every player, however many there are". That is a loop:

Give every online player some bread
1for (Player player : Bukkit.getOnlinePlayers()) {2    player.getInventory().addItem(ItemStack.of(Material.BREAD, 5));3}
  1. Read it as "for each player in the list of online players". The block below runs once per player, and each time player is the next one.
  2. ItemStack.of(Material.BREAD, 5) makes a stack of five bread, and addItem puts it into that player's inventory. Bukkit.getOnlinePlayers() is Paper's list of everyone online right now.

Two lines of real work, and they handle any number of players. By the end of this page, you will understand every part of it and the other loops Java offers.

The for loop

The classic for loop repeats a block a set number of times, using a counter. Run it:

CountUp.javaRuns on Java 25
1void main() {2    for (int wave = 1; wave <= 5; wave++) {3        IO.println("Wave " + wave + " is starting!");4    }5    IO.println("All waves done.");6}
  1. The loop header has three parts separated by semicolons: where the counter starts, the condition to keep going, and how the counter changes after each round. The diagram below takes it apart.
  2. The body. It runs once per round, and wave holds a different number each time: 1, then 2, then 3...
  3. After the loop, outside its braces. It runs once, when the loop has finished.
The three parts of a for loop for ( int wave = 1 ; wave <= 5 ; wave++ ) { body } 1. Start runs once, first 2. Condition checked before every round 3. Step runs after every round The order they run in wave = 1 wave <= 5 ? true run the body wave++ back to the condition false leave the loop, continue below With wave going 1, 2, 3, 4, 5 the body runs five times. When wave becomes 6, the condition is false.
A for loop header has three parts. The start runs once. The condition is checked before every round; while it is true, the body runs, then the step. When the condition becomes false, Java leaves the loop.

Step by step, Java does this:

  1. Start: create wave and set it to 1. This happens only once.
  2. Condition: is wave <= 5? If yes, run the body. If no, the loop is over.
  3. Step: wave++ adds 1 to wave (it is short for wave = wave + 1). Then back to step 2.

Each trip through the body is called an iteration. The counter variable is often called i in examples online (for "index"), but a real name like wave, slot or seconds makes your code easier to read.

Counting down

The counter can go in any direction. Start high, keep going while it is at least 1, and subtract with --:

Countdown.javaRuns on Java 25
1void main() {2    for (int seconds = 10; seconds >= 1; seconds--) {3        IO.println("Game starts in " + seconds + "...");4    }5    IO.println("Go!");6}
  1. Start at 10.
  2. Keep going while the number is 1 or more, so 1 is the last number printed.
  3. Subtract 1 after each round. You could also write seconds -= 2 to count down in steps of two.

This prints all ten lines instantly. A real game countdown needs one number per second; the section Loops and time shows how plugins do that.

The off-by-one mistake

The most common loop bug runs one time too many or one time too few. The hotbar has 9 slots, numbered 0 to 8. This loop tries to visit them all:

OffByOne.javaRuns on Java 25
1void main() {2    for (int slot = 0; slot <= 9; slot++) {3        IO.println("Hotbar slot " + slot);4    }5}
  1. The bug: <= lets slot reach 9, a tenth slot that does not exist. In a plugin, asking for slot 9 of a 9-slot row gives you the wrong item or crashes.

The fix is slot < 9. A good habit: when you count from 0, use < and the number of things. for (int slot = 0; slot < 9; slot++) runs exactly 9 times. This kind of slip is called an off-by-one error.

Looping over arrays and lists

Most loops in plugins do not count; they visit every item in a group: every player, every item in an inventory, every line of a config list. Java holds groups of values in an array (a fixed-size row of values) or a List (a row that can grow). Lists, sets and maps covers them properly; here you only need to loop over them.

ArraysAndLists.javaRuns on Java 25
1void main() {2    String[] names = {"Steve", "Alex", "Notch"};3 4    for (int i = 0; i < names.length; i++) {5        IO.println("Player number " + i + " is " + names[i]);6    }7 8    for (String name : names) {9        IO.println("Welcome, " + name + "!");10    }11 12    List<String> kit = List.of("stone_sword", "bread", "torch");13    for (String item : kit) {14        IO.println("Kit item: " + item);15    }16}
  1. An array of three Strings. The square brackets mean "many of these". Positions are numbered from 0, so names[0] is Steve.
  2. The classic way: count through the positions. names.length is how many items the array holds (3), so i goes 0, 1, 2.
  3. Reads the item at position i.
  4. The for-each loop: "for each name in names". No counter, no length, no off-by-one mistakes. Java hands you each item in turn.
  5. A small fixed list. In a plugin class you would add import java.util.List; at the top; these practice files import it for you.
  6. The for-each loop works the same way on lists.

Use the for-each loop whenever you just need every item. Use the counting loop only when you need the position number, for example to print slot numbers or to skip every second item.

In a plugin: a gift for everyone

Here is the /gift command from the top of the page, complete. A command class has an execute method that Paper calls when someone types the command; Commands, part 1 explains the rest. For now, focus on the loop inside it:

GiftCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainjavacomexamplejavaloopsdemoGiftCommand.java

The package com.example.javaloopsdemo is the folder path com/example/javaloopsdemo 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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownTask.javaHelper class
        • DiamondCountCommand.javaCommand (BasicCommand)
        • GiftCommand.javayou are hereCommand (BasicCommand)
        • LoopsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javaCommand (BasicCommand)
        • PlatformCommand.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.

13@Override14public void execute(CommandSourceStack source, String[] args) {15    int gifted = 0;16    for (Player player : Bukkit.getOnlinePlayers()) {17        player.getInventory().addItem(ItemStack.of(Material.BREAD, 5));18        player.sendRichMessage("<gold>You received 5 bread. Enjoy!");19        gifted++;20    }21    source.getSender().sendRichMessage("<green>Gave bread to <count> players.",22        Placeholder.unparsed("count", String.valueOf(gifted)));23}
  1. Paper calls this method when someone types /gift. source knows who typed it.
  2. A counter that starts at zero, before the loop. It must be created outside the loop, or it would restart at 0 every round.
  3. Bukkit.getOnlinePlayers() is a collection of everyone online right now. The for-each loop visits each of them once.
  4. Creates a new stack of 5 bread for this player. If their inventory is full, addItem cannot fit it and the bread is lost; Inventories shows how to drop leftovers on the ground.
  5. Counts this player. After the loop, gifted holds the number of players who got bread.
  6. After the loop: tell whoever ran the command how it went.
You received 5 bread. Enjoy!
Gave bread to 3 players.

while and do-while

A for loop is best when you know how many rounds you want, or you have a group to walk through. A while loop is simpler: it repeats as long as a condition is true, and you do not need to know in advance how many rounds that takes. This one packs 200 cobblestone into stacks of at most 64:

FillChest.javaRuns on Java 25
1void main() {2    int cobblestone = 200;3    int stacks = 0;4 5    while (cobblestone > 0) {6        int stackSize = Math.min(64, cobblestone);7        cobblestone -= stackSize;8        stacks++;9        IO.println("Stack " + stacks + ": " + stackSize + " cobblestone, " + cobblestone + " left");10    }11 12    IO.println("Used " + stacks + " slots.");13}
  1. Checked before every round: is there still cobblestone left? If yes, run the body again.
  2. Take a full stack of 64, or whatever is left if that is less. You met Math.min in Operators and math.
  3. This line is what makes the loop end one day: each round, the amount gets smaller, until the condition becomes false.
How a for loop runs A for loop sets i to 0 once, then checks i less than 3. If true it runs the body, adds 1 to i and checks again. When the check is false the loop ends and the code after it runs. int i = 0 runs once, at the start i < 3 ? yes Run the body sendMessage(...) i++ add 1 to i repeat no The loop is over code after it runs next Trace i is 0: run body, i becomes 1 i is 1: run body, i becomes 2 i is 2: run body, i becomes 3 i is 3: 3 < 3 is false, stop The body ran 3 times: for i = 0, 1 and 2.
A while loop checks its condition, runs the body, and goes back to check again. The moment the condition is false, Java continues after the loop.

A do-while loop is the same, except the condition is checked at the end, so the body always runs at least once. That fits "ask, and ask again if the answer was not valid":

AskUntilValid.javaRuns on Java 25
1void main() {2    String answer;3    do {4        answer = IO.readln("Reset your island? Type yes or no: ").strip();5    } while (!answer.equalsIgnoreCase("yes") && !answer.equalsIgnoreCase("no"));6 7    IO.println("You answered: " + answer);8}
  1. Created before the loop, so the condition at the bottom can still see it.
  2. Run the body first, with no question asked.
  3. Then check: was the answer neither yes nor no? Then go round again. The recorded run typed maybe (rejected) and then YES.

You will write do-while loops rarely. In plugins, input comes from commands and events, not from a loop that waits for typing.

Infinite loops, and why they freeze a server

If the condition of a loop never becomes false, the loop never ends. This is an infinite loop, and it is almost always a bug:

This loop never endsHas a mistake
1int cobblestone = 200;2while (cobblestone > 0) {3    IO.println("Packing a stack...");4}

Nothing inside the loop changes cobblestone, so it stays 200 forever. In a practice program, you just press Stop. On a server it is much worse. Paper runs the whole game on one main thread: one worker that handles every player, mob and block, 20 times per second (each of these steps is a tick). Your event handlers and commands run on that same thread. If your code never finishes, the next tick never starts: mobs freeze, chat stops, and every player times out. After a while Paper's watchdog decides the server has stopped responding, prints a long report in the console, and shuts it down.

break and continue

Two keywords let you change a loop's plan from inside the body:

  • break leaves the loop immediately. Use it when you found what you were looking for.
  • continue skips the rest of this round and jumps to the next one. Use it to skip items you do not care about.
FindPlayer.javaRuns on Java 25
1void main() {2    String[] online = {"Steve", "Alex", "Notch", "Jeb", "Dinnerbone"};3    String wanted = "notch";4 5    String found = null;6    for (String name : online) {7        IO.println("Checking " + name + "...");8        if (name.equalsIgnoreCase(wanted)) {9            found = name;10            break;11        }12    }13 14    if (found == null) {15        IO.println("Nobody called " + wanted + " is online.");16    } else {17        IO.println("Found " + found + "!");18    }19}
  1. null means "nothing yet". If the loop never finds the player, it stays null.
  2. Found? Names are compared with equalsIgnoreCase, as you learned in Making decisions.
  3. Stop searching. The output shows Java never checks Jeb or Dinnerbone, which saves work on a big server.
  4. After the loop, the variable tells you whether the search succeeded.
SkipEmptySlots.javaRuns on Java 25
1void main() {2    String[] hotbar = {"diamond_sword", "", "bread", "", "", "torch", "", "", "shield"};3 4    for (int slot = 0; slot < hotbar.length; slot++) {5        if (hotbar[slot].isEmpty()) {6            continue;7        }8        IO.println("Slot " + slot + ": " + hotbar[slot]);9    }10}
  1. An empty String stands for an empty slot here.
  2. Skip this slot: jump straight to slot++ and the next round. The print below does not run for empty slots.

continue is the loop version of the guard clauses you saw in Making decisions: deal with the boring cases first, and keep the real work flat.

In a plugin: a pillar that stops at obstacles

This command builds a gold pillar up to 10 blocks tall, two blocks in front of the player. If something is in the way, it stops instead of destroying it:

PillarCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainjavacomexamplejavaloopsdemoPillarCommand.java

The package com.example.javaloopsdemo is the folder path com/example/javaloopsdemo 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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownTask.javaHelper class
        • DiamondCountCommand.javaCommand (BasicCommand)
        • GiftCommand.javaCommand (BasicCommand)
        • LoopsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javayou are hereCommand (BasicCommand)
        • PlatformCommand.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.

15@Override16public void execute(CommandSourceStack source, String[] args) {17    if (!(source.getExecutor() instanceof Player player)) {18        source.getSender().sendRichMessage("<red>Only players can build pillars.");19        return;20    }21 22    Block base = player.getLocation().getBlock().getRelative(player.getFacing(), 2);23    int placed = 0;24    for (int height = 0; height < MAX_HEIGHT; height++) {25        Block block = base.getRelative(BlockFace.UP, height);26        if (!block.getType().isAir()) {27            break;28        }29        block.setType(Material.GOLD_BLOCK);30        placed++;31    }32 33    player.sendRichMessage("<gold>Built a pillar <height> blocks tall.",34        Placeholder.unparsed("height", String.valueOf(placed)));35}
  1. source.getExecutor() is whoever the command runs as: a player or the console. Only a player has a position and a facing direction, so the console gets a friendly error instead. This is the pattern you met in Making decisions.
  2. The block at the player's feet, then 2 blocks further in the direction they are facing (north, south, east or west).
  3. Up to 10 rounds: heights 0 to 9. MAX_HEIGHT is a constant at the top of the class, so the limit is easy to change.
  4. The block height blocks above the base. Each round goes one block higher.
  5. Something is here (stone, a tree, a house)...
  6. ...so stop building completely. The blocks above are not even checked.
  7. Counts the blocks actually placed, which may be fewer than 10.
Built a pillar 10 blocks tall.

Nested loops: grids

A loop inside another loop is a nested loop. The inner loop runs completely, every round, for each round of the outer loop. That is perfect for anything with rows and columns. First a small times table:

MultiplicationTable.javaRuns on Java 25
1void main() {2    for (int row = 1; row <= 5; row++) {3        for (int column = 1; column <= 5; column++) {4            IO.print(String.format("%4d", row * column));5        }6        IO.println("");7    }8}
  1. The outer loop: one round per row.
  2. The inner loop: for each row, it runs all 5 columns. In total, the inner body runs 5 x 5 = 25 times.
  3. Prints without starting a new line. %4d pads each number to 4 characters so the columns line up.
  4. After a full row, end the line.

Plugins need exactly this for inventories. A chest has 3 rows of 9 slots, and Paper numbers the slots from 0, left to right, row by row. The slot number for any row and column is row * 9 + column:

InventoryGrid.javaRuns on Java 25
1void main() {2    int rows = 3;3    int columns = 9;4 5    for (int row = 0; row < rows; row++) {6        for (int column = 0; column < columns; column++) {7            int slot = row * columns + column;8            IO.print(String.format("%3d", slot));9        }10        IO.println("");11    }12}
  1. Row 0 holds slots 0 to 8. Each row further down adds 9. Row 1, column 4 is slot 13: the middle of the chest.
Chest slot numbers A large chest has 6 rows of 9 slots, numbered 0 to 53 from left to right and top to bottom. The slot number is the row times 9 plus the column, counting from 0. col 0 col 1 col 2 col 3 col 4 col 5 col 6 col 7 col 8 row 0 0 1 2 3 4 5 6 7 8 row 1 9 10 11 12 13 14 15 16 17 row 2 18 19 20 21 22 23 24 25 26 row 3 27 28 29 30 31 32 33 34 35 row 4 36 37 38 39 40 41 42 43 44 row 5 45 46 47 48 49 50 51 52 53 slot = row * 9 + column Row 2, column 4 is slot 2 * 9 + 4 = 22 (the purple one). Counting always starts at 0, so the first slot is 0 and the last is 53.
Slot numbers in a 3-row chest: 0 to 8 on the top row, 9 to 17 in the middle, 18 to 26 at the bottom.
0: lime_stained_glass_pane, 8: lime_stained_glass_pane, 13: diamond, 18: red_stained_glass_pane, 26: red_stained_glass_pane

You will use this formula when you build menus in Building GUI menus, for example to put a border around a chest menu.

In a plugin: a glass platform

Nested loops also cover areas of the world. This command places a 5 by 5 glass floor under the player, filling only empty spots so it never breaks anything:

PlatformCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainjavacomexamplejavaloopsdemoPlatformCommand.java

The package com.example.javaloopsdemo is the folder path com/example/javaloopsdemo 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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownTask.javaHelper class
        • DiamondCountCommand.javaCommand (BasicCommand)
        • GiftCommand.javaCommand (BasicCommand)
        • LoopsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javaCommand (BasicCommand)
        • PlatformCommand.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.

13@Override14public void execute(CommandSourceStack source, String[] args) {15    if (!(source.getExecutor() instanceof Player player)) {16        source.getSender().sendRichMessage("<red>Only players can build platforms.");17        return;18    }19 20    Block center = player.getLocation().getBlock().getRelative(BlockFace.DOWN);21    int placed = 0;22    for (int x = -2; x <= 2; x++) {23        for (int z = -2; z <= 2; z++) {24            Block block = center.getRelative(x, 0, z);25            if (block.getType().isAir()) {26                block.setType(Material.GLASS);27                placed++;28            }29        }30    }31 32    player.sendRichMessage("<aqua>Placed <count> glass blocks.",33        Placeholder.unparsed("count", String.valueOf(placed)));34}
  1. The block just below the player's feet: the center of the platform.
  2. From 2 blocks west of the center to 2 blocks east: -2, -1, 0, 1, 2. That is 5 rounds.
  3. For each of those, from 2 blocks north to 2 blocks south. 5 x 5 = 25 blocks in total.
  4. The block x blocks east and z blocks south of the center, on the same height (0).
  5. Only replace air, so grass, chests and other players' builds stay untouched.

Counting, summing and finding the biggest

Three small patterns show up in loops all the time. Each one uses a variable created before the loop, changed inside it, and read after it:

KillStats.javaRuns on Java 25
1void main() {2    int[] killsPerMatch = {3, 7, 0, 12, 5, 9};3 4    int total = 0;5    int best = 0;6    int bigGames = 0;7    for (int kills : killsPerMatch) {8        total += kills;9        if (kills > best) {10            best = kills;11        }12        if (kills >= 5) {13            bigGames++;14        }15    }16 17    double average = (double) total / killsPerMatch.length;18    IO.println("Matches: " + killsPerMatch.length);19    IO.println("Total kills: " + total);20    IO.println("Best match: " + best);21    IO.println("Matches with 5 or more kills: " + bigGames);22    IO.println("Average: " + average);23}
  1. Summing: start at 0 and add every value.
  2. Finding the biggest: start with the smallest possible answer and replace it whenever you see something bigger.
  3. Counting: start at 0 and add 1 whenever an item matches.
  4. A for-each loop works on arrays of numbers too.
  5. Adds this match's kills to the running total.
  6. A new record? Remember it.
  7. Turns the total into a decimal number first, so the division keeps the decimals. Here 36 / 6 happens to be exactly 6, but with 37 kills, dividing two whole numbers would give 6 instead of 6.17.

In a plugin: counting diamonds

An inventory is a group of slots, so you can loop over it. This command adds up every diamond a player carries and finds their biggest stack, using all the patterns above plus continue:

DiamondCountCommand.java (the counting loop)Compiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainjavacomexamplejavaloopsdemoDiamondCountCommand.java

The package com.example.javaloopsdemo is the folder path com/example/javaloopsdemo 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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownTask.javaHelper class
        • DiamondCountCommand.javayou are hereCommand (BasicCommand)
        • GiftCommand.javaCommand (BasicCommand)
        • LoopsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javaCommand (BasicCommand)
        • PlatformCommand.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.

19int total = 0;20int biggestStack = 0;21for (ItemStack item : player.getInventory().getContents()) {22    if (item == null || item.getType() != Material.DIAMOND) {23        continue;24    }25    total += item.getAmount();26    if (item.getAmount() > biggestStack) {27        biggestStack = item.getAmount();28    }29}
  1. The total and the record are created before the loop, so they survive from slot to slot.
  2. Every slot of the player's inventory, as an array of items. Empty slots are null.
  3. Skip empty slots first. Thanks to short-circuiting, getType() is never called on an empty slot.
  4. Not a diamond: skip to the next slot.
  5. A slot can hold up to 64 diamonds, so add the stack's amount, not 1.
  6. Remember the largest stack seen so far.
You carry 75 diamonds. Biggest stack: 64.

Building text with a loop

Loops can build text piece by piece. This is how plugins draw progress bars in the action bar or a boss bar title:

ProgressBar.javaRuns on Java 25
1void main() {2    int done = 7;3    int total = 10;4 5    String bar = "";6    for (int i = 0; i < total; i++) {7        bar += i < done ? "|" : ".";8    }9 10    int percent = done * 100 / total;11    IO.println("[" + bar + "] " + percent + "%");12}
  1. Start with empty text.
  2. One round per character: 10 characters in total.
  3. The ternary operator from the last chapter: a bar for the finished part, a dot for the rest. += adds the character to the end.
  4. Multiply first, then divide, so integer division does not round the result down to 0.

In a plugin, you would add color tags and send it to the action bar. A mining quest at 7 of 10 blocks might look like this:

Quest: |||||||... 70%

Loops and time: use the scheduler

A loop runs as fast as the computer can go: your countdown earlier printed ten numbers in a blink. Sooner or later every plugin developer wants "one number per second" and writes this:

Never do this in a pluginHas a mistake
1for (int seconds = 5; seconds >= 1; seconds--) {2    Bukkit.getServer().sendRichMessage("<yellow>Starting in " + seconds);3    Thread.sleep(1000);4}

Thread.sleep(1000) pauses the thread for one second. But your command runs on the server's main thread, so the whole server sleeps: for five seconds no mob moves, no block breaks and no other command runs, for every player on the server. Players see the whole world freeze, then jump forward.

The fix is to let Paper's scheduler be the loop. Instead of one method that loops five times, you write a task that does one round, and the scheduler calls it once every 20 ticks (one second), carrying on with the game in between:

CountdownTask.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainjavacomexamplejavaloopsdemoCountdownTask.java

The package com.example.javaloopsdemo is the folder path com/example/javaloopsdemo 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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownTask.javayou are hereHelper class
        • DiamondCountCommand.javaCommand (BasicCommand)
        • GiftCommand.javaCommand (BasicCommand)
        • LoopsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javaCommand (BasicCommand)
        • PlatformCommand.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.

8public final class CountdownTask implements Consumer<BukkitTask> {9 10    private int secondsLeft;11 12    public CountdownTask(int seconds) {13        this.secondsLeft = seconds;14    }15 16    @Override17    public void accept(BukkitTask task) {18        if (secondsLeft == 0) {19            Bukkit.getServer().sendRichMessage("<green><bold>Go!");20            task.cancel();21            return;22        }23        Bukkit.getServer().sendRichMessage("<yellow>Starting in <seconds>...",24            Placeholder.unparsed("seconds", String.valueOf(secondsLeft)));25        secondsLeft--;26    }27}
  1. A class the scheduler can call over and over. Each call is one round of our "loop".
  2. The loop counter. It lives in the object, not in a method, so it keeps its value between calls.
  3. The scheduler calls this method once per second. It does one round and then returns, so the server is free again right away.
  4. The loop's end condition, checked at the start of each round.
  5. The scheduler's version of break: stop calling this task.
  6. Sends a chat message to everyone online.
  7. The step, just like seconds-- in a for loop.
CountdownCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainjavacomexamplejavaloopsdemoCountdownCommand.java

The package com.example.javaloopsdemo is the folder path com/example/javaloopsdemo 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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javayou are hereCommand (BasicCommand)
        • CountdownTask.javaHelper class
        • DiamondCountCommand.javaCommand (BasicCommand)
        • GiftCommand.javaCommand (BasicCommand)
        • LoopsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javaCommand (BasicCommand)
        • PlatformCommand.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.

15@Override16public void execute(CommandSourceStack source, String[] args) {17    plugin.getServer().getScheduler().runTaskTimer(plugin, new CountdownTask(5), 0L, 20L);18}
  1. Starts the repeating task: wait 0L ticks before the first round, then run it every 20L ticks. The L marks the numbers as long, the type the scheduler expects.
  2. A fresh countdown from 5 each time someone runs the command.
Starting in 5...
Starting in 4...
Starting in 3...
Starting in 2...
Starting in 1...
Go!

Some of this code (implements, the constructor, this.plugin) belongs to later chapters. The idea to keep from this page is simple: a loop is for doing many things right now; the scheduler is for doing things over time. Timing and tasks: the scheduler covers it all, and the Tick Calculator converts seconds to ticks for you.

The whole plugin

All five commands are registered in the main class:

LoopsDemoPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainjavacomexamplejavaloopsdemoLoopsDemoPlugin.java

The package com.example.javaloopsdemo is the folder path com/example/javaloopsdemo 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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownTask.javaHelper class
        • DiamondCountCommand.javaCommand (BasicCommand)
        • GiftCommand.javaCommand (BasicCommand)
        • LoopsDemoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javaCommand (BasicCommand)
        • PlatformCommand.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.javaloopsdemo;2 3import io.papermc.paper.command.brigadier.Commands;4import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;5import org.bukkit.plugin.java.JavaPlugin;6 7public final class LoopsDemoPlugin extends JavaPlugin {8 9    @Override10    public void onEnable() {11        getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {12            Commands commands = event.registrar();13            commands.register("gift", "Give every online player some bread", new GiftCommand());14            commands.register("diamonds", "Count the diamonds in your inventory", new DiamondCountCommand());15            commands.register("pillar", "Build a gold pillar in front of you", new PillarCommand());16            commands.register("platform", "Build a 5 by 5 glass platform under you", new PlatformCommand());17            commands.register("countdown", "Count down from 5 in chat, one number per second", new CountdownCommand(this));18        });19        getLogger().info("Loop commands registered: /gift, /diamonds, /pillar, /platform, /countdown");20    }21}
  1. The modern Paper way to register commands. Commands, part 1 explains each piece.
  2. Each line connects a command name and a description to the class that runs it.
  3. The countdown needs the plugin to start scheduler tasks, so it gets this (the plugin object).
plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-demosrcmainresourcesplugin.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-loops-demo/
    • src/main/
      • java/com/example/javaloopsdemo/Package com.example.javaloopsdemo
        • CountdownCommand.javaCommand (BasicCommand)
        • CountdownTask.javaHelper class
        • DiamondCountCommand.javaCommand (BasicCommand)
        • GiftCommand.javaCommand (BasicCommand)
        • LoopsDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PillarCommand.javaCommand (BasicCommand)
        • PlatformCommand.javaCommand (BasicCommand)
      • 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: LoopsDemo2version: '1.0.0'3main: com.example.javaloopsdemo.LoopsDemoPlugin4api-version: '26.3'5description: Commands built from loops - gifts for everyone, a diamond counter, a pillar, a platform and a countdown.6permissions:7  loopsdemo.gift:8    description: Lets a player give bread to everyone online.9    default: op10  loopsdemo.build:11    description: Lets a player use /pillar and /platform.12    default: op13  loopsdemo.countdown:14    description: Lets a player start a countdown.15    default: op
  1. One permission covers both building commands, so a server owner can allow or deny them together.
  • java-loops-demo/
    • src/
      • main/
        • java/
          • com/example/javaloopsdemo/
            • LoopsDemoPlugin.javaMain class: registers the five commands
            • GiftCommand.javafor-each over online players, with a counter
            • DiamondCountCommand.javaLoop over inventory slots: sum, max and continue
            • PillarCommand.javaCounting loop with break
            • PlatformCommand.javaNested loops over a 5 by 5 area
            • CountdownCommand.javaStarts the repeating countdown task
            • CountdownTask.javaOne round per second, run by the scheduler
        • resources/
          • plugin.ymlName, main class and the three permissions
Server console
[14:02:11 INFO]: [LoopsDemo] Enabling LoopsDemo v1.0.0[14:02:11 INFO]: [LoopsDemo] Loop commands registered: /gift, /diamonds, /pillar, /platform, /countdown

Download the plugin as a Gradle project, start your test server, and try every command (you need to be an operator: type op YourName in the server console). Or open it in the Compile Lab and change the pillar's material or the platform's size.

Mistakes to avoid

  • Off-by-one. When counting from 0, use < with the number of items: i < 9, i < names.length.
  • A loop that never ends. Make sure something in the body moves the loop toward its end. On a server, an endless loop freezes everyone.
  • Creating the counter inside the loop. int total = 0; inside the body resets to 0 every round. Counters and totals go before the loop.
  • Sleeping on the main thread. Never use Thread.sleep in plugin code to wait. Use the scheduler.
  • Forgetting empty slots. Inventory arrays contain null for empty slots. Check for null before calling methods on an item.
  • A semicolon after the loop header. for (int i = 0; i < 5; i++); loops over nothing five times, then runs the block below once. Put { straight after the ).
Try it

Zombie waves with boss waves

A minigame has 12 waves. Each wave has twice as many zombies as its number (wave 3 has 6 zombies), but every 5th wave is a boss wave instead. Change the starter so it prints:

Expected output
Wave 1: 2 zombiesWave 2: 4 zombiesWave 3: 6 zombiesWave 4: 8 zombiesWave 5: BOSS WAVE!Wave 6: 12 zombies...Wave 10: BOSS WAVE!Wave 11: 22 zombiesWave 12: 24 zombies
BossWaves.java (starter)Runs on Java 25
1void main() {2    int waves = 12;3 4    IO.println("Wave 1: 2 zombies");5}
Hint 1

Use a for loop from 1 up to and including waves.

Hint 2

The remainder operator tells you about every 5th: wave % 5 == 0 is true for 5, 10, 15 and so on.

Hint 3

Print the boss line and use continue to skip the zombie line for that wave.

Show the solution

The loop visits each wave once. The boss wave check acts as a guard clause inside the loop: it handles the special case and skips to the next wave with continue.

BossWavesSolution.javaRuns on Java 25
1void main() {2    int waves = 12;3 4    for (int wave = 1; wave <= waves; wave++) {5        if (wave % 5 == 0) {6            IO.println("Wave " + wave + ": BOSS WAVE!");7            continue;8        }9        int zombies = wave * 2;10        IO.println("Wave " + wave + ": " + zombies + " zombies");11    }12}
  1. Up to and including 12. With <, wave 12 would be missing.
  2. The remainder of dividing by 5 is 0 exactly for 5, 10, 15...
  3. Skips the normal zombie line for boss waves.
Try it

A /feedall command

Write a command that feeds every hungry player online (food level below 20), skips players who are already full, and tells the sender how many players it fed:

You are no longer hungry.
Fed 2 hungry players.

Start from a copy of GiftCommand in the loops demo plugin.

Hint 1

Loop over Bukkit.getOnlinePlayers() exactly like /gift does.

Hint 2

player.getFoodLevel() reads the hunger bar (20 is full) and player.setFoodLevel(20) fills it.

Hint 3

If the player is already at 20, continue. Only increase your counter for players you actually fed.

Show the solution

It is the gift loop with a guard clause in front. setSaturation is optional: it keeps players full for longer, like eating a golden carrot does.

FeedAllCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesjava-loops-exercisesrcmainjavacomexamplejavaloopsexerciseFeedAllCommand.java

The package com.example.javaloopsexercise is the folder path com/example/javaloopsexercise 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-loops-exercise/
    • src/main/
      • java/com/example/javaloopsexercise/Package com.example.javaloopsexercise
        • FeedAllCommand.javayou are hereCommand (BasicCommand)
        • LoopsExercisePlugin.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.

11@Override12public void execute(CommandSourceStack source, String[] args) {13    int fed = 0;14    for (Player player : Bukkit.getOnlinePlayers()) {15        if (player.getFoodLevel() >= 20) {16            continue;17        }18        player.setFoodLevel(20);19        player.setSaturation(20.0f);20        player.sendRichMessage("<green>You are no longer hungry.");21        fed++;22    }23    source.getSender().sendRichMessage("<gray>Fed <white><count></white> hungry players.",24        Placeholder.unparsed("count", String.valueOf(fed)));25}
  1. Already full: nothing to do for this player.
  2. Skip to the next player. The counter is not increased.
  3. Only reached for players who were actually hungry.

Register it in onEnable like the other commands. The finished exercise plugin includes the main class and plugin.yml.

Recap

  • A for loop has a start, a condition and a step: for (int i = 0; i < 9; i++).
  • The for-each loop, for (Player player : players), visits every item in an array or collection. Use it whenever you need every item.
  • while repeats while a condition is true; do-while always runs at least once.
  • break leaves the loop; continue skips to the next round.
  • Nested loops handle grids and areas. Chest slot = row * 9 + column.
  • Counters, totals and records are created before the loop, updated inside it and read after it.
  • Loops do many things right now. To do something over time, use the scheduler, never Thread.sleep.

Quick quiz

  1. How many times does the body of for (int slot = 0; slot < 9; slot++) run?

  2. You want to send a message to every online player. Which loop fits best?

  3. What does continue do inside a loop?

  4. A plugin's command loops with Thread.sleep(1000) between messages to make a countdown. What happens?

  5. In a 3-row chest, which slot is in row 2 (the bottom row), column 4 (the middle)? Rows and columns count from 0.

Next steps

  • Methods: package loops like the diamond counter into reusable methods such as countItems(player, Material.DIAMOND).
  • Lists, sets and maps: the groups of values your loops will walk through most.
  • The scheduler: repeating tasks, delays and countdowns done properly.
  • Practice Arena: plugin challenges where you loop over players and inventories, checked by real tests.