Paper Plugin Guide
File mode0

Projects

Project: Live sidebar scoreboard

A per-player sidebar with kills, deaths, playtime and online count that updates every second.

IntermediateProject, about 52 min read

In this project you build a live sidebar on the right side of the screen. It shows each player's kills, deaths, playtime and the number of players online, it updates every second, it remembers the numbers after a restart, and players can switch it off with /sidebar.

What you will build

Almost every server has a sidebar, and players love numbers that go up. This project is a good test of what you know, because the sidebar looks simple but touches many parts of a plugin: events to count things, saved data to remember them, the scheduler to refresh the screen, and the scoreboard API to draw it. This is what a player sees:

Kills: 12
Deaths: 3
Playtime: 1h 5m
Online: 5

play.example.com

Every line has a different source. The table also shows which of the numbers the plugin has to remember itself:

LineWhere the number comes fromSaved by
KillsCounted by a listener each time the player kills somethingThe plugin, in the player's persistent data
DeathsCounted by a listener each time the player diesThe plugin, in the player's persistent data
PlaytimeMinecraft's own statistic PLAY_ONE_MINUTEMinecraft itself
OnlineThe size of the server's list of online playersNothing: it is always current

Players can hide the sidebar and bring it back with a command. The plugin remembers that choice too, so a player who hid it does not see it again after the next login:

Sidebar hidden. Type /sidebar to bring it back.

The plan

The sidebar needs data from three places and one task that puts it all on the screen. This picture shows the flow of the numbers:

Where the sidebar numbers come from Kills and deaths are counted by event listeners and saved in the player's persistent data. Playtime is read from Minecraft's statistics, and the online count from the server. Every 20 ticks a repeating task reads all of these, and the sidebar changes only the lines whose text is different. StatsListener kills and deaths happen PlayerStats saved in the player Statistic and server playtime, online count Repeating task: every 20 ticks BoardManager.refreshAll() reads every number Sidebar.update(...) builds the text of each line Same text as before? yes Do nothing no flicker no Set the line customName(...)
Kills and deaths are saved when they happen. Once a second the task collects every number and redraws only the lines that changed.

The pieces, with the chapter that teaches each idea:

RequirementThe piece that does itChapter
Count kills and deathsStatsListener, with two event handlersEvents and listeners
Remember them for goodPlayerStats, using the player's PersistentDataContainerSaving data on things
Show a clean sidebarSidebar: an objective with custom line names and hidden numbersScoreboards
One sidebar per playerBoardManager: a map from UUID to sidebarCollections
Update every secondA repeating task every 20 ticks (a tick is one twentieth of a second)The scheduler
Hide and showSidebarCommand, a BasicCommandCommands, part 1

The build steps are:

  1. Create the project and plugin.yml

    Name the plugin and declare the toggle permission.

  2. Save kills and deaths on the player

    A small class that reads and writes numbers in the persistent data container.

  3. Count kills and deaths

    A listener with two handlers, and one surprise about how events inherit.

  4. Turn ticks into a playtime text

    A tiny formatter, tried out in a plain Java program first.

  5. Build the sidebar for one player

    A title with a gradient, hidden numbers and lines that change in place.

  6. Manage every player's sidebar

    Create on join, forget on quit, toggle on command, refresh for everyone.

  7. Wire it up and test

    Listeners, command, the repeating task, and a testing checklist.

If you have not read Scoreboards yet, do that first: this page builds the same kind of sidebar, and it explains the scoreboard parts (objective, score, entry) more slowly than we can here.

Step 1: create the project and plugin.yml

Create a project as in the earlier projects (copy the starter, or run new-plugin.cmd -Name SidebarStats from the paper-templates folder), with the package com.example.projectscoreboard. The plugin needs one permission:

plugin.ymlCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainresourcesplugin.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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.javaListener: reacts to events
      • resources/Files copied into the jar as they are
        • plugin.ymlyou are hereTells Paper the plugin's name, version and main class
    • build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
    • gradle.propertiesVersion numbers used by the build
    • gradlew.batRuns Gradle on Windows without installing it
    • settings.gradle.ktsThe project's name

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

1name: SidebarStats2version: '1.0.0'3main: com.example.projectscoreboard.SidebarPlugin4api-version: '26.3'5description: A live sidebar with kills, deaths, playtime and online players, saved per player, with a toggle command.6permissions:7  sidebarstats.toggle:8    description: Lets a player hide and show their own sidebar with /sidebar.9    default: true
  1. The permission for /sidebar. It defaults to true, which means every player has it, because hiding your own sidebar is harmless.

Step 2: save kills and deaths on the player

The first question of every stats plugin is: where do I keep the numbers? A HashMap in memory is the easy answer, but it forgets everything when the server restarts. A file or database works, but needs a lot of code. For a few numbers per player there is a third way: the player itself can carry them.

Every player has a persistent data container, a little notebook that Minecraft saves with the player's data. You write a number into it under a name, and the number is still there after a relog or a restart. You never open a file. All our numbers go in this notebook: kills, deaths and also the choice to hide the sidebar.

PlayerStats.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javayou are hereHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

15PlayerStats(Plugin plugin) {16    this.killsKey = new NamespacedKey(plugin, "kills");17    this.deathsKey = new NamespacedKey(plugin, "deaths");18    this.hiddenKey = new NamespacedKey(plugin, "sidebar_hidden");19}
  1. A namespaced key is the name a value is saved under. It has two parts: your plugin's name and the word kills, so it can never clash with another plugin's kills.
  2. Keys may only contain lowercase letters, digits and the characters _ - . /. A capital letter or a space would throw an error.
PlayerStats.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javayou are hereHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

21int kills(Player player) {22    return player.getPersistentDataContainer().getOrDefault(killsKey, PersistentDataType.INTEGER, 0);23}
  1. Opens the player's notebook.
  2. Reads the number saved under the key. PersistentDataType.INTEGER says "it is a whole number". If the player has never killed anything, nothing is saved yet, and the last argument, 0, is returned instead.
PlayerStats.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javayou are hereHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

45private static void increase(PersistentDataContainer container, NamespacedKey key) {46    int current = container.getOrDefault(key, PersistentDataType.INTEGER, 0);47    container.set(key, PersistentDataType.INTEGER, current + 1);48}
  1. Reads the current count, or 0 for a new player.
  2. Writes the new count. Saving it again under the same key replaces the old value.
PlayerStats.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlayerStats.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javayou are hereHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

37boolean isSidebarHidden(Player player) {38    return player.getPersistentDataContainer().getOrDefault(hiddenKey, PersistentDataType.BOOLEAN, false);39}
  1. The same notebook can hold yes-or-no values. A new player has no entry, so the default is false: the sidebar is shown.

One class owns every read and write. The rest of the plugin says stats.kills(player) and never needs to know about keys or types. If you later move the numbers into a database, only PlayerStats changes.

Step 3: count kills and deaths

Counting needs two events. When something dies, Paper fires an EntityDeathEvent. When the dead thing is a player, Paper fires a PlayerDeathEvent, which is a more specific version of it:

StatsListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardStatsListener.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

17@EventHandler18public void onEntityDeath(EntityDeathEvent event) {19    Player killer = event.getEntity().getKiller();20    if (killer != null) {21        stats.addKill(killer);22    }23}
  1. Called whenever anything dies: a zombie, a cow, a villager, or a player.
  2. The player who dealt the final blow, or null when nobody did: a creeper explosion, falling, lava, or a mob killing another mob.
  3. Always check for null here. Most deaths in the world have no player killer, and calling a method on null would crash the handler.
  4. Adds one to the killer's saved kill count.
StatsListener.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardStatsListener.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

25@EventHandler26public void onPlayerDeath(PlayerDeathEvent event) {27    stats.addDeath(event.getPlayer());28}
  1. Called only when a player dies.
  2. The player who died. Their death count goes up by one.

Here is the part that surprises beginners: a PlayerDeathEvent is also an EntityDeathEvent, because it is a more specific kind of the same event (see Inheritance and interfaces). That means when Alex kills Steve, both handlers run: onEntityDeath gives Alex a kill, and onPlayerDeath gives Steve a death. We never wrote a special case for player-versus-player fights, and nothing is counted twice. Our "kills" number includes players and mobs.

Step 4: turn ticks into a playtime text

Playtime needs no counting from us, because Minecraft already tracks it. The statistic is called Statistic.PLAY_ONE_MINUTE. Despite the name, its value is the time played in ticks, and there are 20 ticks in a second. A player with 78,000 ticks has played for 3,900 seconds, which is 1 hour and 5 minutes.

Players do not want to read 78000, so we turn it into text like 1h 5m. The logic is plain Java, so here is the same method in a small program you can run before it goes into the plugin. Press the green arrow:

PlaytimeDemo.javaRuns on Java 25
1import java.time.Duration;2 3String format(int ticks) {4    Duration played = Duration.ofSeconds(ticks / 20);5    if (played.toHours() > 0) {6        return played.toHours() + "h " + played.toMinutesPart() + "m";7    }8    return played.toMinutesPart() + "m " + played.toSecondsPart() + "s";9}10 11void main() {12    int[] samples = {0, 90, 1200, 72000, 78000, 1500000};13    for (int ticks : samples) {14        IO.println(ticks + " ticks = " + format(ticks));15    }16}
  1. Divides the ticks by 20 to get seconds (whole-number division drops the leftover ticks) and wraps them in a Duration, a Java object that knows how to split seconds into hours, minutes and seconds.
  2. Plays for an hour or more? Then show hours and minutes, and drop the seconds, which would only flicker.
  3. The minutes left over after the whole hours are taken out: 0 to 59. toMinutes() without "Part" would give the total.
  4. A list of test values: no time, a few seconds, exactly one minute, exactly one hour, and so on. Try your own.

The plugin uses the same method inside a small helper class:

Playtime.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardPlaytime.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javayou are hereHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

12static String format(int ticks) {13    Duration played = Duration.ofSeconds(ticks / TICKS_PER_SECOND);14    if (played.toHours() > 0) {15        return played.toHours() + "h " + played.toMinutesPart() + "m";16    }17    return played.toMinutesPart() + "m " + played.toSecondsPart() + "s";18}
  1. A static method belongs to the class, not to an object, so you call it as Playtime.format(ticks).

Step 5: build the sidebar for one player

Now the part players actually see. Each player gets their own Sidebar object, which owns a scoreboard, because every player has different numbers. The scoreboard is created together with the object:

Sidebar.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javayou are hereHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

22private final Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
  1. Creates a brand new scoreboard that only this player will see. The server's main scoreboard stays untouched.

Then the constructor sets the objective up:

Sidebar.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javayou are hereHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

26Sidebar() {27    objective = scoreboard.registerNewObjective("sidebar", Criteria.DUMMY, MINI_MESSAGE.deserialize(TITLE));28    objective.setDisplaySlot(DisplaySlot.SIDEBAR);29    objective.numberFormat(NumberFormat.blank());30    for (int index = 0; index < LINE_COUNT; index++) {31        objective.getScore(entryName(index)).setScore(LINE_COUNT - index);32    }33    setLine(0, Component.space());34    setLine(5, Component.space());35    setLine(6, MINI_MESSAGE.deserialize("<yellow>play.example.com"));36}
  1. An objective is the thing the sidebar displays. We name it sidebar, give it the DUMMY criterion (a number that only our plugin changes), and a title.
  2. Turns the title text into a component. The <gradient:#f7971e:#ffd200> tag fades the letters from orange to yellow, as in MiniMessage.
  3. Says where the objective is shown: the right side of the screen.
  4. Hides the red numbers on the right of each line. We only need them to sort the lines, not to read.
  5. The sidebar sorts lines from the highest score down. Line 0 gets the biggest score, so it appears on top.
  6. An empty line, as a gap between the title and the numbers. The last line is the server address.

A sidebar line is really a score entry, a name with a number. Our entry names are line-0, line-1 and so on, and we never show them. Instead we give each entry a customName, which is the text players really see. That lets us use colors and change the text without ever renaming the entry, which is what makes the update flicker-free.

Sidebar.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javayou are hereHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

42void update(int kills, int deaths, String playtime, int online) {43    setLine(LINE_KILLS, MINI_MESSAGE.deserialize("<gray>Kills: <green>" + kills));44    setLine(LINE_DEATHS, MINI_MESSAGE.deserialize("<gray>Deaths: <red>" + deaths));45    setLine(LINE_PLAYTIME, MINI_MESSAGE.deserialize("<gray>Playtime: <white>" + playtime));46    setLine(LINE_ONLINE, MINI_MESSAGE.deserialize("<gray>Online: <aqua>" + online));47}
  1. The sidebar receives plain numbers. It does not know where they come from, which keeps this class about drawing only.
  2. Builds the line from a MiniMessage text with the number attached. Gray label, green number.
Sidebar.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebar.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javayou are hereHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

49private void setLine(int index, Component text) {50    if (text.equals(shownLines[index])) {51        return;52    }53    shownLines[index] = text;54    objective.getScore(entryName(index)).customName(text);55}
  1. The task runs every second, but a kill count changes only now and then. If the new text equals what the line already shows, there is nothing to do. Skipping needless updates keeps the sidebar from flickering and saves network traffic.
  2. Remembers what is on screen now, for the next comparison.
  3. Changes the visible text of that line in place.

Step 6: manage every player's sidebar

BoardManager keeps one Sidebar per player in a map, and everything else in the plugin goes through it. The map's key is the player's UUID, the permanent unique id (see Time, UUIDs and randomness), not the Player object, which stops being valid when the player leaves.

BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

55private void show(Player player) {56    Sidebar sidebar = new Sidebar();57    sidebars.put(player.getUniqueId(), sidebar);58    refresh(player, sidebar, Bukkit.getOnlinePlayers().size());59    player.setScoreboard(sidebar.scoreboard());60}
  1. A new sidebar with the title and layout.
  2. Stores it under the player's UUID so the task can find it later.
  3. Fills in the real numbers before showing the sidebar, so it never appears empty for a moment.
  4. The moment the player sees the sidebar. A player can look at one scoreboard at a time.
BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

62private void refresh(Player player, Sidebar sidebar, int online) {63    String playtime = Playtime.format(player.getStatistic(Statistic.PLAY_ONE_MINUTE));64    sidebar.update(stats.kills(player), stats.deaths(player), playtime, online);65}
  1. Minecraft's own playtime counter, in ticks.
  2. Reads the saved kills from the player's persistent data.
BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

45void refreshAll() {46    int online = Bukkit.getOnlinePlayers().size();47    for (Player player : Bukkit.getOnlinePlayers()) {48        Sidebar sidebar = sidebars.get(player.getUniqueId());49        if (sidebar != null) {50            refresh(player, sidebar, online);51        }52    }53}
  1. The number of players online. We ask once and hand the same number to every sidebar.
  2. Looks for this player's sidebar. A player who hid it has none, so null comes back and we skip them.

Hiding and showing are two sides of one toggle. The player's choice is saved in the same persistent data, so it survives a relog:

BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

25boolean toggle(Player player) {26    if (sidebars.containsKey(player.getUniqueId())) {27        stats.setSidebarHidden(player, true);28        hide(player);29        return false;30    }31    stats.setSidebarHidden(player, false);32    show(player);33    return true;34}
  1. If the player has a sidebar right now, the toggle hides it. Otherwise it shows it.
  2. Saves the choice, so the sidebar stays hidden on the next login.
  3. The method answers "is the sidebar visible now?". The command uses the answer to pick the message.
BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

36void hide(Player player) {37    sidebars.remove(player.getUniqueId());38    player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());39}
  1. Forgets the sidebar object, so the task stops updating it.
  2. Gives the player the server's main scoreboard back. Without a sidebar objective there, the sidebar disappears.
BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardBoardManager.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

41void forget(Player player) {42    sidebars.remove(player.getUniqueId());43}
  1. Called when a player quits. It only drops the map entry. The player is gone, so there is no screen left to clean.

Step 7: wire it up and test

The connection listener creates the sidebar when a player joins and drops it when they quit. On join we also refresh everyone at once, so the "Online" number goes up for the players who were already there:

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

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javayou are hereListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.projectscoreboard;2 3import org.bukkit.event.EventHandler;4import org.bukkit.event.Listener;5import org.bukkit.event.player.PlayerJoinEvent;6import org.bukkit.event.player.PlayerQuitEvent;7 8final class ConnectionListener implements Listener {9 10    private final BoardManager boards;11 12    ConnectionListener(BoardManager boards) {13        this.boards = boards;14    }15 16    @EventHandler17    public void onJoin(PlayerJoinEvent event) {18        boards.showIfWanted(event.getPlayer());19        boards.refreshAll();20    }21 22    @EventHandler23    public void onQuit(PlayerQuitEvent event) {24        boards.forget(event.getPlayer());25    }26}
  1. Shows the sidebar unless the player turned it off earlier.
  2. Updates everybody's online count.
  3. When a player leaves, we forget their sidebar. Their count drops on everyone else's screen within a second, at the next refresh.
SidebarCommand.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebarCommand.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javayou are hereCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

15@Override16public void execute(CommandSourceStack source, String[] args) {17    if (!(source.getExecutor() instanceof Player player)) {18        source.getSender().sendRichMessage("<red>Only players have a sidebar.");19        return;20    }21    if (boards.toggle(player)) {22        player.sendRichMessage("<green>Sidebar shown.");23    } else {24        player.sendRichMessage("<gray>Sidebar hidden. Type <yellow>/sidebar</yellow> to bring it back.");25    }26}
  1. Only a player can have a sidebar. The console gets a polite refusal.
  2. Does the work and tells us which way it went.

The main class puts the pieces together and starts the repeating task:

SidebarPlugin.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboardsrcmainjavacomexampleprojectscoreboardSidebarPlugin.java

The package com.example.projectscoreboard is the folder path com/example/projectscoreboard 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-scoreboard/
    • src/main/
      • java/com/example/projectscoreboard/Package com.example.projectscoreboard
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.projectscoreboard;2 3import org.bukkit.Bukkit;4import org.bukkit.entity.Player;5import org.bukkit.plugin.java.JavaPlugin;6import org.bukkit.scheduler.BukkitTask;7 8public final class SidebarPlugin extends JavaPlugin {9 10    private static final long TICKS_PER_SECOND = 20L;11 12    private BoardManager boards;13    private BukkitTask refreshTask;14 15    @Override16    public void onEnable() {17        PlayerStats stats = new PlayerStats(this);18        boards = new BoardManager(stats);19 20        getServer().getPluginManager().registerEvents(new StatsListener(stats), this);21        getServer().getPluginManager().registerEvents(new ConnectionListener(boards), this);22        registerCommand("sidebar", "Show or hide your sidebar", new SidebarCommand(boards));23 24        refreshTask = getServer().getScheduler().runTaskTimer(25            this, boards::refreshAll, TICKS_PER_SECOND, TICKS_PER_SECOND);26 27        for (Player player : Bukkit.getOnlinePlayers()) {28            boards.showIfWanted(player);29        }30    }31 32    @Override33    public void onDisable() {34        refreshTask.cancel();35        for (Player player : Bukkit.getOnlinePlayers()) {36            boards.hide(player);37        }38    }39}
  1. One stats object, shared by the listener and the manager, so both use the same keys.
  2. Starts counting kills and deaths. Two listeners are registered, because they do different jobs.
  3. Registers /sidebar. The permission comes from the command class.
  4. Runs boards::refreshAll again and again. The first TICKS_PER_SECOND is the delay before the first run, and the second is the gap between runs: 20 ticks is one second. See The scheduler.
  5. A method reference: "call refreshAll on boards".
  6. When the plugin starts while players are already online, such as when the plugin is enabled while somebody is logged in, those players never trigger a join event. This loop gives them their sidebar.
  7. Stops the repeating task when the plugin shuts down. A task that is never stopped would keep running after the plugin is gone.
  8. Gives every player the normal scoreboard back. Otherwise our sidebar stays on their screens until they relog.
  • project-scoreboard/
    • src/
      • main/
        • java/
          • com/example/projectscoreboard/
            • SidebarPlugin.javaMain class: creates everything and starts the one-second task
            • PlayerStats.javaReads and writes kills, deaths and the hidden choice in the player's data
            • StatsListener.javaCounts kills and deaths
            • Playtime.javaTurns ticks into text like 1h 5m
            • Sidebar.javaOne player's scoreboard: title, lines and in-place updates
            • BoardManager.javaMap from UUID to sidebar: show, hide, toggle, forget, refresh
            • ConnectionListener.javaShows the sidebar on join and forgets it on quit
            • SidebarCommand.java/sidebar toggles the sidebar
        • resources/
          • plugin.ymlName, main class and the toggle permission

You can download the finished project and compare it with yours.

Testing checklist

Build, start the test server and join. A second player or a second account makes a few checks easier, but you can do most alone.

  • The sidebar appears as soon as you join, with 0 kills, 0 deaths, a small playtime and 1 online. The title fades from orange to yellow.
  • Watch the playtime. It should grow every second, and nothing else on the sidebar should blink.
  • Spawn a mob with /summon zombie and kill it by hand. Kills goes to 1 within a second. Kill a mob with /kill @e[type=zombie] instead: kills stays the same, because no player dealt the blow.
  • Run /kill on yourself. Deaths goes to 1.
  • Quit and rejoin. Kills and deaths are still there: they were saved in your player data.
  • Restart the server and join again. The numbers are still there.
  • Run /sidebar. It disappears and you get the gray message. Run it again and it comes back.
  • Hide the sidebar, then quit and rejoin. It stays hidden, and /sidebar brings it back.
  • With two players, let one quit. The other's "Online" drops to 1 within a second, and the console shows no errors.
  • Let one player kill the other. The killer's kills and the victim's deaths each go up by exactly one.
  • Stop the server with stop and look at the console: no errors from SidebarStats.

Mistakes to avoid

  • Counting in a HashMap only. The numbers vanish on every restart. Anything players care about must be saved.
  • Calling getKiller() without a null check. Most deaths have no player killer.
  • Rebuilding the whole scoreboard every second. It flickers. Change the existing lines in place, and skip lines whose text did not change.
  • Sharing one scoreboard for everyone. Then every player sees the same numbers. Each player needs their own scoreboard to have their own kills.
  • Keeping players in a map forever. Remove them on quit.
  • Forgetting to stop the repeating task. Cancel it in onDisable.
  • Using a capital letter in a key. new NamespacedKey(plugin, "Kills") throws an error. Keys are lowercase.
  • Reading the playtime as seconds. The statistic is in ticks. Divide by 20 first.

Exercises

Try it

Add a world line

Add a line to the sidebar that shows the name of the world the player is in, such as World: world_nether. It goes under "Online", and the blank line and the server address move down one place.

Hint 1

The sidebar has a fixed number of lines, LINE_COUNT. Make it one bigger, add a constant for the new line number, and move the two lines after it.

Hint 2

player.getWorld().getName() gives the world name. BoardManager collects the numbers, so it passes the name to Sidebar.update as a new parameter.

Show the solution

Three small changes. The sidebar has eight lines now, the new line is number 5, the blank line moves to 6 and the address to 7:

Sidebar.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboard-exercisesrcmainjavacomexampleprojectscoreboardexerciseSidebar.java

The package com.example.projectscoreboardexercise is the folder path com/example/projectscoreboardexercise 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-scoreboard-exercise/
    • src/main/
      • java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javayou are hereHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

27Sidebar() {28    objective = scoreboard.registerNewObjective("sidebar", Criteria.DUMMY, MINI_MESSAGE.deserialize(TITLE));29    objective.setDisplaySlot(DisplaySlot.SIDEBAR);30    objective.numberFormat(NumberFormat.blank());31    for (int index = 0; index < LINE_COUNT; index++) {32        objective.getScore(entryName(index)).setScore(LINE_COUNT - index);33    }34    setLine(0, Component.space());35    setLine(6, Component.space());36    setLine(7, MINI_MESSAGE.deserialize("<yellow>play.example.com"));37}
  1. The blank line moved one place down.
  2. The address is last, at the new bottom.
Sidebar.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboard-exercisesrcmainjavacomexampleprojectscoreboardexerciseSidebar.java

The package com.example.projectscoreboardexercise is the folder path com/example/projectscoreboardexercise 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-scoreboard-exercise/
    • src/main/
      • java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
        • BoardManager.javaHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javayou are hereHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

43void update(int kills, int deaths, String playtime, int online, String world) {44    setLine(LINE_KILLS, MINI_MESSAGE.deserialize("<gray>Kills: <green>" + kills));45    setLine(LINE_DEATHS, MINI_MESSAGE.deserialize("<gray>Deaths: <red>" + deaths));46    setLine(LINE_PLAYTIME, MINI_MESSAGE.deserialize("<gray>Playtime: <white>" + playtime));47    setLine(LINE_ONLINE, MINI_MESSAGE.deserialize("<gray>Online: <aqua>" + online));48    setLine(LINE_WORLD, MINI_MESSAGE.deserialize("<gray>World: <light_purple>" + world));49}
  1. The new parameter: the world name arrives as plain text.
  2. Sets the new line. The same in-place rule applies, so it never flickers.
BoardManager.javaCompiles on Paper 26.3Compile Lab
Where this file livesproject-scoreboard-exercisesrcmainjavacomexampleprojectscoreboardexerciseBoardManager.java

The package com.example.projectscoreboardexercise is the folder path com/example/projectscoreboardexercise 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-scoreboard-exercise/
    • src/main/
      • java/com/example/projectscoreboardexercise/Package com.example.projectscoreboardexercise
        • BoardManager.javayou are hereHelper class
        • ConnectionListener.javaListener: reacts to events
        • PlayerStats.javaHelper class
        • Playtime.javaHelper class
        • Sidebar.javaHelper class
        • SidebarCommand.javaCommand (BasicCommand)
        • SidebarPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
        • StatsListener.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.

62private void refresh(Player player, Sidebar sidebar, int online) {63    String playtime = Playtime.format(player.getStatistic(Statistic.PLAY_ONE_MINUTE));64    sidebar.update(stats.kills(player), stats.deaths(player), playtime, online, player.getWorld().getName());65}
  1. Reads the world each time. When the player walks through a portal, the line changes within a second.

The constants at the top also change: LINE_COUNT becomes 8 and LINE_WORLD is 5.

Try it

Change the update speed

Change both TICKS_PER_SECOND values in runTaskTimer so the sidebar refreshes every 5 seconds instead (100 ticks). What do you notice about the playtime line, and about how fast a new kill shows up?

Hint

You do not need a new constant: replace the arguments by TICKS_PER_SECOND times 5, or just write 100L for the gap between runs.

Show the solution

Playtime now jumps in steps of five seconds and a new kill takes up to five seconds to show. Nothing breaks, because each refresh reads the real numbers again. The sidebar is cheaper to run, though: players barely notice a 5-second delay for kills, but they do notice a clock that moves in jumps. A good rule is to refresh as slowly as players will accept. For a sidebar with a seconds counter, one second is right.

Try it

Count mob kills and player kills separately

Show two lines instead of one: Mobs: 7 and Players: 2. Which classes must change? List them before you start.

Hint

You need a second key in PlayerStats, a check event.getEntity() instanceof Player in the listener, and one more line in the sidebar.

Show the solution

Four places change: PlayerStats gets a second key with its own getter and adder; StatsListener.onEntityDeath calls addPlayerKill when the dead entity is a Player and addMobKill otherwise; Sidebar gets one more line, as in the world-line exercise above; and BoardManager.refresh passes both numbers. Notice that the design pays off here: the listener only counts, the stats class only saves, and the sidebar only draws, so each change is small.

Extension ideas

  • Per-player settings. Let each player choose which lines they see, and save the choice in the persistent data container.
  • A config file. Move the title, the server address and the line texts to config.yml. See Configuration files.
  • A kill streak. Add a line with the current streak and reset it on death.
  • Health below the name. Add a second objective that shows hearts under players' names, as in Scoreboards.
  • A leaderboard. Show the top three killers. The Leaderboard page shows how to keep a sorted list.
  • Placeholders. Offer the numbers to other plugins. See Working with other plugins.
  • Animated title. Change the gradient a little each second, to make the title shimmer.

The next project, Chat formatter and filter, changes what players see in chat.

Recap

  • A sidebar is an objective whose entries are the lines. Give each entry a fixed name and change its customName to change the text.
  • NumberFormat.blank() hides the red numbers on the right of the lines.
  • Each player needs their own scoreboard to see their own numbers, so keep one Sidebar per UUID in a map, and remove it on quit.
  • The player's persistent data container stores small values that survive relogs and restarts, under a namespaced key.
  • PlayerDeathEvent is a kind of EntityDeathEvent. getKiller() is null when no player dealt the blow, so always check.
  • Statistic.PLAY_ONE_MINUTE counts ticks. Divide by 20 for seconds.
  • A repeating task with a 20-tick gap runs once a second. Cancel it in onDisable.
  • Only redraw a line when its text changed. That removes flicker.

Quick quiz

  1. Why does the plugin save kills in the player's persistent data container instead of a HashMap?

  2. A zombie falls into lava and dies with no player nearby. What does event.getEntity().getKiller() return?

  3. Alex kills Steve. Which handlers of StatsListener run?

  4. Why does setLine compare the new text with shownLines[index] before changing the line?

  5. What happens if BoardManager.forget is never called when players quit?

Next steps