Diodes and the observer
Verified against Minecraft 26.3 · Part V · A repeater, a comparator and an observer in one circuit — three blocks that learn about the world three different ways, and one of them is not listening to redstone at all.
Put a repeater, a comparator and an observer side by side and they look like
variations on one idea: blocks that take a signal in one side and
push one out the other. Two of them genuinely are — RepeaterBlock and
ComparatorBlock are both DiodeBlocks and share their output machinery,
differing in the number they emit and in what calls it. The observer is not a diode at all — it is
a DirectionalBlock, a full cube, with a six-way facing — and the way it finds
out that
something changed is the page’s hook: ObserverBlock fires from
ObserverBlock.updateShape, a shape update, so the one block whose entire
job is noticing change is not on the channel that carries change
notifications. That is not a curiosity. It is why an observer sees a door
opened by hand, an event that writes with Block.UPDATE_NEIGHBORS clear and
so fires no neighbour update of its own. And the repeater quietly uses the
same trick for its lock.
The cast
| class | what it decides | thread |
|---|---|---|
DiodeBlock | everything the repeater and the comparator share: what counts as input, what counts as a side input, and how output leaves | Server |
RepeaterBlock | a delay in two-tick units, and whether it is locked | Server |
ComparatorBlock | two modes over one pair of inputs, and how far in front of itself it can see | Server |
ComparatorBlockEntity | one integer — the comparator’s whole reason for having a block entity at all | Server |
ObserverBlock | that a neighbour’s state changed, on the channel the repeater uses for its lock | Server |
Level | that a write carrying the neighbour bit, of a state with an analog output, pokes the comparators around it — and so does BlockEntity.setChanged | Server |
The observer shares one row with the other two
The table below is two comparisons wearing one grid, and it repays reading in that order. Read the first two columns against each other and you get the repeater against the comparator — four rows of difference, about reach, arithmetic, urgency and storage, and all of them inside machinery the two share. Then read the third column against the other two and there is only one row where it does what both of them do by the same calls: how it outputs. It reads no signal, offers no side input, and is told about the world on a different channel from their input, which is why nothing is the honest answer in two of its cells.
RepeaterBlock | ComparatorBlock | ObserverBlock | |
|---|---|---|---|
| what it reads from the front | DiodeBlock.getInputSignal — the signal at the block it faces, and if that is under 15, the raw RedstoneWireBlock.POWER of a wire there | the same, then overridden: an analog output if the block in front has one, else one block further through a conductor | nothing. It reads no signal at all |
| what it reads from the sides | DiodeBlock.getAlternateSignal, restricted to other diodes (DiodeBlock.sideInputDiodesOnly is true), and used only to lock | the same, unrestricted, and used as the second operand | nothing |
| how it books its turn | DiodeBlock.checkTickOnNeighbor unchanged: RepeaterBlock.DELAY doubled, at one of the three priorities that method can choose | overrides it entirely: always a delay of 2, at TickPriority.HIGH or TickPriority.NORMAL | ObserverBlock.startSignal from a shape update: delay 2 at the default TickPriority.NORMAL, and only if one is not already booked |
| what it stores | everything, in the block state | the same, plus one int in a ComparatorBlockEntity | everything, in the block state |
| how it outputs | DiodeBlock.updateNeighborsInFront | the same | ObserverBlock.updateNeighborsInFront, an independent copy making the same two calls |
A diode never writes into its target
The output half is the least-known part of all three blocks, and the two diodes
genuinely share it: one method on DiodeBlock, inherited unchanged. The
observer does the same two things by its own copy of the method and offers its
signal the same way, which is where its machinery converges with theirs.
A diode declares itself a source unconditionally
(DiodeBlock.isSignalSource), answers DiodeBlock.ownSignal with
DiodeBlock.getOutputSignal when DiodeBlock.POWERED and zero otherwise, and
restricts DiodeBlock.getSignal to the one neighbour at its output — so a repeater
offers its 15 to precisely one neighbour, and DiodeBlock.getDirectSignal
hands out the same value, which is what makes a diode able to strongly power
the block in front of it.
A diode’s HorizontalDirectionalBlock.FACING points at the input.
DiodeBlock.getStateForPlacement takes the player’s horizontal direction and
reverses it, so the output is at
Direction.getOpposite of its facing, and that is the position
DiodeBlock.updateNeighborsInFront acts on. It does two things there: a
direct Level.neighborChanged on the output block, and a
Level.updateNeighborsAtExceptFromFacing around that same block, skipping the
direction that points back at the diode. It never writes a state into the
target. It notifies, and lets the target read back through
SignalGetter.getSignal — which is why the signal passes from a repeater to a
repeater without either of them knowing what the other is; only how urgently
the feeding one books its turn, below, depends on that.
The repeater’s signal leaves by an unexpected door. DiodeBlock.tick writes
DiodeBlock.POWERED with Block.UPDATE_CLIENTS alone — flags 2, no
neighbour bit (block update
flags) — so Level.setBlock’s neighbour fan-out never runs, though its
three shape passes still do. What actually
propagates the change is DiodeBlock.onPlace, which
LevelChunk.setBlockState runs on the server for any write without
Block.UPDATE_SKIP_ON_PLACE, and which calls
DiodeBlock.updateNeighborsInFront
(blocks and states). The
comparator’s tick is its own, ComparatorBlock.refreshOutputState, which calls
DiodeBlock.updateNeighborsInFront directly as well (below).
What each one can see
DiodeBlock.getInputSignal reads the block it faces and then, if that gave
less than 15, takes the maximum with the raw RedstoneWireBlock.POWER of a
wire sitting there — the special case that lets a diode read a wire whose
connection state does not point at it.
DiodeBlock.getAlternateSignal reads the two horizontals perpendicular to the
facing through SignalGetter.getControlInputSignal, and
DiodeBlock.sideInputDiodesOnly decides what counts. For a repeater it is
true, so only another diode can reach a repeater’s side, and the value is used
solely by RepeaterBlock.isLocked. For a comparator it is false, so a
redstone block reads as 15, a wire reads as its power, and any signal source
reads as its strong output — and the value is the comparator’s second operand.
ComparatorBlock.getInputSignal is where comparators earn their reputation.
If the block in front has an analog output, that value replaces the redstone
reading outright. Otherwise, if the reading is under 15 and the block in front
is a redstone conductor, the comparator looks one block further and takes
the best of two things there: the analog output of whatever block is at that
position, and the reading of an ItemFrame — but only if exactly one frame
in that block’s space faces the way the comparator’s HorizontalDirectionalBlock.FACING points. The
direction test comes first and the count second, so a frame pointing some other
way is not counted at all; two frames facing the comparator’s way, and the
comparator reads neither. Note what it falls back to when both of those come
up empty: not zero, but the ordinary front reading it already had. A comparator
in front of a conductor with two item frames behind it is not broken — it is
reading the conductor.
What it does with the two numbers
Having read a front value and a side value, ComparatorBlock.calculateOutputSignal
is two lines. A side reading greater than the front one puts the output at
zero, whatever the mode. Otherwise ComparatorBlock.MODE decides, and it holds
one of exactly two values: in ComparatorMode.SUBTRACT the output is front
minus side, and in ComparatorMode.COMPARE — the default, and the mode a
freshly placed comparator is in — it is the front value unchanged. So the
comparing mode does no arithmetic at all; what it compares is only whether the
side beats the front. The lit pair of torches at its input end is a separate question
again: ComparatorBlock.shouldTurnOn decides DiodeBlock.POWERED, which
lights them, true when the front beats the side, and on a non-zero tie only
in compare mode — which is why a subtract comparator with equal inputs outputs
zero and its pair goes out, while a compare comparator with equal inputs
outputs the full front value and stays lit. The lone torch at its output end is
the mode’s, lit in subtract.
A container’s analog output is
AbstractContainerMenu.getRedstoneSignalFromContainer: every slot’s count
divided by that stack’s maximum size, summed, divided by the number of slots,
and mapped onto 0–15. (The formula takes the smaller of that maximum and the
container’s own cap, but the cap is 99 for every container that reaches it.)
So a chest of shulker boxes and a chest of cobblestone at the same item count
read very differently.
Booking a turn, and why a repeater turns off first
All three answer a change by booking a turn rather than acting on it; the queue itself, its dedup rule and the drain are scheduled ticks, which traces a repeater in full. What belongs here is which priority each one asks for, because that is where the two diodes stop agreeing.
DiodeBlock.checkTickOnNeighbor books only when the diode is not locked, only
when the current DiodeBlock.POWERED disagrees with the current input, and
only when LevelTickAccess.willTickThisTick says nothing is already about to
run there. It picks TickPriority.EXTREMELY_HIGH when
DiodeBlock.shouldPrioritize holds — when the block it outputs into is itself
a diode whose own input is not on the far side of it, so a diode reading this
one or standing sideways to it, but not one aimed back at it —
TickPriority.VERY_HIGH when the diode is currently on, and
TickPriority.HIGH otherwise. So a diode’s turn-off beats another’s turn-on
due on the same tick, and a diode feeding a diode beats both. The only
TickPriority.NORMAL booking a repeater makes is DiodeBlock.setPlacedBy, at
delay 1, when you place one into an already-powered spot.
ComparatorBlock.checkTickOnNeighbor throws all of that away. It has no lock
to consult — DiodeBlock.isLocked is false unconditionally and the comparator
does not override it — its delay is a flat 2 whatever the state says, and it
chooses between TickPriority.HIGH and TickPriority.NORMAL alone; the two
urgent priorities a repeater relies on are not available to it. It also books
on a second condition the repeater does not have: not only when the powered
flag disagrees with the input, but whenever the computed output value
differs from the int currently in the block entity.
DiodeBlock.tick is what pulse extension is made of. Finding itself off, it
turns on regardless of whether the input is still there — and then, if the
input has already gone, books its own turn-off one delay later at
TickPriority.VERY_HIGH. A pulse shorter than the delay is not swallowed; it
is stretched to the delay.
The channel the observer listens on
Two channels carry news of a change, and the difference between them is the
whole of this page’s hook, so it is worth stating before the figure draws it.
The neighbour channel — Level.updateNeighborsAt reaching each neighbour’s
BlockBehaviour.neighborChanged — is what a write enters when it carries
Block.UPDATE_NEIGHBORS, and it exists only on the server, where those methods
have bodies. The shape channel — Level.neighborShapeChanged reaching each
neighbour’s BlockBehaviour.updateShape — runs on every write that does not
say Block.UPDATE_KNOWN_SHAPE, and it runs on both sides. So the neighbour
channel says something near you changed and here is who says so, and the
shape channel says your neighbour’s state is now this; do you still fit? —
which arrives unless the writer opts out with that bit, where the neighbour
channel arrives only if the writer opts in (blocks and
states owns both).
flowchart TD
NC["the neighbour channel: Level.updateNeighborsAt"]
SC["the shape channel: Level.neighborShapeChanged"]
RB["DiodeBlock.neighborChanged, on the repeater and the comparator"]
RL["RepeaterBlock.updateShape writes LOCKED"]
DONE["no appointment: the lock is already correct"]
OB["ObserverBlock.updateShape"]
SS["ObserverBlock.startSignal"]
BOOK["the appointment book"]
DT["a diode's tick writes POWERED under UPDATE_CLIENTS"]
OT["ObserverBlock.tick writes POWERED under UPDATE_CLIENTS"]
OUT["the block in front, then its five other neighbours"]
NC -- "the write set UPDATE_NEIGHBORS" --> RB
SC -- "the write omitted UPDATE_KNOWN_SHAPE" --> RL
SC --> OB
RB -- "DiodeBlock.checkTickOnNeighbor" --> BOOK
RL -- "server only, off the axis" --> DONE
OB -- "facing direction, while unpowered" --> SS
SS -- "server only, none booked" --> BOOK
BOOK --> DT
BOOK --> OT
DT -- "DiodeBlock.onPlace inside the write, and a comparator's direct call" --> OUT
OT -- "ObserverBlock.updateNeighborsInFront, directly" --> OUT
Two channels, one queue, and one end that never reaches it: a diode hears about a change on the neighbour channel and the observer on the shape channel, both book a turn, and only the repeater’s lock is written where it stands. The paths out of the book differ too — the repeater’s pulse leaves through the side effect of its own write, while the observer calls for it and the comparator does both.
An observer watches for ObserverBlock.updateShape arriving from the one
direction it faces, while ObserverBlock.POWERED is false, and books a
two-tick appointment. Two ticks later ObserverBlock.tick writes the powered
state with flags 2, schedules its own turn-off two ticks after that, and
pulses through ObserverBlock.updateNeighborsInFront. The shape channel is
the right one for the job because it carries your neighbour’s state changed
unless the writer opted out: a door opened by hand writes
with flags 10 and issues no neighbour update at all, and the observer still
sees it (block interaction), and dust the observer is watching
carries the same news through the flag-2 writes of signal and
dust.
RepeaterBlock.LOCKED works the same way, and it is one of exactly two diode
properties computed from a redstone reading at all: DiodeBlock.POWERED is the
other, and the difference between them is when. DiodeBlock.POWERED is
written at tick time, from the appointment the block booked, and a comparator’s
mode click refreshes it on the spot; RepeaterBlock.LOCKED is written inside a
shape update or at placement, with no appointment anywhere.
RepeaterBlock.updateShape recomputes it whenever a neighbour off the
facing axis changes, which is the two sides and, harmlessly, up and down; the
value itself comes from the two sides alone. So locking follows a neighbouring
repeater’s state without either block scheduling anything.
The tempting conclusion is that both blocks chose the shape channel because it
is the half of the update machinery a client also runs. That is not what the
code does: both hooks refuse to do their redstone work on the client.
RepeaterBlock.updateShape recomputes the lock only when the level is not
client-side, and ObserverBlock.startSignal returns immediately on a
ClientLevel. Nor would it help the observer if it did — a client keeps no
appointment book at all, so a scheduled tick could never fire there. Beyond what
the client predicts for its own clicks and placements, everything else it knows about these three blocks the server sends it.
One int, and the fan-out that exists to deliver it
A comparator has a block entity for one reason:
ComparatorBlock.calculateOutputSignal can produce a number that the block
state has nowhere to keep. DiodeBlock.POWERED is one bit, and a comparator’s
output is 0–15. Plenty of redstone blocks have block entities — the sculk
sensor keeps its last vibration frequency in one and answers with it while it is
active, and most containers answer from their contents — but a DaylightDetectorBlockEntity
stores nothing at all, so having one is no evidence of state that a block state
could not hold.
ComparatorBlockEntity.getOutputSignal is that number, written by
ComparatorBlock.refreshOutputState — which also flips the powered bit when
it needs to, and then calls DiodeBlock.updateNeighborsInFront whether or not
anything changed, whenever the mode is ComparatorMode.COMPARE.
The other half of making comparators work is
Level.updateNeighbourForOutputSignal. It walks the four horizontals and
notifies any Blocks.COMPARATOR it finds; failing that, where the neighbour is
a redstone conductor, it reaches one further and notifies a comparator there
instead, mirroring the comparator’s own reach in the opposite direction. Twelve
call sites reach it, and the two general ones are the pair that matters here.
Level.setBlock calls it on the server for a write that carries
Block.UPDATE_NEIGHBORS and whose new state has an analog output — but an item
entering a chest is not a write at all. That path is BlockEntity.setChanged,
which calls it unconditionally, and it is what makes a comparator notice a
hopper filling a chest that nothing else in redstone would have reported.
Where to look
Read the shared half first, because it is where the two diodes stop being two
blocks: DiodeBlock.getInputSignal and DiodeBlock.getAlternateSignal are the
two readings, DiodeBlock.sideInputDiodesOnly is the one boolean that turns a
repeater’s side input into a lock, DiodeBlock.checkTickOnNeighbor books the
turn and DiodeBlock.shouldPrioritize decides how urgently, DiodeBlock.tick
is where pulse extension lives, and DiodeBlock.updateNeighborsInFront with
DiodeBlock.onPlace is the whole of how a repeater’s signal leaves. Then the two
divergences: ComparatorBlock.getInputSignal with
ComparatorBlock.calculateOutputSignal and ComparatorBlock.refreshOutputState
for the arithmetic and the int in the block entity, and
ObserverBlock.updateShape with ObserverBlock.startSignal for the channel.
RepeaterBlock.updateShape is the lock, and
Level.updateNeighbourForOutputSignal is the fan-out built for comparators.
AbstractContainerMenu.getRedstoneSignalFromContainer is the door into what a
chest is worth.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.