<!-- https://minecraftdocs.dev/systems/blocks/diodes-and-observers — this page as markdown. The book's index: https://minecraftdocs.dev/llms.txt; the whole book in one file: https://minecraftdocs.dev/llms-full.txt -->

# 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 `DiodeBlock`s 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](block-entities#create-keep-replace-remove) 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](../../reference/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](blocks-and-states#the-two-update-channels)). 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](../world/scheduled-ticks#a-repeater-appointment-by-appointment),
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](blocks-and-states#the-two-update-channels) owns both).

```mermaid
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](block-interaction#the-shape-channel-which-both-sides-run)), and dust the observer is watching
carries the same news through the flag-2 writes of [signal and
dust](signal-and-dust#what-one-neighbour-update-to-a-wire-costs).

`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`.*
