Scoreboard
A config-driven sidebar: display conditions, line animation, per-group and per-player overrides.
Commands 1#
Examples#
First setup: tune the title live and then make it permanent#
See what the sidebar looks like without restarting the server, and understand that command edits are temporary.
/scoreboard info
/scoreboard set title default &6&l{server_name}
/scoreboard preview
/scoreboard reload
info prints Enabled/Disabled, the board count, refreshInterval in ticks and board names. set title immediately changes the title for everyone currently shown the default board and collapses the title animation to a single frame. preview rebuilds the panel for you. reload re-reads scoreboard.json (and animations.json too) — the temporary title disappears and the screen shows what is in the file again.
A custom title and line for the vip group#
Test a group's text before writing it into scoreboard.json → scoreboard.groups.
/scoreboard group vip title <gradient:FFD700-FF8C00>&lVIP
/scoreboard group vip line 2 &6Balance: &f${balance} &6[VIP]
/scoreboard info
/scoreboard group vip reset
Players whose primary group is vip immediately see the new title and the replaced line at index 2 (0-based, the third line of the selected board's lines array). Others are unaffected. group vip reset removes the title and all line overrides of that group at once. For the text to survive reload, move it into scoreboard.json → scoreboard.groups.vip (titleOverride / lineOverrides).
A separate board in the Nether#
Show a different set of lines in the_nether without touching the overworld. Conditions are an implicit AND; the first board with the highest priority whose conditions all pass wins.
/scoreboard reload
/scoreboard board list
/scoreboard preview
In scoreboard.json → scoreboard.boards add a board with a priority higher than default (shipped default has 0) and conditions: ["world:the_nether"]. An empty conditions on default always matches — keep it lowest in priority as the fallback. After reload board list lists the names; in the Nether preview shows the new board, in the overworld — default. The dimension name is the path (overworld, the_nether, the_end), not minecraft:the_nether.
A player hides the panel for themselves#
Give players a switch that survives restarts without turning off the whole system.
/scoreboard toggle
/scoreboard toggle
The first call hides the sidebar and writes the state to the DataStore collection scoreboard_toggles (key — UUID). The second brings the panel back. By default the panel is on for a player who has never pressed toggle. Requires the neoessentials.scoreboard.toggle node (the registry grants it to everyone by default). The branch does not run from the console: .requires needs a player.
A multi-frame title and slowing down one board#
Make an animated title without hitting an expensive placeholder every global cycle.
/scoreboard reload
/scoreboard preview
Put a JSON array of strings in the board's title — those are the frames (the loader does not read a separate titleFrames key). Frames cycle by this board's ownAnimFrame. refreshMultiplier (integer ≥ 1, default 1) advances this board's frames once every N global refreshInterval cycles without changing the packet send tick itself. After reload preview shows the first frame; the animation then runs on its own.
Edge cases#
The tree merges with vanilla `/scoreboard`#
NeoEssentials registers the same scoreboard literal, and Brigadier adds child nodes to the vanilla command instead of replacing it. Vanilla objectives / players / teams remain. No name collisions: vanilla has players (plural), here it is player; vanilla enable lives under players, here it is at the root. Bare /scoreboard with no arguments runs the NeoEssentials .executes and prints the mod's help, not vanilla's.
What to do: Vanilla objectives work as before: /scoreboard objectives add …. The mod's help is bare /scoreboard. To remove the mod's command entirely, disable commands.scoreboard in config.json and restart — the vanilla tree remains.
Which board a player sees#
Boards are sorted by descending priority. For each viewer the first board whose conditions lines are all true (implicit AND) is used. An empty condition list always passes — such a board must have the lowest priority, otherwise it overrides the rest. If no board matches, the sidebar is hidden for that player. There is no separate OR — two adjacent priorities with different conditions give the same effect.
What to do: Keep the fallback board last with priority: 0 and conditions: []. Check which board you currently see with /scoreboard preview in the right world and with the right permissions.
`conditions` grammar and the short `{balance}` in a condition#
Parsed in ConditionEvaluator, in order: perm:<node> (PermissionAPI), world:<path> (dimension path, no namespace), {placeholder} == value / != value (case-insensitive comparison); otherwise the string is run through PlaceholderAPI and is true if the result is non-empty and not false.
Conditions do not go through the sidebar's shorthand replacements. In board lines {balance}, {online}, {player} are substituted by ScoreboardManager itself. In a condition, PlaceholderAPI looks for the identifier balance and does not find it (the default expansion is {neoessentials_balance}). An unresolved token stays as the text {balance}: an == comparison fails, and a lone unresolved {token} as a boolean is true because the string is non-empty.
What to do: Check world and permissions with the world: / perm: forms. For placeholders in a condition write {neoessentials_balance}, {neoessentials_group}, {neoessentials_world} and so on. Do not use a lone unknown {token} as a condition — it silently passes.
`lineOverrides` target the array index; hidden lines collapse#
A line can have its own condition. Lines that fail are dropped, and the rest shift up into slots 0…14 — there is no blank gap. An override (lineOverrides in the file or /scoreboard group|player … line) changes only the text and does not cancel the original line's condition: a hidden line stays hidden. The override key is the 0-based index in the selected board's lines array, not the visible slot number after collapsing.
What to do: Count indices by the JSON array, including lines with conditions. If line 2 is hidden by a condition, lineOverrides["2"] still refers to it, not to what the player sees third.
Override precedence and what is lost on reload#
Text chain: player (UUID) → group (PermissionAPI primary) → board value. /scoreboard set title|line edits the board itself in memory (for title, the whole frame list is replaced with one frame). /scoreboard player / group write override maps in memory. loadConfig() on reload fully re-reads boards/groups/players from the file and clears these maps — temporary edits are lost. enable/disable are also memory-only: reload restores scoreboard.enabled from the file.
What to do: Permanent changes go only in scoreboard.json. The dashboard save (saveConfig) can flush the current memory to disk; the commands never write the file themselves.
{online} is server.getPlayerList().getPlayers().size(), without a vanish filter (unlike the tablist). {displayname} is substituted as player.getName(), i.e. the real IGN, not /nick. {max} — server slots. {time} — HH:mm of the system clock, not the world's in-game time.
What to do: Vanished players are still counted in {online}. For a nickname in a line use the PlaceholderAPI nickname placeholder if one is registered, not {displayname}.
Two independent switches#
modules.scoreboardEnabled: false in config.json (restart): the tick/join/quit handler exits immediately, no packets are sent, /scoreboard is not registered.
commands.scoreboard: false (restart): the command is not registered, but the sidebar tick runs as before — the panel stays, you just cannot reload it from the game.
scoreboard.enabled: false + /scoreboard reload (or /scoreboard disable): a live switch-off, the command stays, hideAll is called for everyone (SIDEBAR slot, line teams ne_sb*, objective ne_sidebar).
What to do: To remove the system entirely — modules.scoreboardEnabled. To temporarily hide the panel while keeping the command — enabled in scoreboard.json or /scoreboard disable.
Join delay and anti-flicker#
joinDelayTicks is counted in server ticks (globalTick), not refresh cycles. Until the delay expires, updatePlayer calls hidePlayer. Packets are sent only if the title or the set of visible lines changed since the last send to that viewer. The slot score is fixed (15 − index) with BlankFormat so the client does not reorder lines by number.
What to do: If the panel "flashes" on join, increase joinDelayTicks (shipped 40). If a line does not update, the placeholder most likely returned the same value: the dirty check stays silent on purpose.
Command permissions#
Bare /scoreboard checks no nodes. toggle requires a player and neoessentials.scoreboard.toggle. Admin branches: p == null || neoessentials.scoreboard.admin. Plus the common opsBypassPermissions and PermissionAPI's vanilla OP fallback.
What to do: If /scoreboard "does not exist" even for an operator, check modules.scoreboardEnabled and commands.scoreboard, not permissions. If help works but reload is missing from completion, you lack neoessentials.scoreboard.admin.
Known issues#
The `titleFrames` key from the original wiki is not read#
ScoreboardManager.loadConfig() takes title frames only from title (string or array). The loader has no titleFrames field. Title animation is defined by an array in title. The shipped comment and the ScoreboardSystem.md page disagree with the code.
What to do: Write frames like this: "title": ["frame1", "frame2"]. Do not add a separate titleFrames — it will be ignored.
`toggleCommandEnabled` has no effect#
The original wiki describes a toggleCommandEnabled key. The loader does not read it; the /scoreboard toggle branch is registered with the rest of the command as long as the module and commands.scoreboard are on.
What to do: To take toggle away from players, revoke the neoessentials.scoreboard.toggle node. To remove the whole command — commands.scoreboard: false and a restart.
Saving a board through the web panel drops `refreshMultiplier`#
toConfigJson() / boardToJson() serialize name, priority, conditions, title, lines — and do not write refreshMultiplier. The next save from the panel overwrites scoreboard.json without that field; after reload the multiplier is 1 again.
What to do: Set refreshMultiplier manually in the file and do not save that board with the panel button, or restore the field in the file after saving.
resolveText substitutes both {player} and {displayname} via player.getName(). /nick does not affect this line. The placeholder name promises a display name; the code returns the registry name.
What to do: Do not expect a nickname in {displayname} on the sidebar. For lines that need a nickname, use the nickname placeholder that PlaceholderAPI actually resolves and check it with /scoreboard preview.
Configuration#
scoreboard.json#
Permissions 2#
Detailed guide#
The section below is the project's original documentation (docs/Wiki/ScoreboardSystem.md). Links to other wiki pages are rewritten to this site; the guides themselves are not edited, so upstream syncs stay conflict-free.
Config file: config/neoessentials/scoreboard.json
Reload in-game: /scoreboard reload
Admin permission: neoessentials.scoreboard.admin · Toggle permission: neoessentials.scoreboard.toggle
Introduced: v1.0.6
Overview#
The sidebar scoreboard system renders the classic right-hand-side Minecraft scoreboard —
title + up to 15 lines — fully server-side, config-driven, with animation, conditional
boards, and per-group/per-player overrides.
| Feature |
Details |
| Named boards |
Multiple boards, each with its own conditions; the first matching one wins |
| Conditions |
perm:<node>, world:<name>, {placeholder} == value / != value |
| Animated title/lines |
Frame arrays, cycled at refreshInterval |
| Per-group / per-player overrides |
Title and individual line overrides, same priority order as Tablist |
| Persisted per-player toggle |
/scoreboard toggle remembers each player's preference across sessions |
| Join delay |
Configurable grace period before a player's board first appears |
| True per-viewer rendering |
Unlike a vanilla /scoreboard objective (server-wide, one value for everyone), different players can see different boards or different placeholder values simultaneously — see Technical Notes |
| Dashboard endpoint |
/api/scoreboard — read/create/update boards, toggle enabled |
Why This Isn't Just a Vanilla /scoreboard Objective#
Vanilla's scoreboard Objective/Team/Score state lives on the server's single shared
ServerScoreboard and is broadcast identically to every connected player. That's fine for a
tablist row (a player's own nametag looks the same to every viewer) but wrong for a sidebar:
two players might be shown different boards (one in the Nether, one in the Overworld) or the
same board with different values (each player's own balance).
NeoEssentials solves this by building the four scoreboard packets
(ClientboundSetObjectivePacket, ClientboundSetPlayerTeamPacket, ClientboundSetScorePacket,
ClientboundResetScorePacket) against a private, never-registered Scoreboard instance
purely to serialize them, then sending them directly to one player's connection — the
same technique already used for the tablist header/footer packet. No two players' boards can
interfere with each other, and nothing is ever visible to a player other than the one the
packet was sent to.
Configuration#
Full scoreboard.json structure#
{
"_configVersion": 1,
"scoreboard": {
"enabled": true,
"refreshInterval": 20, // ticks between updates (20 = 1s)
"joinDelayTicks": 40, // wait this long after join before showing a board
"toggleCommandEnabled": true,
"boards": [
{
"name": "default",
"priority": 0,
"conditions": [], // empty = always matches — MUST be last (fallback)
"title": "<gradient:FFD700-FF8C00>&l{server_name}",
"titleFrames": [], // optional: animated title, same shape as tablist header
"lines": [
{ "text": "&7&m " },
{ "text": "&eOnline: &f{online}&7/&f{max}" },
{ "text": "&eBalance: &f${balance}" },
{ "text": "&eGroup: &f{group}" },
{ "text": "&6Richest: &f{leaderboard_money:1:name}" }
]
},
{
"name": "nether_board",
"priority": 10,
"conditions": [ "world:the_nether" ],
"title": "&c&lNETHER",
"lines": [ { "text": "&7Danger zone" } ]
}
],
"groups": { },
"players": { }
}
}
Core Fields#
| Field |
Type |
Default |
Description |
enabled |
boolean |
true |
Enable/disable the whole system |
refreshInterval |
int |
20 |
Update interval in ticks |
joinDelayTicks |
int |
40 |
Ticks after join before the board first appears |
boards |
array |
— |
Named board definitions (see below) |
groups |
object |
— |
Per-group titleOverride/lineOverrides |
players |
object |
— |
Per-player UUID titleOverride/lineOverrides |
Board Fields#
| Field |
Type |
Description |
name |
string |
Board identifier |
priority |
int |
Boards are evaluated highest-priority first; first whose conditions all pass wins |
conditions |
array |
List of condition strings, implicit AND. Empty = always matches |
title |
string or array |
Static string, or array for animated frames |
titleFrames |
array |
Alternate way to specify animated title frames |
lines |
array |
Up to 15 { "text": ..., "condition": ... } objects |
A line's own condition (optional, single string) controls whether that specific line is
shown — hidden lines are skipped entirely (remaining visible lines compact upward into
sequential slots), not left as a blank gap.
Conditions Grammar#
| Form |
Matches when |
perm:<node> |
The player has that permission |
world:<name> |
The player's current dimension path equals <name> (e.g. overworld, the_nether, the_end) |
{placeholder} == value |
The resolved placeholder equals value (case-insensitive) |
{placeholder} != value |
The resolved placeholder does not equal value |
| anything else |
Resolved as a placeholder and treated as boolean ("false"/empty = false) |
This is deliberately a small comparison grammar, not a scripting engine — it covers
permission gates, world checks, and placeholder-threshold checks, which covers the vast
majority of real use cases without the security/maintenance surface of an embedded
expression language.
A board's conditions list is implicit AND. There's no OR — approximate it with two boards
at adjacent priorities if you need it.
Overrides#
Same priority order as Tablist: per-player > per-group > board default.
"groups": {
"vip": {
"titleOverride": "&6&lVIP SERVER",
"lineOverrides": { "2": "&6Balance: &f${balance} &6[VIP]" }
}
},
"players": {
"<uuid>": {
"titleOverride": "&dWelcome back!"
}
}
lineOverrides keys are 0-based indexes into the resolved board's lines array. An override
replaces the line's text but doesn't change its condition — a conditionally-hidden line
stays hidden even with an override text set.
Placeholders#
Shorthand tokens (matching tablist's convention):
| Placeholder |
Description |
{player} {displayname} |
Viewing player's name |
{online} {max} |
Online count / max slots |
{ping} |
Viewing player's ping |
{world} |
Current dimension path |
{balance} |
Economy balance |
{prefix} {suffix} {group} |
Permission group data |
{x} {y} {z} |
Block coordinates |
{server_name} |
Plain server name (general.serverName in config.json) |
{server_motd} |
Configured MOTD (/motd if set, otherwise server.properties) — usually multi-line/formatted for the server list, not a great fit for a scoreboard row |
{time} |
Server time (HH:mm) |
Plus {animation:NAME} (from animations.json, shared with the Tablist System),
the full {neoessentials_*} registered placeholder set, and
{leaderboard_<board>:<rank>:name|value} — see Leaderboard System.
Commands#
| Command |
Permission |
Description |
/scoreboard |
— |
Show help |
/scoreboard toggle |
neoessentials.scoreboard.toggle |
Show/hide your own sidebar (persisted) |
/scoreboard reload |
neoessentials.scoreboard.admin |
Reload scoreboard.json and push to all players |
/scoreboard enable / disable |
admin |
Enable/disable the whole system |
/scoreboard info |
admin |
Status summary |
/scoreboard preview |
admin |
Re-send your own board now |
/scoreboard board list |
admin |
List configured boards |
/scoreboard set title <board> <text> |
admin |
Replace a board's title (single static frame) |
/scoreboard set line <board> <index> <text> |
admin |
Replace one line's text (0-based index) |
/scoreboard player <player> title\|line\|reset |
admin |
Per-player overrides |
/scoreboard group <group> title\|line\|reset |
admin |
Per-group overrides |
Runtime overrides via /scoreboard player//scoreboard group are not persisted — add
them to scoreboard.json manually to survive /scoreboard reload.
Toggle Persistence#
/scoreboard toggle is backed by its own DataStore collection (scoreboard_toggles, one
record per player, written immediately on change) — the same single-collection,
write-immediately pattern used by PayToggleManager for /paytoggle. A player's preference
defaults to on and survives server restarts.
Disabling Completely#
Three independent layers, from most to least aggressive:
modules.scoreboardEnabled: false in config.json (restart required) — the event
handlers driving tick/join/quit never fire at all, so no scoreboard packet is ever sent.
Same restart-required module-toggle pattern as every other subsystem.
/scoreboard disable (live, no restart) — immediately tears down the sidebar for
every currently-online player: clears the display slot, resets every visible line's score,
removes the per-line teams, and removes the objective itself (not just hides it).
scoreboard.json's "enabled": false + /scoreboard reload — same effect as #2.
Technical Notes#
- Fake score-holder entries. Each of the 15 possible line slots uses a fixed invisible
string (
§0–§e) as its score-holder name — these aren't real players, just unique
client-side row identifiers. The actual line text lives entirely in that slot's team prefix.
- Anti-flicker. Packets are only sent when the player's resolved title/lines actually
changed since the last send — same dirty-check philosophy as the Tablist System's team
prefix caching.
- Score ordering. Vanilla's client sorts sidebar rows by descending score value; each
slot is given a fixed score (
15 − slotIndex) so line order stays stable regardless of
content, with the number itself hidden via a blank NumberFormat.
Web Dashboard API#
/api/scoreboard (auth required for reads, auth-admin for writes):
| Method |
Path |
Description |
GET |
/api/scoreboard |
Full config overview |
GET |
/api/scoreboard/boards/{name} |
Single board detail |
PUT |
/api/scoreboard/enabled |
{ enabled } |
POST |
/api/scoreboard/boards |
Create/update a board |
DELETE |
/api/scoreboard/boards/{name} |
Delete a board |
PUT |
/api/scoreboard/boards/{name}/line/{index} |
Quick single-line edit |
Back to Wiki Home