<!-- https://minecraftdocs.dev/systems/commands/advancements — this page as markdown. The book's index: https://minecraftdocs.dev/llms.txt; the whole book in one file: https://minecraftdocs.dev/llms-full.txt -->

# Advancements

> Verified against **Minecraft 26.3** · Part XIII · "Stone Age": a cobblestone lands in your inventory and the toast is on its way before the next tick ends — delivered by a subscription table that shrinks as you play, over a packet that never says what a criterion tests.

Mine a stone block. Nothing about advancements happens when the item enters
the inventory. What happens is that the pickup's `ServerPlayer.take` runs
`AbstractContainerMenu.broadcastChanges` — the same diff that keeps
your client's inventory in sync ([containers and
menus](../items/containers-and-menus#where-in-the-tick-a-broadcast-happens))
— which notices that a slot's contents differ from its remembered copy, and reports
the difference. Detection is a **diff, not
an event**, and the advancement system is a subscriber to it.

That is the first of two things this system does backwards from
expectation. The second is bigger: there is **no global list of who is
listening for what**. Each player carries their own subscription table, it
contains only the criteria that player has *not yet satisfied*, and it
shrinks as they play. A veteran player is cheaper to run than a new one, and a save's cost falls as its players play.

Which is also why advancements are the game's general-purpose *"tell me when
the player does X"* facility rather than only a goal list. The recipe book
is unlocked by advancements. `PlayerPredicate` reads advancement progress
back out as a loot condition. Of the fifty-eight registered triggers, fifty-seven are hooks something in the game fires, and the remaining one, `minecraft:impossible`, is never fired at all.

## The cast

| class | what it decides | side |
|---|---|---|
| `Advancement` | the immutable definition — parent, `DisplayInfo`, rewards, criteria, requirements, and a **pre-rendered display name** built in the six-argument constructor, which is the `[Title]` every announcement quotes | both |
| `AdvancementHolder` | the id plus the advancement, with **id-only equality** — so a map keyed by holder survives a pack changing an advancement's contents | both |
| `AdvancementTree` | the parent/child graph, built by a fixed-point loop that refuses any advancement whose parent is not yet a node. An orphan is **discarded**, not re-rooted | both |
| `Criterion` / `CriterionTrigger` | a trigger plus a decoded `CriterionTriggerInstance`, and nothing else — a criterion has no name of its own, the name is the map key, and the **trigger object is stateless** | server |
| `SimpleCriterionTrigger` | the base class for all but one trigger, and the owner of the per-fire sweep | server |
| `AdvancementRequirements` | a list of lists of criterion names: an **AND of ORs**. `AdvancementRequirements.size` counts *clauses*, not criteria | both |
| `PlayerAdvancements` | the per-player subscription table and the dirty sets. The only class here with interesting state | server |
| `TreeNodePosition` | a full tidy-tree layout — three walks, threads, ancestors, shifts — run on the **server**, mutating `AdvancementNode`'s coordinates in place | server |

The shared model is `net/minecraft/advancements`, the triggers are in
`advancements/triggers` and the predicates in `advancements/predicates`
(with the entity half a level down). All 111 classes ship in both jars — the
*side* column above says where a class does its work, not which jar carries
it — and `PlayerAdvancements` sits in `net/minecraft/server` beside them.
`CriteriaTriggers` registers **fifty-eight** triggers into
`BuiltInRegistries.TRIGGER_TYPES` over **forty-four** classes; the gap is
re-use, with `PlayerTrigger` alone accounting for six registrations.

## From a slot that changed to a toast

`minecraft:story/mine_stone` has one criterion, *get_stone*, whose trigger
is `minecraft:inventory_changed` and whose condition is a single
`ItemPredicate` over the `#minecraft:stone_tool_materials` tag. It has no
rewards at all.

```mermaid
sequenceDiagram
    box Server
    participant SP as ServerPlayer
    participant ACM as Abstract<br/>ContainerMenu
    participant ICT as Inventory<br/>ChangeTrigger
    participant PA as PlayerAdvancements
    end
    box Client
    participant CPL as ClientPacketListener
    participant CAdv as ClientAdvancements
    end

    rect rgba(0, 0, 0, 0.04)
    Note over SP,PA: the pickup, inside ServerPlayer.doTick, after the levels
    SP->>ACM: broadcastChanges, from ServerPlayer.take
    ACM->>SP: ContainerListener.slotChanged, on the player's own listener
    SP->>ICT: trigger — count all 43 slots first
    ICT->>ICT: SimpleCriterion<br/>Trigger.trigger
    ICT->>ICT: test the changed stack
    ICT->>PA: award, after the sweep
    PA->>PA: unregisterListeners, then the rewards
    PA->>PA: markForVisibilityUpdate — the root
    end
    rect rgba(0, 0, 0, 0.04)
    Note over SP,PA: the next tick's ServerPlayer.tick
    SP->>PA: flushDirty, second to last in the tick
    PA->>PA: updateTreeVisibility — the whole tree
    end
    PA->>CPL: ClientboundUpdate<br/>AdvancementsPacket
    CPL->>CAdv: update — rebuild, reconcile progress, add a toast
```

*One stone pickup through the advancement system — the award happens inside the pickup, after the levels have ticked, and the one packet that tells the client waits for the next tick's flush.*

The paragraphs below take its arrows in order.

**Counting comes before knowing whether anyone cares.** The slot change
reaches the trigger through the `ContainerListener` every `ServerPlayer`
installs on its own menus, whose `ContainerListener.slotChanged` fires
`CriteriaTriggers.INVENTORY_CHANGED`.
`InventoryChangeTrigger.trigger` walks all forty-three slots — thirty-six
inventory plus seven equipment — to compute the occupied, full and empty
counts *before* it asks whether any criterion is listening. That is the floor cost of every change to a player's inventory, forever. (The one fired unconditionally is `CriteriaTriggers.TICK`, once a tick for every player.)

**The sweep itself is the cheap part.** `SimpleCriterionTrigger.trigger`
fetches this trigger's map from `PlayerAdvancements.getTriggerMapForType` and
returns immediately if it is null — and it *is* null once every criterion
for that trigger is satisfied, because `PlayerAdvancements.removeListener`
deletes the per-trigger map when it empties. When there is work, it builds
**one** `LootContext` and reuses it for the whole sweep, evaluates the
caller's cheap matcher first and the *player* predicate only for matches,
and does not allocate the results list until the first hit. Note the scope:
one player. Nothing here is a broadcast.

**Matches are collected, then awarded**, because awarding calls
`PlayerAdvancements.unregisterListeners`, which mutates the very map being
iterated.

**Completion is checked against the requirements, not the criteria.**
`AdvancementRequirements.test` is the AND of ORs. Here it is one clause of
one name, so granting the criterion completes the advancement — which fires
the rewards, the chat announcement (built by
`AdvancementType.createAnnouncement`, broadcast to every player, gated on the display's own flag and on `GameRules.SHOW_ADVANCEMENT_MESSAGES`) and the visibility dirty flag.

**The rewards are `AdvancementRewards.EMPTY` here, as for most advancements outside the recipe book.**
The record holds experience, loot tables, recipes and a function, and the
recipe field is why the recipe book works at all: every recipe advancement
is generated with a `RecipeUnlockedTrigger` criterion and rewards naming the
recipe, so earning it calls `ServerPlayer.awardRecipes`
([recipes](../items/recipes#the-recipe-book-unlocked-glowing-and-filled-in-for-you)).
`RecipeUnlockedTrigger` then closes the loop by letting *other* advancements
observe an unlock — comparing the recipe key by **reference identity**,
which is safe only because `ResourceKey`s are interned. The function reward
is a `CacheableFunction`, which holds the id and resolves it once
([functions and macros](functions-and-macros)).

## The table shrinks, and the two things that refill it

`PlayerAdvancements.registerListeners` subscribes a player only to criteria
that are not yet done in advancements that are not yet done, and every award
unsubscribes. The longer you play, the smaller your table, and the cost of the system to a save falls as its players play.

Two things run it backwards. `/advancement revoke` re-subscribes what it
revoked, and a reload — `/reload`, or `/datapack` enabling or disabling a pack — re-subscribes everything unfinished in the new pack.
Neither is something the game does on its own, which is why the table shrinks as you play rather than only ever shrinking.

Two oddities sit on the same machinery. `ImpossibleTrigger` has no trigger
method at all — it is the one trigger implementing `CriterionTrigger`
directly, and it exists so a node can anchor a tree while being ungrantable
except by command, which vanilla uses for exactly one file: the invisible
root of every recipe advancement. (It has nothing to do with `/trigger`,
which is a scoreboard feature.) And
`PlayerAdvancements.checkForAutomaticTriggers` is dead code in the strictest
sense: it walks every advancement on every player load, but its whole body
sits behind *this advancement has no criteria at all*, and `Advancement`'s
criteria codec rejects an empty map outright. No loaded advancement can
satisfy the guard, so the loop never does anything.

## Visibility is per root, and it gates the wire

`PlayerAdvancements.markForVisibilityUpdate` dirties the **root**, and the
flush — `PlayerAdvancements.updateTreeVisibility`, once per dirty root — re-runs `AdvancementVisibilityEvaluator` (the "how far past your frontier can you see" rule, with a depth of two) over that root's entire subtree. Finishing one advancement in a large tree re-evaluates the whole
tree, and only the nodes whose visibility *flipped* go on the wire.

`PlayerAdvancements.flushDirty` then sends one
`ClientboundUpdateAdvancementsPacket`, with progress **only for advancements
in `PlayerAdvancements.visible`**. Progress on something hidden, or beyond
your frontier, accumulates server-side and reaches the client the moment it
becomes visible.

Where the flush sits in the tick is worth pinning down, because it produces
a real one-tick delay that nobody expects. Inside `ServerPlayer.tick`,
`AbstractContainerMenu.broadcastChanges` is the fifth statement,
`CriteriaTriggers.TICK` fires mid-tick, and
`PlayerAdvancements.flushDirty` is **second to last**, with only a
post-effects check after it. So everything awarded between those points — a `minecraft:tick` criterion, or an item that another advancement's reward grants, whose diff runs inside that award — coalesces into one packet, and so does everything awarded earlier in the tick: a melee kill arrives in the packet drain of `MinecraftServer.processPacketsAndTick`, and a typed `/advancement grant` as a task the server runs before it, both before the levels tick at all.

But `ServerPlayer.tick` is not the last thing that happens to a player: it is
only the first of the two brackets a player is ticked in ([the two-phase
tick](../player/the-two-phase-tick#the-bracket-and-what-survives-it)).
`ServerGamePacketListenerImpl.tick` calls `ServerPlayer.doTick` during the
**connection** phase, which runs *after* the levels
([the server tick](../server/server-tick)) — i.e. after
`PlayerAdvancements.flushDirty` has already run. `CriteriaTriggers.LOCATION`, which fires there
every twenty ticks and is what most vanilla biome and structure
advancements hang on, therefore **always lands in the next tick's packet** —
and so does any award a pickup earns, the stone above included, because
`ServerPlayer.take` runs the diff there.

## A criterion's conditions are loot conditions

Each entity condition a trigger carries — the *player* test every simple
trigger has, a kill's victim — is one `LootItemCondition`, held in a
registry holder and evaluated against a `LootContext` reached through
`EntityPredicate.createContext`. So
a trigger's conditions are exactly the machinery of
[contexts and predicates](../items/contexts-and-predicates), and that is
where most descriptions of the system stop.

It is worth going one step further, because the predicate package is where
four shapes were invented that the whole data-driven half of the game now
reuses.

| shape | what it generalises | where else it turns up |
|---|---|---|
| `MinMaxBounds` | the numeric range, with both a codec **and** a `StringReader` grammar | `3..7` means the same in a predicate, an entity selector and `/random` |
| `CollectionPredicate` | one generic "N of these match", composing `CollectionContentsPredicate` and `CollectionCountsPredicate` | its only users are the seven component predicates in `core/component/predicates` |
| `EntitySubPredicate` | every part of an entity predicate as a **registry element** instead of a code branch | its twenty-five kinds are each a record and a codec, a few with a builder beside them |
| `DataComponentMatchers` | testing a stack's components without knowing what any of them are | [data components](../foundations/data-components#the-readers-and-the-predicates) |

Two details change behaviour rather than shape.
`LootItemCondition.CODEC` accepts *either* the id of a data-pack predicate
or one inline condition — though vanilla's own advancements take the inline
form every time, so in a criterion the reference exists for pack authors
rather than for the game.
And `EntityPredicate` declares an explicit type-check-first, NBT-last
ordering for its own sub-tests — a performance invariant hiding inside a
predicate class.

## The screen at the other end

The client's half is five classes in `net/minecraft/client/gui/screens/advancements`, `ClientAdvancements` over in `client/multiplayer`, and the toast, and it is the payoff for
everything the server did. It does **no tree layout**: it is drawing
positions a data-pack reload decided, which is why the tree looks the same
on every client connected to a server.

The layout itself is `TreeNodePosition`, run once per root by `AdvancementTree.repositionNodes`
from `ServerAdvancementManager`'s constructor — which a reload runs on a background worker,
after `RegistryDataLoader` has read the JSON ([the resource
system](../foundations/resource-system#reload-the-same-pipeline-on-the-server)).
It writes each `AdvancementNode`'s coordinates in place, and they ride the packet in
`ClientboundUpdateAdvancementsPacket.PositionedAdvancement`. A
root with no `DisplayInfo` is never laid out and never becomes a tab, and a
display-less node in the middle of a tree is transparent — the layout skips
it and adopts its children. One wrinkle in an otherwise deterministic
algorithm: `AdvancementNode.children` is an unordered hash set, so sibling
order inside a tidy-tree layout is hash-dependent.

`AdvancementsScreen` is a `ClientAdvancements.Listener`, and its `AdvancementsScreen.onAdvancementsUpdated`
rebuilds every tab from the tree. `ClientAdvancements.setListener` makes that call on a new listener
at once, which is how the screen catches up on open. Each packet is applied by
`ClientAdvancements.update`: it removes and adds tree nodes, places each added one at the coordinates
the packet carries, reconciles every progress it was sent against that advancement's requirements, and — for each
now done whose display asks for it — adds an `AdvancementToast`, which plays
a sound only for a challenge. Then it makes the same call, so an open screen rebuilds after every packet.
`AdvancementsScreen` owns the tab strip; `AdvancementTab` owns one root's
pan-and-scroll bounds, auto-centres on first render and clamps the drag;
`AdvancementWidget` scales the server-decided coordinates by fixed factors,
draws the connector lines to its parent, and wraps its own tooltip text.

`AdvancementTabType` is the one with a hard limit in it: four header
geometries with room for eight tabs above, eight below and five on each
side. **A data pack's twenty-seventh root is silently unreachable.**

Two more things ride this boundary for almost nothing.
`ServerboundSeenAdvancementsPacket` has a "closed screen" action that is
serialised, deserialised and dropped. And `Advancement.sendsTelemetryEvent`
is consumed **only** on the client, by `WorldSessionTelemetryManager`, and
only for advancements in the *minecraft* namespace — which is the whole
reason a flag rides a wire form that drops the criteria and the rewards
([what this book skips](../anatomy/what-this-book-skips)).

## What the package holds that this page does not name

Two families make up most of `net/minecraft/advancements`, and the answer to
both is the same: the page explains the shape and the members are instances of
it. The **forty triggers it does not explain** — `KilledTrigger`,
`FishingRodHookedTrigger`, `RecipeCraftedTrigger` and the rest — are each a
record, a codec and a `CriterionTriggerInstance`, registered by `CriteriaTriggers` and fired
from wherever in the game the thing happens; knowing `SimpleCriterionTrigger`
is knowing all of them. The **concrete predicates** under
`advancements/predicates` are the same story one level down:
`StatePropertiesPredicate`, `EntityFlagsPredicate`, `EntityEquipmentPredicate`,
`MobEffectsPredicate`, `DamageSourcePredicate` and their neighbours are each a small record over one kind of thing, the entity ones instances of `EntitySubPredicate`, and the one that matters most to another part, `LocationPredicate`, is named where it does work, in [contexts and predicates](../items/contexts-and-predicates#what-reads-a-context).

## Questions players ask

**Why does the client show "3/7" if it does not know what the criteria
are?** Because `AdvancementRequirements` *is* on the wire and
`AdvancementProgress.update` reconciles against it. `Advancement.STREAM_CODEC`
reconstructs the record with an **empty criteria map** and
`AdvancementRewards.EMPTY`, so a client cannot know what any criterion
tests, or that an advancement grants anything at all. And
`AdvancementProgress.getProgressText` returns nothing at all when there is
one clause, which is why a single-criterion advancement never shows "1/1".
A criterion's whole per-player state is one class,
`CriterionProgress`, and one field in it: a nullable timestamp. That is why
the wire form can carry progress without carrying a single condition.
(An `AdvancementProgress` with no requirement clauses cannot be complete — `AdvancementRequirements.test` returns false for an empty list rather than vacuously true — and every progress starts that way, decoded from the wire or the disk or built empty, until `AdvancementProgress.update` gives it its requirements, which both sides do at once.)

**Does `/reload` roll back my progress?** No, and the order is the point: a
reload's completion list saves every player before it reloads them ([the
resource
system](../foundations/resource-system#reload-the-same-pipeline-on-the-server)),
so `PlayerAdvancements.reload` re-reads a file written moments earlier. What is genuinely lost is progress for any
advancement the new pack has removed or renamed — logged once each, and
invisible to the player except as a full reset packet — plus the selected
tab, which is silently forgotten with no packet, so the client keeps a stale
one.

**When is progress written to disk?** Only when the player is saved. There is
no write on award: `PlayerAdvancements.save` runs from `PlayerList.save`, on disconnect, on every save of the world — the autosave, `/save-all`, shutdown — or from the reload above. The definitions come
from `data/<ns>/advancement/<id>.json` through `Advancement.CODEC` — a
file that fails to decode aborts the reload outright — and per-player state is one JSON
file at `players/advancements/<uuid>.json`
(`LevelResource.PLAYER_ADVANCEMENTS_DIR`), data-fixed on load through
`DataFixTypes.ADVANCEMENTS`.

**Why is `/advancement grant` usable as a conditional?** Because a no-op is
a hard failure: granting an advancement the player already has throws rather
than reporting zero. `AdvancementCommands.Mode` — *only*, *through*, *from*, *until* — picks one advancement or collects its parents or children or both (its *everything* constant goes unused), and `/advancement grant … everything`, which takes every advancement directly, calls
`PlayerAdvancements.flushDirty` before the loop with the packet's "show
advancements" flag **true** and after it with the flag **false**, which is
the only purpose that flag has: suppressing a toast storm from the batch it
just granted.

**Why does the toast stay silent?** Because `AdvancementToast` asks for a
sound only when the advancement is an `AdvancementType.CHALLENGE` — one of the three
`AdvancementType`s, and the one that also gets its own title colour. For a
*task* and a *goal* the method returns nothing at all, so the toast slides in
in silence. The client can tell which it is because `DisplayInfo` carries the
type; what it cannot tell is whether the advancement was meant to announce
itself in chat, because that flag **never goes on the wire**. The
serialiser packs three flags into an int and omits it, and the reader
hard-codes it false while the codec defaults it true, so the client's copy is
wrong for the common case rather than merely unused.

## Where to look

`Advancement` and `AdvancementRequirements` for the model;
`PlayerAdvancements` for everything that happens;
`SimpleCriterionTrigger` and `InventoryChangeTrigger` for the hot path;
`MinMaxBounds` and `CollectionPredicate` for the predicate shapes that recur
everywhere else; and `AdvancementVisibilityEvaluator` for the one rule
nobody guesses right.

---

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