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
ClientboundBlockChangedAckPacketis 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
| class | what it decides | thread |
|---|---|---|
Minecraft | that the button is down and the crosshair is on a block, and whether this tick starts a dig or continues one | Render |
MultiPlayerGameMode | the client’s clock: accumulated progress, the five-tick pause after a break, when to predict the removal and send STOP | Render |
ClientLevel | the predicted air, and the crack overlays — every breaker’s, including the local player’s | Render |
ServerGamePacketListenerImpl | which of the nine ServerboundPlayerActionPacket.Actions this is, and when the receipt for its sequence is flushed | Server |
ServerPlayerGameMode | the server’s clock, the reach and permission gates, the 0.7 verdict, and the deferral | Server |
BlockBehaviour.BlockStateBase | hardness, whether the block needs the right tool for drops, and the per-tick fraction | either |
Tool | how fast this stack mines this block, and whether it drops — two separate answers from one rule list | either |
Block | the particles and sound event, the stat, the exhaustion and the loot roll | Server, 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 order | what Tool.getMiningSpeed does | what Tool.isCorrectForDrops does |
|---|---|---|
Tool.Rule.deniesDrops on BlockTags.INCORRECT_FOR_IRON_TOOL — no speed field | skips it | obsidian matches, answers no |
Tool.Rule.minesAndDrops on BlockTags.MINEABLE_WITH_PICKAXE at 6.0 | obsidian matches, answers 6.0 | never reached |
| nothing matched | Tool.defaultMiningSpeed, 1.0 | false |
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,
ServerboundPunchPacketdoes 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.