Paper Plugin Guide
File mode0

Projects

Project: Parkour course with timer

A parkour course with start and finish plates, checkpoints, a live timer and a saved leaderboard.

AdvancedProject, about 78 min read

In this project you build a parkour course with a start plate, checkpoints and a finish plate. Players see a live timer on their screen, fall back to their last checkpoint when they miss a jump, and compete on a leaderboard that is saved in a file. You learn pressure plate events, a per-player state machine, repeating tasks and saving data.

What you will build

Parkour is a classic mini-game: a path of tricky jumps, and the goal is to get to the end as fast as you can. Your plugin turns any set of pressure plates into a timed course. An admin builds the jumps with normal blocks and places pressure plates as the start, the checkpoints and the finish. Then the admin uses a few commands to tell the plugin where each plate is.

When a player steps on the start plate, the timer starts. A clock runs in the action bar, the line of text above the hotbar, updated every tick:

Time 00:12.345 | Checkpoint 2/4

Each checkpoint plate saves the player's position. If they fall off the course, or use /parkour checkpoint, they are teleported back to the last checkpoint with the timer still running:

Checkpoint 2 of 4 at 00:12.345.
You fell! Back to your last checkpoint.

At the finish plate the time stops, is shown for a moment, and is saved if it beats the player's old record. /parkour top shows the leaderboard:

Finished in 00:41.230!
New personal best! You are number 1 on the leaderboard.
Parkour top 3
1. Mia - 00:41.230
2. Steve - 00:47.905
3. Alex - 01:02.118
A parkour course seen from the side A side view of a course. The start plate is on the first platform, followed by two checkpoint plates and the finish plate on the last platform. A dashed line far below marks the fall height. A player who drops under it is teleported back to the last checkpoint. Start Checkpoint 1 Checkpoint 2 Finish Fall height Below this line the player is sent back to the last checkpoint fell!
The course from the side. The start, checkpoints and finish are pressure plates. Below the fall height, players are sent back.

The plan

FeatureThe piece that does itChapter
Notice a player stepping on a platePlayerInteractEvent with Action.PHYSICALEvents and listeners
Remember where the plates areBlock coordinates stored in config.ymlConfiguration files
Course setup commands for adminsBasicCommand with a permission checkSimple commands, Permissions
What each player is doingAn enum state and a Map from player to runEnums and records, Cooldowns, toggles and player state
A live timerA repeating task, the action bar and System.nanoTime()The scheduler, Titles, action bars and sounds
Falling resets youComparing the player's Y position, then teleportWorlds, locations and movement
Best timesYamlConfiguration saved to times.ymlSaving player data
The leaderboardSorting with a ComparatorLists, sets and maps, Leaderboard
Cleaning up when players leavePlayerQuitEventEvents and listeners
  1. Create the project

    A plugin.yml with the admin permission.

  2. Detect pressure plates

    Your first PlayerInteractEvent handler.

  3. Store the course in config.yml

    Course points, and the admin commands that set them.

  4. Give every player a run state

    An enum, a Run object and the three plate handlers.

  5. Add the live timer

    A task that runs every tick and shows the time.

  6. Checkpoints and falling

    Return to the last checkpoint, by hand or automatically.

  7. Save best times and show the leaderboard

    A YAML file and a sorted list.

  8. Clean up and wire it together

    Quit handling, the main class and a test checklist.

The finished plugin is in the download. This is an advanced project, so it is fine to read a step, run it, and come back.

Step 1: create the project

Create a plugin project as in Your first plugin and Anatomy of a plugin project. This project uses the package com.example.projectparkour. The plugin.yml has one permission, for the course builders. Everyone can play, so players need no permission:

plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainresourcesplugin.yml

Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlyou are hereTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1name: ParkourTimer2version: '1.0.0'3main: com.example.projectparkour.ParkourPlugin4api-version: '26.3'5description: A parkour course with start and finish plates, checkpoints, a live timer and a saved leaderboard.6permissions:7  parkour.admin:8    description: Lets a player build and change the course.9    default: op
  1. The name in logs and in the data folder plugins/ParkourTimer, where config.yml and times.yml will live.
  2. Who may use the setup commands. default: op means operators have it automatically.

Step 2: detect pressure plates

When a player steps on a pressure plate, Paper fires PlayerInteractEvent, the same event that fires for clicks, with the action set to Action.PHYSICAL. "Physical" means the player did not click anything: their body touched the block. Walking on a pressure plate, stepping on a tripwire or jumping on farmland are all physical actions.

Because PHYSICAL covers farmland and tripwire too, the handler must always check which block was touched. Here is a first version that only prints a message whenever a player steps on any pressure plate:

PlateListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkour-step1srcmainjavacomexampleprojectparkourstep1PlateListener.java

The package com.example.projectparkourstep1 is the folder path com/example/projectparkourstep1 inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour-step1/
    • src/main/
      • java/com/example/projectparkourstep1/Package com.example.projectparkourstep1
        • ParkourStep1Plugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javayou are hereListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkourstep1;2 3import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;4import org.bukkit.Tag;5import org.bukkit.block.Block;6import org.bukkit.entity.Player;7import org.bukkit.event.EventHandler;8import org.bukkit.event.Listener;9import org.bukkit.event.block.Action;10import org.bukkit.event.player.PlayerInteractEvent;11 12public final class PlateListener implements Listener {13 14    @EventHandler15    public void onPlate(PlayerInteractEvent event) {16        if (event.getAction() != Action.PHYSICAL) {17            return;18        }19        Block block = event.getClickedBlock();20        if (block == null || !Tag.PRESSURE_PLATES.isTagged(block.getType())) {21            return;22        }23        Player player = event.getPlayer();24        player.sendRichMessage("<gray>You pressed a <white><plate></white> at <x>, <y>, <z>.",25            Placeholder.unparsed("plate", block.getType().name()),26            Placeholder.unparsed("x", String.valueOf(block.getX())),27            Placeholder.unparsed("y", String.valueOf(block.getY())),28            Placeholder.unparsed("z", String.valueOf(block.getZ())));29    }30}
  1. Called for clicks and for physical touches. That is why the next lines filter.
  2. Stop right away unless the player touched something with their body. A left or right click is not interesting here.
  3. The block that was touched. For physical actions it is the plate itself. The API allows null (clicks in the air have no block), so we check.
  4. A Tag is a ready-made group of materials. This one holds every kind of pressure plate, so you do not have to list them yourself.
  5. The name of the material, such as STONE_PRESSURE_PLATE.
  6. The placeholder wants text, so the number is converted to a String.

The step 2 main class only registers this listener in onEnable:

ParkourStep1Plugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkour-step1srcmainjavacomexampleprojectparkourstep1ParkourStep1Plugin.java

The package com.example.projectparkourstep1 is the folder path com/example/projectparkourstep1 inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour-step1/
    • src/main/
      • java/com/example/projectparkourstep1/Package com.example.projectparkourstep1
        • ParkourStep1Plugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkourstep1;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class ParkourStep1Plugin extends JavaPlugin {6 7    @Override8    public void onEnable() {9        getServer().getPluginManager().registerEvents(new PlateListener(), this);10    }11}

Build it and step on a plate. You will see a gray line in chat. If you step on a plate and nothing happens, check that the listener is registered in onEnable and that you really have a pressure plate and not a button.

You pressed a STONE_PRESSURE_PLATE at 104, 64, -231.

Step 3: store the course in config.yml

The plugin has to know which plates are the start, the finish and the checkpoints. We identify a plate by its world and its block coordinates, and save those in config.yml, so the course survives a restart. This small record is one such point:

CoursePoint.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourCoursePoint.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javayou are hereRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkour;2 3import org.bukkit.block.Block;4import org.bukkit.configuration.ConfigurationSection;5import org.bukkit.configuration.file.FileConfiguration;6 7public record CoursePoint(String world, int x, int y, int z) {8 9    public static CoursePoint of(Block block) {10        return new CoursePoint(block.getWorld().getName(), block.getX(), block.getY(), block.getZ());11    }12 13    public static CoursePoint read(ConfigurationSection section) {14        if (section == null || !section.contains("world")) {15            return null;16        }17        return new CoursePoint(18            section.getString("world", ""),19            section.getInt("x"),20            section.getInt("y"),21            section.getInt("z"));22    }23 24    public boolean matches(Block block) {25        return block.getX() == x26            && block.getY() == y27            && block.getZ() == z28            && block.getWorld().getName().equals(world);29    }30 31    public void writeTo(FileConfiguration config, String path) {32        config.set(path + ".world", world);33        config.set(path + ".x", x);34        config.set(path + ".y", y);35        config.set(path + ".z", z);36    }37}
  1. A record holding four values: the world's name and three whole numbers, the block coordinates.
  2. Builds a point from a block in the world. We use int coordinates, not decimals, because a block always sits at whole numbers.
  3. If the section does not exist or has no world, there is no saved point. Returning null means "not set yet".
  4. True when this block is exactly the saved block. This is how the listener asks "is that the start plate?".
  5. Writes the four values under a path, for example course.start.world.

The Course class holds the start, the finish, the list of checkpoints and the fall height. It loads from the config and writes itself back:

Course.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourCourse.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javayou are hereHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

16public static Course load(FileConfiguration config) {17    Course course = new Course();18    course.start = CoursePoint.read(config.getConfigurationSection("course.start"));19    course.finish = CoursePoint.read(config.getConfigurationSection("course.finish"));20    course.fallY = config.getInt("course.fall-y", -64);21    ConfigurationSection checkpointSection = config.getConfigurationSection("course.checkpoints");22    if (checkpointSection != null) {23        for (String key : checkpointSection.getKeys(false)) {24            CoursePoint point = CoursePoint.read(checkpointSection.getConfigurationSection(key));25            if (point != null) {26                course.checkpoints.add(point);27            }28        }29    }30    return course;31}
  1. Reads course.start into a point, or null when no start was set yet.
  2. The height below which a player has fallen. The second number is the default.
  3. The names 1, 2, 3 in the order they are written. Order matters: checkpoint 1 must come first on the course.
Course.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourCourse.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javayou are hereHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

33public void writeTo(FileConfiguration config) {34    config.set("course", null);35    if (start != null) {36        start.writeTo(config, "course.start");37    }38    if (finish != null) {39        finish.writeTo(config, "course.finish");40    }41    for (int index = 0; index < checkpoints.size(); index++) {42        checkpoints.get(index).writeTo(config, "course.checkpoints." + (index + 1));43    }44    config.set("course.fall-y", fallY);45}
  1. Setting a path to null deletes it. We wipe the old course first, then write the current one, so removed checkpoints do not stay behind.
  2. Computer counting starts at 0, but humans count checkpoints from 1, so the file uses index + 1.

After you build a course with three plates, config.yml looks like this:

config.yml (after setup)
1course:2  start:3    world: world4    x: 1045    y: 646    z: -2317  finish:8    world: world9    x: 13110    y: 7111    z: -22812  checkpoints:13    1:14      world: world15      x: 11716      y: 6617      z: -22918  fall-y: 58

The setup commands

Admins should never edit coordinates by hand. They place a plate, stand on it, and run a command:

CommandWhat it does
/parkour setstartMarks the plate you are standing on as the start.
/parkour setfinishMarks it as the finish.
/parkour addcheckpointAdds it as the next checkpoint. Add them in the order players meet them.
/parkour clearcheckpointsRemoves every checkpoint.
/parkour setfallSets the fall height to the height you are standing at.
ParkourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourParkourCommand.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javayou are hereCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

98private void runAdminWord(Player player, String word) {99    if (word.equals("clearcheckpoints")) {100        course.clearCheckpoints();101        saveCourse();102        player.sendRichMessage("<green>All checkpoints removed.");103        return;104    }105    if (word.equals("setfall")) {106        course.setFallY(player.getLocation().getBlockY());107        saveCourse();108        player.sendRichMessage("<green>Players who drop below height <y> are sent back.",109            Placeholder.unparsed("y", String.valueOf(course.fallY())));110        return;111    }112    Block plate = player.getLocation().getBlock();113    if (!Tag.PRESSURE_PLATES.isTagged(plate.getType())) {114        player.sendRichMessage("<red>Stand on a pressure plate first, then run the command again.");115        return;116    }117    CoursePoint point = CoursePoint.of(plate);118    switch (word) {119        case "setstart" -> course.setStart(point);120        case "setfinish" -> course.setFinish(point);121        default -> course.addCheckpoint(point);122    }123    saveCourse();124    player.sendRichMessage("<green>Saved: <word> at <x>, <y>, <z>.",125        Placeholder.unparsed("word", word),126        Placeholder.unparsed("x", String.valueOf(point.x())),127        Placeholder.unparsed("y", String.valueOf(point.y())),128        Placeholder.unparsed("z", String.valueOf(point.z())));129    if (course.isComplete()) {130        player.sendRichMessage("<gray>The course has a start and a finish and is ready to play.");131    }132}
  1. Two of the five words need no plate, so they are handled first, with an early return.
  2. The whole-number height of the player. Stand at the height of the line you want: players who drop below it are rescued.
  3. The block the player's feet are in. A pressure plate is a thin block, and when you stand on it your feet are inside that block.
  4. Refuses anything that is not a pressure plate, so an admin cannot save a plain stone block by mistake.
  5. Only setstart, setfinish or addcheckpoint can reach this point, so a switch picks the right setter. The default branch is addcheckpoint.
  6. Writes the whole course to the config straight away, so a crash cannot lose the setup.
ParkourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourParkourCommand.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javayou are hereCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

134private void saveCourse() {135    course.writeTo(plugin.getConfig());136    plugin.saveConfig();137}
  1. Writes the config object in memory to config.yml on disk.

The main execute method is the doorman. It sends top to everyone, requires a player for everything else, and checks parkour.admin before any setup word:

ParkourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourParkourCommand.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javayou are hereCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

36@Override37public void execute(CommandSourceStack source, String[] args) {38    CommandSender sender = source.getSender();39    if (args.length == 0) {40        sender.sendRichMessage("<yellow>Parkour commands: <white>top, checkpoint, leave");41        return;42    }43    String word = args[0].toLowerCase(Locale.ROOT);44    if (word.equals("top")) {45        showTop(sender);46        return;47    }48    if (!(source.getExecutor() instanceof Player player)) {49        sender.sendRichMessage("<red>Only players can use this command.");50        return;51    }52    switch (word) {53        case "checkpoint" -> goToCheckpoint(player);54        case "leave" -> leave(player);55        case "setstart", "setfinish", "addcheckpoint", "clearcheckpoints", "setfall" -> {56            if (!player.hasPermission(ADMIN_PERMISSION)) {57                player.sendRichMessage("<red>Only parkour admins can build the course.");58                return;59            }60            runAdminWord(player, word);61        }62        default -> player.sendRichMessage("<red>Unknown parkour command. Try top, checkpoint or leave.");63    }64}
  1. The first word the player typed after /parkour, in lowercase, so TOP and top both work.
  2. Checks that a player (not the console) runs the command, and names it player in one step.
  3. One case can list several words that share the same code.
  4. Normal players see a polite refusal. Server owners can give parkour.admin to builders with a permissions plugin.

Step 4: give every player a run state

At any moment, each player is in exactly one of three situations: not running, running, or just finished. When something can only be in one of a few states, an enum is the right tool. It makes impossible values impossible: a player can never be "kind of running".

RunState.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunState.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javayou are hereEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkour;2 3public enum RunState {4    IDLE,5    RUNNING,6    FINISHED7}
The three run states of a player A player starts IDLE. Stepping on the start plate makes the state RUNNING. Reaching the finish plate after all checkpoints makes it FINISHED. After four seconds, or when the player leaves, the run is removed and the player is IDLE again. IDLE no Run object RUNNING timer counts up FINISHED time is frozen start plate finish plate after 4 seconds the run is removed leave or quit Stepping on a checkpoint plate keeps the state RUNNING and saves a respawn spot.
The three states. A player without a Run object is IDLE.

Everything we know about one running player lives in a Run object: its state, when it started, how many checkpoints were reached and where to send the player back to:

Run.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRun.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javayou are hereHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkour;2 3import org.bukkit.Location;4 5public final class Run {6 7    private static final long FINISHED_DISPLAY_NANOS = 4_000_000_000L;8 9    private RunState state = RunState.RUNNING;10    private final long startNanos;11    private long finishNanos;12    private int checkpointsReached;13    private Location respawn;14 15    public Run(Location startLocation) {16        this.startNanos = System.nanoTime();17        this.respawn = startLocation;18    }19 20    public RunState state() {21        return state;22    }23 24    public long elapsedMillis() {25        long endNanos = state == RunState.FINISHED ? finishNanos : System.nanoTime();26        return (endNanos - startNanos) / 1_000_000L;27    }28 29    public void finish() {30        this.finishNanos = System.nanoTime();31        this.state = RunState.FINISHED;32    }33 34    public boolean finishedLongAgo() {35        return state == RunState.FINISHED && System.nanoTime() - finishNanos > FINISHED_DISPLAY_NANOS;36    }37 38    public int checkpointsReached() {39        return checkpointsReached;40    }41 42    public void reachCheckpoint(Location location) {43        checkpointsReached++;44        respawn = location;45    }46 47    public Location respawn() {48        return respawn;49    }50}
  1. After finishing, the time stays on screen for four seconds. This number is four billion nanoseconds.
  2. A new run starts RUNNING. There is no state for "not yet started", because then there is no Run at all.
  3. The moment the run began, taken from System.nanoTime(). final because it never changes.
  4. A clock that counts nanoseconds (a billionth of a second) and is made for measuring how much time has passed. Unlike the wall clock, it never jumps when the computer corrects its time.
  5. While running, the end of the measurement is "now". After finishing it is the frozen finish moment, so the time stops growing.
  6. Counts the checkpoint and remembers where the player stood, which becomes the new respawn spot.

The RunManager keeps one Run per player in a Map, a table from the player's UUID (their permanent unique id) to their run. Its three handler methods are called by the plate listener. First, the start:

RunManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunManager.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javayou are hereHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

33public void handleStart(Player player) {34    if (stateOf(player.getUniqueId()) == RunState.RUNNING) {35        return;36    }37    runs.put(player.getUniqueId(), new Run(player.getLocation()));38    player.sendRichMessage("<green>Go! Your timer is running. Reach the finish plate.");39    player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f);40}
  1. If the player is already running, stepping on the start plate again does nothing. This also handles the repeated PHYSICAL events while standing on the plate.
  2. Replaces any old (finished) run with a fresh one. The start location is where the player stands now.
  3. A quick pling, so the player hears that the timer started. See Titles, action bars and sounds.

Next, the checkpoints. They must be reached in order, so a player cannot skip the hard part by jumping to a later plate:

RunManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunManager.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javayou are hereHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

42public void handleCheckpoint(Player player, int checkpointIndex) {43    Run run = runs.get(player.getUniqueId());44    if (run == null || run.state() != RunState.RUNNING) {45        return;46    }47    if (checkpointIndex != run.checkpointsReached()) {48        return;49    }50    run.reachCheckpoint(player.getLocation());51    player.sendRichMessage("<aqua>Checkpoint <number> of <total> at <time>.",52        Placeholder.unparsed("number", String.valueOf(run.checkpointsReached())),53        Placeholder.unparsed("total", String.valueOf(course.checkpointCount())),54        Placeholder.unparsed("time", TimeFormat.format(run.elapsedMillis())));55    player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 1.5f);56}
  1. Only runners can reach checkpoints. Players who just walk over the plates are ignored.
  2. Checkpoint numbers start at 0. If you have reached 2 checkpoints, the next valid index is 2. Any other index is an old plate you already passed, or a skipped one, and is ignored. That also makes repeated events harmless.
  3. Saves where the player is standing as the new respawn spot.

And the finish, which has the most rules: the player must be running, must have visited every checkpoint, and then the time is frozen, saved and announced:

RunManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunManager.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javayou are hereHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

58public void handleFinish(Player player) {59    Run run = runs.get(player.getUniqueId());60    if (run == null || run.state() != RunState.RUNNING) {61        return;62    }63    if (run.checkpointsReached() < course.checkpointCount()) {64        player.sendRichMessage("<red>You skipped a checkpoint. Go back to checkpoint <number>.",65            Placeholder.unparsed("number", String.valueOf(run.checkpointsReached() + 1)));66        return;67    }68    run.finish();69    long millis = run.elapsedMillis();70    boolean personalBest = bestTimes.submit(player.getUniqueId(), player.getName(), millis);71    player.sendRichMessage("<green>Finished in <yellow><time></yellow>!",72        Placeholder.unparsed("time", TimeFormat.format(millis)));73    if (personalBest) {74        player.sendRichMessage("<gold>New personal best! You are number <place> on the leaderboard.",75            Placeholder.unparsed("place", String.valueOf(bestTimes.placeOf(player.getUniqueId()))));76    }77    player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);78}
  1. Checkpoints missed. The player gets a hint and the run keeps going, so they can still go back.
  2. Freezes the clock and switches the state to FINISHED.
  3. Offers the time to the record book. It returns true only if this is the player's best so far.
  4. Works out which place that time takes on the leaderboard.

The plate listener only decides which plate was touched and passes the player to the manager. This is the finished version of the handler from step 2:

PlateListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourPlateListener.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javayou are hereListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

20@EventHandler21public void onPlate(PlayerInteractEvent event) {22    if (event.getAction() != Action.PHYSICAL) {23        return;24    }25    Block block = event.getClickedBlock();26    if (block == null) {27        return;28    }29    Player player = event.getPlayer();30    if (course.isStart(block)) {31        runs.handleStart(player);32        return;33    }34    if (course.isFinish(block)) {35        runs.handleFinish(player);36        return;37    }38    int checkpointIndex = course.checkpointIndex(block);39    if (checkpointIndex >= 0) {40        runs.handleCheckpoint(player, checkpointIndex);41    }42}
  1. Asks the course whether this block is the saved start plate.
  2. Returns the checkpoint's position in the list (0, 1, 2, ...) or -1 for "not a checkpoint".

Step 5: the live timer

The timer needs to refresh on the player's screen many times per second. Paper's scheduler can run a task again and again. The main class starts one repeating task for the whole plugin, not one per player:

Java
1getServer().getScheduler().runTaskTimer(this, runs::tick, 0L, 1L);
  1. Runs a piece of code again and again on the main thread. runs is the RunManager from step 4.
  2. A method reference: "call the tick method of runs".
  3. Wait 0 ticks before the first run, then run every 1 tick. One tick is 1/20 of a second, 50 milliseconds, so the clock refreshes 20 times per second.

A tick is one step of the game's clock. The server tries to do 20 of them per second. Updating the display every tick is as fast as anything on the server can change.

The task calls tick(), which walks through every run once:

RunManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunManager.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javayou are hereHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

93public void tick() {94    Iterator<Map.Entry<UUID, Run>> iterator = runs.entrySet().iterator();95    while (iterator.hasNext()) {96        Map.Entry<UUID, Run> entry = iterator.next();97        Player player = Bukkit.getPlayer(entry.getKey());98        Run run = entry.getValue();99        if (player == null) {100            iterator.remove();101            continue;102        }103        switch (run.state()) {104            case RUNNING -> tickRunning(player, run);105            case FINISHED -> tickFinished(player, run, iterator);106            case IDLE -> iterator.remove();107        }108    }109}
  1. An iterator walks through a collection one item at a time. We need one, because we want to remove entries while we walk. Removing from a map inside a normal for loop would crash with a ConcurrentModificationException.
  2. Looks up the online player for this UUID. It returns null if they left.
  3. Safely removes the current entry from the map.
  4. A switch on an enum: one arrow line for each state, and no break is needed. RUNNING refreshes the timer, FINISHED shows the final time for a while, and IDLE is dropped.
RunManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunManager.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javayou are hereHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

111private void tickRunning(Player player, Run run) {112    if (player.getLocation().getY() < course.fallY()) {113        sendBack(player, run);114        player.sendRichMessage("<red>You fell! Back to your last checkpoint.");115        return;116    }117    Component bar = MINI_MESSAGE.deserialize("<gold>Time <white><time> <dark_gray>| <aqua>Checkpoint <done>/<total>",118        Placeholder.unparsed("time", TimeFormat.format(run.elapsedMillis())),119        Placeholder.unparsed("done", String.valueOf(run.checkpointsReached())),120        Placeholder.unparsed("total", String.valueOf(course.checkpointCount())));121    player.sendActionBar(bar);122}
  1. The player's exact height compared to the fall line.
  2. Teleports the player to their last checkpoint. We write it once and also use it for /parkour checkpoint.
  3. Turns the milliseconds into 00:12.345.
  4. Shows the text above the player's hotbar. Because we send a fresh bar every tick, it never fades out.

Turning a number of milliseconds into 00:12.345 takes three small calculations. Try the logic in this runnable program. Change the numbers and press the green arrow again:

TimeFormatDemo.javaRuns on Java 25
1String format(long millis) {2    long minutes = millis / 60_000L;3    long seconds = millis / 1_000L % 60L;4    long thousandths = millis % 1_000L;5    return String.format("%02d:%02d.%03d", minutes, seconds, thousandths);6}7 8void main() {9    long[] runs = {42_318L, 5_007L, 61_000L, 754_999L};10    for (long millis : runs) {11        IO.println(millis + " ms is " + format(millis));12    }13 14    long startNanos = System.nanoTime();15    long endNanos = startNanos + 12_345_678_000L;16    long elapsedMillis = (endNanos - startNanos) / 1_000_000L;17    IO.println("12345678000 ns is " + format(elapsedMillis));18}
  1. How many whole minutes fit. One minute is 60,000 milliseconds. The underscores are only there to make long numbers readable.
  2. Whole seconds, then the remainder after dividing by 60 (% is the "remainder" operator), so 61 seconds is 1.
  3. A format pattern. %02d means "a whole number, at least two digits wide, filled with zeros".
  4. The same clock the Run class uses. Here we fake an end time 12.345678 seconds later.

The plugin uses the same method, in its own class TimeFormat, so the action bar, the chat and the leaderboard all show times the same way:

TimeFormat.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourTimeFormat.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javayou are hereHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkour;2 3public final class TimeFormat {4 5    private TimeFormat() {6    }7 8    public static String format(long millis) {9        long minutes = millis / 60_000L;10        long seconds = millis / 1_000L % 60L;11        long thousandths = millis % 1_000L;12        return String.format("%02d:%02d.%03d", minutes, seconds, thousandths);13    }14}

Step 6: checkpoints and falling

You already wrote most of this. When a player reaches a checkpoint, run.reachCheckpoint(player.getLocation()) stores a copy of where they stood. Going back is one teleport:

RunManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunManager.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javayou are hereHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

133private void sendBack(Player player, Run run) {134    Location target = run.respawn();135    player.setFallDistance(0.0f);136    player.teleport(target);137}
  1. Resets the fall counter. Without it, a player who fell far would take all that fall damage the moment they arrive.
  2. Moves the player. Because our task runs on the main thread, this is allowed here. Teleporting from another thread is not.

Two places call sendBack: the timer task (the player fell below the fall height) and the /parkour checkpoint command:

RunManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourRunManager.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javayou are hereHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

80public boolean sendToCheckpoint(Player player) {81    Run run = runs.get(player.getUniqueId());82    if (run == null || run.state() != RunState.RUNNING) {83        return false;84    }85    sendBack(player, run);86    return true;87}
  1. Returns true if the player was sent back, false if they have no running run. The command uses that to choose its message.
ParkourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourParkourCommand.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javayou are hereCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

82private void goToCheckpoint(Player player) {83    if (!runs.sendToCheckpoint(player)) {84        player.sendRichMessage("<red>You have no run going. Step on the start plate first.");85        return;86    }87    player.sendRichMessage("<aqua>Back at your last checkpoint. The timer keeps running.");88}

Step 7: best times and the leaderboard

Records must survive restarts, so we keep them in a small file, plugins/ParkourTimer/times.yml. The class BestTimes holds a Map from UUID to a record with the player's name and best time, and reads and writes the file. A player's name is saved too, because the leaderboard must show names of players who are offline.

times.yml (written by the plugin)
1times:2  3b1c9a54-0b46-4b61-9f3a-5a7f2f6f4b10:3    name: Mia4    millis: 412305  c6a1e1d2-72c4-4b3b-8d1c-0d2f9e3a1c55:6    name: Steve7    millis: 47905
BestTime.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourBestTime.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javayou are hereRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkour;2 3import java.util.UUID;4 5public record BestTime(UUID playerId, String name, long millis) {6}
BestTimes.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourBestTimes.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

26public void load() {27    times.clear();28    if (!file.exists()) {29        return;30    }31    YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);32    ConfigurationSection section = yaml.getConfigurationSection("times");33    if (section == null) {34        return;35    }36    for (String key : section.getKeys(false)) {37        try {38            UUID playerId = UUID.fromString(key);39            String name = section.getString(key + ".name", "unknown");40            long millis = section.getLong(key + ".millis");41            times.put(playerId, new BestTime(playerId, name, millis));42        } catch (IllegalArgumentException exception) {43            logger.warning("Skipping a broken entry in " + file.getName() + ": " + key);44        }45    }46}
  1. The first time, no file exists yet, so we start with an empty table.
  2. Reads a YAML file into an object you can ask for values. It is the same kind of object as getConfig(), but for a file of your choice.
  3. Turns the text back into a UUID. It throws IllegalArgumentException if someone damaged the file, so a broken entry is skipped with a warning instead of breaking the whole plugin.
BestTimes.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourBestTimes.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

48public void save() {49    YamlConfiguration yaml = new YamlConfiguration();50    for (BestTime best : times.values()) {51        String path = "times." + best.playerId();52        yaml.set(path + ".name", best.name());53        yaml.set(path + ".millis", best.millis());54    }55    try {56        yaml.save(file);57    } catch (IOException exception) {58        logger.severe("Could not save " + file.getName() + ": " + exception.getMessage());59    }60}
  1. An empty YAML document in memory. We fill it from the table and then write it out.
  2. Writes the file. It can fail (a full disk, no permission), so Java makes us handle IOException and we log it.
BestTimes.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourBestTimes.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

62public boolean submit(UUID playerId, String name, long millis) {63    BestTime old = times.get(playerId);64    if (old != null && old.millis() <= millis) {65        return false;66    }67    times.put(playerId, new BestTime(playerId, name, millis));68    save();69    return true;70}
  1. The old time is as good or better, so nothing changes. Returning false tells the caller this was no record.
  2. Saves right away, so a crash after the finish cannot lose the record.

The leaderboard is a sorted list. Java's Comparator describes how to sort: here, by the number of milliseconds, smallest first, because the fastest time is the best:

BestTimes.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourBestTimes.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

91public List<BestTime> top(int limit) {92    return times.values().stream()93        .sorted(Comparator.comparingLong(BestTime::millis))94        .limit(limit)95        .toList();96}
  1. Turns the collection into a stream, a pipeline of steps, explained in Lambdas, method references and streams.
  2. Sort by each record's millis value, lowest first.
  3. Keep only the first ten.
  4. Collect the result into a list that cannot be changed.
BestTimes.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourBestTimes.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

77public int placeOf(UUID playerId) {78    BestTime best = times.get(playerId);79    if (best == null) {80        return -1;81    }82    int faster = 0;83    for (BestTime other : times.values()) {84        if (other.millis() < best.millis()) {85            faster++;86        }87    }88    return faster + 1;89}
  1. Counts how many players are faster. If nobody is faster, you are first, because the place is that count plus one.

The command prints the list. Anyone may run /parkour top, even the console:

ParkourCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourParkourCommand.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javayou are hereCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

66private void showTop(CommandSender sender) {67    List<BestTime> top = bestTimes.top(TOP_SIZE);68    if (top.isEmpty()) {69        sender.sendRichMessage("<gray>Nobody has finished the course yet. Be the first!");70        return;71    }72    sender.sendRichMessage("<gold><bold>Parkour top " + top.size());73    for (int index = 0; index < top.size(); index++) {74        BestTime best = top.get(index);75        sender.sendRichMessage("<yellow><place>. <white><name> <gray>- <green><time>",76            Placeholder.unparsed("place", String.valueOf(index + 1)),77            Placeholder.unparsed("name", best.name()),78            Placeholder.unparsed("time", TimeFormat.format(best.millis())));79    }80}
  1. An empty board gets a friendly message instead of a lonely heading.
  2. A counting loop. The place on the board is index + 1.
  3. A saved name is inserted as plain text, so no one can use their name to add formatting.

Step 8: clean up and wire it together

When a player leaves in the middle of a run, their Run would stay in the map forever, holding a Location that points into a world. That is a small memory leak. A one-line listener fixes it, and the task would also drop it the next tick, because Bukkit.getPlayer returns null. Cleaning up in both places is deliberate: the listener is the main way, and the check in the task is a safety net.

ConnectionListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourConnectionListener.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javayou are hereListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

15@EventHandler16public void onQuit(PlayerQuitEvent event) {17    runs.abandon(event.getPlayer().getUniqueId());18}
  1. Removes the player's run. If they have none, nothing happens.

The main class creates each piece once and hands it to the pieces that need it:

ParkourPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-parkoursrcmainjavacomexampleprojectparkourParkourPlugin.java

The package com.example.projectparkour is the folder path com/example/projectparkour inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.

  • project-parkour/
    • src/main/
      • java/com/example/projectparkour/Package com.example.projectparkour
        • BestTime.javaRecord: a small data class
        • BestTimes.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • Course.javaHelper class
        • CoursePoint.javaRecord: a small data class
        • ParkourCommand.javaCommand (BasicCommand)
        • ParkourPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • PlateListener.javaListener: reacts to events
        • Run.javaHelper class
        • RunManager.javaHelper class
        • RunState.javaEnum: a fixed list of choices
        • TimeFormat.javaHelper class
      • resources/Files copied into the jar as they are
        • config.ymlDefault settings that server owners can change
        • plugin.ymlTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.

1package com.example.projectparkour;2 3import io.papermc.paper.command.brigadier.Commands;4import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;5import java.io.File;6import org.bukkit.plugin.java.JavaPlugin;7 8public final class ParkourPlugin extends JavaPlugin {9 10    @Override11    public void onEnable() {12        saveDefaultConfig();13        Course course = Course.load(getConfig());14 15        BestTimes bestTimes = new BestTimes(new File(getDataFolder(), "times.yml"), getLogger());16        bestTimes.load();17 18        RunManager runs = new RunManager(course, bestTimes);19 20        getServer().getPluginManager().registerEvents(new PlateListener(course, runs), this);21        getServer().getPluginManager().registerEvents(new ConnectionListener(runs), this);22        getServer().getScheduler().runTaskTimer(this, runs::tick, 0L, 1L);23 24        getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {25            Commands commands = event.registrar();26            commands.register("parkour", "Parkour course commands",27                new ParkourCommand(this, course, runs, bestTimes));28        });29 30        getLogger().info("ParkourTimer is ready. Course complete: " + course.isComplete());31    }32}
  1. Reads the saved course. When the plugin runs for the first time, the course is empty and the log says so.
  2. The record file inside the plugin's data folder. The file may not exist yet; BestTimes.load() handles that.
  3. One manager for all runs. It is given the same Course object the commands change, so changes apply immediately.
  4. Starts the timer task. Paper cancels it automatically when the plugin is disabled.
  • project-parkour/
    • src/
      • main/
        • java/
          • com/example/projectparkour/
            • ParkourPlugin.javaMain class: loads everything and registers it
            • Course.javaThe start, finish, checkpoints and fall height
            • CoursePoint.javaOne saved plate: world and block coordinates
            • RunState.javaIDLE, RUNNING or FINISHED
            • Run.javaOne player's run: state, start time, checkpoints, respawn spot
            • RunManager.javaAll runs; the plate handlers and the per-tick timer
            • PlateListener.javaTurns a plate touch into a start, checkpoint or finish
            • ConnectionListener.javaRemoves a run when its player quits
            • ParkourCommand.java/parkour: top, checkpoint, leave and the admin setup words
            • BestTimes.javaThe record book, saved in times.yml
            • BestTime.javaOne leaderboard entry
            • TimeFormat.javaTurns milliseconds into 00:12.345
        • resources/
          • plugin.ymlName, main class and the parkour.admin permission
          • config.ymlThe default course settings

Test it properly

Server console
[16:02:41 INFO]: [ParkourTimer] Enabling ParkourTimer v1.0.0[16:02:41 INFO]: [ParkourTimer] ParkourTimer is ready. Course complete: false

"Course complete: false" is correct: you have not set anything up yet. Build a tiny course in a flat creative world: a start plate, one gap to jump, a checkpoint plate, a second gap and a finish plate. Then follow this checklist:

What to doWhat you should see
Stand on the start plate and run /parkour setstart"Saved: setstart at ..." in chat, and the numbers appear in config.yml.
Stand on the finish plate and run /parkour setfinish, then stand on the checkpoint plate and run /parkour addcheckpointAfter the finish, "The course has a start and a finish and is ready to play."
Fly a few blocks below the lowest platform and run /parkour setfall"Players who drop below height ... are sent back." Never run it on a plate, or players standing lower than that height are sent back at once.
Step on the start plate"Go!", a sound, and the clock in the action bar.
Walk straight to the finish, skipping the checkpoint"You skipped a checkpoint ...", and the timer keeps running.
Jump off the course"You fell!" and you land on your last checkpoint (or the start) with no fall damage.
Finish properly, then /parkour topYour time at number 1, and a file times.yml in the plugin folder.
Restart the server and run /parkour topThe time is still there.

Mistakes you will probably make

  • Nothing happens on the plate. The plate must be exactly the block you saved. If you saved the block below the plate, the coordinates are off by one in height. Always run the setup commands while standing on the plate.
  • The setup refuses with "Stand on a pressure plate first". You are standing next to the plate, or on top of a slab. Step onto the plate itself.
  • Checkpoints are skipped or "missed" in the wrong order. Add them in the order players pass them. /parkour clearcheckpoints starts the list again.
  • Players fall forever and are never rescued. The fall height is lower than the world's void, or was never set. Run /parkour setfall.
  • Changes in config.yml by hand do nothing. The plugin reads the file only when the server starts, and the commands write it. Edit the file, then restart the server (never /reload).

Practice

Try it

Add a time penalty for returning to a checkpoint

Using /parkour checkpoint is too easy: it costs nothing. Make every use add 5 seconds to the player's time.

Hint 1

The penalty belongs in the Run, because that is where the elapsed time is calculated. Store the extra milliseconds in a new field.

Hint 2

Add the penalty inside elapsedMillis(), so that the action bar, the checkpoint message and the saved record all include it.

Show the solution

In Run, add a field and a small method, and add the field to the result of elapsedMillis():

Run.java (new parts)
1private long penaltyMillis;2 3public void addPenalty(long millis) {4    penaltyMillis += millis;5}
Run.java, last line of elapsedMillis()
1return (endNanos - startNanos) / 1_000_000L + penaltyMillis;

In RunManager.sendToCheckpoint, call run.addPenalty(5_000L); right before sendBack(player, run);. The /parkour checkpoint command should tell the player about the penalty too.

Try it

Add /parkour best

Let a player see their own record with /parkour best. Show "No time yet" when they have never finished.

Hint 1

BestTimes.bestOf(uuid) returns an OptionalLong. An OptionalLong is a box that holds either a number or nothing. Check it with isPresent() and read it with getAsLong().

Hint 2

You need two changes in ParkourCommand: a new case in the switch and a new method. Add best to PLAYER_WORDS so tab completion offers it.

Show the solution

The new method:

ParkourCommand.java (new method)
1private void showBest(Player player) {2    OptionalLong best = bestTimes.bestOf(player.getUniqueId());3    String text = best.isPresent() ? TimeFormat.format(best.getAsLong()) : "No time yet";4    player.sendRichMessage("<gray>Your best: <green><time>", Placeholder.unparsed("time", text));5}

And its line in the switch of execute, next to the other player words: case "best" -> showBest(player);. The class also needs import java.util.OptionalLong;, which IntelliJ adds when you press Alt+Enter on the red name.

Extension ideas

  • Particles at checkpoints. Spawn a small burst when a player reaches a checkpoint. See Effects, particles and sounds.
  • More than one course. Give each course a name and store them under courses.<name>. Players pick one by stepping on its start plate.
  • Splits. Remember the time at every checkpoint of the personal best and show "+0.8s" or "-1.2s" against it as the player passes each one.
  • Anti-cheat basics. Cancel the run if the player's game mode is not survival, if they fly, or if they teleport a long way. Check PlayerTeleportEvent.
  • A sidebar. Show the best time on a live scoreboard, as in the sidebar project.
  • Async saving. Move the file write off the main thread, carefully, as described in Threads, performance and lag.
  • Messages in the config. Move every chat text into config.yml, as in the Welcome kit project.

Recap

  • PlayerInteractEvent with Action.PHYSICAL reports a body touching a block, such as stepping on a pressure plate. Always check which block it was.
  • A plate is identified by world and block coordinates, saved in config.yml through a small record, and set up by admin commands that read the block under the admin's feet.
  • An enum models the run state, and a map from UUID to Run keeps one run per player.
  • One repeating task, running every tick, refreshes the action bar for all runners. The time itself comes from subtracting System.nanoTime() values, never from counting ticks.
  • Reaching checkpoints in order, falling below the fall height and /parkour checkpoint all end in one teleport to the stored respawn location.
  • Records live in times.yml and are sorted with a Comparator. Handlers must tolerate being called twice in a row.
  • Remove per-player data when the player quits.

Quick quiz

  1. Which action tells you a player stepped on a pressure plate?

  2. Why does the timer subtract System.nanoTime() values instead of adding 50 ms on every tick?

  3. Why is an Iterator used when walking through the runs in tick()?

  4. A player stands on a checkpoint plate and the event fires three times. What keeps the checkpoint from counting three times?

  5. Which of these makes the leaderboard show the fastest time first?

Next steps

Parkour combined events, state, a scheduler and a data file, which is most of what plugins do. To go further, read Organizing a bigger plugin for how to keep projects like this tidy, or Releasing and sharing your plugin to share your course plugin with others.