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

Loot tables

Verified against Minecraft 26.3 · Part VII · A player opens a dungeon chest for the first time, and every item in it comes into existence between the click and the screen.

You break into a mossy cobblestone room, there is a chest against the wall, and you right-click it. Between that click and the inventory screen the server does something it will do exactly once for this chest: it looks up a loot table, rolls it, scatters the results across the empty slots, and throws the recipe away. Until that moment the chest is genuinely empty on disk — the region file holds a table key, a seed where the chest was given one, and no items at all. And the roll is a one-shot in the strictest sense: RandomizableContainer.unpackLootTable clears the stored key before it rolls, and the thing that triggers it is not “a player opens this chest” but “anything reads this container”. A hopper underneath taking one item, or a comparator behind the wall asking how full it is, will commit the roll with no player present, for good — and in vanilla that costs nothing but a bastion chest’s advancement, since no chest table reads your luck.

The typed parameters the table is handed, the predicates it tests and the sets those belong to are not loot machinery — they are the general context engine that enchantment effects, advancement triggers, /execute if predicate and villager trade filters all run on, and contexts and predicates is where they are explained. Loot is that engine’s largest client, fourteen of its thirty-one parameter sets, and one consequence of borrowing an engine is worth knowing before you start: a table’s own declared type is read once at load, by the validator, and never compared against the parameters it is actually handed. Whether a chest table works is entirely down to what the caller put in the map. This page is the worked example: what a table is, how one draw picks an item, and why the chest was empty.

The cast

classwhat it decidesthread
LootTablethe parameter set, an optional random sequence, the pools, the table’s own modifier, and the ways out — LootTable.fill into a container, LootTable.getRandomItems into a list, and the unsplit LootTable.getRandomItemsRaw a nested table usesServer
LootPoolwhether the pool runs at all, and how many draws it makesServer
LootPoolEntryContainerthe entry algebra — ComposableEntryContainer.expand answers did I contributeServer
UniformContainerBaseweight and quality — one of the two places luck reaches anythingServer
LootItemFunctionforty-two registered kinds, all conditional, one optional modifier per tier — several functions make one SequenceFunctionServer
RandomizableContainerthe stored table key and seed, and the one-shot unpackServer
LootContextwhich random source this draw uses, and the recursion guardServer
ReloadableServerRegistriesloading the eight reloadable registries, and validating the six loot ones among themthe worker pool

The chest was empty before you got there

Nothing wrote items into that chest at world generation. MonsterRoomFeature places the room, then places up to two chests, giving each up to three tries at a position — every try needing an air block with exactly one solid horizontal neighbour — and for each chest that lands calls RandomizableContainer.setBlockEntityLootTable with BuiltInLootTables.SIMPLE_DUNGEON and a fresh long from the feature’s own random. What lands on the live ChestBlockEntity is a key and a seed, and nothing else. Structure pieces do the same through StructurePiece.createChest and StructurePiece.createDispenser. BuiltInLootTables holds a hundred and twenty-one named keys and two per-dye-colour families for sheep — one under entities and one under shearing — spread over fifteen directories that are worth a glance for the range of things that roll a table at all: not only chests and dispensers but brushing, shearing, harvesting, carving, decorated pots, mob equipment, spawners and a charged creeper’s.

The seed reaches disk on the next save, and from then on the emptiness is self-perpetuating. RandomizableContainer.trySaveLootTable writes the key — and the seed only when it is non-zero — and answers yes, I handled this, so ChestBlockEntity.saveAdditional never writes an item list. RandomizableContainer.tryLoadLootTable answers the same way on the way back in, and ChestBlockEntity.loadAdditional skips reading items entirely. A chest nobody has touched has no inventory in any file the game has ever written.

What counts as reading it

The opening said anything reads this container, and the rule is exactly that literal. RandomizableContainerBlockEntity overrides five of the container methods so that each unpacks before answering: RandomizableContainerBlockEntity.isEmpty, RandomizableContainerBlockEntity.getItem, RandomizableContainerBlockEntity.removeItem, RandomizableContainerBlockEntity.removeItemNoUpdate and — the surprising one — RandomizableContainerBlockEntity.setItem. Writing into an unrolled chest rolls it first, and the write then replaces whatever the roll put in that slot.

Every way a chest is rolled without its player goes through one of those five, and every one of them unpacks with a null player, so the roll gets no Attributes.LUCK and fires no CriteriaTriggers.GENERATE_LOOT:

  • A comparator reaches Container.getItem over every slot through AbstractContainerMenu.getRedstoneSignalFromContainer (what a comparator can see).
  • A hopper underneath asks the same question before it takes anything — and so does a hopper pointing into the chest, which checks whether the destination is full before it pushes. Both directions commit the roll by reading.
  • Breaking the block does it too, and this is the one players do not expect: BlockEntity.preRemoveSideEffects drops a container’s contents by walking Container.getItem over every slot, so mining an untouched dungeon chest rolls it and scatters the result on the ground, with no player in the roll.

What does not unpack is as precise. Clearable.clearContent and Container.getContainerSize do not, and neither does saving — which is why /data get block on an unopened chest reports the loot table key instead of committing the roll, and why a structure that has never been visited costs nothing to keep.

From the click to the screen

Right-clicking a dungeon chest is one call that does four things in an order worth watching: it resolves a provider, commits the roll, builds a menu, and only then tells the client anything.

sequenceDiagram
    participant SPGM as ServerPlayer<br/>GameMode
    participant SP as ServerPlayer
    participant CBE as ChestBlockEntity
    participant LT as LootTable
    participant ChestM as ChestMenu
    participant Wire as the network

    Note over SPGM,CBE: ChestBlock.useWithoutItem picks the provider, and a single chest is its own
    SPGM->>SP: openMenu, handed the block entity
    SP->>CBE: createMenu, which opens by asking canOpen
    CBE->>CBE: unpackLootTable, inside createMenu, before a menu exists
    Note over CBE,LT: RandomizableContainer.<br/>setLootTable to null <br/>BEFORE the roll
    CBE->>LT: fill, on the CHEST set, with the stored seed
    LT->>LT: getRandomItemsRaw, every pool, inside createStackSplitter
    LT->>LT: getAvailableSlots, then shuffleAndSplitItems across them
    LT->>CBE: setItem, once per surviving stack
    CBE->>ChestM: threeRows over the now-filled container
    SP->>Wire: ClientboundOpenScreenPacket, and only then the contents
    SP->>ChestM: setSynchronizer, from inside ServerPlayer.initMenu
    ChestM->>Wire: one ClientboundContainer<br/>SetContentPacket,<br/>from sendAllDataToRemote

The roll is three messages deep inside RandomizableContainerBlockEntity.createMenu, and the screen is the last three arrows. Look at where the chest is filled relative to where the client is told anything — the contents exist before the screen is asked for.

The click arrives as ServerPlayerGameMode.useItemOn and reaches ChestBlock.useWithoutItem (block interaction), which asks ChestBlock.getMenuProvider for something to open. A single chest is its own provider — the combiner hands back the ChestBlockEntity. A double chest is the interesting case: it gets an anonymous provider wrapped round a CompoundContainer that requires both halves to pass RandomizableContainerBlockEntity.canOpen, unpacks both loot tables itself, and never enters either block entity’s own menu factory.

Opening goes through ServerPlayer.openMenu, which closes whatever menu was already open, allocates a container id, and calls RandomizableContainerBlockEntity.createMenu. Both gates on opening are in one predicate: RandomizableContainerBlockEntity.canOpen adds a spectator clause to the lock check it inherits from BaseContainerBlockEntity.canOpen, and the spectator half bites only while a table is still pending — so a spectator cannot commit the roll by peering into an unopened chest, though they can open one that has already been rolled.

The unpack is RandomizableContainer.unpackLootTable, and its order matters more than anything else on this page. It looks the key up through ReloadableServerRegistries.Holder.getLootTable — which answers LootTable.EMPTY for a missing key, never null — fires CriteriaTriggers.GENERATE_LOOT if a ServerPlayer is doing the opening, and then clears the stored key with RandomizableContainer.setLootTable, before a single die is rolled. It builds the parameters with LootContextParams.ORIGIN at the block centre and, only if a player is present, that player’s Player.getLuck — the live value of Attributes.LUCK, whose default is zero and which nothing but a potion or a command moves (attributes) — and LootContextParams.THIS_ENTITY. Then it calls LootTable.fill with the container, those parameters and the stored seed.

The screen comes last and is no part of the roll. The menu itself is a ChestMenu, built by ChestMenu.threeRows over the container the roll has just filled. ServerPlayer.openMenu sends ClientboundOpenScreenPacket and then calls ServerPlayer.initMenu, which attaches the listener and the synchronizer; AbstractContainerMenu.setSynchronizer ends in AbstractContainerMenu.sendAllDataToRemote, and that is the single ClientboundContainerSetContentPacket carrying the freshly rolled contents (containers and menus).

One roll, drawn

LootTable.fill runs every pool of the table, each pool makes some number of independent draws, and each draw picks at most one entry. That draw is the engine’s smallest complete unit, and it narrows the whole way down — from however many entries a pool declares, through the expansion where composites become candidates, to at most one candidate.

flowchart TD
    A["LootPool.addRandomItems"] --> B{"the pool condition, if there is one"}
    B -->|"it fails"| Z["the pool contributes nothing"]
    B -->|"it passes, or none"| C["draws: rolls plus floor of LootPool.bonusRolls times luck"]
    C --> D["one draw, repeated that many times"]
    D --> E["expand every entry container, in declaration order, into candidates"]
    E --> K["weight: floor of weight plus quality times luck, clamped at zero"]
    K --> L{"is that above zero?"}
    L -->|"no"| M["dropped from this draw entirely"]
    L -->|"yes"| N["kept, and added to the running total"]
    N --> O{"how many candidates survived?"}
    O -->|"none, or the total is zero"| Z2["this draw yields nothing"]
    O -->|"exactly one"| P["taken, with no roll to pick it"]
    O -->|"two or more"| Q["one RandomSource.nextInt over the total, then walk subtracting weights"]

A pool’s draws, each narrowing to at most one candidate. Note the middle arm out of the last diamond, where a draw with one surviving candidate is taken without a roll to pick it.

The one box that hides a fan-out is expand, and what it expands are the five containers:

the containerwhat expanding it yields
AlternativesEntryan or: stops at the first child that contributes
SequentialEntryan and: stops at the first child that does not
EntryGroupevery child expands, and the contribution is ignored
TagEntry in expand modeone candidate per item in the tag
NestedLootTableone candidate that runs every table in its set, or in expand mode one per table

What becomes of the stack that survives — the tiers of modifiers, the splitter, the shuffle and the write — is the rest of this section, the trace above and the scatter below; it is a straight line and needs no second picture.

The algebra is boolean, not weighted. ComposableEntryContainer.expand returns a plain did I contribute, and each composite folds its children into one of those in its own CompositeEntryBase.compose — the two-child case literally calling ComposableEntryContainer.or or ComposableEntryContainer.and, and the longer cases a hand-written loop with the same short-circuit. So AlternativesEntry behaves like a boolean or and SequentialEntry like a boolean and — neither is a weighted choice between branches, and the validator warns about an AlternativesEntry whose non-final children carry no conditions, since a leaf with none always contributes and hides every alternative after it. Nine entry types are registered in LootPoolEntries — one more registry of kinds in a page made of them (data-driven types) — and the thing each one produces is a LootPoolEntry, the candidate the funnel above weighs. They are the leaves LootItem, EmptyLootItem, TagEntry, NestedLootTable, DynamicLoot and SlotLoot, and the composites AlternativesEntry, SequentialEntry and EntryGroup. TagEntry has two modes and they are not variations on a theme: expanded, it becomes one weighted candidate per item in the tag, each carrying the entry’s own modifier; unexpanded, it is a single candidate that emits every item in the tag at once, the modifier running on each.

Luck touches exactly two things, and neither is what players think it is. LootPool.bonusRolls is multiplied by luck and floored to add whole extra draws, and UniformContainerBase.EntryBase.getWeight is weight + quality × luck, floored, then clamped at zero. Because a candidate whose weight comes out at zero or below is discarded rather than merely made rare, a negative quality with enough luck removes an entry from the pool altogether. No vanilla chest table uses either: quality appears only in the fishing table, where Luck of the Sea feeds the hook’s luck. Everything else a player calls luck on a drop is a loot function or condition reading an enchantment level off the tool or off the killer, which is why Fortune has no effect component at all and why Looting’s one, a higher chance of dropping the mob’s equipment, is not what adds to its loot.

Functions apply innermost first. Nothing on this path returns a stack: a draw is handed a consumer to push its results into, and each tier wraps that consumer before passing it down, with LootItemFunction.decorate over its one optional modifier, several functions at one tier being a single SequenceFunction. So a drop passes the entry’s modifier first, then any enclosing composite’s, then the pool’s, then the table’s. All forty-two registered functions, SequenceFunction among them, extend LootItemConditionalFunction, whose LootItemConditionalFunction.apply is final and hands the stack back untouched when its own condition fails — which is why a function with a failing condition is a no-op and not a veto on the drop. Most also mutate the stack in place and return it (SetItemFunction, SmeltItemFunction, DiscardItem, SetRandomDyesFunction, and the three enchanting functions handed a plain book, return a different one), which is safe because every leaf hands out something of its own: LootItem and TagEntry construct fresh stacks and SlotLoot emits copies. DynamicLoot is the exception: it calls straight out to a callback the caller registered with LootParams.Builder.withDynamicDrop, and the one ShulkerBoxBlock.getDrops registers hands back the block entity’s live stacks uncopied, though no vanilla table names it; the one that does, the decorated pot’s sherds, builds fresh stacks.

The scatter

LootTable.fill does not simply place what it rolled. LootTable.getAvailableSlots collects the container’s empty slot numbers and shuffles them; LootTable.shuffleAndSplitItems then pulls the multi-count stacks out of the result list and repeatedly splits one — taking a random amount between one and half its count — until the number of pieces roughly matches the slot count. That is why one rolled stack of arrows arrives as several partial ones in unrelated slots — how many depends on how much of the chest is free. If the pieces outnumber the free slots, the remainder is logged as a warning and silently discarded.

Above that sits LootTable.createStackSplitter, which every public LootTable.getRandomItems and LootTable.fill wraps its output in: it drops items the level’s feature flags disable, and cuts anything at or over its maximum stack size into stack-sized pieces. NestedLootTable deliberately calls LootTable.getRandomItemsRaw instead, so a nested table’s results are split once by the outer table rather than twice. Nesting the same table twice is fine, from two pools or two rolls of one: the recursion guard is a stack that pops on the way out rather than a list of everything seen (contexts and predicates), so only a table genuinely inside itself would trip it, and such a table fails the whole reload: validation reports the cycle as fatal.

Where the randomness comes from

A LootContext picks its random source by trying three things in order — an explicitly supplied source or seed, then the server’s saved sequence for a named random sequence, then the plain Level.getRandom (contexts and predicates owns the mechanism). For a chest the load-bearing part is the condition on the first of the three: a seed is installed only when it is non-zero, the zero LootTable.RANDOMIZE_SEED names.

So a seed of zero means unseeded, is indistinguishable from having no seed at all, and is never written to NBT — which is why a chest given a loot table by command rolls whatever its table’s named sequence is up to (every vanilla table names one; a table that names none falls to the level’s random), where a structure chest, carrying a seed, rolls the same contents whoever opens it and whenever. Neither rolls twice: the key is gone after the first unpack either way. A table that declares a named random sequence draws from a stored, saved one instead of the level random, which is what keeps the same table in the same world reproducible across a restart even when no chest ever carried a seed.

And which thread does the rolling

One, everywhere. Loading is the only part of any of this that is not on the Server thread — one RegistryLoadTask per reloadable registry on the worker pool, with the layer frozen before anything is validated (the resource system). Rolling is on the Server thread without exception, and the guarantee is a type rather than a thread check: a LootParams demands a ServerLevel to be built at all (contexts and predicates). The strongest form of it is a fact about the whole tree: no client class references the loot package at all.

A loot table that travels inside an item

Everything above is a table anchored to a position. One kind is not. DataComponents.CONTAINER_LOOT carries a SeededContainerLoot — a key and a seed living in a stack’s component patch — which a container broken with no player to roll it (an explosion, a piston) writes into the stack it drops when its own loot table copies the component, as the shulker boxes’ do (RandomizableContainerBlockEntity.collectImplicitComponents; creative pick-block writes it too), and which the loot function SetContainerLootTable writes directly, as against SetContainerContents, which writes the stacks themselves. So a shulker box given a loot table and blown up unopened comes out already owing a roll, and carries that debt around in your inventory.

The component is persistent with no network codec of its own, which under the rule on data components means one is derived for it, so it does reach the client. What it cannot carry is the contents: the client has no loot registry at all, and SeededContainerLoot.addToTooltip does not try — it prints the unknown-contents line and nothing else, on either side. That tooltip is the only place in the game where a player is shown, in so many words, that the items do not exist yet.

For a 1.21-era reader. LootPoolSingletonContainer is gone: UniformContainerBase holds weight and quality, with SingleEntryContainerBase and ExpandableContainerBase beneath it. Each list of functions is one optional modifier, several functions making one SequenceFunction, so LootItemFunctions.compose is gone too, and each list of conditions is one optional condition.

Where to look

RandomizableContainer.unpackLootTable is one short method and holds the whole of why the chest was empty, including the order of the clear and the roll. Read RandomizableContainerBlockEntity straight after it for the five overrides that decide what counts as reading.

Then the roll, outside in: LootTable.fill, LootPool.addRandomItems, and UniformContainerBase.EntryBase.getWeight for the two lines where luck touches anything. ComposableEntryContainer is the boolean algebra, and AlternativesEntry beside TagEntry is the shortest way to see that a composite and a leaf are the same interface.

LootItemConditionalFunction is all forty-two functions, and its final LootItemConditionalFunction.apply is why a failing condition is a no-op rather than a veto. LootTable.createStackSplitter and LootTable.shuffleAndSplitItems are the scatter.

Three callers on the other side of the door are worth opening to see the range of what rolls a table: BlockBehaviour.BlockStateBase.getDrops for every block broken (block breaking), LivingEntity.dropFromLootTable for every mob killed (damage and death), and EquipmentUser.equip, the one caller that compares the looked-up table against LootTable.EMPTY by identity to decide whether to bother. EnchantWithLevelsFunction, a function a table runs rather than a caller, is the cost-based function on the loot side of enchanting. Contexts and predicates has the thirty-one sets they build against, and the count of which of them roll a table.


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