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
| class | what it decides | thread |
|---|---|---|
MobEffect | what the effect does, and on what rhythm — the singleton, shared by every holder | Server (the client reads its colour, blend durations, name and category, and for a tooltip its attribute templates) |
MobEffectInstance | duration, amplifier, flags, and the masked effect underneath | both |
MobEffects | the forty built-in holders | — |
LivingEntity | LivingEntity.activeEffects, the tick, and the three server-guarded hooks | both |
AttributeInstance | where an effect’s modifier lands | Server (a syncable attribute’s base and modifiers are sent to the client) |
ServerPlayer | who gets told, and how often | Server |
MobEffectUtil | the questions the rest of the game asks about effects | both |
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.
| hook | when it runs |
|---|---|
MobEffect.shouldApplyEffectTickThisTick | every tick, to ask whether this is a pulse — false by default |
MobEffect.applyEffectTick | on a pulse; returning false ends the effect |
MobEffect.applyInstantaneousEffect | never from the tick — only from the splash potion, the lingering cloud and the drink |
MobEffect.onEffectAdded | when it lands on an entity that did not already have it |
MobEffect.onEffectStarted | on every LivingEntity.addEffect that passes LivingEntity.canBeAffected, whether or not it changed anything |
MobEffect.onMobHurt | when the holder takes damage |
MobEffect.onMobRemoved | when 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.