Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scores, teams and stored data

Verified against Minecraft 26.3 · Part XIII · execute as @a store result score @s ticks_frozen run data get entity @s TicksFrozen: one command writing a scoreboard through a callback the inner command has never heard of, and two of the three data models execute store exists to join.

Look at the sidebar on a well-built server and some of the names in it are not players. #total, constant, .timer — rows belonging to nothing alive. That is not a hack layered on top of the scoreboard; it is the scoreboard working exactly as written, and one method override explains it.

Entity.getScoreboardName returns the entity’s UUID string. Player.getScoreboardName overrides it with the profile name. There is one flat map from string to a PlayerScores — a row of scores, holding players by name, mobs by UUID, and anything else you care to type. From that single override follows the whole of scoreboard folk practice: why fake players exist (nothing checks that a key belongs to an entity), why a mob’s score can never appear in the tab list (whose holder is built from a profile name), and why renaming a player orphans their scores.

execute store is why four systems share this page. It is the only construct in the game that takes the result of an arbitrary command and writes it somewhere, and it has exactly three sinks — a score; a path into a block, an entity or a storage; and a boss bar’s value or maximum. Each of those is a named thing on this server that a number can be written into, and each brings its own model with it: the scoreboard, which is the game’s general-purpose number per thing; the NBT path language, a query language for reaching into any tag at all, over command storage, which is a place to put a tag belonging to no block and no entity; and the boss bar, which turns out to be the scoreboard’s shape one floor up. Teams are the fourth, and they are here because they live in the scoreboard’s package and its class — read by systems that have nothing to do with scores.

The instinct they share is worth stating before the classes, because it explains three otherwise-odd decisions: the server is the only participant that knows anything. The client is sent scores it can draw and nothing else — not an objective’s criteria, not a score’s lock bit, not which objectives exist. There is no serverbound packet in this whole system. Every write a player asks for is a command.

The cast

classwhat it decidesside
Scoreboardsix maps and nothing else, plus ten empty hooks for a subclass to fill. A pure data structure, used unmodified by the clientboth
Objectivecriteria, display name, render type, number format, auto-update — and a back-pointer to the scoreboard, so every setter reports its own changeboth
Scorefour mutable fields: the value, a lock bit, a display component and a number format. ReadOnlyScoreInfo, PlayerScoreEntry and ScoreAccess are the other three faces of itboth
ScoreHolderthe interface Entity implements, with ScoreHolder.forNameOnly minting anonymous ones — the most consequential class on the pageboth
PlayerTeamthe only subclass of Team: the mutable state, the setters, a precomputed display style, and friendly-fire plus see-invisibles packed into one wire byteboth
ServerScoreboardthree fields — the server, ServerScoreboard.trackedObjectives, and one dirty boolean — and thirteen overrides, the ten hooks and three methods that change team membership and display slots, which between them broadcast what changed and mark the save dirty. It lives in net/minecraft/server, not beside Scoreboardserver
NbtPathArgument874 lines, the largest argument type in the game, and a whole query language: six node kinds and a depth limit of 512both
DataCommands/data, over three DataAccessors — BlockDataAccessor, EntityDataAccessor and StorageDataAccessor. Beside it, ScoreboardCommand, TeamCommand and TriggerCommand write the scoreboard, as execute store doesserver

net/minecraft/world/scores is the whole model, and every class in it ships in both jars. Beside it: CommandStorage, a lazy façade over one SavedData per namespace, so minecraft:foo and mypack:foo are different files; and net/minecraft/network/chat/numbers, holding NumberFormat with the three kinds NumberFormatTypes registers.

One command, two models, and a number that lands in a third place

The figure follows that command from the engine’s stage walk to the client’s sidebar; BuildContexts is the engine’s walker over a parsed chain (the execution engine).

sequenceDiagram
    box Server
    participant BC as BuildContexts
    participant ExecC as ExecuteCommand
    participant DataC as DataCommands
    participant SS as ServerScoreboard
    participant SA as ScoreAccess
    end
    box Client
    participant CPL as ClientPacketListener
    end

    BC->>BC: as @a — one source becomes one per player
    BC->>ExecC: store result score — the modifier, once per source
    ExecC->>ExecC: storeValue — @s and the objective already resolved
    BC->>DataC: getData, the leaf, queued once per source
    DataC->>DataC: build the entity's whole save tag
    DataC->>DataC: follow the path, collapse the tag to one int
    DataC-->>ExecC: the result, to the source's callback
    ExecC->>SS: getOrCreatePlayerScore
    ExecC->>SA: set
    SA->>SS: onScoreChanged — only if changed or new
    SS->>CPL: ClientboundSetScorePacket — only for a displayed objective
    CPL->>CPL: handleSetScore — the owner is only a name

One execute store result score for one player — the store is fixed on the source before the inner command runs, the number reaches it by callback, and two separate gates decide whether the write is a packet.

The paragraphs below take its arrows.

The store target is resolved before the inner command runs, not after. ExecuteCommand.wrapStores builds the store node as a redirect with a modifier, not as a step after the leaf. The modifier resolves the score holder and the objective against this source, then decorates the source with a callback chained onto whatever was already there. So the store is a property of the source, which is why several stores compose, and why each forked player writes their own row without the leaf command knowing a scoreboard exists.

The result travels by callback, not by return value — the source’s own callback, which is precisely why execute store works on a command typed in chat whose frame callback is empty (the execution engine).

A failing command under store result writes 0, whichever kind it is. On the custom-executor path that is CustomCommandExecutor.WithErrorHandling doing what it does for every executor built on it (the execution engine), so a failing /function writes 0. For an ordinary leaf the result consumer is driven by Brigadier, and ContextChain.runExecutable catches the CommandSyntaxException and calls the consumer with success false, result 0 before rethrowing. The game hands that consumer straight through from the source’s own callback, so the two paths agree: a store target written by a command that threw holds zero, not its previous value.

/data get builds the whole entity to read one field. EntityDataAccessor.getData produces the entity’s entire save tag, freshly, and the path then walks it. That is the real cost of /data get, and it is why a per-tick /data get on a busy entity is a measurable expense.

Four rules collapse a tag to one integer. A numeric tag floors its double value; a collection and a compound both yield their size; a string yields its length. Nothing yields the value you might have meant: data get entity @s Inventory returns the number of stacks you are carrying, not 36 — Inventory.save writes only the non-empty slots, so the collection whose size you get is as short as your inventory is empty. A path matching more than one tag is an error; a path matching none is a different error.

The write can be a silent no-op, and may never reach the wire at all. ScoreAccess.set writes nothing and sends nothing when the value is unchanged and the score is not new — unless the objective has auto-update on, in which case it refreshes the display name first and a changed display counts as a change on its own. And ServerScoreboard.onScoreChanged is gated on the objective being in ServerScoreboard.trackedObjectives, whose only entrance is occupying a display slot.

Why a write is a handle

Scoreboard.getOrCreatePlayerScore does not return a Score. It returns a ScoreAccess — an anonymous object closing over the score, the objective and the holder, plus two decisions computed once, at handle-creation time: may this be modified (did the caller ask for a force-writable handle, or is the criteria not read-only), and was this score newly created by the lookup that produced this handle.

That is the whole answer to “why not a setter”. A setter would have to re-derive the first fact on every call, and the second is unrecoverable after the fact — once the Score exists, nothing can tell whether this call created it. Newness is what decides whether an unchanged value still needs a packet, so it has to survive from the lookup to the write. The handle is also the one place that knows when to fire the change hook, which is why a write goes through ScoreAccess.set, ScoreAccess.add, ScoreAccess.increment, ScoreAccess.reset, ScoreAccess.lock, ScoreAccess.unlock, ScoreAccess.display or ScoreAccess.numberFormatOverride; only removal — Scoreboard.resetSinglePlayerScore, Scoreboard.resetAllPlayerScores and Scoreboard.removeObjective — and loading from the save go round it.

Nothing here is ticked. MinecraftServer mentions the scoreboard five times in total — the field, the constructor, the load, the getter, and one call to ServerScoreboard.storeToSaveDataIfDirty inside MinecraftServer.saveAllChunks. There is no periodic sweep and no dirty-queue drain: every mutation that reaches the wire sends its packet synchronously, inside the call that made it, and there is one scoreboard per server, not per level, so scores and teams are global across dimensions.

Names that belong to nobody, and the # that hides them

The opening’s fake players are not one mechanism but two, and the # in front of #total does a different job in each.

In ScoreHolderArgument a leading # skips entity resolution entirely, so the token is taken as a literal name and no lookup happens. Its literal side has four branches in order — the wildcard *, a # name, a UUID searched across every level, and an online player — and the UUID and player branches fall back to a bare name when they find nothing. The wildcard does not: with no tracked holders at all it throws.

In the sidebar the same character means something unrelated: PlayerScoreEntry.isHidden filters the row out of the display. One character, two mechanisms, and together they are the whole hidden-fake-player idiom — a row that can be written by name, read by name, and never drawn.

Nothing about that requires an entity to exist. ScoreHolder.forNameOnly mints a holder from a string — it is all ClientPacketListener.handleSetScore ever builds from a score packet — and the scoreboard’s one flat map does not care where the string came from.

What a criterion can be, which is nearly anything

ObjectiveCriteria looks like an enum of eleven values and is not. Forty-three constants exist, thirty-two of them generated — sixteen team-kill and sixteen killed-by-team criteria, one per team colour. Six are read-only: health, food, air, armour, experience and level.

And then the tail. Stat extends ObjectiveCriteria, so every statistic in the game is a criterion, and ObjectiveCriteria.byName parses a colon-separated name by looking the left half up as a stat type and the right half in that stat type’s own registry. minecraft.mined:minecraft.stone is not a special case; it is the statistics registry addressed through a string. Nine stat types over the block, item, entity-type and custom-stat registries make thousands of valid criteria names, which is why /scoreboard objectives add accepts far more than the forty-three bare criteria names — its suggestions offer those and every stat name in every stat type’s registry.

The identity-keyed reverse index is what makes that cheap. Scoreboard.objectivesByCriteria is an identity map from criteria to the objectives watching it, ServerPlayer.awardStat hands the Stat object itself to Scoreboard.forAllObjectives, and object identity finds the watchers — sound only because each stat object is interned, one per value, in its StatType.

Criteria-driven scores are the one part of this page with a schedule, and it is narrower than it sounds. Scoreboard.forAllObjectives has seven call sites and every one is in ServerPlayer: one inside the helper each of the six read-only criteria is updated through, one for the death count, two for the kill counts, one that fires both team-kill criteria, and two for statistics — one awarding, one resetting. No criterion is driven from Entity, LivingEntity or Mob, so a skeleton killing a zombie increments nobody’s kill count — though Entity.load does write to it, calling Scoreboard.addPlayerToTeam when the tag it loads names a Team — a field the game reads but never saves, so it arrives only in a tag a command or an item supplies. The six read-only criteria are change-detection diffs — six consecutive comparisons against remembered fields — living in ServerPlayer.doTick, which runs in the connection phase, after the levels have ticked (the level tick). So damage taken during the level tick reaches the scoreboard, and the wire, later in the same tick rather than during it.

The path language, and the accessor that is coarser than it looks

Six node kinds — a named child, a match on an object, a match on the root object, a match on a list element, all elements, and an index — with a depth limit of 512.

The elegant part is creation. The parent-creating walk goes through the nodes and, for each one, asks the next node what shape its parent has to be: a named child wants a compound, an index wants a list. So a set through a.b.c materialises two compounds without any node knowing more than its own type — though an index never creates an element, so through a.b[0].c the walk makes a and an empty list b, finds nothing at [0], and fails. Removal is the same walk with a plain lookup instead, so it never creates.

The three accessors, by contrast, are coarse. DataAccessor has two methods that matter — read the whole tag, write the whole tag — and no path-aware write anywhere. Every /data modify is read everything, mutate in memory, write everything back, which is why a block-entity write reloads the block entity and marks the chunk dirty, and why an entity write round-trips the entity through its own load path and then restores the UUID by hand, because loading would have overwritten it. What each accessor does contribute is its own grammar subtree, which is how DataCommands builds the target half and the source half of every subcommand from one list of three providers applied twice.

Teams, which systems far from scores read

Team declares everything a reader asks for and PlayerTeam is its only subclass. What makes teams worth their own paragraph is who else consults them: collision through EntitySelector.pushableBy; nametag visibility through LivingEntityRenderer.shouldShowName; invisibility through Entity.isInvisibleTo; friendly fire through Entity.doTeamsAllowDamage; and death-message routing through ServerPlayer.die. Only ServerPlayer.die and LivingEntityRenderer.shouldShowName are reached from anywhere near one place; EntitySelector.pushableBy has six call sites, Entity.doTeamsAllowDamage seven, among them Player.canHarmPlayer and the attacks of a few mobs, and Entity.isInvisibleTo two. A sixth system is joined to teams both ways. Every team join, leave and modification calls through to ServerWaypointManager to remake the locator bar’s connections, and remaking one reads the team’s colour to re-colour its icon.

Two team behaviours are worth pinning. Team.isAlliedTo is reference equality, so two teams with byte-identical settings are never allied; every “same team?” test in the game is really “same object?”, safe only because Scoreboard.teamsByName is the single owner of every instance. And a team has two visibility settings of which only one ships: the wire parameters carry nametag visibility, while death-message visibility has a single reader in play, ServerPlayer.die, and the client never learns the rule because it does not need to. Nametag visibility also widens: when an entity has a team, LivingEntityRenderer.shouldShowName returns from the team switch directly and never reaches the checks that hide a name behind F1, for the camera entity, or for a vehicle — so a mob on a team set to always keeps its name through the HUD toggle.

What the client is ever told

Five packets, and one field that gates four of them

Five packets, all server → client, and no serverbound counterpart exists: ClientboundSetObjectivePacket, ClientboundSetDisplayObjectivePacket, ClientboundSetScorePacket, ClientboundResetScorePacket and ClientboundSetPlayerTeamPacket. All five reach every player, through PlayerList.broadcastAll or a loop over the player list — no distance filter, no dimension filter (what the client is told).

An objective in no display slot does not exist on the network. ServerScoreboard.onObjectiveAdded sends nothing at all; the only path into the tracked set is ServerScoreboard.setDisplayObjective. A scoreboard with two hundred objectives and an empty sidebar costs zero bandwidth — and putting one into a slot then ships every score it holds, to every player, at once. The join burst is the same shape: PlayerList.updateEntireScoreboard sends every team with its full member list, then walks all nineteen display slots and ships each distinct occupying objective with all of its scores.

What it is not told, and how it draws anyway

What the client is told is also less than it looks. ClientPacketListener constructs every objective it receives with ObjectiveCriteria.DUMMY, so a client cannot tell a health objective from a dummy one — it only knows to draw hearts — and the score packet carries no lock bit, which is why /trigger’s suggestions have to be computed on the server.

How a score is drawn is settled by a third object again, and it has its own fall-back order. A NumberFormat may ignore the number entirely — FixedFormat renders a constant component whatever the score is — and resolution runs per-score override, then per-objective, then a per-site default: red in the sidebar, yellow in the tab list, unstyled below the name. Below-name numbers are the odd one out: they are computed in Entity rather than in a renderer, and their range is an attribute, Attributes.BELOW_NAME_DISTANCE, syncable, default ten, maximum 512, so a server can change how far away a player’s below-name score is legible, per entity (attributes).

There is a third route by which a score reaches a client, and it carries no score packet at all: a {"score":…} or {"nbt":…} in a text component. ScoreContents and NbtContents resolve on the server, against the authoritative scoreboard, and put the result on the wire — never the reference (text components). A /tellraw is a photograph, not a subscription.

What survives a restart, and what does not

Saving is one boolean for the entire scoreboard, cleared by re-packing the whole thing, and it happens only when the world is saved — the autosave, /save-all, or shutdown. A crash thrown by the tick itself is a shutdown: it falls into the same finally, which calls MinecraftServer.saveAllChunks, whose very first statement is ServerScoreboard.storeToSaveDataIfDirty. What loses a score is an ending that never reaches that method — a crash relayed from a worker thread, which the shutdown’s own drain throws again before the save, a watchdog kill, a kill -9, a power cut — and then every score since the last autosave goes with it (how a server dies). ScoreboardSaveData sits under minecraft:scoreboard beside the world, with one command-storage file per namespace, both through the data fixer (level data and rules). The NBT field names are the archaeology — Objectives, PlayerScores, DisplaySlots, Teams, and inside them Name, CriteriaName, RenderType, Locked — capitalised, and preserved by codec.

The third sink is a boss bar, and it is this page’s shape again

execute store result bossbar <id> value writes into a CustomBossEvent, and a reader who has the scoreboard already knows the model. A boss bar is a named server-side thing holding a number, saved with the world, broadcast to the players attached to it, and writable only by a command.

BossEvent is the shape everything shares: an id, a name, a progress float from zero to one, a BossEvent.BossBarColor, a BossEvent.BossBarOverlay, and three booleans that ask the client to darken the screen, play the boss music and draw the world fog. ServerBossEvent adds the live membership — a Set<ServerPlayer> — and turns every setter into a broadcast, which is the whole of the sending side the book had not named: one ClientboundBossEventPacket with six operations, add and remove and one update apiece for progress, name, style and properties. There is no serverbound counterpart, exactly as there is none for a score. Four things in the game own one: WitherBoss, Raid, EnderDragonFight — and CustomBossEvent, which is /bossbar.

What CustomBossEvent adds is the part that rhymes. It keeps an integer CustomBossEvent.value and an integer CustomBossEvent.max, derives the float progress from them on every write, and holds its membership as a Set<UUID> beside the superclass’s set of live players. The UUID set is what persists and what /bossbar set … players edits; the live set is maintained by CustomBossEvent.onPlayerConnect, which re-attaches a player whose UUID is on the list, and by CustomBossEvent.onPlayerDisconnect, which calls the superclass’s removal, so that the UUID stays. A bar’s membership outlives the session the same way a score outlives the entity.

CustomBossEvents is the SavedData that holds them, one file at data/minecraft/custom_boss_events.dat (level data and rules), and its NBT is the same archaeology as the scoreboard’s — Name, Visible, Value, Max, Players, capitalised, preserved by codec. BossBarCommands is the write surface, at gamemaster, and an overload of ExecuteCommand.storeValue, the score sink’s own name, is the store sink: a sibling of the score sink rather than the same code, chaining the same kind of callback onto the source and writing the value or the maximum instead of a ScoreAccess. What the client does with the packet — the interpolation, the sky it darkens — is the HUD.

Questions players ask

Why is my sidebar not DisplaySlot.SIDEBAR? If the local player is on a team with a colour, Hud uses that colour’s own display slot and falls back to the plain sidebar otherwise. The colour-to-slot mapping lives on TeamColor, not on DisplaySlot, and it is what the sixteen team sidebars are for. The sidebar shows fifteen rows and hides before it cuts: hidden rows are filtered out, then the rest sorted by value descending and name case-insensitively, and then truncated to fifteen.

Why did /trigger say the objective is not enabled? Score’s lock bit starts locked and its codec defaults unlocked, so a score created by /scoreboard players set is locked while a score loaded from a file that omits the field is not. /trigger is the only command an unprivileged player can use to write a score, and its gate is three-part — the criteria must be the trigger criteria, the score must already exist, and it must be unlocked — and the command re-locks it immediately, so each enable buys exactly one use. It is also, with /teammsg (and its /tm alias), one of the two commands in this area registered with no permission requirement at all.

Why did my execute store throw an internal error? Because /scoreboard refuses a read-only objective and execute store does not check. The command resolves its write targets through ObjectiveArgument.getWritableObjective; ExecuteCommand.wrapStores uses the plain lookup and never asks for a force-writable handle. So execute store result score @s <a health objective> reaches ScoreAccess.set with modification disallowed and raises a raw runtime exception from inside a result callback rather than a command error. The same hole is reachable through /scoreboard players operation with ><, the one operator that writes both sides.

Why did my execute store into a data target do nothing at all? Its read-mutate-write is wrapped in a catch with an empty body: a player target, an uncreatable path, a path too deep — no message, no failure, no write. The score sink has no such catch.

Why can I read a player’s NBT but not write it? The entity accessor’s write path rejects any Player before doing anything else, and the read path has no such check. That one asymmetry is why every player-NBT technique is read-only. Relatedly, a no-op is a hard failure across the area — /data merge, /data modify, /data remove and /scoreboard players enable all throw when they changed nothing, as do most of /team’s setters and every one of /bossbar’s, which makes them usable as conditionals in a function, the same choice /advancement grant makes.

Do a mob’s scores survive it despawning? They survive unloading and die with the mob: Scoreboard.entityRemoved runs from the level’s destruction callback and is gated on the entity being both non-player and not alive.

Where to look

Scoreboard for the six maps, ScoreAccess for why a write is a handle, and ServerScoreboard.trackedObjectives for the one field that decides which objectives and scores a client’s scoreboard ever holds. ObjectiveCriteria.byName for the statistics bridge, NbtPathArgument.NbtPath for the tidiest piece of the area, and ExecuteCommand.wrapStores for the one that makes execute store stop feeling like magic.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.