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

The sword swing

Verified against Minecraft 26.3 · Part VIII · Left-click on a pig: the client picks a target and sends one integer, and the server rebuilds every part of the hit from scratch.

You put the crosshair on a pig and click. The client has already decided what you are looking at — earlier in this same tick — checks a handful of reasons not to swing, and sends a packet with almost nothing in it: ServerboundAttackPacket is a record of one int, the entity id. No hand, no sneak flag, no hit position, no damage. Everything else the server re-derives: the weapon from your main hand, the geometry from the target’s bounding box measured against your eye, and the damage from an attribute, a cooldown curve applied twice in two different shapes, and a multiplication order in which the mace’s fall bonus lands before the critical hit and is therefore multiplied by it.

The cast

classwhat it decidesthread
Minecraftwhat you are looking at, and whether the click swings at allRender
LocalPlayerthe raycast, and which range its winner is judged againstRender
MultiPlayerGameModesends the attack, and predicts almost nothingRender
ServerGamePacketListenerImplresolves the id, re-checks the range and the itemServer
PlayerPlayer.attack: one method, and the order inside it is load-bearingboth (only the server’s answer counts)
LivingEntitythe swing animation state, and the two attack clocksboth
AttackRangea weapon’s own reach, with a minimum as well as a maximum—

Picking: what is under the crosshair

Minecraft.pick runs before Minecraft.handleKeybinds in the client tick (the client loop), and the keybind drain is what turns Options.keyAttack into Minecraft.startAttack — so the hit result a click uses was computed earlier in the same tick. Minecraft.pick also runs per frame, for the crosshair and the block outline, but that value is not what the attack sees.

It asks the local player, through LocalPlayer.raycastHitResult, with the camera entity as the ray’s origin. A player carries two reaches, and the whole of this section turns on their being different numbers: Attributes.BLOCK_INTERACTION_RANGE (4.5) and Attributes.ENTITY_INTERACTION_RANGE (3.0) (player anatomy).

If the active item (the one being used, if any, else the main hand) carries an AttackRange (DataComponents.ATTACK_RANGE), that component’s own search runs first, and the classic algorithm runs only when that search comes back empty: a block clip out to the greater of the two ranges, an entity sweep with ProjectileUtil.getEntityHitResult over the bounding box expanded along the view direction and inflated by one, each candidate inflated by Entity.getPickRadius (zero for everything but a pickable projectile) — and the entity wins only if it is strictly nearer than the block. Then LocalPlayer.filterHitResult measures the winner against its own one of the two reaches, an entity against 3.0 and a block against 4.5. That is the divergence outside creative: a wall four blocks away is in reach and a mob four blocks away is not — and a mob at four blocks in front of a wall hides the wall, because the mob wins the ray and is then turned into a miss.

AttackRange is worth a second look, because it is a reach floor as well as a ceiling: a minimum and a maximum, separate creative values, a hitbox margin and a mob factor. AttackRange.isInRange is the test, AttackRange.defaultFor falls back to Attributes.ENTITY_INTERACTION_RANGE, and AttackRange.effectiveMinRange / AttackRange.effectiveMaxRange apply the mob factor — only for non-players.

Deciding: most branches do not swing

Minecraft.startAttack is the branch point. It returns early — no swing, no packet — while Minecraft.missTime is running, when there is no hit result at all (setting a ten-tick miss time), while LocalPlayer.isHandsBusy, for a disabled item, and when Player.cannotAttackWithItem refuses with a tolerance of zero; spectators take a branch of their own. Two branches do swing: the piercing short-circuit to MultiPlayerGameMode.piercingAttack when the item carries DataComponents.PIERCING_WEAPON — the spear — and the tail of the hit-result switch: entity to MultiPlayerGameMode.attack, block to MultiPlayerGameMode.startDestroyBlock (block breaking), a miss (nothing in reach, or an air block) to Player.resetAttackStrengthTicker and the ten-tick miss time. Even the entity branch is conditional — a weapon with its own AttackRange that the hit falls outside of swings but sends no attack packet at all. The ten-tick miss time only exists outside creative, and opening any screen parks the timer at a very large number in every mode.

On the server, ServerGamePacketListenerImpl.handleAttack requires the client to have loaded and the player not to be a spectator, resolves the id with ServerLevel.getEntityOrPart, checks the world border, applies Player.isWithinAttackRange with a 3.0-block server buffer — applied to both ends, so a weapon’s minimum range effectively vanishes server-side — rejects a piercing weapon (that path arrives elsewhere), checks the item is enabled, re-checks Player.cannotAttackWithItem with a tolerance of five ticks, more lenient than the client’s zero, and calls Player.attack. Attacking something absurd — an ItemEntity, an ExperienceOrb, an unattackable AbstractArrow, yourself — is a disconnect, not a rejection; failing the range check is a silent drop.

The packet is drained from PacketProcessor at the top of the tick, before MinecraftServer.tickServer and therefore before any level ticks. That ordering matters: Player.attack runs before the victim’s damage cooldown comes down for the tick (damage and death owns LivingEntity.damageCooldownTime and who decrements it when). Each of the resulting feedback packets is written and flushed on its own: the connection suspends flushing across MinecraftServer.tickChildren (and across a player’s join), and the attack was handled before that bracket opened.

One click, one integer, one round trip

Two packets go out, and the one that comes back for the hit, the damage event, has no damage number in it.

sequenceDiagram
    box transparent the client
    participant LP as LocalPlayer
    participant MPGM as MultiPlayer<br/>GameMode
    participant CPL as ClientPacketListener
    end
    box transparent the server
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant Player as Player
    participant Entity as Entity
    end

    LP->>LP: raycastHitResult — AttackRange first, then the classic pick
    LP->>MPGM: attack, from Minecraft.startAttack
    MPGM->>SGPL: ServerboundAttackPacket — one varint, the entity id
    LP->>SGPL: ServerboundPunchPacket — no fields, from Minecraft.startAttack
    SGPL->>SGPL: ServerLevel.<br/>getEntityOrPart
    SGPL->>Player: isWithinAttackRange — the reach, plus a 3.0 buffer
    SGPL->>Player: attack — the damage rebuilt from nothing but the id
    Player->>Entity: hurtOrSimulate — its answer gates the tail
    Entity->>CPL: ClientboundDamageEventPacket — a damage type and three ids
    Player->>Player: causeExtraKnockback, doSweepAttack, itemAttackInteraction

Everything the client sent is on the two arrows that cross to the right, and between them they carry one integer: every number in the box on the right was rebuilt there, and the arrow coming back carries none of them.

The two clocks a swing is charged against

Before the arithmetic, the thing the arithmetic reads. A player carries two attack clocks, and both are declared two rungs up, on LivingEntity, while being read, reset and incremented only from Player: LivingEntity.attackStrengthTicker and LivingEntity.itemSwapTicker.

The first is the one you watch. Player.getCurrentItemAttackStrengthDelay is twenty divided by Attributes.ATTACK_SPEED, and the scale the damage uses is how far through that delay the ticker has got. Player.resetAttackStrengthTicker clears both clocks; Player.resetOnlyAttackStrengthTicker clears one. Player.tick resets both when the main-hand item type changes — swap weapons and you start again.

The second exists only to drive the held-item swap animation. Player.onAttack clears the attack ticker and leaves the swap ticker alone, but it is not the only reset a swing gets: both sides reset the attack ticker twice per swing, once inside Player.attack and once after it with Player.resetAttackStrengthTicker, which clears both clocks — the client in MultiPlayerGameMode.attack, the server on the punch packet that follows, because ServerGamePacketListenerImpl.handlePunch resets the ticker too.

The damage: one number, two curves, one order

Player.attack is a single method, and everything interesting about melee combat is the order in which it touches one float.

flowchart TD
    S["s = Player.getAttackStrengthScale, at a partial tick of 0.5"]
    BASE["base = Attributes.ATTACK_DAMAGE"]
    BOOST["boost = Player.getEnchantedDamage minus base, times s"]
    BQ["base × (0.2 + s² × 0.8) — Player.baseDamageScaleFactor"]
    RESET["Player.onAttack — the ticker resets, the scale already read"]
    GATE{"base or boost<br/>above zero?"}
    NONE["nothing below runs"]
    KB["sprint knockback, if s is above 0.9 and you are sprinting"]
    ITEM["base += Item.getAttackDamageBonus — the mace's fall bonus"]
    CRIT["base × 1.5, if full strength and Player.canCriticalAttack"]
    TOTAL["total = base + boost"]
    HURT["Entity.hurtOrSimulate — was anything damaged?"]
    S --> BOOST
    S --> BQ
    BASE --> BOOST
    BASE --> BQ
    BQ --> RESET
    RESET --> GATE
    GATE -- "neither" --> NONE
    GATE -- "either" --> KB
    KB --> ITEM
    ITEM --> CRIT
    CRIT --> TOTAL
    BOOST --> TOTAL
    TOTAL --> HURT

One float, followed down: the short branch is the enchantment bonus, which is scaled once and then left alone, and the long one is the base damage, which is scaled, gated, added to and multiplied before the two meet again at the bottom.

One node in it needs its meaning before the rest of the page spends it. Entity.hurtOrSimulate is the wrapper that branches on the side — Entity.hurtServer on the server, Entity.hurtClient on the client (damage and death owns the wrapper itself) — and what it returns is not the obvious thing: the boolean means was anything damaged, not did the hit land. That is what gates the knockback, the sweep, the durability and the hit particles below.

Read the picture for the two things it makes obvious. The cooldown is applied twice, differently — Player.baseDamageScaleFactor is the quadratic ramp on the base damage, and a plain multiplication is the linear one on the enchantment bonus, both from the same scale read with the same 0.5 partial tick. And the item bonus is inside the crit, because Item.getAttackDamageBonus is added before the ×1.5 rather than after it. One substitution the figure leaves out: while an auto-spin attack is in flight the base is LivingEntity.autoSpinAttackDmg, the riptide number, instead of the attribute.

The gates along the way are as particular as the arithmetic, and the first two are put to the target rather than to the attacker. Player.cannotAttack asks it two things: Entity.isAttackable, which is true by default and false for a dropped item, an experience orb, a falling block, a thrown eye of ender, a firework rocket and any arrow outside EntityTypeTags.REDIRECTABLE_PROJECTILE, and Entity.skipAttackInteraction, the hook by which a thing claims the swing for itself — an Interaction entity uses it to record who hit it, and a BlockAttachedEntity to re-enter through Entity.hurtOrSimulate with zero damage. A no from the first or a yes from the second ends the swing (damage outside LivingEntity has the per-class table). Then Player.deflectProjectile, which is why a ghast fireball can be batted back: anything in EntityTypeTags.REDIRECTABLE_PROJECTILE is turned around here rather than damaged, and the attack ends — all but a player’s wind charge in its first five ticks, which WindCharge will not let be deflected.

Then three tests that are not gates but switches, each deciding what kind of hit this is. Sprint knockback needs the scale above 0.9, plays a sound, and adds a flat 0.5 to the knockback later; it is the step the item bonus is added immediately after, which is why the item-bonus node in the figure above sits where it does. Player.canCriticalAttack needs falling, not on the ground, not climbing, not in water, not mobility-restricted, not a passenger, not sprinting, a LivingEntity target — and full strength as well. Player.isSweepAttack needs full strength, not a crit, not sprint knockback, on the ground, moving slower than 2.5× the walking speed, and something in ItemTags.SWORDS.

If the hit landed, the tail runs in order: Player.causeExtraKnockback — which is also where the attacker’s own motion is damped and sprinting cancelled, using LivingEntity.getKnockback computed from Attributes.ATTACK_KNOCKBACK through the enchantments and halved — then Player.doSweepAttack, Player.attackVisualEffects, LivingEntity.setLastHurtMob, Player.itemAttackInteraction, Player.damageStatsAndHearts, and Player.causeFoodExhaustion of 0.1 (hunger and experience). If it did not land, a no-damage sound. Either way LivingEntity.postPiercingAttack runs at the end — the same hook a stab ends on, which is why the name says piercing on a method that closes an ordinary swing (the spear).

Player.itemAttackInteraction is itself three steps in a particular order: ItemStack.hurtEnemy (the item’s own hook and the use statistic, not durability), then EnchantmentHelper.doPostAttackEffectsWithItemSource (enchantments), then ItemStack.postHurtEnemy, which is where Weapon’s per-attack durability cost is applied. Weapon (DataComponents.WEAPON) is a pair: that cost, and Weapon.disableBlockingForSeconds, the axe’s shield-breaking rule. LivingEntity.getSecondsToDisableBlocking reads it back, but only when the weapon in the attacker’s hand is also their active item — so an axe swung while the other hand is using something disables no shield.

Player.doSweepAttack damages every living entity in a box around the primary target inflated by (1, 0.25, 1), for candidates within three blocks of the attacker, excluding the attacker, the primary target, allies and marker armour stands. Each takes 1.0 + Attributes.SWEEPING_DAMAGE_RATIO × base, run through Player.getEnchantedDamage and then scaled by the attack-strength scale, plus a flat 0.4 knockback. Its sweep sound is unguarded; the damage and the ParticleTypes.SWEEP_ATTACK particles sit behind the server check.

Armour, the hurt cooldown, DataComponents.BLOCKS_ATTACKS and knockback resistance decide how much of that damage arrives, and all four belong to damage and death.

Questions players ask

Why does mashing do less damage? Because the base damage is quadratic in the charge and the enchantment bonus is linear in it. At half charge the base is 0.2 + 0.25 × 0.8 — that is s² with s = 0.5 — so 40% of full, while the enchantment bonus is at the full 50%. Mashing costs you more of the sword than of the enchantments on it.

Why does my sword make no sound until the server answers? Because the attack sounds are the counter-example to the client’s own prediction rule: Player.playServerSideSound excludes nobody, so every hit sound the attacker hears arrives as a ClientboundSoundPacket, one round trip late (what makes a sound).

Does the client predict any of this against a mob? Almost none. Entity.hurtClient returns false and neither LivingEntity nor Mob overrides it, so on the client Entity.hurtOrSimulate reports that the hit did not land and the entire block after it is skipped: no predicted knockback, no sweep, no visual effects, no durability, no exhaustion. The exceptions are the nine classes that do override it, and none of them is a mob — a RemotePlayer, a vehicle, a hanging thing, an orb (damage outside LivingEntity has the roll-call). Against four of them — another player, a vehicle, an end crystal, a shulker bullet — the whole block runs locally; a dropped item and an orb cannot be attacked at all, and the block-attached ones either claim the swing through Entity.skipAttackInteraction or refuse it in Entity.hurtClient. Player.getEnchantedDamage does nothing on Player either; it returns its argument unchanged and only ServerPlayer overrides it. With Attributes.ATTACK_DAMAGE not being client-syncable (attributes), the client’s damage figure is never authoritative wherever that block does run. That the client may not decide a hit at all is the rule of authority, applied to combat.

Can a weapon be too close to swing? No: it swings, and what an AttackRange minimum withholds on the client is the attack packet. The server’s attack path would enforce at most the floor less three blocks — the 3.0-block leniency is subtracted from the minimum as well as added to the maximum, which erases any floor under three — but the one built-in weapon with a floor, the spear, never takes that path, and its stab’s own search starts at the floor.

How does my client know how badly the pig was hurt? It learns the new health, not the hit. ClientboundDamageEventPacket carries no amount at all — a damage-type holder, three entity ids and an optional source position — and the victim’s red flash and damage cooldown are reconstructed from it, with the hurt sound too when the victim is you; a pig’s hurt sound arrives as a sound packet of its own (damage and death owns the packet). The health itself comes from synched entity data.

Are sweep and knockback enchantment effects? They are attributes, and the two enchantments reach them differently: Sweeping Edge raises Attributes.SWEEPING_DAMAGE_RATIO with an attribute modifier, while Knockback leaves Attributes.ATTACK_KNOCKBACK alone and adds to the number read from it, through an effect on the weapon. Attributes.SWEEPING_DAMAGE_RATIO defaults to zero, so a vanilla sweep does 1.0 — scaled by the attack-strength ratio, so slightly less than 1.0 anywhere in the sweep’s legal window below full charge. And Attributes.ATTACK_KNOCKBACK defaults to zero, so for an unenchanted sword the entire attacker-side knockback is the sprint bonus of 0.5.

What makes my own arm move? Your own client does, before the server hears about it. Every branch of Minecraft.startAttack that swings at all calls LivingEntity.swing, which on the client only sets the animation state, and the tail of the hit-result switch then sends ServerboundPunchPacket, a packet with no fields at all. That is why a miss on an air block still animates, and why the animation never waits for a round trip. The server’s own LivingEntity.swing then broadcasts ClientboundSwingAnimationPacket to your trackers — the players who can see you — and not back to you, because you played it a round trip ago.

Why does my swing look different with a different weapon? Because swing duration is a data component — ItemStack.getAttackAnimation returns a SwingAnimation — not a constant six ticks; MobEffects.MINING_FATIGUE stretches it and haste shortens it. The animation state itself is one LivingEntity.SwingState, which holds the current LivingEntity.SwingDescription (hand, animation and length in ticks), a tick count and the progress from zero to one, and ServerGamePacketListenerImpl.handlePunch is where the server receives yours. Crit particles are the exception to the never-echoed rule: they go to the attacker’s trackers and to the attacker too, while naming the victim.

Is this the only way to hit something in melee? No. Two other paths end in damage and neither goes through Player.attack: a PiercingWeapon short-circuits before the hit-result switch, and a KineticWeapon is reached from item use rather than attack. Both are the spear.

For a 1.21-era reader. ServerboundSwingPacket and LocalPlayer.swing are gone: after the local LivingEntity.swing, Minecraft.startAttack sends ServerboundPunchPacket itself, and the server passes the swing on as ClientboundSwingAnimationPacket rather than as a ClientboundAnimatePacket action. The fields swinging, swingingArm, swingTime and attackAnim are now one LivingEntity.SwingState, and the hurt cooldown that was Entity.invulnerableTime is LivingEntity.damageCooldownTime; the old name is now a separate timer that a hit does not set.

Where to look

Two methods are the whole lecture, and they are worth reading in this order. Minecraft.startAttack is the client’s branch point: almost every reason not to swing is in it (the rest are around it: Minecraft.tick skips the keybind drain while a screen or an overlay is up, and Minecraft.handleKeybinds swallows the click while you are using an item), and so are the two branches that do. Player.attack is the server’s, and everything surprising about melee combat is the order of the lines in it — read it once for the shape and a second time for where Player.baseDamageScaleFactor and Item.getAttackDamageBonus land relative to the ×1.5.

Around them: ServerGamePacketListenerImpl.handleAttack for the nine checks between the packet and the method, AttackRange for the reach floor the server subtracts away, and Weapon for the two-field component that charges the durability. Player.doSweepAttack and Player.itemAttackInteraction are the two halves of the tail worth opening on their own. On the wire, ServerboundAttackPacket takes ten seconds to read and ClientboundDamageEventPacket is worth the same for what it leaves out. Two doors this page only points at: ProjectileUtil, which is where the entity sweep happens, and SwingAnimation, the component behind the arm.


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