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

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

classwhat it decidesthread
Containerstorage, and nothing about who is looking at it — Container.getItem, Container.setItem, Container.stillValidwherever its owner runs
AbstractContainerMenuthe 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 wireServer and Render
SlotGUI policy — Slot.mayPlace, Slot.mayPickup, Slot.getMaxStackSize — and the guarded mutations a click goes throughServer and Render
Inventorythe player’s own storage, present as slots in nearly every menu that opens — the lectern’s one book slot is the exceptionServer and Render
MenuType / MenuProviderthe registry entry with the screen-side constructor, and the server-side factory a block hands to ServerPlayer.openMenuRender / Server
ContainerSynchronizerthe 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 opensServer
RemoteSlotwhat the server believes the client is holding in one slot, as either a stack or a hashServer (RemoteSlot.PLACEHOLDER on the Render thread)
HashedStackthe client’s claim about one slot after its own clickcreated 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), 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 RemoteSlots 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 Slots 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.

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, and diodes and observers 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 testwhat a failure does
does AbstractContainerMenu.containerId match the open menudropped in total silence — nothing logged, nothing sent
is the player a spectator, or dead or dyingAbstractContainerMenu.sendAllDataToRemote, a full resync, and the click never runs
AbstractContainerMenu.stillValidlogged at debug, nothing sent, and the menu is not closed here
AbstractContainerMenu.isValidSlotIndexlogged at debug, nothing sent, nothing corrected

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

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 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), 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). 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.

valuethe gesturewhat the button number meanspackets per gesture
ContainerInput.PICKUPthe ordinary click0 left, 1 right — and on AbstractContainerMenu.SLOT_CLICKED_OUTSIDE, drop all or drop one1
ContainerInput.QUICK_MOVEshift-click0 or 1, the same in a real slot — and outside the window, the drop-all and drop-one of ContainerInput.PICKUP1, or one per matching slot for the shift-double-click sweep
ContainerInput.SWAPa hotbar key, or Inventory.SLOT_OFFHAND for the offhand keythe destination index: 0–8, or 401
ContainerInput.CLONEcreative middle-clickignored, but the player must pass Player.hasInfiniteMaterials1
ContainerInput.THROWQ 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 index0 drops one, 1 (control-Q) drops the stack and keeps going while the slot yields the same item1
ContainerInput.QUICK_CRAFTdrag-paintinga 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_ALLdouble-click to collect0 scans forwards, 1 backwards, over two passes that skip already-full stacks first1

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). 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. 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). 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.