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

Prediction and acknowledgement

Verified against Minecraft 26.3 · Part X · a block placed against a wall the server will not allow: the client shows it, the server refuses it, and a numbered receipt decides when the lie ends.

The client cannot wait a round trip to show you the block you just placed, so it places it locally and tells the server afterwards. Everybody who has described this system has described it as a rollback: the client guesses, the server judges, the ack says yes or no. That is not what happens. ClientboundBlockChangedAckPacket carries no verdict at all — it is a receipt for a number, sent (once the server considers the client loaded) for actions the server refused exactly as it is sent for actions it allowed, and sent carrying zero for an aborted dig or a changed dig direction. Nothing in the protocol ever says no. What makes the system correct instead is an ordering rule: any correction the server intends travels earlier in the stream than the receipt for it, because the correction leaves from inside the handler or with the level’s own tick, while the receipt is only a number the connection flushes after both.

This page is the machinery under that rule: one ledger per level, one counter per connection, six windows in which a prediction can be opened, and four methods that between them decide whether the world moves. It is also the client’s second answer to the question authority asks — the client level answers what may I simulate, and this answers what may I show before I am told — and Part V’s block interaction and block breaking are where the two applications are watched.

The cast

classwhat it decidesthread
MultiPlayerGameModewhen a prediction window opens, and what goes in itRender thread
BlockStatePredictionHandlerthe ledger: what was there before, under which sequence numberRender thread
ClientLevelthe four writes — the prediction, the absorbed correction, the trigger, the settleRender thread
PredictiveActiona one-method interface: given the sequence, produce the packetRender thread
ServerGamePacketListenerImplone integer per connection, and when it is emittedServer thread
ServerPlayerGameModethe authoritative version of the same actionServer thread

Two state machines, running against each other

The whole mechanism is one small state machine on each side, and neither knows the other’s state. The client’s runs per position; the server’s runs per connection.

stateDiagram-v2
    state "Client — one position in the ledger" as CLIENT {
        [*] --> Absent
        Absent --> Retained : ClientLevel.setBlock
        Retained --> Retained : again, sequence refreshed
        Retained --> Corrected : ClientLevel.setServerVerifiedBlockState
        Retained --> [*] : BlockStatePredictionHandler.<br/>endPredictionsUpTo
        Corrected --> [*] : the same call
    }
    state "Server — one integer per connection" as SERVER {
        [*] --> Idle
        Idle --> Raised : ServerGamePacketListenerImpl.<br/>ackBlockChangesUpTo
        Raised --> Raised : another acked action, same tick
        Raised --> Idle : flushed, head of the listener's tick
    }

Two machines that never read each other, on different clocks: the client’s runs once per position, the server’s once per connection. Both exits to the end are the same call, which writes the entry’s state wherever it differs from the screen: the state from before the prediction if no update arrived, the server’s if one was absorbed.

Read the two columns as running at different rates. The client’s machine advances several times per tick, once per position touched. The server’s advances on every packet that reaches a ServerGamePacketListenerImpl.ackBlockChangesUpTo call. There are three such call sites: ServerboundUseItemOnPacket, ServerboundUseItemPacket, and the four destroying actions inside ServerboundPlayerActionPacket’s handler. ServerboundPlayerActionPacket carries nine actions in all, and the other five — dropping one item, dropping a stack, releasing a use, swapping hands and the stab — raise nothing. The counter then empties once per connection tick, which is why five acked actions in one tick produce one receipt carrying the highest number, and why one ack can drive a dozen positions out of the ledger at once.

Note what the diagram does not contain: any transition on which the server says no. There is none. Both of the client’s exit transitions are driven by the same packet, and which of the two a position takes is decided entirely by whether a block update reached it first.

The four writes

Everything the ledger does happens through four methods on ClientLevel, and the difference between them is the whole mechanism.

ClientLevel.setBlock — the ordinary write. While a prediction is open, and only if the write succeeded, it calls BlockStatePredictionHandler.retainKnownServerState with the state that was there before. If the position already has an entry, only the sequence is refreshed: the ledger keeps the first pre-change state it ever saw for that position, and the player position recorded with it.

ClientLevel.setServerVerifiedBlockState — every inbound block update goes through here. If the position is in the ledger it overwrites the entry and the world is not touched, so the prediction stays on screen. If it is not in the ledger — the common case, for blocks the player did not touch — it writes the world immediately. The absorption is per position, not per packet.

ClientLevel.handleBlockChangedAck — the trigger. The receipt’s way into the ledger: it hands the receipt’s number straight to BlockStatePredictionHandler.endPredictionsUpTo, which removes every entry at or below it and passes each one’s recorded state to the settle.

ClientLevel.syncBlockState — the settle. Applies the recorded state only if it differs from what is there, with flags Block.UPDATE_NEIGHBORS plus Block.UPDATE_CLIENTS plus Block.UPDATE_KNOWN_SHAPE (the whole set is in block update flags). That third flag suppresses the shape pass, and neighbour updates are inert on the client anyway — so the restore is a bare state write plus a remesh. A prediction can touch more than the block you clicked: placing one half of a door writes the other, and a shape update can walk outwards from the block that changed. Call that spread the cascade. The cascade does not re-run on the way back. Reconciliation is correct only because every position the cascade touched got its own ledger entry on the way out.

A placement the server refuses

The state diagram says what the states are; this says what order the packets arrive in, which is the part correctness rests on.

sequenceDiagram
    participant MPGM as MultiPlayer<br/>GameMode
    participant BSPH as BlockState<br/>PredictionHandler
    participant CL as ClientLevel
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SPGM as ServerPlayer<br/>GameMode

    MPGM->>BSPH: startPredicting — currentSequenceNr becomes n
    MPGM->>CL: setBlock, from performUseItemOn through ItemStack.useOn
    CL->>BSPH: retainKnownServerState — the truth, filed under n
    MPGM->>SGPL: ServerboundUseItemOnPacket(hand, hit, n)
    MPGM->>BSPH: close — the window shuts and the block is on screen
    SGPL->>SGPL: hasClientLoaded? then ackBlockChangesUpTo — first
    SGPL->>SPGM: useItemOn — the place fails canPlace, so nothing changes
    SGPL->>CL: two ClientboundBlockUpdatePackets, whatever the outcome
    CL->>BSPH: updateKnownServerState — the entry is overwritten, the world is not
    Note over SGPL,SPGM: later in the same server tick, the listener's tick opens by flushing it
    SGPL->>CL: Clientbound<br/>BlockChangedAckPacket, carrying n
    CL->>BSPH: endPredictionsUpTo(n)
    BSPH->>CL: syncBlockState — air goes back
    CL->>CL: Entity.absSnapTo — only if the restored block now intersects you

The two ClientboundBlockUpdatePackets leave from inside the handler; the ack is only a field the listener’s tick flushes, later in the same server tick. That is the ordering rule for a refusal, visible here as the distance between the block-update arrow and the note.

The ack is recorded before the action is attempted, so by the time the refusal happens the receipt is already promised. The correction is not the ordinary chunk broadcast but a targeted pair of resends the handler makes for every click that passes the build-height test, whether or not the action was then attempted. It skips them only when an earlier gate turns the packet away — an item a feature flag disables, a target out of reach, a hit point more than a block from the target’s centre, a target past the build limit — and then the ledger’s own record undoes the prediction. Whenever a correction is sent it is ahead of the receipt: the resends go out inside the handler, an accepted change’s broadcast with the level’s tick, and the ack is only a field assignment that ServerGamePacketListenerImpl.tick flushes after both. The settle is what finally moves the world, and it is a no-op when the absorbed state is already what is on screen. And the snap only happens when the restored block turns out to be inside the player.

Because the ack is buffered rather than sent, where a handler records it does not affect stream order — ServerGamePacketListenerImpl.handleUseItemOn and ServerGamePacketListenerImpl.handleUseItem record it as the first statement inside the ServerGamePacketListenerImpl.hasClientLoaded test, ServerGamePacketListenerImpl.handlePlayerAction after running the break action, and in both cases the correction still reaches the client first.

The six windows

A window is a few microseconds long and entirely synchronous: on the Render thread, inside one call from Minecraft.tick. The counter is raised, the local effect runs, the packet is built with the new sequence and sent, and the window closes — in that order in five of the six, MultiPlayerGameMode.useItem building its packet before its effect — so the packet is always constructed while the ledger is still recording. MultiPlayerGameMode.startPrediction is the private method all six go through, and it is not the same thing as BlockStatePredictionHandler.startPredicting, which is what it calls: the first is the window, the second is the pre-increment of BlockStatePredictionHandler.currentSequenceNr that gives the window its number, and the switch that sets ClientLevel.setBlock recording. ClientLevel.getBlockStatePredictionHandler is package-private, so nothing outside client/multiplayer can reach the ledger at all.

wherewhat it predicts locallywhat it sends
MultiPlayerGameMode.startDestroyBlock, creativethe block removed at onceServerboundPlayerActionPacket START
MultiPlayerGameMode.startDestroyBlock, survivalBlockBehaviour.attack, then removal if the block breaks instantlySTART
MultiPlayerGameMode.continueDestroyBlock, creativeremoval, every sixth tickSTART
MultiPlayerGameMode.continueDestroyBlock, at full progressremovalSTOP
MultiPlayerGameMode.useItemOnMultiPlayerGameMode.performUseItemOn — the block’s hook, the empty-hand hook, ItemStack.useOnServerboundUseItemOnPacket
MultiPlayerGameMode.useItemItemStack.use, including a transformed held itemServerboundUseItemPacket

So the ledger covers rather more than “blocks the player placed or broke”: it covers every ClientLevel.setBlock performed inside one of those windows. That includes the second half of a door, every position a shape cascade revisits, a door a right-click opens or shuts, and — the one attack hook that does it — RedStoneOreBlock.attack, whose lighting change is not side-gated and therefore files a ledger entry when you merely left-click redstone ore.

What the ledger does not cover

Half the value of this page is the list of things people assume it handles. MultiPlayerGameMode’s non-predicting verbs are the giveaway: MultiPlayerGameMode.attack, MultiPlayerGameMode.interact, MultiPlayerGameMode.stopDestroyBlock, MultiPlayerGameMode.releaseUsingItem, MultiPlayerGameMode.piercingAttack and every container verb open no window at all.

  • Breaking progress. MultiPlayerGameMode.destroyProgress and its companions are a plain parallel clock with no sequence and no reconciliation; the crack overlay is written straight into the client level. Of the progress clock itself nothing is predicted — only the removals at either end of it are.
  • Item use. MultiPlayerGameMode.useItem opens a window, but a consumed item, a started use and a cooldown are not block states and the ack does nothing for them. They are corrected by other means: the living-entity flags in synched data, ClientboundCooldownPacket, and the menu resend the server performs when the stack changed.
  • Dropping. MultiPlayerGameMode.dropItem predicts the removal from the selected slot and sends its action packet with sequence zero — a local mutation with no rollback path at all.
  • Movement. Rubber-banding is a different mechanism entirely: an id-matched teleport handshake with no sequence and no ledger, described in input to movement. The two systems touch at one point in each direction — ClientPacketListener.handleMovePlayer calls BlockStatePredictionHandler.onTeleport, so a teleport disarms the ledger’s position snap until a receipt passes the teleport’s number, and the settle calls Entity.absSnapTo on the LocalPlayer when the restored block is inside you.

What one ack does, and the numbers that are not sequences

One receipt is not one action undone. Every entry at or below the acknowledged sequence is removed and handed to the settle, and there the paths part: write nothing, because the recorded state is already on screen (the correct prediction); write the state; or write the state and snap the player. A single ack can produce all three across the map in one pass — which is what the player-position recorded in each entry is for, and the only thing that reads it. BlockStatePredictionHandler.lastTeleportSequence is the exception that makes the snap coarse: it is compared against the acknowledged sequence rather than each entry’s, and is never reset. While the receipt is at or below the teleport’s number every snap in the batch is suppressed; once a receipt passes it, every snap is allowed, those of entries filed before the teleport included.

Zero is not a sequence. BlockStatePredictionHandler.startPredicting pre-increments, so the first real sequence is one and no genuine prediction is ever numbered zero. The three-argument ServerboundPlayerActionPacket constructor defaults the sequence to zero and aborting a dig and changing its direction use it, so the server dutifully sends a ClientboundBlockChangedAckPacket carrying zero — and it settles nothing.

Nothing expires. There is no timeout and no cap on the ledger: nothing clears it except a settle, and while the server considers the client not yet loaded, sequenced packets are dropped before the ack is recorded. So a wrong guess made then stands on screen until a later receipt settles it with everything below it. The only reset is a new ClientLevel.

And the two directions do not use the same flags. The predicted removal in MultiPlayerGameMode.destroyBlock — the write inside the four breaking rows of the window table — goes out with Block.UPDATE_IMMEDIATE in the mix; the restore comes back with Block.UPDATE_KNOWN_SHAPE. One cascades, the other deliberately does not, which is the four writes’ asymmetry stated in flags.

Questions players ask

Why does the block come back and then vanish again? Because the two progress clocks disagreed and the ack does not care — block breaking follows both clocks. What is this page’s is the consequence: the action is acknowledged whatever the server decided, so the entry settles, the stone comes back, and it vanishes again when the server’s own delayed destroy finishes. Releasing the mouse does not do this — that is MultiPlayerGameMode.stopDestroyBlock, which opens no window at all.

Why can I not right-click while mining? Because Minecraft.startUseItem is gated on MultiPlayerGameMode.isDestroying, one of the click gates block interaction lists. A spectator is stopped in the other direction, and the ledger treats the two halves of that asymmetrically: MultiPlayerGameMode.useItem returns early, before the window opens, while MultiPlayerGameMode.useItemOn returns inside it — so the sequence is burned and the packet is sent anyway.

Where to look

MultiPlayerGameMode.startPrediction — the whole client side is that one private method and its six call sites. BlockStatePredictionHandler end to end; it is short and every line of it matters, including BlockStatePredictionHandler.currentSequence, BlockStatePredictionHandler.isPredicting and BlockStatePredictionHandler.close. ClientLevel.setBlock, ClientLevel.setServerVerifiedBlockState, ClientLevel.handleBlockChangedAck and ClientLevel.syncBlockState for the four writes, in that order. Then the server’s half, which is smaller than the client’s: ServerGamePacketListenerImpl.ackBlockChangesUpTo — a method and, confusingly, a field of the same name, the field holding minus one when there is nothing to send — and the first statement of ServerGamePacketListenerImpl.tick, which is where it goes out.


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