Game events and vibrations
Verified against Minecraft 26.3 · Part IV · A footstep reaches a sculk sensor.
You walk across a stone floor and, eight blocks away, a sculk sensor’s
tendrils flick up and it pushes redstone power out of its side. Nothing
scanned for you: the step itself posted a GameEvent.STEP into
GameEventDispatcher.post, which walked the loaded chunk sections around you
and called each listener inside that listener’s own radius — all but the
catalyst’s inline — before Entity.move had finished. Nothing about that
broadcast waits for a later tick — and yet
what a player believes about sculk lives entirely in the delay, which the last
stages put back. A sensor never acts on an event in the game tick that
stamped it: VibrationSelector.chosenCandidate hands over a candidate only if
it was recorded on an earlier tick — which costs a mob’s footstep a whole
server tick and yours nothing, because the server handles a player’s movement
packet before the level tick moves the clock on. Everything else a player
thinks is stealth — the wool box, the crouch — is one of the gates between
that footstep and that slot, and the crouch does not work the way the folklore
says.
The cast
| class | what it decides | thread |
|---|---|---|
GameEvent | how far the news travels — one field, GameEvent.notificationRadius | — (a record) |
GameEventDispatcher | which sections are walked, and who is told inline rather than sorted | Server |
EuclideanGameEventListenerRegistry | which listeners in one section Y are close enough to be visited | Server |
DynamicGameEventListener | which section a moving listener is registered in | Server |
VibrationSystem.Listener | the gates between an event and a candidate | Server |
VibrationSelector | which of a tick’s candidates survives, and that none is used before the next tick | Server |
VibrationSystem.Ticker | the travel countdown, the particle, and the moment of arrival | Server, from the host’s own tick |
SculkSensorBlockEntity.VibrationUser | whether this sensor takes a vibration, and what it does with one | Server |
A game event is one number
GameEvent is a record of a single field, GameEvent.notificationRadius.
That is the whole type. Sixty-one constants live in
BuiltInRegistries.GAME_EVENT, fifteen of them GameEvent.RESONATE_1
through GameEvent.RESONATE_15, one per vibration frequency, so a
resonating block can re-emit the frequency it heard. All but three take
GameEvent.DEFAULT_NOTIFICATION_RADIUS, 16 blocks: GameEvent.JUKEBOX_PLAY
and GameEvent.JUKEBOX_STOP_PLAY are 10, GameEvent.SHRIEK is 32. The
radius is the dispatcher’s number, and each listener then applies its
own. Everything else rides beside the event in a GameEvent.Context — the
source entity and the affected block state, either of which may be absent.
The registry is a DefaultedMappedRegistry on step, which is narrower
than it sounds. DefaultedMappedRegistry.getValue and
DefaultedMappedRegistry.byId substitute GameEvent.STEP for an unknown
key, so a raw lookup that misses becomes a footstep — but GameEvent.CODEC
is a RegistryFixedCodec, which errors on an unknown name. Bad data in a
pack fails loudly; only the raw lookup silently becomes a step.
Listeners live in the chunk, one registry per section
A GameEventListener is four methods: a PositionSource
(BlockPositionSource for a block, EntityPositionSource for a mob, the
latter resolving a stored UUID against the level when it is asked, until it
finds the entity), a GameEventListener.getListenerRadius,
GameEventListener.handleGameEvent and a
GameEventListener.getDeliveryMode. They are stored per chunk section in
LevelChunk.gameEventListenerRegistrySections
(chunk anatomy), and LevelChunk.getListenerRegistry
creates an EuclideanGameEventListenerRegistry for a section Y the first
time anyone asks — including the dispatcher, merely walking past. One is
dropped again through LevelChunk.removeGameEventListenerRegistry, but
only when an EuclideanGameEventListenerRegistry.unregister empties it.
ChunkAccess.getListenerRegistry answers
GameEventListenerRegistry.NOOP: proto chunks and the client have none.
Block entities join on the chunk’s terms: LevelChunk.addGameEventListener
runs from LevelChunk.addAndRegisterBlockEntity on placement and from
LevelChunk.registerAllBlockEntitiesAfterLevelLoad when the chunk comes
back, asking EntityBlock.getListener — whose default returns the listener
of any block entity implementing GameEventListener.Provider (block
entities). A sensor’s section is fixed for
the life of the block. Entities move, so they carry a
DynamicGameEventListener instead: a listener plus the last SectionPos
it was filed under. Entity.updateDynamicGameEventListener is empty on
Entity and overridden by Warden and by Allay, which hands over two.
ServerLevel.EntityCallbacks drives it — DynamicGameEventListener.add,
DynamicGameEventListener.remove, and DynamicGameEventListener.move on
every section change. Its two halves are guarded separately and its record of
where it was advances either way, so a move whose old chunk is not loaded to
ChunkStatus.FULL leaves a stale registration behind, and one whose new
chunk is not takes the listener out of the world entirely.
The broadcast is a nested loop, and one listener is the exception
ServerLevel.gameEvent is one line into GameEventDispatcher.post, and
ServerLevel.gameEventDispatcher owns nothing between calls. GameEventDispatcher.post turns
the radius into a range of sections — 3 by 3 by 3 for the default 16, 5 by
5 by 5 for a shriek — fetches each chunk column once with
ServerChunkCache.getChunkNow, and visits each section’s registry.
EuclideanGameEventListenerRegistry.visitInRangeListeners resolves each
listener’s position, compares block-position distance squared against the
listener’s own radius squared — a sphere inside the dispatcher’s cube — and
calls GameEventListener.handleGameEvent there and then. No queue, no
ordering, no next-tick delivery: the broadcast is a nested loop inside
Entity.move, and the emitter’s method has not returned yet.
Two things follow. ServerChunkCache.getChunkNow returns null for a column
that is not already loaded and GameEventDispatcher.post skips it, so an
event at the edge of the loaded world is never delivered over the border.
And because a listener can be told mid-walk, a registry sets
EuclideanGameEventListenerRegistry.processing while it iterates and
defers every EuclideanGameEventListenerRegistry.register and
EuclideanGameEventListenerRegistry.unregister to the end of the visit.
One listener does wait. GameEventListener.getDeliveryMode is
GameEventListener.DeliveryMode.UNSPECIFIED everywhere except
SculkCatalystBlockEntity.CatalystListener, which answers
GameEventListener.DeliveryMode.BY_DISTANCE. Those are collected as
GameEvent.ListenerInfos, sorted by squared distance and delivered by
GameEventDispatcher.handleGameEventMessagesInQueue after the walk,
because a catalyst consumes the dead mob’s experience
(LivingEntity.skipDropExperience) and LivingEntity.wasExperienceConsumed
means only the first told gets any. Sorting is how the nearest one wins.
The gates between a footstep and a candidate
The cascade has a joint in it, and the prose above has already named it: the
walk is the dispatcher’s, and every gate after GameEventListener.getDeliveryMode
belongs to one listener deciding about itself. So it is two pictures. First, the
walk.
flowchart TD
A{"Entity.applyMovementEmissionAndPlaySound"}
A -->|"mid-stride"| X1["no event is posted at all"]
A -->|"a stride has passed"| C["ServerLevel.gameEvent posts GameEvent.STEP at the entity"]
C --> D["GameEventDispatcher.post walks 3 by 3 by 3 sections"]
D --> E{"ServerChunkCache.getChunkNow"}
E -->|"not loaded"| X2["skipped in silence, never retried"]
E -->|"loaded"| F{"EuclideanGameEventListenerRegistry.visitInRangeListeners"}
F -->|"out of its radius"| X3["not visited"]
F -->|"in range"| G{"GameEventListener.getDeliveryMode"}
G -->|"BY_DISTANCE"| Q["queued, sorted by distance, delivered after the walk"]
G -->|"UNSPECIFIED"| H["GameEventListener.handleGameEvent, inline"]
From a footstep to a listener that has been handed it: every exit on the left is silent — nothing is loaded, nothing is retried, nothing is told. The two right-hand ends are the only two ways the walk finishes, and the sculk catalyst is the only listener in the game that takes the queued one.
Both ends are a real delivery, and the difference is when. Everything below
continues from the inline one, and narrows again:
VibrationSystem.Listener.handleGameEvent is the vibration path, which
Allay.JukeboxListener is not on at all.
flowchart TD
H["VibrationSystem.Listener.handleGameEvent"]
H --> B1{"a vibration already in flight"}
B1 -->|"yes"| DROP["dropped, and nothing is remembered"]
B1 -->|"no"| B2{"VibrationSystem.User.isValidVibration"}
B2 -->|"fails"| DROP
B2 -->|"passes"| B3{"the listener's PositionSource"}
B3 -->|"does not resolve"| DROP
B3 -->|"resolves"| B4{"VibrationSystem.User.canReceiveVibration"}
B4 -->|"fails"| DROP
B4 -->|"passes"| B5{"VibrationSystem.Listener.isOccluded"}
B5 -->|"all six rays stopped"| DROP
B5 -->|"one gets through"| L["VibrationSelector.addCandidate"]
S["SculkSensorBlock.stepOn, standing on the sensor"] --> S2{"not a warden, and the sensor can activate"}
S2 -->|"VibrationSystem.User.canReceiveVibration"| L
Five refusals in the order they run, all of them landing in one place, and a short arm that arrives at the answer having asked almost none of them. Read the order rather than the tests: the busy check is first and costs nothing, the six-ray occlusion cast is last and costs the most, and the arm down the right is a player standing on the block.
The order is not the one a player would guess. The busy check comes first,
so a sensor already carrying a vibration ignores everything without
evaluating a single tag, and the occlusion raycast — much the most
expensive gate — comes last, after every cheap refusal has had its chance.
Before any of it, Entity.applyMovementEmissionAndPlaySound decides
whether there is an event to post, by accumulating Entity.moveDist and firing
only when it passes Entity.nextStep — which is why walking emits a footstep
per stride rather than per tick. Three gates ask the user rather than the
system: VibrationSystem.User.getListenableEvents supplies the tag
VibrationSystem.User.isValidVibration tests first, the user’s
PositionSource must resolve, and
SculkSensorBlockEntity.VibrationUser.canReceiveVibration refuses
GameEvent.BLOCK_DESTROY and GameEvent.BLOCK_PLACE at the sensor’s own
position — which is why placing a sensor does not set it off — refuses a
frequency of VibrationSystem.NO_VIBRATION_FREQUENCY, and otherwise defers
to SculkSensorBlock.canActivate: inactive only.
The short arm down the right of that figure is the gate a player most often
meets. SculkSensorBlock.stepOn runs from
Entity.applyEffectsFromBlocks every tick an entity stands on the block, and
calls VibrationSystem.Listener.forceScheduleVibration directly: no section
walk, no radius test, no occlusion raycast, and — the part that matters — no
VibrationSystem.User.isValidVibration, which is where the sneaking tag
lives. Crouching on a sensor does not quiet it. What the shortcut still asks
is VibrationSystem.User.canReceiveVibration, so an already-active sensor
stays quiet, and SculkSensorBlock.canActivate; and it still refuses a warden
by name. It ends where everything else ends, at
VibrationSelector.addCandidate, so it buys no time either.
The occlusion test is worth reading slowly.
VibrationSystem.Listener.isOccluded takes the source block’s centre,
nudges it a hundred-thousandth of a block along each of the six Direction
values in turn, and runs BlockGetter.isBlockInLine with a
ClipBlockStateContext looking for BlockTags.OCCLUDES_VIBRATION_SIGNALS.
It reports occluded only if all six rays are stopped, and returns the moment
one is not — but the six run a hundred-thousandth of a block apart to the same
point, so a block of wool the straight line passes through stops all six, and
only a line grazing a block’s edge can slip one past. A wool box is a box
because vibrations come from every side.
One footstep, and the ticks it takes to arrive
Every gate above passes, and the vibration still does not arrive for eight ticks of game time. This is where they go.
sequenceDiagram
participant Entity as Entity
participant SL as ServerLevel
participant VSL as VibrationSystem.<br/>Listener
participant VSel as VibrationSelector
participant VST as VibrationSystem.<br/>Ticker
participant SSVU as SculkSensor<br/>BlockEntity.<br/>VibrationUser
participant SSB as SculkSensorBlock
Note over Entity,SSB: tick T, a mob moves in its own tick
Entity->>SL: gameEvent, GameEvent.STEP at the entity's feet
SL->>VSL: the dispatcher's walk reaches it, inline
VSL->>VSel: addCandidate, a VibrationInfo stamped with game time T
Note over VST,SSB: still tick T, whenever the sensor's block entity ticks
VST->>VSel: chosenCandidate
VSel-->>VST: nothing, the candidate is not from an earlier tick
Note over Entity,SSB: tick T plus 1
VST->>VSel: chosenCandidate
VSel-->>VST: the VibrationInfo
VST->>SL: sendParticles, one VibrationParticleOption
VST->>VSel: startOver clears the slot
Note over VST,SSB: the countdown starts now, a block a tick
Note over Entity,SSB: tick T plus 8, and only now does it arrive
VST->>SSVU: onReceiveVibration — the event, the entities, the distance
SSVU->>SSB: activate, with the power and the frequency
SSB->>SL: setBlock PHASE active with POWER, then scheduleTick 30
Note over Entity,SSB: 30 ticks later deactivate, then 10 more before inactive
One footstep, and the eight ticks of game time it takes to be heard eight blocks away: a mob’s candidate stamped in tick T is refused in tick T and chosen in tick T plus 1, then travels a block a tick. That first wait is a whole server tick for a mob, which moves inside its tick, and none for a player, whose move is handled from its packet before the level tick moves the clock on.
VibrationSystem.Ticker.tick is the whole of the wait, and runs from
whoever hosts the listener: SculkSensorBlock.getTicker,
CalibratedSculkSensorBlock.getTicker and SculkShriekerBlock.getTicker for
the blocks — server-side only, and
already gated by Level.shouldTickBlocksAt on their own chunk — and
Warden.tick and Allay.tick for the mobs. One call does three things in
order: select, if nothing is in flight; send or re-send the particle; then
decrement the travel time and arrive if it has reached zero.
That particle is the only thing the client is told.
VibrationParticleOption carries the destination PositionSource and the
remaining tick count, and the client animates the flight from that alone:
ClientLevel.gameEvent is an empty method and there is no vibration
packet. When the block entity was loaded from disk,
VibrationSystem.Data.shouldReloadVibrationParticle answers yes and the
ticker re-sends the particle from a point interpolated along the path covered.
One slot, one tick late, and one refusal that waits
VibrationSelector holds at most one candidate — a VibrationInfo, which
carries the event, the source position, the distance and the entity behind it —
stamped with the game time it arrived. VibrationSelector.addCandidate takes an empty slot
unconditionally; against a candidate from the same tick it takes the
closer one, breaking a distance tie in favour of the higher frequency; and
against one held over from an earlier tick it does nothing at all,
because that one is already waiting to be consumed. Reading the slot does not
empty it: VibrationSelector.startOver does, and the ticker calls it in the
same breath as the read. That is the whole of
“the nearest event wins” — per listener, per tick, one slot.
The latency falls out of the read side. VibrationSelector.chosenCandidate
returns the candidate only if its stamp is strictly less than the current
game time, so one added during tick T is invisible for the rest of tick T
however the ordering falls, and the earliest it can be selected is the tick
after — the next server tick for a mob, and the same server tick for a player,
whose move is handled from its packet before the level tick moves the clock
on. Travel is measured from there:
VibrationSystem.User.calculateTravelTimeInTicks is the floor of the
distance — one block per tick — and the countdown’s first decrement happens
inside the same call that selected the vibration, so a source n whole
blocks away arrives n minus 1 ticks after selection, and anything closer
than two blocks arrives on the selecting tick itself.
Arrival is one call — VibrationSystem.User.onReceiveVibration, handed the
event, the source entity, the projectile’s owner and the distance from the
source’s block to the listener’s at arrival — and it can be refused.
VibrationSystem.User.requiresAdjacentChunksToBeTicking
is true for both sculk blocks, and VibrationSystem.Ticker will not deliver
unless all nine columns of the 3 by 3 around the listener are loaded and
pass Level.shouldTickBlocksAt — tickets and
loading is what “ticking” means. It does not drop
the vibration: the travel time is floored at zero, so the ticker asks again
every tick until the neighbourhood ticks. And the whole of
VibrationSystem.Data is written to disk under listener through
VibrationSystem.Data.CODEC, so a vibration survives a reload with its
countdown intact.
What arrival costs the block
VibrationSystem.VIBRATION_FREQUENCY_FOR_EVENT maps game events to
frequencies 1 to 15 and returns 0 for anything unmapped: a step, a swim and
a flap are 1, a death or an explosion 15. That number is kept by
SculkSensorBlockEntity.setLastVibrationFrequency and is what a comparator
reads out of an active sensor — SculkSensorBlock.getAnalogOutputSignal
answers 0 in any other phase. The redstone power is a different number:
VibrationSystem.getRedstoneStrengthForDistance is the larger of 1 and 15
minus the floor of 15 times the distance over the listener’s radius, using
a distance recomputed from the two block positions at arrival, not the
float stored when the candidate was made.
SculkSensorBlock.activate sets SculkSensorBlock.PHASE to
SculkSensorPhase.ACTIVE with that power, schedules a block tick
SculkSensorBlock.getActiveTicks out (30 for a plain sensor, the number
SculkSensorBlock.ACTIVE_TICKS names, and 10 for a calibrated one), runs
SculkSensorBlock.tryResonateVibration — which, for each of the six
neighbours in BlockTags.VIBRATION_RESONATORS, posts the matching
GameEvent.RESONATE_1 … GameEvent.RESONATE_15 at the neighbour’s
position, so an amethyst block beside a sensor rebroadcasts the frequency
it heard — and then emits GameEvent.SCULK_SENSOR_TENDRILS_CLICKING, the
single entry in GameEventTags.SHRIEKER_CAN_LISTEN. Shriekers do not hear
your footsteps; only standing on one sets it off directly. They hear sensors
hearing you.
Coming down takes two scheduled ticks. At 30, SculkSensorBlock.tick calls
SculkSensorBlock.deactivate, which drops the power to zero, moves the
phase to SculkSensorPhase.COOLDOWN and schedules another tick
SculkSensorBlock.COOLDOWN_TICKS (10) out; only that tick returns the
block to SculkSensorPhase.INACTIVE. In between the sensor is dark and
still refuses every vibration, because SculkSensorBlock.canActivate tests
for inactive.
The other listeners
| listener | radius | what it listens to | what a vibration does |
|---|---|---|---|
SculkSensorBlockEntity | 8 | GameEventTags.VIBRATIONS | activates for 30 ticks, power by distance, frequency on the comparator |
CalibratedSculkSensorBlockEntity | 16 | GameEventTags.VIBRATIONS | the same, but active for 10 ticks rather than 30, and when the block behind CalibratedSculkSensorBlock.FACING gives a redstone signal, only that exact frequency is accepted |
SculkShriekerBlockEntity | 8 | GameEventTags.SHRIEKER_CAN_LISTEN | needs a player behind the event, then SculkShriekerBlockEntity.tryShriek raises the warning level on a shrieker that can summon; when the shriek ends, darkness, and a warden at level 4 |
Warden | 16 | GameEventTags.WARDEN_CAN_LISTEN | anger through Warden.increaseAngerAt, a 40-tick MemoryModuleType.VIBRATION_COOLDOWN, and a disturbance location for WardenAi |
Allay | 16 | GameEventTags.ALLAY_CAN_LISTEN, note blocks only | AllayAi.hearNoteblock stores MemoryModuleType.LIKED_NOTEBLOCK_POSITION, after which it accepts that block and no other |
Allay.JukeboxListener | 10 | not a vibration at all | a plain GameEventListener for GameEvent.JUKEBOX_PLAY and GameEvent.JUKEBOX_STOP_PLAY — it makes the allay dance |
SculkCatalystBlockEntity.CatalystListener | 8 | not a vibration at all | on GameEvent.ENTITY_DIE, takes the mob’s experience as sculk cursors and blooms |
The tags are where the personalities live, and they are data
(tags): GameEventTags.WARDEN_CAN_LISTEN covers
shrieks and tendril clicks that GameEventTags.VIBRATIONS leaves out, and
leaves out the flap that sensors hear. The warden is also the one entity
whose Entity.dampensVibrations is unconditionally true — a dropped item
answers true too, but only while it holds something in
ItemTags.DAMPENS_VIBRATIONS — so the warden is invisible to every vibration
listener, wardens included, and the one entity
SculkSensorBlock.stepOn refuses by name. Its brain and the allay’s are
Part VI.
Questions players ask
Does sneaking make me silent? It makes six events silent.
GameEventTags.IGNORE_VIBRATIONS_SNEAKING holds GameEvent.STEP,
GameEvent.SWIM, GameEvent.HIT_GROUND, GameEvent.PROJECTILE_SHOOT,
GameEvent.ITEM_INTERACT_START and GameEvent.ITEM_INTERACT_FINISH, and
the test is Entity.isSteppingCarefully — whether the sneak key is down.
Open a chest while crouched and the sensor hears it. Sneak past one as a
player and CriteriaTriggers.AVOID_VIBRATION notes the advancement,
because sensors and wardens answer VibrationSystem.User.canTriggerAvoidVibration.
Why did my wool floor not stop it? Wool underfoot and wool in the way
are different tags doing different jobs. BlockTags.DAMPENS_VIBRATIONS on
the block being walked on kills the event at
VibrationSystem.User.isValidVibration, and ItemTags.DAMPENS_VIBRATIONS
does the same for a dropped item through ItemEntity.dampensVibrations.
BlockTags.OCCLUDES_VIBRATION_SIGNALS is the six-ray test, and it has to
stop all six.
Why does a sensor near the edge of the world miss? Two reasons, both
silent: GameEventDispatcher.post skips any column
ServerChunkCache.getChunkNow does not already have, and a sensor whose
3 by 3 neighbourhood is not ticking holds a finished vibration until it is.
The second is visible on the debug channel; the first is not, because the
broadcast happens only where a listener was visited —
DebugSubscriptions.GAME_EVENTS
and DebugSubscriptions.GAME_EVENT_LISTENERS, broadcast through
ServerLevel.debugSynchronizers
(debugging the running game).
Where to look
The broadcast, from the event to the door of a listener: GameEvent ·
GameEventDispatcher.post ·
EuclideanGameEventListenerRegistry.visitInRangeListeners ·
LevelChunk.getListenerRegistry. Then the gates, in the order the cascade
asks them: VibrationSystem.Listener.handleGameEvent ·
VibrationSystem.User.isValidVibration ·
VibrationSystem.Listener.isOccluded · VibrationSelector.addCandidate ·
VibrationSystem.Ticker.tick · VibrationSelector.chosenCandidate ·
SculkSensorBlock.activate. And the two doors out of this page:
SculkSensorBlock.stepOn for the shortcut, DynamicGameEventListener.move
for how a listener that walks stays filed.
The other index the world keeps about itself — where things worth walking to are, rather than what just happened — is points of interest. The brain behind a warden or an allay belongs to goals and brains.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.