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

Block interaction

Verified against Minecraft 26.2 · Part V · A player right-clicks the bottom half of an oak door, and the top half opens without a single neighbour update.

You are standing in front of a closed oak door, crosshair on its bottom half, and you press the use key. Before the tick is over the door is open on your screen, both halves of it, and a packet is on its way to a server that has not yet been asked. The obvious guess about how the top half found out is wrong. Opening a door fires no neighbour updates at allDoorBlock writes with flags 10, and the neighbour bit is not among them — and the top half follows anyway, down the shape channel, which is the half of the update machinery the client also runs. That is why a door feels instant on a laggy server and a redstone lamp does not.

The contract both halves run under. The client acts at once and remembers the state it overwrote, under a sequence number it sends with the action. The server’s ClientboundBlockChangedAckPacket is a receipt for that number and not a verdict — it is sent for actions the server refused exactly as for actions it allowed — and correctness comes from ordering instead: any correction the server means to send travels in the same tick and earlier in the stream than the receipt. A correction replaces what the client remembered rather than being weighed against it, so when the receipt arrives the client writes back whatever the entry now holds — and only where that differs from what is on screen. Prediction and acknowledgement owns that machinery; this page and block breaking are its two applications.

The cast

classwhat it decidesthread
Minecraftthat a use-key press becomes one Minecraft.startUseItem, and that the main hand is tried before the off handRender
MultiPlayerGameModethe client’s copy of the whole decision, wrapped in one predictionRender
InteractionResultwhether the caller stops here, who swings, and what the hand ends up holdinga record, no thread
BlockBehaviour.BlockStateBasewhich of the two block hooks a state answers with, and when its six neighbours are asked to re-fiteither side
DoorBlockwhether this door opens by hand, what it writes, and what the other half becomeseither side
CollectingNeighborUpdaterthe order queued updates run in, and where a runaway cascade is cutwhichever thread wrote the block
ServerGamePacketListenerImplthe gate list the packet must pass, and what each refusal answers withServer
ServerPlayerGameModethe same inner order as the client, plus the advancement triggersServer

The whole click, both sides

sequenceDiagram
    participant MC as Minecraft
    participant MPGM as MultiPlayerGameMode
    participant CL as ClientLevel
    participant DB as DoorBlock
    participant CNU as CollectingNeighborUpdater
    participant SGPL as ServerGamePacketListenerImpl
    participant SL as ServerLevel

    Note over MC,CNU: one client tick, all of it before the packet leaves
    MC->>MPGM: startUseItem, main hand first, useItemOn with the BlockHitResult
    MPGM->>MPGM: startPrediction opens sequence n
    MPGM->>DB: useItemOn returns TRY_WITH_EMPTY_HAND, so useWithoutItem runs
    DB->>CL: canOpenByHand, cycle OPEN, setBlock on the lower half with flags 10
    CL->>CNU: updateNeighbourShapes, six directions, limit 511
    CNU->>DB: updateShape on the upper half, direction DOWN
    CNU->>CL: updateOrDestroy writes the upper half, flags 10, limit 511
    DB->>CL: playSound with the clicker as except, so only they hear it
    MPGM->>SGPL: ServerboundUseItemOnPacket carrying hand, hit and n
    MC->>SGPL: ServerboundSwingPacket, because SUCCESS swings on the client
    Note over SGPL,SL: server tick, packets drained before the levels tick
    SGPL->>SGPL: ackBlockChangesUpTo n, then reach, hit box, height, spawn protection
    SGPL->>DB: ServerPlayerGameMode.useItemOn, the same inner order
    DB->>SL: setBlock lower half, then the shape pass writes the upper half
    DB->>SL: playSound to everyone but the clicker, gameEvent BLOCK OPEN
    SGPL-->>CL: ClientboundBlockUpdatePacket, clicked position and face neighbour
    Note over SGPL,SL: still this tick, levels tick, then connections tick
    SL-->>CL: ClientboundSectionBlocksUpdatePacket, both halves in one section
    SGPL-->>CL: ClientboundBlockChangedAckPacket for n
    CL->>CL: endPredictionsUpTo n, both halves already agree, nothing is written

One press, one hand at a time

Minecraft.handleKeybinds runs from the client tick and only when no screen and no overlay is open. Every queued press of the use key becomes its own Minecraft.startUseItem, unthrottled; Minecraft.rightClickDelay gates the held-down auto-repeat alone, and Minecraft.startUseItem is what sets it, to four ticks, and only when MultiPlayerGameMode.isDestroying is false. A player already using an item (drawing a bow, eating) never reaches that branch at all: the queued presses are drained and discarded.

Inside, after LocalPlayer.isHandsBusy, the hands are tried in the order InteractionHand.MAIN_HAND then InteractionHand.OFF_HAND. Each hand’s stack must pass ItemStack.isItemEnabled — a disabled item aborts the whole loop, not just its own hand — and then, for a BlockHitResult, the hand goes to MultiPlayerGameMode.useItemOn. A InteractionResult.Success or an InteractionResult.Fail ends the loop; only an InteractionResult.Pass falls through to MultiPlayerGameMode.useItem (right-click air, ServerboundUseItemPacket) and then to the off hand. The door returns success on the main hand, so the off hand is never asked.

MultiPlayerGameMode.useItemOn sends a ServerboundSetCarriedItemPacket first if the hotbar selection has moved (MultiPlayerGameMode.ensureHasSentCarriedItem), refuses outright if the target is outside the world border, and otherwise opens the prediction with MultiPlayerGameMode.startPrediction, which allocates sequence n, runs the whole client-side interaction inside it, and sends the ServerboundUseItemOnPacket the interaction returned.

Block, then empty hand, then item

MultiPlayerGameMode.performUseItemOn and ServerPlayerGameMode.useItemOn run the same three-step order. First the block is offered the item: BlockBehaviour.BlockStateBase.useItemOn, whose BlockBehaviour.useItemOn default answers InteractionResult.TRY_WITH_EMPTY_HAND. That sentinel — and only when the hand is InteractionHand.MAIN_HAND — routes to BlockBehaviour.BlockStateBase.useWithoutItem, whose BlockBehaviour.useWithoutItem default is InteractionResult.PASS. If the block consumed nothing, the item gets its turn through ItemStack.useOn, provided the stack is non-empty and not held back by ItemCooldowns.isOnCooldown. Sneaking skips the first two steps, but only when some hand holds something: the guard is Player.isSecondaryUseActive and a non-empty main or off hand, so an empty-handed sneak still opens the door.

Three things differ between the two copies, and none of them is the inner order. The server tests the block’s BlockBehaviour.requiredFeatures through FeatureElement.isEnabled as its very first statement, while the client tests the same thing through ClientPacketListener.isFeatureEnabled inside the not-sneaking branch. A spectator gets a flat InteractionResult.CONSUME on the client, but on the server is routed to BlockBehaviour.BlockStateBase.getMenuProvider and may end up with an open container. And the advancement triggers exist only on the server: CriteriaTriggers.ITEM_USED_ON_BLOCK when the item did it, CriteriaTriggers.DEFAULT_BLOCK_USE when the empty-hand hook did, and CriteriaTriggers.ANY_BLOCK_USE from the packet handler for anything that consumed.

The result is the vocabulary the whole pipeline turns on. InteractionResult is a sealed interface of four records — InteractionResult.Success, InteractionResult.Fail, InteractionResult.Pass and InteractionResult.TryEmptyHandInteraction — and the swing is part of it, not a separate decision. InteractionResult.SUCCESS, InteractionResult.SUCCESS_SERVER and InteractionResult.CONSUME are all InteractionResult.Success values differing only in InteractionResult.SwingSource: the client animates and sends ServerboundSwingPacket, the server animates for the trackers, or nobody does. InteractionResult.consumesAction is what the server’s branches test — the client’s own loop matches on the record types instead — but the record carries two more answers besides — InteractionResult.Success.wasItemInteraction, which decides whether Stats.ITEM_USED is awarded, and InteractionResult.Success.heldItemTransformedTo, which both game modes use to swap the stack the hand ends up holding.

The door writes ten

DoorBlock.useWithoutItem asks BlockSetType.canOpenByHand and, if the answer is no, returns InteractionResult.PASS and lets the item try. For oak it is yes: StateHolder.cycle flips DoorBlock.OPEN, and Level.setBlock is called with flags 10Block.UPDATE_CLIENTS and Block.UPDATE_IMMEDIATE, with Block.UPDATE_NEIGHBORS clear. Then DoorBlock.playSound, LevelAccessor.gameEvent with GameEvent.BLOCK_OPEN or GameEvent.BLOCK_CLOSE (posting is game events and vibrations), and InteractionResult.SUCCESS. Exactly this code runs on both sides.

Ten is the whole story of the page. Bit 2 broadcasts, and on the client LevelExtractor.blockChanged reads bit 8 not as immediate but as a player did this, which can buy the section a priority remesh — LevelRenderer acts on that mark only when the Chunk Builder option is set to prioritise nearby or player-affected sections, which the fancy graphics preset does and the default does not. Bit 1 is absent, so Level.setBlock never reaches its neighbour fan-out — and on the client that would be a no-op anyway. What the flags then feed, and the rest of what a write does, is the flowchart on blocks and states; everything below is the part of it the door actually walks.

The shape channel, which both sides run

With Block.UPDATE_KNOWN_SHAPE clear and the update limit still positive, the tail of Level.setBlock calls BlockBehaviour.BlockStateBase.updateNeighbourShapes, which walks all six of BlockBehaviour.UPDATE_SHAPE_ORDER — west, east, north, south, down, up — and asks each neighbour, one top-level cascade at a time, whether it still fits. Note which direction travels: for the block above, the level is handed Direction.DOWN, the direction pointing from that neighbour back at the door. Each hop costs one from the limit, so the upper half is written at 511 and asks its own neighbours at 510.

The whole distinction rests on three method bodies. Level.updateNeighborsAt and Level.neighborChanged are empty on Level and overridden only by ServerLevel; Level.neighborShapeChanged is implemented on Level itself and therefore runs on both sides. Shape updates are predictable because the client genuinely runs them; neighbour updates are not because the client’s copy does nothing.

DoorBlock.updateShape answers four of its six callers with the state it was given: the whole method is behind a test for the vertical axis, so the four horizontals fall through to BlockBehaviour.updateShape, which returns the state unchanged. The other two directions are where the door lives, and there are three outcomes between them. Asked from the matching vertical direction — up for a lower half, down for an upper — it returns the neighbour’s own state with DoorBlock.HALF swapped to its own, so open, facing, hinge and powered are copied wholesale, which is why the top half is already open by the time it is written. Asked from that same direction when the neighbour is not the other half, it returns Blocks.AIR. And a lower half asked from Direction.DOWN returns air when DoorBlock.canSurvive fails — the block beneath must be face-sturdy upward. Block.updateOrDestroy then compares: a different non-air state becomes a Level.setBlock at the inherited limit, and air becomes a Level.destroyBlock, but only when the level is not the client’s. That server-gated destroy branch, which writes with flags 3 and posts GameEvent.BLOCK_DESTROY, is the whole of “break the bottom and the top pops”.

The updater underneath: a stack, drained depth-first

Every Level builds one Level.neighborUpdater, a CollectingNeighborUpdater, in its constructor — on the client too. Requests arrive as four small implementations of CollectingNeighborUpdater.NeighborUpdates, three of them records: CollectingNeighborUpdater.ShapeUpdate for the door’s case, CollectingNeighborUpdater.SimpleNeighborUpdate and CollectingNeighborUpdater.FullNeighborUpdate for a single neighbour, and CollectingNeighborUpdater.MultiNeighborUpdate, one request that walks up to six directions in NeighborUpdater.UPDATE_ORDER — a different order from the shape one, west, east, down, up, north, south.

CollectingNeighborUpdater.addAndRun decides where a request goes by whether a cascade is already running. The first one is pushed on CollectingNeighborUpdater.stack and drained immediately by CollectingNeighborUpdater.runUpdates; anything requested from inside a running hook lands in CollectingNeighborUpdater.addedThisLayer and is pushed on top of the stack before the current record’s remaining work, so the drain is depth-first — a cascade finishes its children before its siblings. NeighborUpdater.executeShapeUpdate and NeighborUpdater.executeUpdate do the actual calls and wrap any throw in a crash report.

The chain limit is coarser than it looks and gentler than folklore says. CollectingNeighborUpdater.count increments once per request, not per depth level and not per block touched — a CollectingNeighborUpdater.MultiNeighborUpdate is one request that can expand to six calls — and past CollectingNeighborUpdater.maxChainedNeighborUpdates further requests are silently dropped after a single logged error, never a crash. The count is reset when the outermost cascade unwinds, so the budget is per top-level cascade. It is not a game rule: it is the max-chained-neighbor-updates line in server.properties (DedicatedServerProperties.maxChainedNeighborUpdates, default one million, read through DedicatedServer.getMaxChainedNeighborUpdates), with MinecraftServer.getMaxChainedNeighborUpdates hard-coding the same number for the integrated server and ClientLevel passing the literal. Keep it distinct from Block.UPDATE_LIMIT, the 512 that bounds nested writes: that one counts recursion depth and is what the door’s 511 and 510 come from.

The gate list, and what each refusal answers with

ServerGamePacketListenerImpl.handleUseItemOn tests in this order, and the interesting column is the second one — the refusals do not answer alike, and one of them lies.

the gatewhat the client gets when it fails
ServerGamePacketListenerImpl.hasClientLoadednothing, not even the receipt
ItemStack.isItemEnabled on the held stacknothing
Player.isWithinBlockInteractionRange, with 1.0 of slacknothing
the hit location lying within one block of the clicked block’s centre on every axis — a 2×2×2 box, not the blocknothing, plus a server-side log line naming the player
above LevelHeightAccessor.getMaxY or below LevelHeightAccessor.getMinYServerPlayer.sendBuildLimitMessage — an action-bar line, and no block update
MinecraftServer.isUnderSpawnProtectionServerPlayer.sendSpawnProtectionMessage, plus both block updates
a pending teleport, or ServerLevel.mayInteract refusing for the world borderServerPlayer.sendBuildLimitMessage — you are told you are building too high, whatever the real reason
everything passedServerPlayerGameMode.useItemOn runs, plus both block updates

The two block updates are a ClientboundBlockUpdatePacket for the clicked position and one for the block on its clicked face, sent whatever the interaction returned — and they sit inside the branch below the build-height test, which is why a click that was out of reach or off the block is answered with silence while a click into spawn protection is answered with the truth. The door’s other half is in neither: it reaches the client with everyone else’s copy, through ChunkHolder.broadcastChanges, which turns two changed positions in one section into a single ClientboundSectionBlocksUpdatePacket (and into two ClientboundBlockUpdatePackets when the halves straddle a section boundary). ServerGamePacketListenerImpl.ackBlockChangesUpTo was called before any of the gates, and ServerGamePacketListenerImpl.tick emits the receipt when MinecraftServer.tickChildren reaches connections — after the levels have already broadcast (the server tick).

Questions players ask

Why can’t I open an iron door by hand? Because DoorBlock.useWithoutItem’s first question is BlockSetType.canOpenByHand, false on BlockSetType.IRON and BlockSetType.GOLD and true on BlockSetType.COPPER. Nothing on this path reads BlockTags.WOODEN_DOORS — the copper door proves it, since it opens by hand and is not in that tag. The tag is for mining and fuel. Mobs that open doors ask somewhere else again: InteractWithDoor reads BlockTags.MOB_INTERACTABLE_DOORS, while the older goals read DoorBlock.isWoodenDoor, which is BlockSetType.canOpenByHand under another name.

Why does the door sound different to me than to everyone else? DoorBlock.playSound passes the clicking player as the except entity, and the two sides read that word oppositely: ClientLevel.playSeededSound plays the sound only when the except entity is the local player, while ServerLevel.playSeededSound broadcasts a ClientboundSoundPacket to everyone in range but them. So you hear your own prediction and never the server’s copy — and since each side draws its own pitch from its own Level.getRandom and its own seed from Level.soundSeedGenerator, your door is genuinely a different sound from the one your friend heard.

Why does breaking the bottom of a door remove the top on the server but not on my screen? Because that removal is a shape update whose destroy half is server-only. Both sides run DoorBlock.updateShape on the upper half, both get Blocks.AIR back, and both hand it to Block.updateOrDestroy — where the air branch is wrapped in a not-client check. Your client leaves the top half standing until the section packet arrives; the server has already dropped it, with the flags-3 neighbour updates that Level.destroyBlock implies.

Why does opening a door lag a busy server when nothing is powered? Because the write still reaches ServerLevel.sendBlockUpdated, which compares the old and new collision shapes and, when they differ, walks ServerLevel.navigatingMobs — every tracked mob in the level, in full — asking each whether the position is near enough to its remaining path to be worth PathNavigation.recomputePath. A door changes shape every time it moves. None of that exists on the client, which is one more reason your half of the click is the fast half.

Left-click is the same contract with a different pipeline: Minecraft.startAttack opens its own prediction and sends a ServerboundPlayerActionPacket instead, and the block hook is BlockBehaviour.BlockStateBase.attack. Block breaking takes it from there.

Where to look

Minecraft.handleKeybinds · Minecraft.startUseItem · MultiPlayerGameMode.useItemOn · MultiPlayerGameMode.performUseItemOn · InteractionResult · BlockBehaviour.BlockStateBase.useItemOn · BlockBehaviour.BlockStateBase.useWithoutItem · DoorBlock.useWithoutItem · Level.setBlock · BlockBehaviour.BlockStateBase.updateNeighbourShapes · Level.neighborShapeChanged · CollectingNeighborUpdater.addAndRun · CollectingNeighborUpdater.runUpdates · NeighborUpdater.executeShapeUpdate · DoorBlock.updateShape · Block.updateOrDestroy · ServerGamePacketListenerImpl.handleUseItemOn · ServerPlayerGameMode.useItemOn · ChunkHolder.broadcastChanges


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