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 breaking

Verified against Minecraft 26.3 · Part V · A survival player holds left-click on stone with an iron pickaxe for eight ticks: two clocks that agree without talking, one loot roll, one cobblestone.

Hold the button on a stone block and two programs start counting. The client adds a fraction to MultiPlayerGameMode.destroyProgress every client tick and paints the crack; the server sets ServerPlayerGameMode.destroyProgressStart to the tick the dig began and recomputes, from scratch, how far along it ought to be. Between the first packet and the last, neither clock is ever mentioned on the breaker’s own connection — a ServerboundPunchPacket goes up and a ClientboundLevelEventPacket comes down every tick, for the swing and for the particles and the sound, and neither says how far along the dig is; the only progress that goes on the wire at all is the server’s, to other players, so that they see your cracks — and on the eighth tick the two answers are the same number. That agreement is what the whole design rests on, and it is also why the failure mode is so strange: once the client has finished, releasing the button cancels nothing. A client that finishes too early gets a deferral, not a rejection. The receipt for the STOP goes out in the same tick, the client dutifully puts the stone back — and then watches it vanish a second time when the server’s deferred break lands, with nothing the player can do in between.

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 owns that machinery; block interaction and this page are its two applications.

The cast

classwhat it decidesthread
Minecraftthat the button is down and the crosshair is on a block, and whether this tick starts a dig or continues oneRender
MultiPlayerGameModethe client’s clock: accumulated progress, the five-tick pause after a break, when to predict the removal and send STOPRender
ClientLevelthe predicted air, and the crack overlays — every breaker’s, including the local player’sRender
ServerGamePacketListenerImplwhich of the nine ServerboundPlayerActionPacket.Actions this is, and when the receipt for its sequence is flushedServer
ServerPlayerGameModethe server’s clock, the reach and permission gates, the 0.7 verdict, and the deferralServer
BlockBehaviour.BlockStateBasehardness, whether the block needs the right tool for drops, and the per-tick fractioneither
Toolhow fast this stack mines this block, and whether it drops — two separate answers from one rule listeither
Blockthe particles and sound event, the stat, the exhaustion and the loot rollServer, and the breaker’s own client for the particles and sound

One dig, end to end

sequenceDiagram
    box Client
        participant MC as Minecraft
        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 SL as ServerLevel
    end

    Note over MC,SL: client tick 1: the button goes down, and the same lap runs the first continue too
    MC->>MPGM: startDestroyBlock, the block is not air
    MPGM->>CL: destroyBlockProgress, stage -1 at progress 0
    MPGM->>SGPL: ServerboundPlayerAction<br/>Packet START, N
    MC->>MPGM: continueDestroyBlock adds the first 0.133
    MPGM->>CL: destroyBlockProgress, my own crack at stage 1
    SGPL->>SPGM: handleBlockBreakAction, the start tick recorded
    SPGM->>SL: destroyBlockProgress, everyone else's crack within 32 blocks
    SGPL-->>CL: ClientboundBlockChangedAck<br/>Packet N, nothing to reconcile

    loop client ticks 2-8, beside server ticks
        par the client adds
            MC->>MPGM: continueDestroyBlock adds 0.133
            MPGM->>CL: destroyBlockProgress, my own stage
            MC->>SGPL: ServerboundPunchPacket, no fields at all
        and the server recomputes the whole dig and throws the answer away
            SPGM->>SPGM: incrementDestroy<br/>Progress
            SPGM->>SL: destroyBlockProgress, only when the tenth changes
            SPGM->>SL: levelEvent with no source, sound every fourth tick
            SL-->>CL: ClientboundLevelEventPacket, the face but no progress
        end
    end

The two clocks, with the machines boxed and the loop in parallel compartments that have no order between them: the client adds a fraction, the server recomputes the lot, and what crosses between the boxes, a punch up and a level event down, carries neither clock’s count, which is the page’s whole argument.

The eighth add is where the two stop being symmetrical. It reaches about 1.067 on the client, and the client acts on its own number first.

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 SL as ServerLevel
        participant Block as Block
    end

    Note over MPGM,Block: still client tick 8: the eighth add reaches about 1.067
    MPGM->>CL: setBlock to air under flags 11, inside prediction M
    MPGM->>SGPL: ServerboundPlayerAction<br/>Packet STOP, M
    Note over MPGM,Block: a server tick, STOP handled off the task queue
    SGPL->>SPGM: handleBlockBreakAction STOP, its own 1.067 clears the 0.7 bar
    SPGM->>Block: playerWillDestroy, event 2001 to all but the breaker
    SPGM->>SL: removeBlock, the fluid-or-air state under flags 3
    SPGM->>Block: playerDestroy, once ItemStack.mineBlock has spent a durability point
    Block->>SL: the blocks/stone roll, then addFreshEntity with the ItemEntity
    Note over MPGM,Block: same tick, later: the levels broadcast, then the connections flush
    SL-->>CL: ClientboundBlockUpdatePacket air, absorbed
    SGPL-->>CL: ClientboundBlockChangedAck<br/>Packet M, air is already there

The break itself, in the order the server runs it. The client writes the air first and on its own authority, and everything that comes back — the block update and the receipt — finds a world that already agrees.

Two clocks, and the plus one that makes them agree

BlockBehaviour.getDestroyProgress is the shared formula, and both sides call it through BlockBehaviour.BlockStateBase.getDestroyProgress with the same arguments. It answers the fraction of the block broken per tick: the player’s speed, divided by the block’s hardness, divided by 30 when Player.hasCorrectToolForDrops says yes — which it does for every block that does not require a tool at all — and by 100 when it says no. Hardness −1 returns zero forever. Hardness zero is not special-cased, so an instabreak block divides by zero and returns infinity: that, and not a branch on hardness, is what sends the START handler down its insta-mine path on the first tick.

Player.getDestroySpeed builds the numerator from Inventory.getSelectedItem in one pass, and the surprising parts are all constants rather than data. The stack’s ItemStack.getDestroySpeed gives the base. If that is above 1.0 — only then — Attributes.MINING_EFFICIENCY is added, which is where Enchantments.EFFICIENCY lands, at level² + 1. Haste and conduit power are read together through MobEffectUtil.hasDigSpeed and MobEffectUtil.getDigSpeedAmplification, which returns the greater of the two amplifiers — a beacon and a conduit are interchangeable and do not stack — and multiply by 1 + 0.2 × (amplifier + 1). Mining fatigue is not an attribute at all but a literal power of 0.3, with the amplifier plus one as the exponent — 0.3, 0.09, 0.027 and on down for anything higher. Then Attributes.BLOCK_BREAK_SPEED, then Attributes.SUBMERGED_MINING_SPEED (0.2 by default) if the eyes are in FluidTags.WATER, and finally divide by five if the player is not on the ground.

For stone at hardness 1.5 and an iron pickaxe at 6.0, that is 6 ÷ 1.5 ÷ 30 = 0.133 per tick, so the eighth tick is the one that passes 1.0.

Why the two answers match

The two clocks count differently and still land on the same number, and the reason is a matter of when the server is asked rather than what it computes. The client accumulates: MultiPlayerGameMode.continueDestroyBlock adds one tick’s fraction each time it runs, and the first of those runs in the same client tick as the Minecraft.startAttack that opened the dig through MultiPlayerGameMode.startDestroyBlock, so after k client ticks the client holds k fractions. The server keeps no accumulator. For a live dig, ServerPlayerGameMode.incrementDestroyProgress multiplies the per-tick fraction by elapsed ticks plus one, and that plus one is exactly the client tick that both started and continued.

Now the timing. ServerPlayerGameMode.tick increments its own tick counter before recomputing, so the number it reaches inside the level tick is one fraction ahead of the client’s — and it uses that number only for the crack stage other players see, because the live branch throws the value away and only the delayed branch compares a value with 1.0. What the live branch sends every tick is a level event with no source entity, so the breaker is sent it too: LevelEvent.PARTICLES_DESTROY_PROGRESS, or LevelEvent.PARTICLES_AND_SOUND_DESTROY_PROGRESS on every fourth, naming the face being dug and not the progress. The comparison that decides anything happens somewhere else: the STOP is handled in the packet drain, before that tick’s increment (the server tick), so ServerPlayerGameMode.handleBlockBreakAction measures elapsed ticks over the same span the client counted and arrives at the same 1.067. The agreement is not a coincidence in the arithmetic; it is the packet drain running before the levels do.

Recomputing rather than accumulating has a second consequence: lose haste mid-dig and the server rescales the whole dig retroactively, while the client keeps the progress it already banked. Swapping tools is different: the client sees a new stack in MultiPlayerGameMode.sameDestroyTarget, sends an ABORT and a START, and both clocks begin again.

They agree without talking because almost every input is either static data both sides loaded — hardness, the block tags, the Tool component travelling with the stack — or a syncable attribute, or a synced effect. The inputs that could drift are the ones the client reports rather than shares, where the player stands and, sharpest of all, which slot is selected: MultiPlayerGameMode.ensureHasSentCarriedItem runs at the top of every MultiPlayerGameMode.continueDestroyBlock to send a ServerboundSetCarriedItemPacket the moment it changes.

The button is not the switch

Seventy per cent of the server’s own clock is what a STOP must have run before the block breaks immediately (ServerPlayerGameMode.handleBlockBreakAction).

For stone that is about two ticks of slack. Below the bar the STOP is not refused: unless a delayed dig is already pending, when it does nothing at all, the handler sets ServerPlayerGameMode.hasDelayedDestroy, copies the position and the original start tick into ServerPlayerGameMode.delayedDestroyPos and ServerPlayerGameMode.delayedTickStart, and leaves the rest to the delayed branch of ServerPlayerGameMode.tick. The sequence is acknowledged regardless — ServerGamePacketListenerImpl.handlePlayerAction calls ServerGamePacketListenerImpl.ackBlockChangesUpTo for all four break actions, unconditionally, after the game mode has run. The delayed branch does not count the ticks since the dig began: it multiplies the per-tick fraction by the start tick itself, plus one, and that tick is the game mode’s count since the player joined or respawned, so the answer does not grow with waiting. For any dig begun more ticks after that than it takes, the product clears 1.0 on the branch’s first run, later in the same server tick, and ServerPlayerGameMode.destroyBlock runs — after the level has broadcast that tick’s block changes. So the receipt still arrives with no correction in front of it, the client settles prediction M against the stone it recorded, and ClientLevel.syncBlockState puts the stone back. The block is visibly there again for a tick, until the next broadcast carries the air as an ordinary block update. The prediction was not wrong — it was undone and then redone.

Letting go changes nothing: a client that has sent its STOP is no longer digging, so releasing the button sends nothing, and an ABORT from a later dig clears ServerPlayerGameMode.isDestroyingBlock and erases the crack without touching ServerPlayerGameMode.hasDelayedDestroy — and ServerPlayerGameMode.tick tests the delayed dig first, before the live one. A delayed dig begun too soon after the player joined or respawned never clears on its own, and then starting a dig on a different block does not help either: the START is processed normally, but the delayed branch keeps winning the tick. The delayed path re-checks almost nothing on its way through — not reach, not MinecraftServer.isUnderSpawnProtection, not ServerLevel.mayInteract, not that the player is still in the same room. It calls ServerPlayerGameMode.destroyBlock directly rather than ServerPlayerGameMode.destroyAndAck, so two of the three ways it can fail there are silent, with no corrective block update. Inside the tick it ends only when the block turns to air, the one condition ServerPlayerGameMode.tick tests before recomputing progress, or when that recomputation, made with whatever the player now holds and has, reaches 1.0; otherwise it lasts until the player leaves or respawns.

What a real refusal looks like

The refusals that are refusals differ in what they send back, and the difference is observable. A failed Player.isWithinBlockInteractionRange check — which allows a full block of slack — sends nothing but the receipt, and it guards STOP as well as START; only an abort gets past it, and only while a dig is live, so an abort from too far away still ends one. Being above LevelHeightAccessor.getMaxY, failing ServerLevel.mayInteract or failing Player.blockActionRestricted each answer with a ClientboundBlockUpdatePacket carrying the true state. Spawn protection answers with an overlay message from ServerPlayer.sendSpawnProtectionMessage and no block update whatsoever. Every one of those exits is named in a string behind SharedConstants.DEBUG_BLOCK_BREAK, which is the best map of this state machine there is.

Worth holding the two click lectures side by side here, because the same two gates answer oppositely. Breaking above the build height sends the true state back; placing above it sends an action-bar line and no block update at all. Breaking inside spawn protection sends the message and no block update; placing there sends the message and both updates (block interaction). The rule is not the gate but the pipeline. A placement’s two corrective updates sit in one branch below the build-height test and go out for every outcome that reaches it, refusal or not. A break has no such branch: each of its five exits decides for itself. Three send the true state back — the build height, ServerLevel.mayInteract and Player.blockActionRestricted. Spawn protection sends its message and no state. And being out of reach sends nothing but the receipt, which puts the block back like any other; what a STOP refused for reach leaves wrong is the server, whose live dig goes on sending the breaker its level events until a START or ABORT it accepts, or until the block turns to air.

Speed and drops are two scans of one list

DataComponents.TOOL holds a Tool: a list of Tool.Rule, a Tool.defaultMiningSpeed, a Tool.damagePerBlock and a Tool.canDestroyBlocksInCreative. Each rule names a set of blocks and carries an optional speed and an optional drop verdict, and the game builds its rules in three shapes, each with a factory that says so: Tool.Rule.minesAndDrops carries both, Tool.Rule.deniesDrops carries only the verdict and Tool.Rule.overrideSpeed only the speed. Tool.getMiningSpeed and Tool.isCorrectForDrops are two independent walks of the same list, each taking the first rule that both matches the block and carries the field it came for. ToolMaterial.applyToolProperties builds every pickaxe, axe, shovel and hoe from exactly two rules, in this order:

the iron pickaxe’s rules, in orderwhat Tool.getMiningSpeed doeswhat Tool.isCorrectForDrops does
Tool.Rule.deniesDrops on BlockTags.INCORRECT_FOR_IRON_TOOL — no speed fieldskips itobsidian matches, answers no
Tool.Rule.minesAndDrops on BlockTags.MINEABLE_WITH_PICKAXE at 6.0obsidian matches, answers 6.0never reached
nothing matchedTool.defaultMiningSpeed, 1.0false

That is why an iron pickaxe mines obsidian and drops nothing. The speed scan falls through the deny rule — it has no speed — and takes the full pickaxe 6.0; the drop scan stops at the deny. The block still takes forever, but for a different reason: Player.hasCorrectToolForDrops is false, so BlockBehaviour.getDestroyProgress divides by 100 instead of 30. No item in the game uses all three shapes at once, and the two that never share a Tool are Tool.Rule.deniesDrops and Tool.Rule.overrideSpeed: the four mining tools pair a deny rule with a minesAndDrops rule, while shears and the sword — which deny nothing — are the only users of Tool.Rule.overrideSpeed. The sword is also one of exactly three whose Tool.canDestroyBlocksInCreative is false; the other two are the mace and the trident.

The cracks belong to everyone but you

ServerLevel.destroyBlockProgress sends a ClientboundBlockDestructionPacket to every player in the level within 32 blocks whose entity id is not the breaker’s. You are never sent your own cracks. What you see is MultiPlayerGameMode writing straight into ClientLevel.destroyBlockProgress each tick — the same method the packet handler calls, reached by a different road. The same asymmetry runs through the break itself: Block.playerWillDestroy posts level event 2001, which ServerLevel.levelEvent broadcasts within 64 blocks excluding the breaker, because the breaker already played it locally inside MultiPlayerGameMode.destroyBlock.

BlockDestructionProgress is a plain holder — id, position, progress, a last touched tick — and BlockDestructionProgress.setProgress clamps only the top, at 10. The 0–9 window everybody quotes is enforced by the caller: ClientLevel.destroyBlockProgress stores a stage only for values in [0, 10), and reads anything else — including the −1 that MultiPlayerGameMode.getDestroyStage returns at zero progress — as an instruction to remove that breaker’s entry. Entries are indexed twice, by breaker id and by position, the latter into a sorted set so the deepest crack at a position wins; LevelExtractor collects those within 32 blocks of the camera each frame. Entries untouched for 400 ticks are swept every twentieth tick, which is what eventually clears the cracks left by someone who disconnected mid-dig.

Remove, damage, roll, drop

ServerPlayerGameMode.destroyBlock runs a short gauntlet before it writes anything: ItemStack.canDestroyBlock, then GameMasterBlock against Player.canUseGameMasterBlocks, then Player.blockActionRestricted. It captures the BlockEntity first, because the write is about to destroy it. Then Block.playerWillDestroy — particles and sound to everyone else, piglins angered for BlockTags.GUARDED_BY_PIGLINS, and a GameEvent.BLOCK_DESTROY posted for sculk, and delivered before this method returns (the broadcast is a nested loop).

The write itself is Level.removeBlock, not Level.destroyBlock. It puts the fluid that was in the block back — water for a waterlogged block, air here — under flags 3, and everything that follows from those flags is the pair of flowcharts on blocks and states.

Drops come last and in a fixed order. If Player.preventsBlockDrops (creative) the method returns here. Otherwise the tool is copied, Player.hasCorrectToolForDrops is asked once and remembered, and ItemStack.mineBlock is called unconditionally — though what it does is conditional four ways over: a DataComponents.TOOL at all, the server side, a non-zero hardness and a damage-per-block above zero. Not one of those four is the drop verdict, which is the whole point and belongs to items and stacks. Only then, and only if the write succeeded and the remembered answer was yes, does Block.playerDestroy run: Stats.BLOCK_MINED, 0.005 of food exhaustion, and Block.dropResources.

The loot side is thin. Block.getDrops supplies LootContextParams.ORIGIN at the block centre, LootContextParams.TOOL and LootContextParams.THIS_ENTITY, BlockBehaviour.getDrops adds LootContextParams.BLOCK_STATE, and the set is LootContextParamSets.BLOCK. The table key is not looked up by name at break time: BlockBehaviour.Properties.effectiveDrops resolves the block’s id under blocks/ once, when the block is constructed. What blocks/stone then does is two lines of JSON — a silk-touch alternative, else cobblestone if it survives an explosion — rolled from a seeded per-table sequence rather than the level random (loot tables). Each surviving stack goes to Block.popResource, which respects GameRules.BLOCK_DROPS, jitters the position ±0.25 on all three axes around the block centre, gives the ItemEntity its small upward kick in the constructor and a ten-tick pickup delay. Ores add their experience afterwards, in BlockBehaviour.BlockStateBase.spawnAfterBreak.

Questions players ask

Why does my pickaxe lose durability on obsidian, which drops nothing, but not on short grass, which does? Durability is spent by ItemStack.mineBlock, which runs before the drop verdict is consulted and does not care about it — what it cares about is that the block’s hardness is non-zero and the tool has a damage-per-block above zero. Obsidian is hard and drops nothing: you pay. Short grass is Blocks.SHORT_GRASS, hardness zero: you never pay, whatever it drops. Shears are the exception at both ends — ShearsItem is the only override of Item.mineBlock in the game, and it tests neither hardness nor drops: the one thing it asks of the block is that it is not in BlockTags.FIRE. Shearing grass costs a point.

Why can’t I break blocks with a sword in creative? Because Tool.canDestroyBlocksInCreative is false on the sword’s component and ItemStack.canDestroyBlock checks it on both sides. It is a property of the item, not a special case in the game mode — which is why the client refuses first, and why the correcting block update the server sends back is a no-op: the client predicted nothing to correct.

For a 1.21-era reader. ServerboundSwingPacket is gone; on a dig, ServerboundPunchPacket does its work, and it names no hand.

Where to look

The two clocks are the thing to read first, and they are three methods: BlockBehaviour.getDestroyProgress is the shared formula, MultiPlayerGameMode.continueDestroyBlock is the client’s accumulator, and ServerPlayerGameMode.incrementDestroyProgress is the server’s recomputation. ServerPlayerGameMode.handleBlockBreakAction is the state machine behind all four break actions and the 0.7 bar, with ServerPlayerGameMode.tick for the delayed destroy it can leave behind. Then the ends: Minecraft.startAttack and Minecraft.continueAttack for where a dig begins each tick, ServerPlayerGameMode.destroyBlock for the gauntlet, and Block.playerWillDestroy with Block.playerDestroy and Block.popResource for what a broken block leaves behind. Tool.getMiningSpeed and Tool.isCorrectForDrops are the two walks of one rule list.


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