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

Hunger and experience

Verified against Minecraft 26.2 · Part VIII · Two bars above the hotbar that the server owns outright: one you empty by sprinting, one you fill by mining — and they meet in the enchanting table.

The food bar and the experience bar look like the same kind of thing: a number the server keeps and sends you. They are, and they are also the two counters a player argues with most — the meal that does not seem to fill you, the levels that vanish into an anvil. Both are worth following for the same reason: neither is quite what the interface suggests. There is no method called eat on Player or LivingEntity — eating is a walk over the components of a stack that happens to end in FoodData.eat — and the experience packet is change-detected on the total alone, so every mutation that changes only your level has to poison the last-sent value or the bar will not move.

The cast

classwhat it decidesthread
FoodDatathe food bar, saturation and exhaustion — and by type, only on the serverserver main
FoodPropertieshow much a given item is worth, as a data componentboth
Consumablehow long eating takes, what it sounds like, and what else it appliesboth
Playerthe four experience fields, the level curve, and the enchanting seedboth
ExperienceOrba value and a multiplicity, wandering toward youboth
ServerPlayerthe change detection that turns any of this into a packetserver main

Both halves hang off ServerPlayer.doTick, the connection-driven half of the two-phase tick; the level’s entity tick touches essentially none of it. The order inside that half matters: item use is resolved, then Player.aiStep runs ServerPlayer.tickRegeneration — which is the Peaceful refill, its whole body gated on that difficulty — and the orb pickup, then FoodData.tick, and then the change-detection block that emits the packets. So a meal eaten this tick and a Hunger effect’s exhaustion from this tick are both visible to FoodData.tick in the same tick, and the resulting health and food reach the client in that tick too.

The food bar is four numbers and a pile of literals

FoodData (world/food) is a value bag with no back-reference to the player: FoodData.foodLevel (20), FoodData.saturationLevel (5.0), FoodData.exhaustionLevel and FoodData.tickTimer. Its surface is the two FoodData.eat overloads — one taking a FoodProperties, one taking a nutrition and saturation pair — plus FoodData.addExhaustion (which caps at 40), FoodData.needsFood, FoodData.hasEnoughFood, FoodData.setFoodLevel, FoodData.setSaturation, the two accessors, and FoodData.tick — whose signature is FoodData.tick(ServerPlayer), server-only by type. It saves as four loose keys in the player tag, not a sub-compound.

FoodConstants names every threshold in the system — FoodConstants.MAX_FOOD, FoodConstants.HEAL_LEVEL, FoodConstants.HEALTH_TICK_COUNT, FoodConstants.HEALTH_TICK_COUNT_SATURATED, FoodConstants.EXHAUSTION_DROP, FoodConstants.EXHAUSTION_HEAL, FoodConstants.EXHAUSTION_SPRINT, FoodConstants.EXHAUSTION_MINE, FoodConstants.EXHAUSTION_ATTACK, FoodConstants.SPRINT_LEVEL, FoodConstants.SATURATION_FLOOR and the saturation-quality ladder from FoodConstants.FOOD_SATURATION_POOR to FoodConstants.FOOD_SATURATION_SUPERNATURAL — and none of them is referenced by anything. Only FoodConstants.saturationByModifier has call sites; FoodData writes every threshold as an inline literal, so the constants file and the behaviour can drift apart without a compile error.

What FoodData.tick does with those literals is one exhaustion rule and a three-way mutually exclusive chain:

flowchart TD
    EX["exhaustion above 4.0?"]
    EX -- "yes" --> DRAIN["spend 4.0 of exhaustion, and take 1.0 off saturation — or one off the food bar once saturation is spent and the difficulty is not Peaceful"]
    EX -- "no" --> CHAIN
    DRAIN --> CHAIN["then at most one of the three"]
    CHAIN --> FAST["heal fast: every 10 ticks, at a full bar, hurt, with saturation left and the game rule on — the heal is funded by that saturation"]
    CHAIN --> SLOW["heal slowly: every 80 ticks, at 18 or more food, hurt, and the game rule on"]
    CHAIN --> STARVE["starve: every 80 ticks at zero food, and not gated on the game rule at all"]

The game rule is GameRules.NATURAL_HEALTH_REGENERATION, which lives in world/level/gamerules with typed GameRule lookups (level data and rules). The starvation hit only lands if health is above five hearts, or above half a heart on Normal, or unconditionally on Hard: five hearts is the floor on Easy and Peaceful, half a heart on Normal, and death on Hard. DamageTypes.STARVE is declared with zero exhaustion, so starving does not feed itself.

Eating is a component walk

FoodProperties (DataComponents.FOOD) is three things: nutrition, saturation and can always eat. The duration is not on it — that lives on Consumable (DataComponents.CONSUMABLE), which owns Consumable.consumeSeconds (with Consumable.consumeTicks derived from it), the ItemUseAnimation, the sound, the particles and a list of ConsumeEffects: ApplyStatusEffectsConsumeEffect, RemoveStatusEffectsConsumeEffect, ClearAllStatusEffectsConsumeEffect, TeleportRandomlyConsumeEffect, PlaySoundConsumeEffect.

FoodProperties reaches the player by implementing ConsumableListener, and Consumable.onConsume walks every component of that type on the stack. It is not the only implementation — PotionContents, SuspiciousStewEffects and OminousBottleAmplifier implement it too, and PotionContents is how drinking applies an effect, which is where this page and status effects meet in one method. Two routes reach FoodData.eat without any of that: CakeBlock.eat, and the saturation effect, both using the raw nutrition-and-saturation overload.

sequenceDiagram
    participant LE as LivingEntity
    participant IStack as ItemStack
    participant Cons as Consumable
    participant FP as FoodProperties
    participant FD as FoodData
    participant SP as ServerPlayer
    participant CPL as ClientPacketListener

    LE->>LE: updateUsingItem — the zero check is server side only
    SP->>CPL: ClientboundEntityEventPacket(9) — sent first, so the client replays the meal
    LE->>IStack: finishUsingItem — the item decides what finishing means
    IStack->>Cons: onConsume — walks every ConsumableListener on the stack
    Cons->>FP: onConsume — the food component is one such listener
    FP->>FD: eat — nutrition and pre-multiplied saturation, clamped
    Cons->>Cons: onConsumeEffects — server only#59; then consume(1)
    SP->>FD: tick — exhaustion drain, then regen or starvation
    SP->>CPL: ClientboundSetHealthPacket — when health, food or zero-saturation changed

Consumable.canConsume consults Player.canEat only when the stack has DataComponents.FOOD and the user is a player — potions and milk are ungated — and Player.canEat itself passes for invulnerable abilities, can always eat, or FoodData.needsFood. An item whose Consumable.consumeTicks is zero is consumed instantly with no animation. After the food lands, ItemStack.finishUsingItem applies DataComponents.USE_REMAINDER and DataComponents.USE_COOLDOWN (using an item).

The decision to finish is server-only, but the client replays the meal: the server announces it with an entity event, Player.handleEntityEvent turns that back into LivingEntity.completeUsingItem, and the client therefore runs FoodProperties.onConsume and its FoodData.eat locally, with no side guard on it. ClientPacketListener.handleSetHealth then overwrites food and saturation outright, and routes health through LocalPlayer.hurtTo, which works out the delta first so the damage flash still plays. The client also reads its food data for two decisions of its own: sprinting is gated on having more than six food or being able to fly, and the HUD’s food-bar jitter reads saturation.

Eating slowdown is a third component again. UseEffects (DataComponents.USE_EFFECTS) is on every item — UseEffects.canSprint, UseEffects.interactVibrations, UseEffects.speedMultiplier — and its slowdown half is genuinely client-side: LocalPlayer.isSlowDueToUsingItem and LocalPlayer.itemUseSpeedMultiplier are its only readers, which is how a spear overrides the component and lets you sprint while charging (the spear). The vibration half is not client-side: ItemStack.causeUseVibration reads the same component server-side to decide whether using an item emits a game event.

The other bar

Player.experienceLevel, Player.experienceProgress, Player.totalExperience and Player.enchantmentSeed, plus Player.takeXpDelay and Player.lastLevelUpTime — which exists only to throttle the level-up sound — are the whole of it. The arithmetic is Player.giveExperiencePoints, Player.giveExperienceLevels and Player.getXpNeededForNextLevel — the three-segment curve with corners at levels 15 and 30.

Where orbs come from is worth naming, because two game rules gate it: LivingEntity.dropExperience requires the experience not to have been consumed already, and either an always-dropper or a recent player kill with GameRules.MOB_DROPS on and the entity’s own LivingEntity.shouldDropExperience agreeing; a player’s own death drop is Player.getBaseExperienceReward, seven per level capped at 100, unless GameRules.KEEP_INVENTORY — or unless you are a spectator, which drops nothing.

ExperienceOrb is an Entity with ExperienceOrb.DATA_VALUE synched and ExperienceOrb.count, ExperienceOrb.age, ExperienceOrb.health and ExperienceOrb.followingPlayer unsynched — though ExperienceOrb.age and ExperienceOrb.followingPlayer are still mutated by the client’s own tick, which runs the follow behaviour locally. ExperienceOrb.health is not: only ExperienceOrb.hurtServer ever writes it. ExperienceOrb.awardWithDirection splits an amount into denominations via ExperienceOrb.getExperienceValue (a fixed ladder from 2477 down to 1) and calls ExperienceOrb.tryMergeToExisting for each; ExperienceOrb.award is a one-line delegate to it. Merging is by count, not value: an orb carries one value and a multiplicity. The merge candidate search picks a random group number below ExperienceOrb.ORB_GROUPS_PER_AREA and only merges into orbs whose id is congruent to it — which caps how many orbs collapse into one entity rather than reducing the scan.

Questions players ask

Why does walking cost nothing? Because it is multiplied by zero, out loud: ServerPlayer.checkMovementStatistics multiplies distance by a literal zero on both the walking and the crouching branch, while FoodConstants.EXHAUSTION_WALK documents the intent and is referenced by nothing. The exhaustion economy is sprinting, jumping, swimming, mining, attacking, the Hunger effect, the ApplyExhaustion enchantment effect, and being hurt — the last of which is data-driven, since DamageSource.getFoodExhaustion reads the damage type. And in creative or spectator the whole economy is disabled in one line: Player.causeFoodExhaustion returns immediately for invulnerable abilities.

Why does my saturation sit above my food bar on Peaceful? ServerPlayer.tickRegeneration raises saturation directly toward 20, while FoodData.eat clamps saturation to the food level. Only one of the two respects the clamp.

Why does the saturation shown by the HUD lag? Because it is sent but not change-detected. ClientboundSetHealthPacket carries a full float, yet the server only notices whether saturation became zero — so your client’s saturation does not update until health or food moves.

Why does enchanting update the bar when the level packet is change-detected on the total? Because the code forces it. ServerPlayer.lastSentExp is compared against Player.totalExperience, so every mutation that changes only the level — ServerPlayer.setExperienceLevels, Player.giveExperienceLevels, enchanting, respawn — has to poison the last-sent value to make the packet go out.

Why does an orb repair my pickaxe before it reaches my bar? Because ExperienceOrb.playerTouch runs ExperienceOrb.repairPlayerItems first and only the remainder becomes experience — and that method calls itself with the leftover. One orb entity can also be picked up many times: it carries a count, decremented per touch behind a two-tick Player.takeXpDelay, and a player absorbs one orb per tick, chosen at random from those it is touching. The pickup sweep buckets orbs separately from items for exactly that purpose.

Why is the Standard Galactic gibberish stable until I enchant? Because Player.onEnchantmentPerformed subtracts the level cost and re-rolls Player.enchantmentSeed, while AnvilMenu — which also spends levels, through Player.giveExperienceLevels — does not. A seed that loads back as zero is re-rolled on read. Enchanting owns what the seed is for.

What crosses the wire? ClientboundSetHealthPacket (health, food and saturation together, to that player only), ClientboundSetExperiencePacket (progress, level and total), and ClientboundTakeItemEntityPacket for the orb pickup animation. The data-driven side is DataComponents.FOOD, DataComponents.CONSUMABLE, DataComponents.USE_EFFECTS, Registries.CONSUME_EFFECT_TYPE, EnchantmentEffectComponents.REPAIR_WITH_XP for mending, and the three game rules above.

Where to look

FoodData · FoodConstants · FoodProperties · Consumable · ConsumableListener · ConsumeEffect · UseEffects · Foods · Player.giveExperiencePoints · ExperienceOrb · ServerPlayer.tickRegeneration · ClientboundSetHealthPacket · ClientboundSetExperiencePacket


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