<!-- https://minecraftdocs.dev/systems/items/containers-and-menus — 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 -->

# Containers and menus

> Verified against **Minecraft 26.3** · Part VII · A player shift-clicks a stack out of a chest: one packet goes up, the server re-runs the same code against the real container, and nothing comes back down.

A stack of cobblestone sits in the top-left slot of a chest, and a
shift-click lands it in the hotbar before anything has answered. One
`ServerboundContainerClickPacket` leaves for the server; the server runs the
*same* method against the real `ChestBlockEntity`, gets its own answer,
compares the two — and if they agree, sends nothing at all. That is the
steady state: **one packet up, zero packets down.** There is no transaction
acknowledgement in this protocol; agreement is silence. What makes silence
safe is the part nobody expects. The click packet does not carry the stacks
the client thinks it produced; it carries each changed slot as an item, a count
and a CRC32C *hash* per component its patch adds, and the server writes those claims into its own per-slot record of what the
client believes *before* it compares any slot. The record is the server's, and what a click is allowed to fill it with is a hash and never a stack.

## The cast

| class | what it decides | thread |
|---|---|---|
| `Container` | storage, and nothing about who is looking at it — `Container.getItem`, `Container.setItem`, `Container.stillValid` | wherever its owner runs |
| `AbstractContainerMenu` | the slot list, the cursor, the state id, the click path, and two independent records of what has already been reported — one for the advancement listeners, one for the wire | Server and Render |
| `Slot` | GUI policy — `Slot.mayPlace`, `Slot.mayPickup`, `Slot.getMaxStackSize` — and the guarded mutations a click goes through | Server and Render |
| `Inventory` | the player's own storage, present as slots in nearly every menu that opens — the lectern's one book slot is the exception | Server and Render |
| `MenuType` / `MenuProvider` | the registry entry with the *screen-side* constructor, and the server-side factory a block hands to `ServerPlayer.openMenu` | Render / Server |
| `ContainerSynchronizer` | the channel from a menu's diff to the connection, and the writer of every slot update a click causes but one — one per `ServerPlayer`, shared by every menu that player opens | Server |
| `RemoteSlot` | what the server believes the client is holding in one slot, as either a stack or a hash | Server (`RemoteSlot.PLACEHOLDER` on the Render thread) |
| `HashedStack` | the client's claim about one slot after its own click | created on the client, matched on the server |

## The chest you see is not the chest

The server's `ChestMenu` is built from a `MenuProvider` that
`ChestBlock.getMenuProvider` produces and `ChestBlock.useWithoutItem` hands
to `ServerPlayer.openMenu` ([block
interaction](../blocks/block-interaction#block-then-empty-hand-then-item)),
which is a fixed sequence: close whatever menu was already open, take the
next container id, ask the provider for a menu, send
`ClientboundOpenScreenPacket`, and then `ServerPlayer.initMenu` — which
attaches the listener and the synchronizer, and whose attaching of a
synchronizer is what sends the single `ClientboundContainerSetContentPacket`
carrying the contents. The screen arrives before the contents do, and both
arrive after the menu object exists on the server. Its `Container` is the real
`ChestBlockEntity` — or a
`CompoundContainer` over both halves, for a double chest, from a separate
anonymous provider that unpacks both halves' loot tables itself. The client's `ChestMenu` is built
from `MenuType`'s screen-side constructor by `MenuScreens`, and that
constructor makes a **fresh `SimpleContainer`**. The client's chest contents
have no connection to the block entity beyond the packet stream. Its
`AbstractContainerMenu.stillValid` would be a lie for the same reason —
`SimpleContainer.stillValid` returns true unconditionally — but the question is
never put to it: every call site of `AbstractContainerMenu.stillValid` in the
game is on the server. A client never closes its own menu because it walked
away.

### Three asymmetries between the two copies

The second asymmetry, after the fresh container above, is the synchronizer. Only
`ServerPlayer.initMenu` ever calls
`AbstractContainerMenu.setSynchronizer`, so a client menu has none, and
every one of its `RemoteSlot`s stays `RemoteSlot.PLACEHOLDER`, whose
`RemoteSlot.matches` always answers true, so the *wire* half of
`AbstractContainerMenu.broadcastChanges` is inert on the client by
construction. The listener half is not: `BeaconScreen`, `ItemCombinerScreen`,
`LecternScreen` and the creative inventory all register a real
`ContainerListener`, which is how the anvil's rename field repopulates itself
after a slot changes.

The last of the asymmetries is the one the other pages of this part lean
on. A block-anchored menu reaches the world through a `ContainerLevelAccess`,
and the client's copy holds `ContainerLevelAccess.NULL`, whose
`ContainerLevelAccess.evaluate` **runs nothing and returns an empty optional**,
so its `ContainerLevelAccess.execute` runs nothing either. Anything a menu does
inside that call — matching a recipe, spending lapis, chipping an anvil — is therefore skipped wholesale on the client, whatever the body itself tests. What the client really runs in those cases
is whatever guard sits in *front* of the call, which is why a client copy of a
menu can still refuse a click it cannot afford.

### Twenty-eight subclasses, one machine

There are twenty-eight `AbstractContainerMenu` subclasses in `world/inventory`,
and after this page's chest, the crafting grid, the anvil and the enchanting
table the rest are the same machine with a different slot list, `LoomMenu`,
`CartographyTableMenu`, `BeaconMenu`, `BrewingStandMenu`, `CrafterMenu`,
`LecternMenu`, `DispenserMenu`, `HopperMenu`, `ShulkerBoxMenu`, `MerchantMenu`
and the two mount menus among them. A station menu is a `Container` — the block
entity's, or a scratch one the menu builds for itself on each side — a list of
`Slot`s carrying the GUI policy, and, for a furnace's progress or an anvil's
price, a data slot or a `ContainerData`. `ItemCombinerMenuSlotDefinition` is the
small vocabulary the anvil and the smithing table are laid out from; `SlotRanges`
beside it is not a menu's at all, but the table of slot names commands and
predicates read.

### Three smaller facts that will otherwise trip you

**A menu with a null `MenuType` cannot be opened the ordinary way**, because
`ServerPlayer.openMenu` asks for its type and `AbstractContainerMenu.getType` throws. That is why the player's own
`InventoryMenu` is pinned to `InventoryMenu.CONTAINER_ID`, zero, and built
independently on both sides, and why the two `AbstractMountInventoryMenu`
subclasses — the horse's and the nautilus's, which pass a null type up to
`AbstractContainerMenu` explicitly — need a
`ClientboundMountScreenOpenPacket` of their own instead of
`ClientboundOpenScreenPacket`. A chest boat is not one of them: it opens an
ordinary `ChestMenu` through `ServerPlayer.openMenu` like any other
container. Every other menu takes an id from
`ServerPlayer.nextContainerCounter`, which cycles 1 to 100 and so never
collides with the player's own zero.

**`Slot.mayPlace` and `Container.canPlaceItem` are unrelated questions.** The
first is GUI policy and defaults to true without consulting the container at
all; the second is what hopper automation reads. A player and a hopper can
have different rights over the same slot, and neither of them is asking the
other's question.

**`Container.getMaxStackSize` defaults to 99.** The familiar 64 comes only
from the per-stack overload, which takes the minimum with the item's own
maximum — so a player and an item can disagree about a slot's capacity the
same way a player and a hopper disagree about its contents.

## One shift-click, end to end

A three-row `ChestMenu` numbers slots 0–26 for the chest, 27–53 for the
player's three main inventory rows and 54–62 for the hotbar, because
`AbstractContainerMenu.addStandardInventorySlots` adds the main rows before
the hotbar. Watch what the wire carries, and what it does not.

```mermaid
sequenceDiagram
    participant MPGM as MultiPlayer<br/>GameMode
    participant ChestM as ChestMenu
    participant Wire as the network
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant ACM as Abstract<br/>ContainerMenu
    participant RemS as RemoteSlot

    MPGM->>MPGM: copy every slot's stack, before touching anything
    MPGM->>ChestM: clicked with QUICK_MOVE, on the client's own copy
    ChestM->>ChestM: quickMoveStack, then moveItemStackTo backwards
    MPGM->>Wire: ServerboundContainerClickPacket, state id plus changed slots as hashes
    rect rgba(0, 0, 0, 0.04)
        Note over MPGM,RemS: the server tick that drains the packet
        Wire->>SGPL: handleContainerClick
        SGPL->>ACM: suppressRemoteUpdates, then the same clicked on the real chest
        SGPL->>ACM: setRemoteSlotUnsafe per claimed hash, then setRemoteCarried
        ACM->>RemS: receive, dropping any concrete stack it held
        SGPL->>ACM: resumeRemoteUpdates, then broadcastChanges
        ACM->>RemS: matches
        RemS->>RemS: the hash agrees, so it becomes a copy of the server's stack
    end
    Note over Wire,RemS: agreement is silence, and AbstractContainerMenu.synchronizeSlotToRemote sends nothing
```

*One shift-click as one packet up and nothing down. Read the diagonal:
the client runs the click, the server re-runs the identical call on the real
chest, and the comparison happens only after both — the note at the foot is
where a reply would have been.*

**The press.** `AbstractContainerScreen.mouseClicked` resolves the hovered
slot, sees an empty `AbstractContainerMenu.getCarried` and a held shift, and
chooses `ContainerInput.QUICK_MOVE` on the press. Two near neighbours are
release-path instead, from `AbstractContainerScreen.mouseReleased`: a
shift-click with a *non-empty* cursor, and the shift-double-click sweep,
which issues a separate `ContainerInput.QUICK_MOVE` — and a separate packet
— for every matching slot in the same container.

**The snapshot, then the prediction.**
`MultiPlayerGameMode.handleContainerInput` copies every slot's stack before
touching anything, because that copy is what the packet's diff is built
against. Then it runs `AbstractContainerMenu.clicked` on the client's own
menu. The `ContainerInput.QUICK_MOVE` branch loops
`AbstractContainerMenu.quickMoveStack` while the clicked slot keeps yielding
the same item, and it runs only for button 0 or 1, a non-negative slot index
and a slot that `Slot.mayPickup` allows.

**The move, and its two passes.** `ChestMenu.quickMoveStack` sees the index
is inside the chest and calls `AbstractContainerMenu.moveItemStackTo` over
the player's range with the backwards flag, which scans from the last slot
down — **the hotbar first, from the right**. That method makes two passes.
The first runs only if the stack is stackable at all, and merges into every
existing compatible stack across the whole range
(`ItemStack.isSameItemSameComponents`, topped up to `Slot.getMaxStackSize`).
The second runs only if something is left, finds the first empty slot that
`Slot.mayPlace` accepts, places what that slot's cap allows and **breaks**.
One empty slot per call — which is why the caller loops. Note which pass
consults policy: **the merge pass never asks `Slot.mayPlace`**, so a slot
that would refuse the item on placement can still be topped up by a
shift-click when it already holds a matching stack. Note also what
`ChestMenu.quickMoveStack` never calls: `Slot.onTake`.
`InventoryMenu.quickMoveStack` does. Shift-clicking out of a chest and out
of the crafting result are structurally different operations.

**The packet.** The client diffs the post-click slots against its snapshot,
hashes each changed stack with `HashedStack.create`, and sends
`ServerboundContainerClickPacket` with the container id, the client's
*last-known* state id, the slot, the button, the `ContainerInput`, up to
**128** changed slots as hashes, and the cursor as one more.

**The real move.** The server takes the ladder below, then — with
`AbstractContainerMenu.suppressRemoteUpdates` held — runs the identical
`AbstractContainerMenu.clicked` path against the real `ChestBlockEntity` and
the real `Inventory`. `Slot.setChanged` reaches `BlockEntity.setChanged`,
which calls `Level.blockEntityChanged` to mark the chunk and
`Level.updateNeighbourForOutputSignal` to re-derive the comparator output
([block entities](../blocks/block-entities#two-hundred-ticks-nobody-watches), and
[diodes and observers](../blocks/diodes-and-observers#one-int-and-the-fan-out-that-exists-to-deliver-it)
for what a comparator does with it). The whole click is wrapped
in a try/catch that builds a crash report category naming the menu class,
the slot and the button.

**Installing the claim, then comparing.**
`AbstractContainerMenu.setRemoteSlotUnsafe` writes each hash the client sent
into the matching `RemoteSlot` through `RemoteSlot.receive`, and
`RemoteSlot.Synchronized` **discards any concrete stack it was holding** and
keeps the hash alone. An out-of-range
index is logged at debug and ignored rather than rejected. Then
`AbstractContainerMenu.setRemoteCarried`, then
`AbstractContainerMenu.resumeRemoteUpdates`, then
`AbstractContainerMenu.broadcastChanges`, which makes one pass over the
slots offering each to `AbstractContainerMenu.triggerSlotListeners` for
advancements and to `AbstractContainerMenu.synchronizeSlotToRemote` for the
wire, then the cursor, then the data slots. Where the hash agrees,
`RemoteSlot.Synchronized` **promotes it to a concrete copy of the server's
own stack** and nothing is sent; where it disagrees, that one slot gets a
`ClientboundContainerSetSlotPacket` of its own with a fresh state id.

The advancement channel sees one state per click, not one per slot touched:
`AbstractContainerMenu.triggerSlotListeners` runs only from
`AbstractContainerMenu.broadcastChanges` and
`AbstractContainerMenu.broadcastFullState` — and for a chest nothing calls
back into the menu mid-click to reach either, a bundle click aside. That is a
fact about the chest, not about menus: several menus call
`AbstractContainerMenu.slotsChanged` from `Container.setChanged` or
`Slot.setChanged` (the anvil's and the smithing table's `ItemCombinerMenu`
containers, the crafter's `CrafterSlot`, the grindstone's, the lectern's), a bundle click calls it on whatever menu is open,
and the base `AbstractContainerMenu.slotsChanged` is a bare
`AbstractContainerMenu.broadcastChanges`, which the menus that override it
reach only if they call up to it. The click's
own broadcast runs
*after* `AbstractContainerMenu.resumeRemoteUpdates`, so suppression is not
in force by then, and `ServerPlayer`'s `ContainerListener` filters to slots
that are not a `ResultSlot` and whose container is the player's own
`Inventory` before firing `CriteriaTriggers.INVENTORY_CHANGED` anyway.

## The ladder the server climbs before it believes you

`ServerGamePacketListenerImpl.handleContainerClick` is four tests and a
fork, and the interesting thing about it is how much of it ends in *nothing
sent* rather than a correction. The packet reaches the handler on the server
thread through `PacketUtils.ensureRunningOnSameThread`, and then:

| the test | what a failure does |
|---|---|
| does `AbstractContainerMenu.containerId` match the open menu | dropped in total silence — nothing logged, nothing sent |
| is the player a spectator, or dead or dying | `AbstractContainerMenu.sendAllDataToRemote`, a full resync, and the click never runs |
| `AbstractContainerMenu.stillValid` | logged at debug, nothing sent, and the menu is *not* closed here |
| `AbstractContainerMenu.isValidSlotIndex` | logged at debug, nothing sent, nothing corrected |

Past all four the fork begins, and its order is the load-bearing part.

```mermaid
flowchart TD
    ST["compare the packet's state id with the menu's, before anything is applied"]:::server
    ST --> AP["AbstractContainerMenu.suppressRemoteUpdates, the click, the hashes, resume"]:::server
    AP --> Q{"was that state id stale?"}
    Q -->|"stale"| FULL["AbstractContainerMenu.broadcastFullState: one set-content packet, fresh state id"]:::server
    Q -->|"current"| BC["AbstractContainerMenu.broadcastChanges: every slot against its RemoteSlot"]:::server
```

*The compare happens above the apply and the branch below it. Read
the two edges out of the diamond as the whole difference a stale state id
makes — not whether the click runs, only how its result is published.*

**The state id is compared before the click is applied and acted on after**,
so a click that quotes a stale id still runs, and still runs first. Its
result is simply published wholesale instead of diffed.

`AbstractContainerMenu.isValidSlotIndex` deserves suspicion. It accepts −1,
accepts `AbstractContainerMenu.SLOT_CLICKED_OUTSIDE`, and otherwise only
asks whether the index is below the slot count — so **every negative index
passes it**. Each branch of the click that needs a lower bound therefore
tests for one itself. The two that do not, `ContainerInput.SWAP` and the
painting phase of `ContainerInput.QUICK_CRAFT`, index the list directly, and
the click's own
try/catch turns the failure into a `ReportedException` rather than swallowing
it: what swallows it is `PacketProcessor`, which logs a game-listener error and
carries on. An out-of-range click is therefore neither corrected nor fatal, but it is loudly
logged, it leaves the menu's remote updates suppressed until a later click
succeeds (the resume the handler would have run next never runs), and it closes
nothing:
`ServerGamePacketListenerImpl.handleContainerClose` for its part validates
nothing at all, not even the container id, and goes straight to
`ServerPlayer.doCloseContainer`.

## Why hashes, and why only in one direction

`HashedStack.create` produces either `HashedStack.EMPTY` or a
`HashedStack.ActualItem` of item holder, count and a `HashedPatchMap` — one
CRC32C integer per *added* component plus the plain set of *removed* ones.
Each integer comes from running the component's own codec into
`HashOps.CRC32C_INSTANCE`, a `DynamicOps` whose output **is** the hash
([codecs, NBT and
JSON](../foundations/codecs-nbt-json#checksum-a-hash-instead-of-a-stack) owns
that mechanism). Nothing is serialised on the way and there is no intermediate
byte form. `HashedPatchMap.matches` then checks the removed set, the added
count, and each component's hash in turn.

Two things follow. The client is *asserting a belief*, not authoring state —
a hashed component cannot be turned back into its value, so a client that lies
here can only fail to match — and each claimed slot costs an integer per component
rather than a re-encoded `DataComponentPatch`. The asymmetry is exact: **only the client ever calls
`HashedStack.create`, and only the server ever calls `HashedStack.matches`.**
Hashing is not free-standing on the server either: `ServerPlayer`'s
`ContainerSynchronizer` carries a 256-entry component-hash cache shared by
every menu that player opens, and `ContainerSynchronizer.createSlot` hands
each new `RemoteSlot.Synchronized` a `HashedPatchMap.HashGenerator` backed
by it.

### The other channel: `DataSlot`, and why it is never silent

One channel is deliberately outside all of this. `DataSlot` and
`ContainerData` — furnace progress, enchanting cost, lectern page — travel
as `ClientboundContainerSetDataPacket`, whose id and value are written as
**shorts**, and they carry two independent baselines:
`DataSlot.checkAndClearUpdateFlag` for the listeners and
`AbstractContainerMenu.remoteDataSlots` for the network, compared
separately. A slot is a `DataSlot.shared` view onto an array the menu already keeps, a
`DataSlot.standalone` int the menu owns, or one entry of a `ContainerData`
through `DataSlot.forContainer`, and each one that differs costs its own packet. The network comparison is a plain integer test, so a furnace's
progress bar is not covered by the hash-agreement silence and is re-sent
every time it changes.

## The state id, and the three places it moves

`AbstractContainerMenu.incrementStateId` is a wrapping 15-bit counter — it
masks to 32767 — and it answers exactly one question: *has the client
applied every slot correction I have sent?* It has three call sites in the
whole game. Two are inside `ServerPlayer`'s synchronizer, behind
`ContainerSynchronizer.sendInitialData` and
`ContainerSynchronizer.sendSlotChange`;
`ContainerSynchronizer.sendCarriedChange` and
`ContainerSynchronizer.sendDataChange` do not bump it — and
`ClientboundSetCursorItemPacket` carries no id whatever, while
`ClientboundContainerSetDataPacket` carries the container's but never a state
id. The third is `CraftingMenu.slotChangedCraftingGrid`, which bumps the id
on its way past while writing the crafting result's packet by hand — the one write on the click path that goes around the synchronizer, and the
exception the cast row promised. The client never generates one: `AbstractContainerMenu.setItem` and
`AbstractContainerMenu.initializeContents` simply store whatever arrived and
quote it back on the next click. A click quoting a stale id means
corrections are still in flight, and the server stops diffing and resends
everything.

## Where in the tick a broadcast happens

Not one place, and the difference is observable. Packets are drained
**before any level ticks** ([the server tick](../server/server-tick#every-packet-since-last-time-in-one-drain)), so
a click and any correction it produces both happen at the top of the tick
and reach the client the same tick. `ServerPlayer.tick` then calls
`AbstractContainerMenu.broadcastChanges` again from the level's **entity**
phase, which runs *before* the block-entity phase
([the level tick](../server/server-level-tick#the-whole-tick-and-its-three-gates)). And `ServerPlayer.doTick`
— driven by the connection after every level has finished — repeats the
`AbstractContainerMenu.stillValid` distance test and broadcasts nothing of its
own; what does broadcast inside it is a pickup, because `ServerPlayer.take`,
which the player's own tick reaches for every item, orb or arrow it collects,
calls `AbstractContainerMenu.broadcastChanges`.

The drain answers more than slot clicks. A menu **button** click — the
lectern's page turn, the enchanting table's offer, the loom's pattern, the
stonecutter's choice — is answered inside its own handler the same way:
`ServerGamePacketListenerImpl.handleContainerButtonClick` calls
`AbstractContainerMenu.broadcastChanges` directly, the moment
`AbstractContainerMenu.clickMenuButton` accepts, so the correction leaves on the
same packet drain that brought the click; pick-block and the creative slot
broadcast inside their handlers too.

Back in the tick, the distance test happens twice and only the first brings a broadcast
of its own. A hopper that pushes an item into a chest whose menu is open
therefore runs in the block-entity phase, after that tick's regular broadcast, and **nothing calls back into the menu to say so** —
`SimpleContainer.setChanged` is empty, `BlockEntity.setChanged` marks the chunk
and re-derives the comparator output and stops there,
`Inventory.setChanged` only bumps a counter. You see the hopper's item one tick
late, unless a pickup in the player's own tick broadcasts first.

## What the client does with a correction, and what closing rescues

Almost nothing, is the answer to the first. `ClientPacketListener.handleContainerSetSlot`
checks the container id, writes the stack into that one slot and stores the
state id that came with it. There is no rollback and nothing to roll back:
the client's menu is the only copy of its own prediction, so a correction
overwrites it and the stack snaps. Nor, after a click the server ran to its end, is a mispredicted slot ever left standing — the server compares **every** slot against its record on every
broadcast, not only the ones the click claimed, so a wrong guess is corrected
on that tick or on the next one the menu is broadcast in.

Closing has its own surprise. The cursor belongs to the menu, not the
player, so closing one would destroy it: `AbstractContainerMenu.removed`
rescues it explicitly, dropping it in the world if the player has been removed for any reason but a
change of dimension, or has disconnected, and calling `Inventory.placeItemBackInInventory`
otherwise, the whole method gated on being a `ServerPlayer`, which is what
makes that safe. The rescue sends one `ClientboundSetPlayerInventoryPacket`
per slot it fills, and runs *before* `AbstractContainerMenu.transferState`,
which copies both the listener baseline and the remote beliefs across every
container-and-slot pair the closing menu and `InventoryMenu` share. For a
chest that is the 36 main and hotbar slots — not armour, not the offhand, not the 2×2 grid, not the crafting result — so changes to any of those are re-sent, and so is every slot the rescue has just filled, which it wrote before
the beliefs were copied.

## The seven click kinds

`ContainerInput` has seven values, and the button number means something different in nearly every one. `ClickAction` — `ClickAction.PRIMARY` and
`ClickAction.SECONDARY` — is *not* on the wire; it is derived inside
`AbstractContainerMenu.doClick` on both sides and handed to the item
override hooks `ItemStack.overrideStackedOnOther` and
`ItemStack.overrideOtherStackedOnMe`, which is how a `BundleItem`
intercepts a click before ordinary slot logic runs.

| value | the gesture | what the button number means | packets per gesture |
|---|---|---|---|
| `ContainerInput.PICKUP` | the ordinary click | 0 left, 1 right — and on `AbstractContainerMenu.SLOT_CLICKED_OUTSIDE`, drop all or drop one | 1 |
| `ContainerInput.QUICK_MOVE` | shift-click | 0 or 1, the same in a real slot — and outside the window, the drop-all and drop-one of `ContainerInput.PICKUP` | 1, or one per matching slot for the shift-double-click sweep |
| `ContainerInput.SWAP` | a hotbar key, or `Inventory.SLOT_OFFHAND` for the offhand key | the destination index: 0–8, or 40 | 1 |
| `ContainerInput.CLONE` | creative middle-click | ignored, but the player must pass `Player.hasInfiniteMaterials` | 1 |
| `ContainerInput.THROW` | Q over a slot, cursor empty — and also a click *outside* the window with an empty cursor, which the server's branch then ignores because it demands a non-negative index | 0 drops one, 1 (control-Q) drops the stack and keeps going while the slot yields the same item | 1 |
| `ContainerInput.QUICK_CRAFT` | drag-painting | a packed mask that `AbstractContainerMenu.getQuickcraftHeader` and `AbstractContainerMenu.getQuickcraftType` split into a phase (start, continue, end) and a mode (even split, one each, clone) | **painted slots plus two** — `AbstractContainerScreen.quickCraftToSlots` sends a start, one continue per slot, and an end |
| `ContainerInput.PICKUP_ALL` | double-click to collect | 0 scans forwards, 1 backwards, over two passes that skip already-full stacks first | 1 |

An unknown id on the wire is not rejected. `ContainerInput`'s id mapper is
built with a zero-out-of-bounds strategy, so a malformed click **decodes as
`ContainerInput.PICKUP`**.

## Two paths that are not this protocol at all

### Creative mode is a parallel protocol, not a variation

`CreativeModeInventoryScreen` overrides
`AbstractContainerScreen.slotClicked` and drives a menu of its own, and its
writes go up as `ServerboundSetCreativeModeSlotPacket`. That is the only
serverbound packet in the game that carries an `ItemStack` at all, and the
server takes it verbatim: `ServerGamePacketListenerImpl.handleSetCreativeModeSlot`
puts the client's stack into the slot through `Slot.setByPlayer`, behind only
a `Player.hasInfiniteMaterials` check, a feature-flag check, a slot range of
1 to 45 and a count cap — then writes the same stack into the server's record
of the client's belief with `AbstractContainerMenu.setRemoteSlot`, to suppress
the echo. (Other packets let a client author *text* the server keeps — a
rename, a sign, a jigsaw block — but no other lets it author a stack.) A negative
slot number means *drop it in the world instead*, and that branch is the one
place here with a rate limiter on it.

### The crafting result is a second, unsuppressed channel

The result slot's
packet is written by hand and goes straight down the connection, bumping the
state id on its way past and bypassing `ContainerSynchronizer` entirely
([recipes](recipes#eight-planks-the-trace)). What matters here is that it
fires *during* a click and is therefore the one path
`AbstractContainerMenu.suppressRemoteUpdates` does not cover: that flag guards
`AbstractContainerMenu.synchronizeSlotToRemote` and its two siblings, and
nothing else.

Everything else a menu puts on the wire is bookkeeping around those two —
opening and closing, the full resync, the button clicks, the crafter toggle,
the bundle selection, the hotbar key — and the whole set is one table in
[the packet catalogue](../../reference/packets). Only one of them surprises: `ServerboundSetCarriedItemPacket` is the *hotbar
selection*, which despite the name has nothing to do with the cursor; the
packet the server sends for the same selection is `ClientboundSetHeldSlotPacket`.

Two things about a menu's opening are worth keeping here. A structure chest
fills itself on the first open, and that is the roll rather than the sync
([loot tables](loot-tables#the-chest-was-empty-before-you-got-there)). And
none of the click protocol is data-driven at all: `BuiltInRegistries.MENU`
supplies the type and nothing else, which is why nothing on this page has a
JSON file behind it.

## Where to look

Follow the click in the order the two machines run it.
`MultiPlayerGameMode.handleContainerInput` is the snapshot and the
prediction; `AbstractContainerMenu.doClick` is the branch table both sides
share, with `AbstractContainerMenu.moveItemStackTo` under the shift-click.
`ServerGamePacketListenerImpl.handleContainerClick` is the ladder, and
`AbstractContainerMenu.broadcastChanges` is where the silence is decided.

For the belief mechanism, read `RemoteSlot` and then `HashedStack`: one is
the server's record, the other is what a click is allowed to put in it.
`HashOps` under them is the trick that makes a hash out of a codec with no
bytes in between.

For the object model, `Container` and `Slot` are the two halves nobody
expects to be separate, and `ChestMenu` beside `InventoryMenu` is the
shortest way to see what a menu subclass adds. `MenuType` and
`MenuProvider` are the two ends of opening one, and `MenuScreens` is where
the client's copy is built from a constructor the server never runs.

Two doors lead out of this protocol rather than through it:
`CraftingMenu.slotChangedCraftingGrid` writes the result slot by hand, and
`ServerGamePacketListenerImpl.handleSetCreativeModeSlot` is the one place a
client's stack is taken at its word.

---

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