Skip to main content

Scoreboard Guide

Nukkit-MOT has a complete scoreboard API for sidebar, player list, and below-name displays. It supports fake text rows, player-bound rows, entity-bound rows, manager-level objective registration, and JSON-backed storage.

Source-backed scope

This page is written against the current Nukkit-MOT source, especially IScoreboardManager, ScoreboardManager, IScoreboard, Scoreboard, ScoreboardLine, FakeScorer, PlayerScorer, EntityScorer, and the built-in /scoreboard command.

Import the Right Scoreboard Class​

There are two different Scoreboard classes in the source tree:

  • current API: cn.nukkit.scoreboard.scoreboard.Scoreboard
  • deprecated compatibility wrapper: cn.nukkit.scoreboard.Scoreboard

For new plugin code, use the class under cn.nukkit.scoreboard.scoreboard.

Core Model​

The main pieces are:

TypeRole
IScoreboardManagerGlobal registry, display-slot assignment, storage access
IScoreboard / ScoreboardOne objective with a display name, criteria, sort order, viewers, and lines
IScoreboardLine / ScoreboardLineOne scorer + one score value
IScorerThe row owner: fake text, player, or entity
DisplaySlotSIDEBAR, LIST, BELOW_NAME
SortOrderASCENDING or DESCENDING

Important entry point:

IScoreboardManager manager = this.getServer().getScoreboardManager();

1. Private Sidebar for One Player​

If you only want to show a sidebar to one player, you do not need the global manager display slot system. You can create a scoreboard and attach that player directly as a viewer.

import cn.nukkit.Player;
import cn.nukkit.network.protocol.types.DisplaySlot;
import cn.nukkit.network.protocol.types.SortOrder;
import cn.nukkit.scoreboard.scoreboard.IScoreboard;
import cn.nukkit.scoreboard.scoreboard.Scoreboard;
import cn.nukkit.scoreboard.scorer.FakeScorer;

public void showProfileSidebar(Player player, int kills, int coins) {
IScoreboard scoreboard = new Scoreboard("profile_sidebar", "Profile", "dummy", SortOrder.DESCENDING);

FakeScorer killsLine = new FakeScorer("Kills");
FakeScorer coinsLine = new FakeScorer("Coins");

scoreboard.addLine(killsLine, kills);
scoreboard.addLine(coinsLine, coins);
scoreboard.addViewer(player, DisplaySlot.SIDEBAR);
}

To hide it again:

scoreboard.removeViewer(player, DisplaySlot.SIDEBAR);

This path is useful for per-player UI that does not need command integration or global objective lookup.

2. Updating Scores​

Keep the scorer or line reference if you want to update a row later. ScoreboardLine#setScore(...) automatically sends the update to current viewers.

import cn.nukkit.scoreboard.scoreboard.IScoreboardLine;
import cn.nukkit.scoreboard.scorer.FakeScorer;

FakeScorer killsLine = new FakeScorer("Kills");
scoreboard.addLine(killsLine, 0);

IScoreboardLine line = scoreboard.getLine(killsLine);
if (line != null) {
line.setScore(12);
}

You can also remove rows:

scoreboard.removeLine(killsLine);

3. Bulk Rebuilds and resend()​

For many changes at once, rebuild the scoreboard in memory and resend it instead of pushing dozens of incremental updates.

import cn.nukkit.scoreboard.scorer.FakeScorer;

scoreboard.removeAllLine(false);
scoreboard.addLine(new FakeScorer("Kills"), kills);
scoreboard.addLine(new FakeScorer("Coins"), coins);
scoreboard.addLine(new FakeScorer("Rank"), rankPoints);
scoreboard.resend();

There is also a shortcut for simple fake-text sidebars:

scoreboard.setLines(List.of(
"Profile",
"Kills",
"Coins"
));

setLines(List<String>) rebuilds all rows using FakeScorer and then calls resend().

4. Global Objectives Through ScoreboardManager​

Register a scoreboard in the manager when you want it to behave like a real server objective:

  • retrievable by objective name
  • usable by /scoreboard
  • visible to features that resolve score objectives by name
  • storable through the scoreboard storage
import cn.nukkit.scoreboard.manager.IScoreboardManager;
import cn.nukkit.scoreboard.scoreboard.IScoreboard;
import cn.nukkit.scoreboard.scoreboard.Scoreboard;

IScoreboardManager manager = this.getServer().getScoreboardManager();
IScoreboard scoreboard = new Scoreboard("kills", "Kills", "dummy");

if (!manager.containScoreboard(scoreboard.getObjectiveName())) {
manager.addScoreboard(scoreboard);
}

If you later want to remove it:

manager.removeScoreboard("kills");

5. Manager Display Slots vs Direct Viewers​

There are two display styles:

Direct viewers on the scoreboard object​

Use scoreboard.addViewer(player, slot) when you want precise per-player control.

Manager-controlled slot display​

Use manager.setDisplay(slot, scoreboard) when you want one scoreboard assigned to a global slot for the manager's registered viewers.

import cn.nukkit.network.protocol.types.DisplaySlot;

manager.setDisplay(DisplaySlot.SIDEBAR, scoreboard);

To clear the slot:

manager.setDisplay(DisplaySlot.SIDEBAR, null);

If you use manager-level display in plugin code, register and unregister viewers yourself:

manager.addViewer(player);
manager.removeViewer(player);

6. Choosing a Scorer Type​

The scorer type decides what the row is attached to.

ScorerGood forNotes
FakeScorerText rows such as Kills, Coins, blank separatorsEquality is based on the fake name text
PlayerScorerReal player-bound scoresPackets are only built when the player is online
EntityScorerEntity-bound scoresUseful for selector-driven or entity-specific rows

Example: Below-name Health-style Board​

BELOW_NAME is most useful with PlayerScorer, because the line is associated with a real player.

import cn.nukkit.Player;
import cn.nukkit.network.protocol.types.DisplaySlot;
import cn.nukkit.scoreboard.scoreboard.IScoreboard;
import cn.nukkit.scoreboard.scoreboard.Scoreboard;
import cn.nukkit.scoreboard.scorer.PlayerScorer;

IScoreboard healthBoard = new Scoreboard("health", "HP", "dummy");

for (Player target : this.getServer().getOnlinePlayers().values()) {
healthBoard.addLine(new PlayerScorer(target), (int) target.getHealth());
}

for (Player viewer : this.getServer().getOnlinePlayers().values()) {
healthBoard.addViewer(viewer, DisplaySlot.BELOW_NAME);
}

When a player's health changes, update that player's line:

PlayerScorer scorer = new PlayerScorer(player);
if (healthBoard.containLine(scorer)) {
healthBoard.getLine(scorer).setScore((int) player.getHealth());
}

7. Sort Order and Slots​

The current display slots are:

  • DisplaySlot.SIDEBAR
  • DisplaySlot.LIST
  • DisplaySlot.BELOW_NAME

The current sort modes are:

  • SortOrder.ASCENDING
  • SortOrder.DESCENDING

SortOrder matters most for SIDEBAR and LIST. BELOW_NAME is typically used with one score per tracked player rather than a ranked list.

8. Persistence and scoreboard.json​

The server creates ScoreboardManager with JSONScoreboardStorage, backed by scoreboard.json under the server data path.

You can save and reload through the manager:

manager.save();
manager.read();

Important detail: scoreboard changes are not automatically persisted every time you add or edit a line. If your plugin expects scoreboard state to survive restart, explicitly call manager.save() at the appropriate time.

9. Main-thread Safety​

Scoreboard APIs interact with Player packets and viewer state. Treat scoreboard creation, updates, and display changes as main-thread work.

If your score comes from slow I/O or heavy computation:

  1. compute it asynchronously
  2. switch back to the main thread
  3. update the scoreboard there

See the scheduler guide for the thread handoff pattern.

Common Pitfalls​

1. Importing the Deprecated cn.nukkit.scoreboard.Scoreboard​

That class exists only for compatibility with older plugins. Use cn.nukkit.scoreboard.scoreboard.Scoreboard for new work.

2. Reusing the Same Fake Text Twice​

FakeScorer equality is based on its text. If you use the same fake name twice, the later row replaces the earlier one.

For sidebar separators or multiple blank-looking rows, use distinct strings, for example different formatting codes.

3. Assuming addScoreboard(...) Rejects Duplicates​

Current ScoreboardManager.addScoreboard(...) overwrites by objective name. Check containScoreboard(name) yourself before adding.

4. Expecting Offline PlayerScorer Rows to Display Normally​

PlayerScorer.toNetworkInfo(...) returns null when the player is offline, so those rows are filtered out during packet send. Use FakeScorer if you need pure text rows or offline-safe labels.

5. Treating Manager Display as Automatic for Every Player​

manager.setDisplay(...) only pushes to the manager's current viewers. In plugin code, add or remove viewers explicitly if you rely on manager-level display behavior.