<!-- https://minecraftdocs.dev/systems/blocks/block-interaction — 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 -->

# Block interaction

> Verified against **Minecraft 26.3** · 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 in your
client's world, both halves of it, on your screen a frame or so later, 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 all** — `DoorBlock`
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 — once the server considers the client loaded, 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](../client/prediction-and-acks#two-state-machines-running-against-each-other) owns that machinery; this page and [block breaking](block-breaking) are its two applications.

## The cast

| class | what it decides | thread |
|---|---|---|
| `Minecraft` | that a use-key press becomes one `Minecraft.startUseItem`, and that the main hand is tried before the off hand | Render |
| `MultiPlayerGameMode` | the client's copy of the whole decision, wrapped in one prediction | Render |
| `InteractionResult` | whether the caller stops here, who swings, and what the hand ends up holding | a sealed interface of records, no thread |
| `BlockBehaviour.BlockStateBase` | which of the two block hooks a state answers with, and when its six neighbours are asked to re-fit | either side |
| `DoorBlock` | whether this door opens by hand, what it writes, and what the other half becomes | either side |
| `CollectingNeighborUpdater` | the order queued updates run in, and where a runaway cascade is cut | whichever thread wrote the block |
| `ServerGamePacketListenerImpl` | the gate list the packet must pass, and what each refusal answers with | Server |
| `ServerPlayerGameMode` | the same inner order as the client, plus the advancement triggers | Server |

## The whole click, both sides

```mermaid
sequenceDiagram
    box Client
        participant MC as Minecraft
        participant MPGM as MultiPlayer<br/>GameMode
        participant CL as ClientLevel
        participant DB as DoorBlock
        participant CNU as Collecting<br/>NeighborUpdater
    end

    Note over MC,CNU: one client tick, all of it before the packet leaves
    MC->>MPGM: useItemOn, main hand first, with the BlockHitResult
    MPGM->>MPGM: startPrediction opens sequence n
    MPGM->>DB: useWithoutItem, after useItemOn returned TRY_WITH_EMPTY_HAND
    DB->>CL: setBlock, the lower half with OPEN cycled, flags 10
    CL->>CNU: shapeUpdate, six directions, limit 511
    CNU->>DB: updateShape on the upper half, direction DOWN
    CNU->>CL: setBlock, the upper half through Block.updateOrDestroy
    DB->>CL: playSound, the clicker as except
```

*The client's share of one click up to the moment the use packet is sent. The
two writes are the page's subject: the lower half is written by the door, the
upper half by the shape pass that the first write set off, and no neighbour
update happens at all.*

Only now does the use packet leave the machine. It carries the hand, the hit
and the sequence number, and the server runs the identical inner order against
its own world.

```mermaid
sequenceDiagram
    box Client
        participant MPGM as MultiPlayer<br/>GameMode
        participant CL as ClientLevel
    end
    box Server
        participant SGPL as ServerGamePacket<br/>ListenerImpl
        participant SPGM as ServerPlayer<br/>GameMode
        participant DB as DoorBlock
        participant SL as ServerLevel
    end

    MPGM->>SGPL: ServerboundUseItemOnPacket, hand, hit and n
    Note over MPGM,SGPL: no swing packet goes up: SUCCESS is predicted, so the client has already swung, and the server swings for the trackers itself
    Note over MPGM,SL: server tick, packets drained before the levels tick
    SGPL->>SGPL: ackBlockChangesUpTo n, then reach, hit box, height, spawn protection
    SGPL->>SPGM: useItemOn, the same three-step order
    SPGM->>DB: useWithoutItem
    DB->>SL: setBlock lower half, then the shape pass writes the upper half
    DB->>SL: playSound, everyone but the clicker, and gameEvent BLOCK OPEN
    SGPL-->>CL: two ClientboundBlockUpdatePackets, the clicked position and the face neighbour
    Note over MPGM,SL: still this tick, levels tick, then connections tick
    SL-->>CL: ClientboundSectionBlocks<br/>UpdatePacket, both halves in one section
    SGPL-->>CL: ClientboundBlockChangedAck<br/>Packet for n
    CL->>CL: handleBlock<br/>ChangedAck<br/>ends prediction n
```

*The server's copy of the same click, and the three things that come back. The
receipt arrives last, and by the time it does the client has already been
corrected twice — which is why closing the prediction,
`ClientLevel.handleBlockChangedAck`, writes nothing.*

## 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. The whole of that method sits behind one test —
`MultiPlayerGameMode.isDestroying` — so a use press arriving mid-dig does not
merely skip the delay, it is discarded entirely. A player already using an item
(drawing a bow, eating) gets no further either: `Minecraft.handleKeybinds`
drains the queued presses and throws them away before `Minecraft.startUseItem`
is reached. One level in, `LocalPlayer.isHandsBusy` stops a player who is
paddling a boat.

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. The ledger that
prediction writes into belongs to the level rather than to the game mode:
`ClientLevel.setBlock` hands every state it overwrites while predicting to a
`BlockStatePredictionHandler`, so **both** halves of the door are recorded, and
both are restored together if the receipt arrives with the entries unchanged.

## 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 — a feature flag
narrowing the registry the click may reach ([identifiers and
registries](../foundations/identifiers-and-registries#feature-flags-the-same-registry-narrowed)) — 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.

Those two hooks are how every right-clickable block in the game is written, and
the split between them is lopsided: **25** block classes override
`BlockBehaviour.useItemOn`, the ones that care what you are holding —
`ComposterBlock` taking something compostable, `LecternBlock` taking a book,
`ChiseledBookShelfBlock` reading the hit vector to pick a slot,
`JukeboxBlock`, `CakeBlock`, `RespawnAnchorBlock` — while **52** override
`BlockBehaviour.useWithoutItem`, the ones that answer an empty hand, of which
the door is one. Twelve override both, all six just named among them. A block
that does neither leaves the click to the item in the hand.
That is the whole family, and this door is an instance of it rather than a
special case.

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 at
once and the server animates for everyone else, the server animates for
everyone including the clicker, or nobody does. `InteractionResult.consumesAction`
is what both copies of the inner order test, and the server asks
`InteractionResult.Success.shouldSwing` before it swings; the client's hand loop in `Minecraft.startUseItem` never asks it, and matches on
the record types instead. `InteractionResult.Success` 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 **10** — `Block.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 [a nested loop inside the caller's own
method](../world/game-events-and-vibrations#the-broadcast-is-a-nested-loop-and-one-listener-is-the-exception)), and
`InteractionResult.SUCCESS`. Exactly this code runs on both sides.

Ten is the whole story of the page, so it is worth spelling out as bits: **2 +
8**, with **1** absent. Bit 2 is `Block.UPDATE_CLIENTS` and it broadcasts. Bit
8 is `Block.UPDATE_IMMEDIATE`, and on the client `LevelExtractor.blockChanged`
reads it 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 and fabulous graphics presets do, and fancy is the
preset a new client starts on. Bit 1
is `Block.UPDATE_NEIGHBORS`, and because it is absent `Level.setBlock` never
reaches its neighbour fan-out — which on the client would be a no-op anyway.
Nothing sets bit 16, `Block.UPDATE_KNOWN_SHAPE`, and that omission is what the
shape channel's section below is about. What the flags then feed, and the rest of what a write
does, is the pair of flowcharts on [blocks and
states](blocks-and-states#the-two-update-channels); everything below is
the part of it the door walks.

### The sound only you hear

`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;
`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. Every deferral
in this part that is *not* predicted does the opposite: a lever passes a null
*except* entity, so the clicker hears the server's packet like everybody else
([signal and dust](signal-and-dust#the-lever-two-dust-and-a-powered-piston)).

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

That is why the door is predictable and the redstone lamp is not: the shape
channel is the half of the update machinery a `ClientLevel` genuinely runs,
and the neighbour channel is the half whose methods are empty there ([blocks
and states](blocks-and-states#the-two-update-channels)).

`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
besides leaving the state as it is 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` — which the hub shows is the one
branch of a shape update the client does not run ([blocks and
states](blocks-and-states#the-two-update-channels)). That 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` — the neighbour channel's own
order, and not the shape one this door walks.

`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 reach the **third** gate measures is an attribute, and
players have two of them — one for blocks and a shorter one for entities
([player anatomy](../player/player-anatomy#what-player-owns)).

| the gate | what the client gets when it fails |
|---|---|
| `ServerGamePacketListenerImpl.hasClientLoaded` | nothing, not even the receipt |
| `ItemStack.isItemEnabled` on the held stack | nothing |
| `Player.isWithinBlockInteractionRange`, with 1.0 of slack | nothing |
| the hit location lying within one block of the clicked block's centre on every axis — a 2×2×2 box, not the block | nothing, plus a server-side log line naming the player |
| above `LevelHeightAccessor.getMaxY` or below `LevelHeightAccessor.getMinY` | `ServerPlayer.sendBuildLimitMessage` — an action-bar line, and **no block update** |
| `MinecraftServer.isUnderSpawnProtection` | `ServerPlayer.sendSpawnProtectionMessage`, plus both block updates |
| a pending teleport, or `ServerLevel.mayInteract` refusing for the world border | `ServerPlayer.sendBuildLimitMessage` — you are told you are building too high, whatever the real reason — plus both block updates |
| everything passed | `ServerPlayerGameMode.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
`ClientboundBlockUpdatePacket`s when the halves straddle a section boundary).
`ServerGamePacketListenerImpl.ackBlockChangesUpTo` was called after the first
gate and before the rest, and `ServerGamePacketListenerImpl.tick` emits the receipt when
`MinecraftServer.tickChildren` reaches connections — after the levels have
already broadcast ([the server tick](../server/server-tick#what-minecraftservertickchildren-runs-and-in-what-order)).

## The other half of the same lecture

Left-click is this contract with a different pipeline:
`Minecraft.startAttack` reaches `MultiPlayerGameMode.startDestroyBlock`, which
opens its own prediction and sends a `ServerboundPlayerActionPacket` instead,
and the block hook is `BlockBehaviour.BlockStateBase.attack`. Nothing about the
ledger changes; everything about the *clock* does, because breaking the next
page's stone takes eight ticks where opening takes none. [Block breaking](block-breaking#the-button-is-not-the-switch)
takes it from there, and the two pages are deliberately the same shape.

## 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 only feeds other tags, the axe's and the
doors mobs may open among them. Brain-driven mobs ask that one:
`InteractWithDoor` reads `BlockTags.MOB_INTERACTABLE_DOORS`, the wooden doors
and the copper ones, while the older goals read
`DoorBlock.isWoodenDoor`, which is `BlockSetType.canOpenByHand` under
another name.

**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 every
navigating mob in the level asking whether the change is worth a re-route —
the one place the world pushes back on pathfinding, and unbounded by distance
([pathfinding](../entities/pathfinding#the-one-place-the-world-pushes-back)).
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.

## Where to look

The click comes in at `Minecraft.handleKeybinds` and
`Minecraft.startUseItem`, reaches the level through
`MultiPlayerGameMode.useItemOn` and `MultiPlayerGameMode.performUseItemOn`,
and is answered in `InteractionResult`. The two block hooks a state offers are
`BlockBehaviour.BlockStateBase.useItemOn` and
`BlockBehaviour.BlockStateBase.useWithoutItem`, and `DoorBlock.useWithoutItem`
is this door's. `Level.setBlock` and
`BlockBehaviour.BlockStateBase.updateNeighbourShapes` are the write and the
shape cascade, `CollectingNeighborUpdater.addAndRun` the queue underneath it,
and `DoorBlock.updateShape` with `Block.updateOrDestroy` is where the top half
is decided. On the server, start at
`ServerGamePacketListenerImpl.handleUseItemOn` for the gates and
`ChunkHolder.broadcastChanges` for how the other half reaches everyone else.

---

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