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

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

classwhat it decidesthread
DiodeBlockeverything the repeater and the comparator share: what counts as input, what counts as a side input, and how output leavesServer
RepeaterBlocka delay in two-tick units, and whether it is lockedServer
ComparatorBlocktwo modes over one pair of inputs, and how far in front of itself it can seeServer
ComparatorBlockEntityone integer — the comparator’s whole reason for having a block entity at allServer
ObserverBlockthat a neighbour’s state changed, on the channel the repeater uses for its lockServer
Levelthat a write carrying the neighbour bit, of a state with an analog output, pokes the comparators around it — and so does BlockEntity.setChangedServer

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.

RepeaterBlockComparatorBlockObserverBlock
what it reads from the frontDiodeBlock.getInputSignal — the signal at the block it faces, and if that is under 15, the raw RedstoneWireBlock.POWER of a wire therethe same, then overridden: an analog output if the block in front has one, else one block further through a conductornothing. It reads no signal at all
what it reads from the sidesDiodeBlock.getAlternateSignal, restricted to other diodes (DiodeBlock.sideInputDiodesOnly is true), and used only to lockthe same, unrestricted, and used as the second operandnothing
how it books its turnDiodeBlock.checkTickOnNeighbor unchanged: RepeaterBlock.DELAY doubled, at one of the three priorities that method can chooseoverrides it entirely: always a delay of 2, at TickPriority.HIGH or TickPriority.NORMALObserverBlock.startSignal from a shape update: delay 2 at the default TickPriority.NORMAL, and only if one is not already booked
what it storeseverything, in the block statethe same, plus one int in a ComparatorBlockEntityeverything, in the block state
how it outputsDiodeBlock.updateNeighborsInFrontthe sameObserverBlock.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.