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

Status effects

Verified against Minecraft 26.3 · Part VIII · Poison II is already on you: the server hurts you on a rhythm, and your client never runs a single one of the effect’s hooks — it only counts.

Poison II lands. Your health starts dropping in steps, the swirls appear, the icon appears in the corner, and if the connection stutters the timer beside your inventory keeps counting anyway. That last part is the whole page. The client runs none of a MobEffect’s behaviour. It counts durations down, advances a blend factor and spawns particles from a list the server synched. It does read the effects it holds — jump boost in LivingEntity.getJumpBoostPower, slow falling in LivingEntity.getEffectiveGravity, levitation inside LivingEntity.travelInAir (movement and collision) — because that is shared movement code your own player runs unguarded, your client simulating its own player in earnest (authority). But every attribute modifier, every pulse of damage, every regeneration tick happens on the server behind an explicit server-side guard, and apart from the packets an event sends — a new or changed effect, a hidden one surfacing, a respawn or a change of dimension — the client’s copy of the duration is corrected only when the server happens to notice, which it does by testing whether the remaining duration divides by six hundred. For a finite effect that is once every six hundred ticks. An infinite effect’s duration is −1, which divides by nothing, so that test never re-sends it.

The cast

classwhat it decidesthread
MobEffectwhat the effect does, and on what rhythm — the singleton, shared by every holderServer (the client reads its colour, blend durations, name and category, and for a tooltip its attribute templates)
MobEffectInstanceduration, amplifier, flags, and the masked effect underneathboth
MobEffectsthe forty built-in holders—
LivingEntityLivingEntity.activeEffects, the tick, and the three server-guarded hooksboth
AttributeInstancewhere an effect’s modifier landsServer (a syncable attribute’s base and modifiers are sent to the client)
ServerPlayerwho gets told, and how oftenServer
MobEffectUtilthe questions the rest of the game asks about effectsboth

What an effect is

MobEffect is the behaviour singleton: a category, a colour, a particle factory, blend durations and a map of MobEffect.AttributeTemplates. Its hooks are the interesting part, because one of them is false by default.

hookwhen it runs
MobEffect.shouldApplyEffectTickThisTickevery tick, to ask whether this is a pulse — false by default
MobEffect.applyEffectTickon a pulse; returning false ends the effect
MobEffect.applyInstantaneousEffectnever from the tick — only from the splash potion, the lingering cloud and the drink
MobEffect.onEffectAddedwhen it lands on an entity that did not already have it
MobEffect.onEffectStartedon every LivingEntity.addEffect that passes LivingEntity.canBeAffected, whether or not it changed anything
MobEffect.onMobHurtwhen the holder takes damage
MobEffect.onMobRemovedwhen the holder is killed or discarded

The default false is why each effect has its own rhythm, and poison’s, regeneration’s and wither’s are each one number right-shifted by the amplifier — halved per level, floored at one tick. Poison’s is 25, so Poison I pulses every 25 ticks and the Poison II of this page’s scenario every 12; regeneration’s is 50, wither’s 40, and hunger’s override skips the arithmetic and pulses every tick. Attribute modifiers go on as AttributeInstance.addPermanentModifier with an amount linear in amplifier + 1, computed by MobEffect.AttributeTemplate.create (attributes).

MobEffectInstance is the per-entity half: duration, amplifier, the ambient, visible and show-icon flags, a private blend state, and MobEffectInstance.hiddenEffect.

That last one is a stack, and it is where a weaker effect goes when a stronger but shorter one lands on top of it. MobEffectInstance.update builds the chain, so Strength I with nine minutes left survives underneath a Strength II that has thirty seconds, and surfaces again through MobEffectInstance.downgradeToHiddenEffect when the stronger one expires. Both of the instance’s codecs are recursive, so the save file and the wire format are each capable of carrying the whole chain.

The wire does not. ClientboundUpdateMobEffectPacket never uses that stream codec at all: it writes an entity id, a MobEffect holder, an amplifier, a duration and a flags byte by hand. So the client rebuilds an instance with nothing under it, and learns about the masked one only when the surfacing on the server triggers a fresh packet — which is why a weaker effect can appear from nowhere in your list the instant a stronger one runs out.

MobEffectInstance.INFINITE_DURATION is −1, and MobEffectInstance.compareTo is what orders both lists of effects a player sees — ambient last, infinite next, then by remaining duration, then by colour, except that two ambient effects, or two with more than 32,147 ticks left, skip the infinite and duration steps. The two surfaces sort it in opposite directions — the HUD reversed, the inventory list not — and the HUD also lays its icons out from the right, so the two end up in the same order: left to right along the top of the screen, top to bottom beside your inventory.

The blend state beside those flags is a pure render quantity: it ticks only on the client, is never saved and never sent, and only MobEffects.NAUSEA and MobEffects.DARKNESS declare blend durations to use it. The wire carries one bit for it, set only when an effect is first added — an update clears the bit, and a client that did not have the effect skips the blend, while one that did carries its blend over (MobEffectInstance.copyBlendState). That is why Nausea swims in when you eat a pufferfish and simply carries on when you eat a second.

LivingEntity.canBeAffected is the veto, and it consults three entity tags as well as the effect itself.

MobEffectCategory is the third small vocabulary word, and it is three constants — MobEffectCategory.BENEFICIAL, MobEffectCategory.HARMFUL and MobEffectCategory.NEUTRAL — carrying nothing but a ChatFormatting. Two things read it. A potion’s tooltip takes each line’s colour from MobEffectCategory.getTooltipFormatting, which is why neutral prints blue like a benefit; and the HUD asks MobEffect.isBeneficial, which is true of MobEffectCategory.BENEFICIAL alone, to decide which of its two rows an icon goes in — so a neutral effect sits on the bottom row with the harmful ones while its tooltip is coloured with the good ones.

Those three are the vocabulary. The registry that uses them, MobEffects, is forty entries, and every one is a Holder<MobEffect>, not a bare MobEffect — including MobEffects.BREATH_OF_THE_NAUTILUS. Some point at attributes a reader would not expect: MobEffects.JUMP_BOOST modifies Attributes.SAFE_FALL_DISTANCE, and MobEffects.INVISIBILITY modifies Attributes.WAYPOINT_TRANSMIT_RANGE.

Behind all of it is a fourth family the page’s own hook rests on: world/effect holds a small MobEffect subclass for each effect that needs behaviour — PoisonMobEffect, RegenerationMobEffect, WitherMobEffect, HungerMobEffect, AbsorptionMobEffect and eight more, one of them serving both instant health and instant damage — with InstantaneousMobEffect the base of those two and saturation, which act on every tick an instance of them lasts. Everything else in MobEffects is a plain MobEffect with no code, eleven of them carrying an attribute template. The rhythms above are those subclasses overriding two methods each; the family is the shape, and no one member is worth a lecture.

On the entity itself: LivingEntity.activeEffects (a plain unordered map), LivingEntity.effectsDirty, and two synched values — LivingEntity.DATA_EFFECT_PARTICLES, which is a list of ParticleOptions rather than a packed colour, and LivingEntity.DATA_EFFECT_AMBIENCE_ID. The dirty flag has exactly one reader, and it is not this page’s tick: LivingEntity.updateDirtyEffects drains it into the invisibility and glowing flags, the swirl list and the ambience value from inside LivingEntity.updateDataBeforeSync, which the server’s entity-sync pass calls before it looks at what changed (synched entity data). An effect that expired this tick therefore opens its own gate.

That particle list is also nearly all a watcher gets, and it is deliberately hard to see. The default particle factory bakes ambience into the ParticleOptions itself — alpha 38 of 255 instead of opaque — so an ambient effect really does synch a different, fainter particle; and the client spawns one particle from the list on a one-in-n roll whose n is four, fifteen if the entity is invisible, and multiplied by five again when every visible effect on it is ambient, which is what LivingEntity.DATA_EFFECT_AMBIENCE_ID records. An invisible, wholly ambient entity is rolling one in seventy-five; a visible one in the same state, twenty.

MobEffectUtil is the shared question-asking surface: MobEffectUtil.hasDigSpeed, MobEffectUtil.hasWaterBreathing, MobEffectUtil.shouldEffectsRefillAirsupply, MobEffectUtil.addEffectToPlayersAround and the duration formatter the inventory screen uses.

Poison II counting down on two machines

Effects are ticked from LivingEntity.tickEffects, the last call LivingEntity.baseTick makes before it copies this tick’s rotations into last tick’s — which for a player means inside ServerPlayer.doTick, the connection-driven half of the two-phase tick, not the level’s entity tick.

On the server, that tick is three objects and two packets:

sequenceDiagram
    participant SP as ServerPlayer
    participant MEI as MobEffectInstance
    participant ME as MobEffect
    participant Wire as the network

    SP->>MEI: update, when one is already there — a longer, weaker one goes under
    SP->>ME: addAttributeModifiers — LivingEntity.onEffectAdded, or onEffectUpdated on a change
    SP->>Wire: ClientboundUpdateMobEffect<br/>Packet
    rect rgba(0, 0, 0, 0.04)
        Note over SP,Wire: every server tick after that
        SP->>MEI: tickServer, from LivingEntity.tickEffects
        MEI->>ME: shouldApplyEffectTickThisTick — 25 halved per level, for poison
        MEI->>ME: applyEffectTick — the pulse, and false here ends the effect
        SP->>Wire: ClientboundUpdateMobEffect<br/>Packet, when the duration divides by 600
    end

Two arrows leave for the client and the second is the periodic correction: inside the band the duration is the server’s, and unless something changes the client hears about it only on the ticks where it divides by six hundred.

The client’s half of the same scenario is a different set of objects, and the first thing to notice about it is which class is not in it:

sequenceDiagram
    participant Wire as the network
    participant CPL as ClientPacketListener
    participant LE as LivingEntity
    participant MEI as MobEffectInstance

    Wire->>CPL: ClientboundUpdateMobEffect<br/>Packet
    CPL->>LE: forceAddEffect — a fresh instance, with nothing under it
    rect rgba(0, 0, 0, 0.04)
        Note over Wire,MEI: every client tick after that
        LE->>MEI: tickClient — count down and advance the blend
    end

There is no MobEffect lane here, and that absence is the page: the client holds the instance and counts it, and never once asks the singleton what the effect does.

The client branch of LivingEntity.tickEffects never calls MobEffect.applyEffectTick and never touches an attribute. It does not even remove an expired effect: it keeps a zero-duration instance until told otherwise. A client runs an effect’s hooks only when shared code calls LivingEntity.addEffect on it without a guard — a meal it replays (a suspicious stew, an ominous bottle), a turtle helmet worn out of water, a cookie fed to a parrot — and none of the hooks those reach does anything there.

What survives a save, and why the modifiers do

LivingEntity.addAdditionalSaveData writes the active list under active_effects through MobEffectInstance.CODEC, and only when the list is non-empty; LivingEntity.readAdditionalSaveData reads it back, clears LivingEntity.activeEffects and puts each instance straight in. Nothing on that path runs LivingEntity.onEffectAdded, so the hook that would install the attribute modifier never fires on load — and the effect’s Strength still works, because the modifier was written into the AttributeInstance as a permanent one and was itself saved and restored (attributes). The two halves of an effect are persisted separately and reunited by convention rather than by code, which is why the codec that can carry a hidden-effect chain matters at all.

Questions players ask

Why does my duration sometimes jump? Because it was wrong and got corrected. The client’s duration is a local countdown with nothing keeping it honest between corrections, and the periodic one is a divisibility test: LivingEntity.tickEffects calls LivingEntity.onEffectUpdated whenever the remaining duration divides by six hundred. The re-send only ever reaches the affected player or a player riding them — and never at all for an infinite effect, whose −1 divides by nothing.

Why can I not see how long a mob’s effect has left? Because you were never told. A client watching a mob it is not riding receives no MobEffectInstance at all — only what the effects show: the synched particle list and flags, and any syncable attribute an effect has changed — which is why other entities have swirls and no numbers.

Does an effect pulse on its own clock or the world’s? Never the world’s. MobEffectInstance.tickServer counts an infinite-duration effect’s pulses off the entity’s own age and a finite one off its own countdown.

What happens when one effect adds another? The rest of that tick’s effects are silently skipped. LivingEntity.tickEffects catches a concurrent-modification error and drops it, so an effect that adds or removes another quietly aborts the loop it was in.

Where does the effect come from in the first place? Not from this page. Everything above starts at the moment an instance exists on a LivingEntity; the machinery that puts one there — the use timer, the finish, the replay — is using an item; the components that ride on it, PotionContents and SuspiciousStewEffects among them, belong to hunger and experience, and the consume effects, ApplyStatusEffectsConsumeEffect among them, to using an item. What crosses the wire from here on is ClientboundUpdateMobEffectPacket and ClientboundRemoveMobEffectPacket for the effects you hold, LivingEntity.DATA_EFFECT_PARTICLES through synched entity data for everyone else’s swirls, and ClientboundUpdateAttributesPacket for an effect on a syncable attribute such as speed. The effects themselves are code, registered into BuiltInRegistries.MOB_EFFECT by MobEffects with no JSON behind them; what is data-driven is some of the ways they land.

Where to look

LivingEntity.tickEffects is the page in one method: the server branch and the client branch sit side by side in it, and the asymmetry is visible at a glance. Read MobEffectInstance next, for the hidden-effect chain and the two codecs that can carry it, then MobEffect for the seven hooks and the one that is false by default. PoisonMobEffect is a short subclass and is the cheapest way to see what an override overrides; world/effect has a dozen more of the same shape.

MobEffects is worth one pass for the two attribute targets nobody expects, and MobEffectCategory takes a minute and explains why a neutral effect is coloured like a benefit and filed with the harmful ones. When you want to know what the client is allowed to know, ClientboundUpdateMobEffectPacket is five fields written by hand and is the whole answer. Two doors this page only points at: LivingEntity.canBeAffected, the veto and its three entity tags, and MobEffectUtil, which is where the rest of the game asks its questions.


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