Project: Parkour course with timer
A parkour course with start and finish plates, checkpoints, a live timer and a saved leaderboard.
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:
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:
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:
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
The plan
| Feature | The piece that does it | Chapter |
|---|---|---|
| Notice a player stepping on a plate | PlayerInteractEvent with Action.PHYSICAL | Events and listeners |
| Remember where the plates are | Block coordinates stored in config.yml | Configuration files |
| Course setup commands for admins | BasicCommand with a permission check | Simple commands, Permissions |
| What each player is doing | An enum state and a Map from player to run | Enums and records, Cooldowns, toggles and player state |
| A live timer | A repeating task, the action bar and System.nanoTime() | The scheduler, Titles, action bars and sounds |
| Falling resets you | Comparing the player's Y position, then teleport | Worlds, locations and movement |
| Best times | YamlConfiguration saved to times.yml | Saving player data |
| The leaderboard | Sorting with a Comparator | Lists, sets and maps, Leaderboard |
| Cleaning up when players leave | PlayerQuitEvent | Events and listeners |
Create the project
A
plugin.ymlwith the admin permission.Detect pressure plates
Your first
PlayerInteractEventhandler.Store the course in config.yml
Course points, and the admin commands that set them.
Give every player a run state
An enum, a
Runobject and the three plate handlers.Add the live timer
A task that runs every tick and shows the time.
Checkpoints and falling
Return to the last checkpoint, by hand or automatically.
Save best times and show the leaderboard
A YAML file and a sorted list.
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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1name: 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- The name in logs and in the data folder
plugins/ParkourTimer, whereconfig.ymlandtimes.ymlwill live. - Who may use the setup commands.
default: opmeans 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:
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
- java/com/example/projectparkourstep1/Package com.example.projectparkourstep1
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.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}- Called for clicks and for physical touches. That is why the next lines filter.
- Stop right away unless the player touched something with their body. A left or right click is not interesting here.
- 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. - A
Tagis a ready-made group of materials. This one holds every kind of pressure plate, so you do not have to list them yourself. - The name of the material, such as
STONE_PRESSURE_PLATE. - The placeholder wants text, so the number is converted to a
String.
The step 2 main class only registers this listener in onEnable:
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
- java/com/example/projectparkourstep1/Package com.example.projectparkourstep1
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.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.
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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.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}- A
record holding four values: the world's name and three whole numbers, the block coordinates. - Builds a point from a block in the world. We use
intcoordinates, not decimals, because a block always sits at whole numbers. - If the section does not exist or has no world, there is no saved point. Returning
nullmeans "not set yet". - True when this block is exactly the saved block. This is how the listener asks "is that the start plate?".
- 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- Reads
course.startinto a point, ornullwhen no start was set yet. - The height below which a player has fallen. The second number is the default.
- The names
1,2,3in the order they are written. Order matters: checkpoint 1 must come first on the course.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- Setting a path to
nulldeletes it. We wipe the old course first, then write the current one, so removed checkpoints do not stay behind. - 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:
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: 58The setup commands
Admins should never edit coordinates by hand. They place a plate, stand on it, and run a command:
| Command | What it does |
|---|---|
/parkour setstart | Marks the plate you are standing on as the start. |
/parkour setfinish | Marks it as the finish. |
/parkour addcheckpoint | Adds it as the next checkpoint. Add them in the order players meet them. |
/parkour clearcheckpoints | Removes every checkpoint. |
/parkour setfall | Sets the fall height to the height you are standing at. |
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- Two of the five words need no plate, so they are handled first, with an early
return. - The whole-number height of the player. Stand at the height of the line you want: players who drop below it are rescued.
- 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.
- Refuses anything that is not a pressure plate, so an admin cannot save a plain stone block by mistake.
- Only
setstart,setfinishoraddcheckpointcan reach this point, so aswitchpicks the right setter. Thedefaultbranch isaddcheckpoint. - Writes the whole course to the config straight away, so a crash cannot lose the setup.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
134private void saveCourse() {135 course.writeTo(plugin.getConfig());136 plugin.saveConfig();137}- Writes the config object in memory to
config.ymlon 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- The first word the player typed after
/parkour, in lowercase, soTOPandtopboth work. - Checks that a player (not the console) runs the command, and names it
playerin one step. - One
casecan list several words that share the same code. - Normal players see a polite refusal. Server owners can give
parkour.adminto 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".
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectparkour;2 3public enum RunState {4 IDLE,5 RUNNING,6 FINISHED7}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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.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}- After finishing, the time stays on screen for four seconds. This number is four billion nanoseconds.
- A new run starts RUNNING. There is no state for "not yet started", because then there is no
Runat all. - The moment the run began, taken from
System.nanoTime().finalbecause it never changes. - 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.
- While running, the end of the measurement is "now". After finishing it is the frozen finish moment, so the time stops growing.
- 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- If the player is already running, stepping on the start plate again does nothing. This also handles the repeated
PHYSICALevents while standing on the plate. - Replaces any old (finished) run with a fresh one. The start location is where the player stands now.
- 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- Only runners can reach checkpoints. Players who just walk over the plates are ignored.
- 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.
- 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- Checkpoints missed. The player gets a hint and the run keeps going, so they can still go back.
- Freezes the clock and switches the state to
FINISHED. - Offers the time to the record book. It returns
trueonly if this is the player's best so far. - 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- Asks the course whether this block is the saved start plate.
- 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:
1getServer().getScheduler().runTaskTimer(this, runs::tick, 0L, 1L);- Runs a piece of code again and again on the main thread.
runsis theRunManagerfrom step 4. - A
method reference : "call thetickmethod ofruns". - 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- 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 normalforloop would crash with aConcurrentModificationException. - Looks up the online player for this UUID. It returns
nullif they left. - Safely removes the current entry from the map.
- A
switchon an enum: one arrow line for each state, and nobreakis needed.RUNNINGrefreshes the timer,FINISHEDshows the final time for a while, andIDLEis dropped.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- The player's exact height compared to the fall line.
- Teleports the player to their last checkpoint. We write it once and also use it for
/parkour checkpoint. - Turns the milliseconds into
00:12.345. - 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:
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}- How many whole minutes fit. One minute is 60,000 milliseconds. The underscores are only there to make long numbers readable.
- Whole seconds, then the remainder after dividing by 60 (
%is the "remainder" operator), so 61 seconds is 1. - A format pattern.
%02dmeans "a whole number, at least two digits wide, filled with zeros". - The same clock the
Runclass 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
133private void sendBack(Player player, Run run) {134 Location target = run.respawn();135 player.setFallDistance(0.0f);136 player.teleport(target);137}- Resets the fall counter. Without it, a player who fell far would take all that fall damage the moment they arrive.
- 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- Returns
trueif the player was sent back,falseif they have no running run. The command uses that to choose its message.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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.
1times:2 3b1c9a54-0b46-4b61-9f3a-5a7f2f6f4b10:3 name: Mia4 millis: 412305 c6a1e1d2-72c4-4b3b-8d1c-0d2f9e3a1c55:6 name: Steve7 millis: 47905Where 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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.projectparkour;2 3import java.util.UUID;4 5public record BestTime(UUID playerId, String name, long millis) {6}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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- The first time, no file exists yet, so we start with an empty table.
- 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. - Turns the text back into a UUID. It throws
IllegalArgumentExceptionif someone damaged the file, so a broken entry is skipped with a warning instead of breaking the whole plugin.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- An empty YAML document in memory. We fill it from the table and then write it out.
- Writes the file. It can fail (a full disk, no permission), so Java makes us handle
IOExceptionand we log it.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- The old time is as good or better, so nothing changes. Returning
falsetells the caller this was no record. - 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
91public List<BestTime> top(int limit) {92 return times.values().stream()93 .sorted(Comparator.comparingLong(BestTime::millis))94 .limit(limit)95 .toList();96}- Turns the collection into a stream, a pipeline of steps, explained in Lambdas, method references and streams.
- Sort by each record's
millisvalue, lowest first. - Keep only the first ten.
- Collect the result into a list that cannot be changed.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
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}- An empty board gets a friendly message instead of a lonely heading.
- A counting loop. The place on the board is
index + 1. - 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.
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
15@EventHandler16public void onQuit(PlayerQuitEvent event) {17 runs.abandon(event.getPlayer().getUniqueId());18}- 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:
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
- java/com/example/projectparkour/Package com.example.projectparkour
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.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}- Reads the saved course. When the plugin runs for the first time, the course is empty and the log says so.
- The record file inside the plugin's data folder. The file may not exist yet;
BestTimes.load()handles that. - One manager for all runs. It is given the same
Courseobject the commands change, so changes apply immediately. - 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
- com/example/projectparkour/
- resources/
- plugin.ymlName, main class and the parkour.admin permission
- config.ymlThe default course settings
- java/
- main/
- src/
Test it properly
[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 do | What 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 addcheckpoint | After 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 top | Your time at number 1, and a file times.yml in the plugin folder. |
Restart the server and run /parkour top | The 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 clearcheckpointsstarts 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.ymlby 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
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():
1private long penaltyMillis;2 3public void addPenalty(long millis) {4 penaltyMillis += millis;5}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.
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:
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
PlayerInteractEventwithAction.PHYSICALreports 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.ymlthrough 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
Runkeeps 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 checkpointall end in oneteleportto the stored respawn location. - Records live in
times.ymland are sorted with aComparator. Handlers must tolerate being called twice in a row. - Remove per-player data when the player quits.
Quick quiz
Which action tells you a player stepped on a pressure plate?
The click actions are for clicking with the mouse. Stepping on a plate is a touch with the body, which Paper reports asPHYSICAL.Why does the timer subtract
System.nanoTime()values instead of adding 50 ms on every tick?A tick is 50 ms only when the server keeps up. Under lag, ticks stretch. The clock keeps counting real time, so the shown time is always right. The task only refreshes the display.Why is an
Iteratorused when walking through the runs intick()?Removing entries from a map during a normalforloop throwsConcurrentModificationException.iterator.remove()is the safe way.A player stands on a checkpoint plate and the event fires three times. What keeps the checkpoint from counting three times?
Paper may fire the event repeatedly, so the handler must be safe to repeat. After the first call,checkpointsReached()has grown, so the same index is ignored.Which of these makes the leaderboard show the fastest time first?
A parkour time is better when it is smaller, and comparing by milliseconds sorts the smallest first. Names and file order have nothing to do with speed.
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.