Scoreboards
Track scores and show per-player sidebars, name tags, and list panels.
A scoreboard holds named objectives, and each objective holds a score per entry - a player, an entity, or any plain string you choose. Display an objective in a slot and the client draws it: a sidebar on the right, a number under every player's name tag, or a column in the pause-screen player list.
Display an objective
The server keeps one primary scoreboard at getServer().getScoreboard(). Add an objective to it, drop the objective into a slot, and set scores - here a "blocks mined" tally in the sidebar that climbs as players dig:
#include <endstone/endstone.hpp>
namespace es = endstone;
class MyPlugin : public es::Plugin {
public:
void onEnable() override
{
objective_ = getServer().getScoreboard()->addObjective(
"mined", es::Criteria::Type::Dummy, "Blocks Mined");
objective_->setDisplay(es::DisplaySlot::SideBar, es::ObjectiveSortOrder::Descending);
registerEvent(&MyPlugin::onBlockBreak, *this);
}
void onBlockBreak(es::BlockBreakEvent &event)
{
auto score = objective_->getScore(&event.getPlayer());
score->setValue(score->getValue() + 1);
}
void onDisable() override
{
objective_->unregister();
}
private:
std::unique_ptr<es::Objective> objective_;
};The pieces:
Criteria::Type::Dummymeans the server never touches the score - your plugin owns it. It's the criteria you want for anything you drive yourself.getScore(entry)returns astd::unique_ptr<Score>whose value you read withgetValueand write withsetValue. The entry is aScoreEntry- aPlayer *, anActor *, or astd::string.setDisplayputs the objective in a slot -SideBar,BelowName, orPlayerList- with a sort order.unregister()removes the objective entirely; do it inonDisableso a reload starts clean.
If you've used the in-game /scoreboard command, the API maps onto it one-to-one - each call above is the programmatic form of a command you could type:
| C++ API | /scoreboard command |
|---|---|
addObjective("mined", Criteria::Type::Dummy, "Blocks Mined") | /scoreboard objectives add mined dummy "Blocks Mined" |
setDisplay(DisplaySlot::SideBar, ObjectiveSortOrder::Descending) | /scoreboard objectives setdisplay sidebar mined descending |
getScore(&player)->setValue(value + 1) | /scoreboard players add @s mined 1 |
objective->unregister() | /scoreboard objectives remove mined |
Players, entities, and text rows
An entry is whatever a score hangs off. getScore accepts three kinds, and the type you pass decides what gets tracked:
objective_->getScore(&player)->setValue(10); // a real player, by identity
objective_->getScore(&zombie)->setValue(3); // any Actor (mob, entity)
objective_->getScore("Server Total")->setValue(999); // plain text - a free-standing label row- A
Player *is tracked by the player's identity, so the score follows them across reconnects and renames, and shows under their name tag in theBelowNameslot. - An
Actor *works the same way for entities - useful for per-mob counters. - A
std::stringis just text: a standalone named row tied to nobody. These are how you draw arbitrary text lines, like the"Coins"and"Level"rows in the next section.
Pass the player pointer &player, not player.getName(). getScore(&player) tracks the actual player; getScore(player.getName()) quietly creates a plain text row that merely shares the same characters - a separate entry that won't follow the real player, won't appear under their name tag, and drifts away from their real score the moment they rename. Only pass a string when you genuinely want a text row.
A different scoreboard for each player
This is where Endstone goes beyond what you can do from add-ons. The vanilla script API scoreboard and the /scoreboard command share one world scoreboard - every player necessarily sees the same objective and the same values. Endstone instead lets you build a scoreboard per player and assign it individually:
auto board = getServer().createScoreboard(); // a fresh, independent scoreboard
auto objective = board->addObjective("hud", es::Criteria::Type::Dummy, "Your Stats");
objective->setDisplay(es::DisplaySlot::SideBar, es::ObjectiveSortOrder::Ascending);
objective->getScore("Coins")->setValue(coins); // plain strings make label rows
objective->getScore("Level")->setValue(level);
player.setScoreboard(*board); // only this player sees itcreateScoreboard() hands back a standalone std::shared_ptr<Scoreboard>, and setScoreboard swaps which one that single player renders - so two players can be looking at completely different sidebars at the same moment. Because score entries can be arbitrary strings, each row is really a line of text you control, and the number on the right is just another field to drive.
Keep the std::shared_ptr<Scoreboard> alive for as long as the player should see it - store it in a member (for example, a map keyed by player). When the last reference drops, the scoreboard goes with it and the player falls back to the primary one.
Per-player scoreboards are the backbone of advanced JSON UI work. The sidebar is one of the few server-driven surfaces a resource pack can restyle, so pairing per-player objectives (the live data) with custom JSON UI (the look) is how server-side plugins build rich, personalised HUDs - health bars, quest trackers, shop menus - without a client mod. Drive the values from C++; let the resource pack render them. See the Bedrock Wiki's Intro to JSON UI for how the resource-pack side works.
To send a player back to the shared view, assign the primary scoreboard again:
player.setScoreboard(*getServer().getScoreboard());