<!-- https://minecraftdocs.dev/systems/world/chunk-anatomy — 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 -->

# Chunk anatomy

> Verified against **Minecraft 26.3** · Part IV · One block is placed, and the write travels down through the chunk until it lands in four bits of one long.

You place a single block of deepslate at y −40, in a section that until now
held nothing but stone and air. The click ends in `LevelChunk.setBlockState`,
and the write goes down four objects: a `LevelChunkSection` sixteen blocks
tall, a `PalettedContainer` of 4,096 block states, a palette that turns the
state into a small integer, and a `SimpleBitStorage` that puts that integer
into some fixed number of bits of one long. Everything else in this part
*moves* chunks — tickets load them, the pyramid generates them, the light
engine walks them, the region file stores them. This is the page that looks
inside, and it is the vocabulary the rest of the part spends. Begin with the
part that surprises people: **a section holding two distinct block states
costs exactly what one holding sixteen costs — four bits an entry, 256
longs, on disk as well as in memory — and the block that makes it seventeen
re-encodes all 4,096 entries into a wider storage before it can be written.**

## The cast

| class | what it decides | thread |
|---|---|---|
| `ChunkAccess` | everything a chunk has whatever its shape: position, height, sections, heightmaps, block entities, structures, the two volatile flags | abstract — whichever thread owns the shape below |
| `ProtoChunk` | a chunk under construction: status, entities as NBT, the light engine it reports to | written on the worker pool, one writer at a time |
| `LevelChunk` | a chunk that is part of a `Level`: block entities, tickers, tick containers, the full-status supplier | the Server thread — on the client, the Render thread |
| `ImposterProtoChunk` | what a still-generating neighbour sees when the chunk it asked for is already live | built on the Server thread; read on the worker pool — by a neighbour's step on the worldgen executor or in the *buildTerrain* fork, and by the light engine on the light executor |
| `LevelChunkSection` | 16×16×16: two palette containers and four counters that let a whole section be skipped | whichever thread holds its permit |
| `PalettedContainer` | the mapping from 4,096 (or 64) entries to values, and when to widen it | one writer at a time — a second is detected, blocks, and both threads die; reads are lock-free |
| `Strategy` | which palette and which bit width each entry count deserves, for block states and for biomes | immutable, shared by every container in the level |
| `Heightmap` | the top of each of 256 columns, for one definition of *top* | with the chunk that owns it |

## The four shapes a chunk takes

```mermaid
classDiagram
    class ChunkAccess {
        <<abstract>>
        position and height
        sections, heightmaps, block entities, structures
    }
    class ProtoChunk {
        under construction
        written on the worker pool
    }
    class LevelChunk {
        part of a Level
        owned by the Server thread
    }
    class ImposterProtoChunk {
        what a still-generating neighbour is handed
        sections shared, most other writes dropped
    }
    class EmptyLevelChunk {
        void air, one fixed biome
    }
    ChunkAccess <|-- ProtoChunk
    ChunkAccess <|-- LevelChunk
    ProtoChunk <|-- ImposterProtoChunk
    LevelChunk <|-- EmptyLevelChunk
    ProtoChunk ..> LevelChunk : promoted, by ChunkStatusTasks.full
    ImposterProtoChunk --> LevelChunk : holds the one it stands in for
```

*The four shapes and the one abstract base, with the two relations that are not
inheritance: the dashed arrow is the promotion the whole part exists to reach,
and the solid one is a wrapper holding the chunk it pretends to be. The two
subclasses hang off different parents, which is the thing to look at — an
imposter is a proto chunk and an empty chunk is a live one.*

The diagram is the hierarchy and the two links across it, because the
hierarchy is short enough to be the whole truth. `ChunkAccess` is the abstract
chunk and has
exactly two direct concrete lines — `ProtoChunk` (`ChunkType.PROTOCHUNK`) and `LevelChunk`
(`ChunkType.LEVELCHUNK`) — with `ImposterProtoChunk` a subclass of the first
and `EmptyLevelChunk` of the second. Nothing else extends it.

Every one of them carries the same core. `ChunkAccess.chunkPos` says where
it is, and a `LevelHeightAccessor` says how tall: `LevelHeightAccessor.getMinY`
and `LevelHeightAccessor.getHeight` are the only two facts about height there
are, and the overworld's −64 and 384 give `LevelHeightAccessor.getSectionsCount`
of **24**, section Y −4 through 19. Beside them sit the heightmaps, the block
entities in two maps (`ChunkAccess.blockEntities` live, `ChunkAccess.pendingBlockEntities`
still NBT, `ChunkAccess.getBlockEntitiesPos` the union),
`ChunkAccess.structureStarts` and `ChunkAccess.structureReferences`, the
per-section `ChunkAccess.postProcessing` offsets to revisit
after load (`ProtoChunk.packOffsetCoordinates` packs four bits each of x, y
and z into a short), `ChunkAccess.inhabitedTime` behind local difficulty
([the level
tick](../server/server-level-tick#two-chunk-sets-and-two-different-mob-caps)),
`ChunkAccess.upgradeData`
and the nullable `ChunkAccess.blendingData` whose presence *is*
`ChunkAccess.isOldNoiseGeneration` ([blending](../worldgen/blending)),
and two *volatile* flags —
`ChunkAccess.unsaved`, whose test-and-clear `ChunkAccess.tryMarkSaved` the
saver uses, and `ChunkAccess.isLightCorrect`, saved as *isLightOn*. It is
also three interfaces at once — `LightChunk`, which is what the light engine
reads through `LightChunk.findBlockLightSources` and
`LightChunk.getSkyLightSources`, plus `StructureAccess` and
`BiomeResolver` — and `ChunkAccess.getPersistedStatus` is
the `ChunkStatus` that goes to disk, with `ChunkAccess.getHighestGeneratedStatus`
folding in `BelowZeroRetrogen.targetStatus` for a chunk still being deepened.

A `ProtoChunk` adds what only generation needs: a volatile
`ProtoChunk.status` (`ProtoChunk.setPersistedStatus` also retires a finished
`BelowZeroRetrogen`), a `ProtoChunk.lightEngine` from
`ProtoChunk.setLightEngine` that it reports to only once the status
`ChunkStatus.isOrAfter` `ChunkStatus.INITIALIZE_LIGHT`, its entities as a
list of `CompoundTag` (`ProtoChunk.addEntity` serialises on the spot), and
`ProtoChunkTicks` that
`ProtoChunk.unpackBlockTicks` turns into `LevelChunkTicks` on promotion
([appointments that survive a restart](scheduled-ticks#appointments-that-survive-a-restart)).
The pool that fills all of that in
is [the generation pipeline](chunk-generation-pipeline#the-pyramid-drawn). Ask it for a
biome before `ChunkStatus.BIOMES` and `ProtoChunk.getNoiseBiome` throws
*Asking for biomes before we have biomes*.

A `LevelChunk` adds what only a live chunk needs: `LevelChunk.level`,
`LevelChunk.setLoaded`, a supplier of `FullChunkStatus` the holder owns
([the four statuses](tickets-and-loading#the-four-statuses)), two `LevelChunkTicks` that
`LevelChunk.registerTickContainerInLevel` attaches to the level's queues and
`LevelChunk.unregisterTickContainerFromLevel` detaches, the ticker map, a
one-shot `LevelChunk.postLoad` processor that `LevelChunk.runPostLoad`
fires, the per-section `LevelChunk.gameEventListenerRegistrySections` — an
`EuclideanGameEventListenerRegistry` each, built on demand and only on a
server ([listeners live in the
chunk](game-events-and-vibrations#listeners-live-in-the-chunk-one-registry-per-section)) — and a
`LevelChunk.unsavedListener` that `LevelChunk.markUnsaved` fires **only on
the false-to-true edge**, which is how the server's dirty set learns of a
change without scanning. Its `LevelChunk.getPersistedStatus` is always
`ChunkStatus.FULL`.

`ImposterProtoChunk` exists because a neighbour still generating asks the
holder for "the chunk at status X" and must be handed something
`ProtoChunk`-typed even when that chunk is already live. Reads delegate to
`ImposterProtoChunk.getWrapped`; writes are dropped unless *allowWrites*,
which both of the two places that construct one pass as **false** — heightmaps, structure starts and
references and block-entity NBT unconditionally, and the rest for want of the
flag. The sections are the exception: `ImposterProtoChunk.getSections` hands
back the wrapped chunk's array unconditionally, and the single-section
`ImposterProtoChunk.getSection`, though gated on the flag, falls back to that
same array, so a write through a section lands on the live chunk; and
`ImposterProtoChunk.markUnsaved` and `ImposterProtoChunk.setLightCorrect`
always pass through and `ImposterProtoChunk.canBeSerialized` is false,
because the `LevelChunk` under it is what gets saved. Its
`ImposterProtoChunk.fixType` maps a request for a *_WG* heightmap onto the
live one, but only inside `ImposterProtoChunk.getHeight`: asking it to
*create* a *_WG* heightmap creates a real one on the live chunk.

What makes that wrapper honest is what promotion does not do. `ChunkAccess`
always allocates its own array and copies the section references in, so a
promoted chunk leaves two chunks holding two arrays over one set of
`LevelChunkSection` objects: writing through either is writing the same
blocks, and the proto chunk a generating neighbour is still holding goes on
working. The imposter is a third handle on the same sections.

`EmptyLevelChunk` is the other direction: `Blocks.VOID_AIR` everywhere,
`EmptyLevelChunk.isEmpty` true where `LevelChunk.isEmpty` is false, one
fixed biome, and `EmptyLevelChunk.getFullStatus` a flat
`FullChunkStatus.FULL`. `ClientChunkCache.emptyChunk` is one shared instance
handed out for *any* client miss, and only when the caller asked to load or
generate — otherwise the miss returns null; `PathNavigationRegion` builds
its own for whatever a mob's pathfinder cannot see. The real client chunks
live in `ClientChunkCache.Storage`, an `AtomicReferenceArray` ring whose
`ClientChunkCache.Storage.onSectionEmptinessChanged` and its double-buffered
added and removed sets are the renderer's feed of which sections exist.

## Sections and their four counters

A chunk's block storage is four things nested inside each other — the chunk,
its sections, each section's containers and each container's data — and the
four counters hang off the second. Here is the whole of it.

```mermaid
flowchart TD
    LC["LevelChunk: one 16 by 16 column of the build height"]
    HM["at least four Heightmaps, 256 entries of 9 bits each"]
    ARR["ChunkAccess.sections: 24 LevelChunkSection in the overworld, never a null slot"]
    CNT["four shorts: non-empty blocks, fluids, and a ticking count of each"]
    ST["LevelChunkSection.states: PalettedContainer of BlockState, 4,096 entries"]
    BIO["LevelChunkSection.biomes: PalettedContainerRO of Biome, 64 entries"]
    D1["its own PalettedContainer.Data: a Palette and a BitStorage, sized together"]
    D2["its own PalettedContainer.Data, on a shorter ladder"]
    LC --> HM
    LC --> ARR
    ARR --> CNT
    ARR --> ST
    ARR --> BIO
    ST --> D1
    BIO --> D2
```

*A chunk's block storage, down to the bit storage: the two containers of a
section have a `PalettedContainer.Data` each and never share one, and the four
counters hang off the section rather than the containers, which is why they
can answer without touching either. Nothing in this picture is light — the two
4-bit
fields live in the light engine's own storage, not on the section
([lighting](lighting#one-batch-and-what-it-publishes)).*

The array never has a hole: `ChunkAccess.replaceMissingSections` runs in the
constructor and fills every empty slot with a fresh all-air section from the
level's `PalettedContainerFactory`, so nothing that walks sections checks
for null.

The four counters are what make a section cheap to skip.
`LevelChunkSection.setBlockState` adjusts all four from the outgoing and
incoming state on every single write — `LevelChunkSection.nonEmptyBlockCount`
(zero is `LevelChunkSection.hasOnlyAir`, which is also how
`LevelChunk.getBlockState` answers air without touching a palette),
`LevelChunkSection.fluidCount`, `LevelChunkSection.tickingBlockCount` and
`LevelChunkSection.tickingFluidCount` — and `LevelChunkSection.isRandomlyTicking`
is the *or* of the last two. That one boolean lets `ServerLevel` skip a
section of solid stone without looking at any of its 4,096 blocks. Only a
load from disk recounts: `LevelChunkSection.recalcBlockCounts` runs from the
two-container constructor, whose only caller is `SerializableChunkData`.

On a client, two of the four are permanently wrong and nothing notices.
`LevelChunkSection.write` carries only the non-empty-block and fluid counts,
and `LevelChunkSection.read` takes exactly those two and never recounts, so a
client section starts with `LevelChunkSection.tickingBlockCount` and
`LevelChunkSection.tickingFluidCount` at zero and only ever counts what has
changed since the chunk arrived. The sole reader of
`LevelChunkSection.isRandomlyTicking` is `ServerLevel`, and the client runs no
random ticks.

Biomes share the section but are coarse and read-only.
Two bits per axis, 64 entries of 4×4×4 blocks each — the 2
`LevelChunkSection.BIOME_CONTAINER_BITS` names. The field is a
`PalettedContainerRO` and the
*published* one is never mutated: `LevelChunkSection.fillBiomesFromNoise`,
`LevelChunkSection.read` and `LevelChunkSection.readBiomes` each build a
replacement through `PalettedContainerRO.recreate`, fill it, and swap the
reference. The block-state container is the only one anything writes to in
place.

Two different copies leave a section. The saver takes
`LevelChunkSection.copy`, a deep copy of both containers and all four
counters, on the Server thread inside `SerializableChunkData.copyOf` — the
IO lane never sees a live section, and even the NBT encoding of the copy
runs on the background pool ([copy on the server, encode on a worker, write on
the IO
lane](chunk-storage#copy-on-the-server-encode-on-a-worker-write-on-the-io-lane)).
The client
mesher takes something cheaper: a `SectionCopy` takes `PalettedContainer.copy`
of the block-state container alone (nothing at all when the section is air)
plus an immutable snapshot of the chunk's block-entity map. A worker that
means to write many blocks at once instead brackets its work with
`LevelChunkSection.acquire`
and `LevelChunkSection.release`.

### The permit, and what happens to the thread that misses it

That bracket is the permit the cast table names, and it is not a mutex.
`PalettedContainer.threadingDetector` is a `ThreadingDetector`: a one-permit
semaphore with a reporter attached. `ThreadingDetector.checkAndLock` tries the
permit and, on failure, records itself as the loser and then blocks. It is the
**winner** that notices, in `ThreadingDetector.checkAndUnlock`: it builds
`ThreadingDetector.makeThreadingException` — *Accessing PalettedContainer from
multiple threads*, with both stack traces — and throws it, and the loser
re-throws the same report the instant the permit comes free. Both threads die,
deliberately: an interleaved section write would be a corrupt world rather than
a crash.

Exactly one thread writes a section at a time, then — the Server thread for a
live chunk, and on the worker pool whoever holds the permit: most generation
takes it for one write at a time, and two hold it across many through
`LevelChunkSection.acquire`, `NoiseBasedChunkGenerator`, which holds every
section across its noise range, and the `BulkSectionAccess` that
`OreFeature`, its only user, holds over every section it touches until it
closes. Those hold the permit already, so they write through the unchecked
five-argument `LevelChunkSection.setBlockState` and
`PalettedContainer.getAndSetUnchecked`.

## The palette and the ladder it climbs

A `PalettedContainer`'s whole state is one *volatile* record,
`PalettedContainer.Data`, holding a `Configuration`, a `Palette` and a
`BitStorage`. `PalettedContainer.get` reads the record once into a local and
takes no lock, because a resize swaps in a whole new record rather than
editing the old one. `PalettedContainer.read` from the wire is the exception
that proves it: `PalettedContainer.createOrReuseData` hands back the *existing*
record whenever the incoming bit count wants the same configuration, and the
palette and the long array are then overwritten in place — and on a client the
record overwritten can be one already in use, whenever `ClientChunkCache`
reuses a `LevelChunk` it still holds
at that position. Which record a given entry count deserves is
decided by a top-level `Strategy` — no longer nested inside the container —
whose `Strategy.createForBlockStates` and `Strategy.createForBiomes` are
called once per level by `PalettedContainerFactory.create` over
`Block.BLOCK_STATE_REGISTRY` and the biome registry
([registries](../foundations/identifiers-and-registries#the-table)), which is why the
global palette's width is a runtime number and not a constant.

`Strategy.getConfigurationForBitCount` is the ladder, and block states and
biomes climb different ones:

| distinct values (bits needed) | block states | biomes |
|---|---|---|
| 1 (0 bits) | `Strategy.ZERO_BITS` → `SingleValuePalette` + `ZeroBitStorage` | the same |
| 2 … 8 (1–3 bits) | `Strategy.FOUR_BITS_LINEAR` → `LinearPalette`, **already 4 bits** | `Strategy.ONE_BIT_LINEAR` / `Strategy.TWO_BITS_LINEAR` / `Strategy.THREE_BITS_LINEAR` → `LinearPalette` at its own width |
| 9 … 16 (4 bits) | `Strategy.FOUR_BITS_LINEAR`, **still 4 bits** | `Configuration.Global` already |
| 17 … 256 (5–8 bits) | `Strategy.FIVE_BITS_HASHMAP` … `Strategy.EIGHT_BITS_HASHMAP` → `HashMapPalette` | `Configuration.Global` — there is no hashed tier for biomes |
| more | `Configuration.Global` → `GlobalPalette`, the registry's own `IdMap` | `Configuration.Global` |

`SingleValuePalette` holds one value and asks for width 1 the moment a
second arrives — which for block states means jumping straight to the 4-bit
rung. `LinearPalette` is a flat array of *2^bits* slots scanned by identity,
`HashMapPalette` a `CrudeIncrementalIntIdentityHashBiMap`, and
`GlobalPalette` writes nothing on the wire, maps an unknown value to id 0
and answers `Palette.maybeHas` with an unconditional yes. Each of the first
three calls `PaletteResize.onResize` when it fills, and the container *is* its
own
`PaletteResize`: `PalettedContainer.onResize` builds the next record,
`PalettedContainer.Data.copyFrom` walks every entry of the old storage through
the old palette into the new one (or, from a single value, fills), the record
is published, and only
then is the value that triggered the growth added — under
`PaletteResize.noResizeExpected`, which throws if a second growth were
somehow needed.

The palette is also what lets a search rule out a section it never reads.
`LevelChunkSection.maybeHas` puts a predicate to the palette alone, so
the scan for block-light sources behind `ChunkAccess.findBlockLightSources`,
and the points-of-interest check on a loaded chunk, can dismiss 4,096 blocks
with a handful of comparisons. The one rung where that
stops paying is `Configuration.Global`, whose `Palette.maybeHas` is an
unconditional yes.

`SimpleBitStorage` never lets an entry straddle a long: its
`SimpleBitStorage.valuesPerLong` is 64 divided by the width, so 4,096
entries are 256 longs at four bits, 342 at five and 512 at eight, and the
cell index comes from a multiply-shift table (`SimpleBitStorage.MAGIC`)
rather than a division. `ZeroBitStorage` answers 0 for everything and shares
one empty `ZeroBitStorage.RAW` array. An array of the wrong length raises
`SimpleBitStorage.InitializationException`, which `PalettedContainer.unpack`
turns into a `DataResult` error instead of a crash.

### What packing actually buys

The two serialised forms differ. `PalettedContainer.write` is the wire: a
bits byte, the palette, a fixed-size long array at exactly the in-memory
width. `PalettedContainer.pack` is the disk (the *palette* and optional
*data* fields of a `PalettedContainerRO.PackedData`, behind
`PalettedContainer.codecRW` and
`PalettedContainer.codecRO` —
[codecs](../foundations/codecs-nbt-json#disk-a-chest-writes-a-list-of-slots)),
and it re-encodes into a fresh `HashMapPalette` before asking
`Strategy.getConfigurationForPaletteSize` for the width — **the same ladder
memory climbs**. What packing recomputes is the *palette*: unreferenced entries
are dropped, and the smaller palette is then measured against the same rungs.
Narrower entries are the consequence, in two cases only — when the shrunken
palette lands a rung lower than the one memory was using, and when a
`Configuration.Global` container writes at `Configuration.bitsInStorage`
instead of `Configuration.bitsInMemory`.
`PalettedContainer.unpack` re-encodes on the way back only for
`Configuration.Global`, whose `Configuration.alwaysRepack` is true — every
`Configuration.Simple` rung reports one width for both, so its long array is
adopted exactly as it lies on disk.

### The third form, and what the client is handed

Memory and disk are two of three. The wire form is
`ClientboundLevelChunkWithLightPacket`, whose
`ClientboundLevelChunkPacketData` carries the heightmaps for which
`Heightmap.Types.sendToClient` is true, one buffer holding *every* section's
`LevelChunkSection.write` — empty ones included — and the block-entity update
tags. The writer pre-sizes that buffer from the sum of
`LevelChunkSection.getSerializedSize` and throws if
`ClientboundLevelChunkPacketData.extractChunkData` does not fill it to the
byte, and the reader refuses anything over two megabytes. Light rides beside
it in `ClientboundLightUpdatePacketData`.

The client applies the chunk through `ClientPacketListener.handleLevelChunkWithLight` →
`ClientChunkCache.replaceWithPacketData` → `LevelChunk.replaceWithPacketData`,
which clears the block entities, gives each section `LevelChunkSection.read`,
installs the raw heightmaps with `ChunkAccess.setHeightmap` and rebuilds the
sky-light sources, while the light it carries is queued, for
`ClientLevel.pollLightUpdates` to apply later in the same frame unless a
backlog is ahead of it.
Biome-only refreshes come later as
`ClientboundChunksBiomesPacket` → `LevelChunk.replaceBiomes`, and the
block-entity tags travel as
`ClientboundLevelChunkPacketData.BlockEntityInfo`. When a chunk is sent, and
to whom, belongs to [what the client is
told](../networking/what-the-client-is-told#what-a-chunk-packet-carries).

## The six heightmaps

A `Heightmap` is 256 entries — one per column, indexed *x + z·16* — in a
`SimpleBitStorage` whose width is `Mth.ceillog2` of height + 1, so **9 bits**
for a 384-tall world, stored relative to the minimum Y.
`Heightmap.getFirstAvailable` is the first free Y and `Heightmap.getHighestTaken`
the one below it. `Heightmap.primeHeightmaps` fills several types in a
single top-down column scan (starting from the deprecated-for-removal
`ChunkAccess.getHighestSectionPosition`) into maps that
`ChunkAccess.getOrCreateHeightmapUnprimed` makes on demand. `Heightmap.update`
is the incremental path, raising the height when an opaque block is placed
at or above it and rescanning downward only when the block that turned
transparent was the top one.

| type | *opaque* means | usage | saved | sent |
|---|---|---|---|---|
| `Heightmap.Types.WORLD_SURFACE_WG` | not air | `Heightmap.Usage.WORLDGEN` | proto only | no |
| `Heightmap.Types.WORLD_SURFACE` | not air | `Heightmap.Usage.CLIENT` | yes | yes |
| `Heightmap.Types.OCEAN_FLOOR_WG` | in `BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP` | `Heightmap.Usage.WORLDGEN` | proto only | no |
| `Heightmap.Types.OCEAN_FLOOR` | in the same tag | `Heightmap.Usage.LIVE_WORLD` | yes | **no** |
| `Heightmap.Types.MOTION_BLOCKING` | in that tag, or holds fluid | `Heightmap.Usage.CLIENT` | yes | yes |
| `Heightmap.Types.MOTION_BLOCKING_NO_LEAVES` | in `BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES`, or holds fluid | `Heightmap.Usage.CLIENT` | yes | yes |

Which of the six a chunk carries follows its status:
`ChunkStatus.heightmapsAfter` is the two *_WG* maps through
`ChunkStatus.BIOMES` and `ChunkStatus.FINAL_HEIGHTMAPS` — the other four —
from `ChunkStatus.TERRAIN` on, and a `LevelChunk` is constructed with
exactly those four. A `ProtoChunk` primes any of its status's maps that are
missing the first time a block is written. What is *saved*, though, is not
`Heightmap.Types.keepAfterWorldgen`: the saver writes every heightmap the
chunk **holds**, so a proto chunk saves its two *_WG* maps along with any it
has primed since, and a `LevelChunk` saves the four it was built with and any
asked for since, which `ChunkAccess.getHeight` primes on demand. Separately
and privately,
`ChunkAccess.skyLightSources` is a *second* 256-entry bit storage — a
`ChunkSkyLightSources` — that only the sky-light engine reads
([the sky column is a table, not a flood](lighting#the-sky-column-is-a-table-not-a-flood)).

## What placing a block actually does

`LevelChunk.setBlockState` is the ordinary write path into a live chunk, and its
order matters more than any single step in it. Twelve steps, of which **three**
consult the caller's flag word — and one of those three has only a part of
itself skipped. The bits are enumerated in [block update
flags](../../reference/block-update-flags):

| in order | what happens | when it is skipped |
|---|---|---|
| 1 | the section is fetched and its emptiness remembered | air into an all-air section returns null immediately |
| 2 | `LevelChunkSection.setBlockState` writes the palette entry and moves all four counters | — |
| 3 | the old state is compared to the new | identical state returns null, and nothing below runs |
| 4 | all four heightmaps take `Heightmap.update` | — |
| 5 | if the section's emptiness flipped: `LevelLightEngine.updateSectionStatus` and `ChunkSource.onSectionEmptinessChanged` | when it did not flip |
| 6 | if `LightEngine.hasDifferentLightProperties`: `ChunkSkyLightSources.update`, then `LevelLightEngine.checkBlock` | when opacity and emission are unchanged **and** neither state uses a shape for light occlusion |
| 7 | the old block entity is dropped, preceded on the server by `BlockEntity.preRemoveSideEffects` | when the block did not change, had no block entity, or `BlockBehaviour.BlockStateBase.shouldChangedStateKeepBlockEntity` — the side effects alone are skipped on the client and under `Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS` |
| 8 | `BlockBehaviour.BlockStateBase.affectNeighborsAfterRemoval` | when the block did not change and the new one is no `BaseRailBlock`, off the server, or without `Block.UPDATE_NEIGHBORS` and not moved by a piston |
| 9 | the section is re-read — if step 8 changed the block again, the call returns null | — |
| 10 | `BlockBehaviour.BlockStateBase.onPlace` | on the client, or under `Block.UPDATE_SKIP_ON_PLACE` |
| 11 | the new block entity is created or re-validated, and its ticker rebound | when the new state has no block entity, or step 10 changed the block |
| 12 | `LevelChunk.markUnsaved` | — |

One step there is easy to misread. Step 9 exists because step 8 runs arbitrary
block code that may write the same position again, and
`LevelChunk.setBlockState` will not claim a placement step 8 took from it —
which is the sharpest thing this write path says about itself: it is the only
step whose whole job is to notice that the world moved underneath it. Step 8's
own place in the wider update story, and the shape updates and redstone
notifications that fire only *after* the chunk returns, are told in [back in
`Level.setBlock`'s
tail](../blocks/blocks-and-states#back-in-levelsetblocks-tail).

`ProtoChunk.setBlockState` is the same idea with everything live stripped
out: section write, light only past `ChunkStatus.INITIALIZE_LIGHT`, the
status's heightmaps updated (and any absent primed after the write), and no
block entity,
no `BlockBehaviour.BlockStateBase.onPlace`, no neighbours at all.

### What step 11 leaves behind, and what the chunk goes on holding

The ticker step matters to a chunk's anatomy for one reason: the handle it
creates belongs to the chunk, not to the block entity in it.
`LevelChunk.addAndRegisterBlockEntity` puts a wrapper in
`LevelChunk.tickersInLevel`, and the level's flat ticker list holds *that*: a
block entity swapped in place, or kept through a state change, is rebound into
the same wrapper, and one that is removed takes its wrapper out of the map and
rebinds it to nothing, which the level's list then drops. What the wrappers
are and how the level's list prunes itself is told in [loaded is not enough to
tick](../blocks/block-entities#loaded-is-not-enough-to-tick).

The other thing the chunk goes on holding is a debt from generation (and, for
a chunk from an old save, from its upgrade), paid when
the chunk starts ticking:
`LevelChunk.postProcessGeneration` replays the post-processing offsets,
promotes every pending block entity and applies `UpgradeData.upgrade`.

*Pending* is the word to take seriously. A block entity placed during
generation that nothing has asked for since, every one in a half-generated
chunk read back from disk, and one saved while still pending are not
`BlockEntity`s yet (a structure's loot chest is made live when its loot table
is set, and a full chunk read from disk makes the rest live as it loads); each
is a `CompoundTag` in
`ChunkAccess.pendingBlockEntities` ([block
entities](../blocks/block-entities#create-keep-replace-remove)). Any call to
`LevelChunk.getBlockEntity` promotes one through
`LevelChunk.promotePendingBlockEntity` on the first touch, whatever the
`LevelChunk.EntityCreationType` asked for; `LevelChunk.postProcessGeneration`
promotes the rest in bulk when the chunk starts ticking. Of the three creation
types only
`LevelChunk.EntityCreationType.IMMEDIATE` and
`LevelChunk.EntityCreationType.CHECK` have callers left in the game —
`LevelChunk.EntityCreationType.QUEUED` has none.

## Where to look

In the order the write takes them: `ChunkAccess` · `LevelChunk.setBlockState` ·
`LevelChunkSection.setBlockState` · `PalettedContainer.onResize` ·
`Strategy.createForBlockStates` · `SimpleBitStorage` · `Heightmap.Types`.
Then the shapes a chunk is not: `ProtoChunk.setBlockState` ·
`ImposterProtoChunk` · `ChunkStatusTasks.full`. And the two serialised forms:
`PalettedContainer.pack` · `ClientboundLevelChunkPacketData.extractChunkData`.
Every field no page names is in the [class
index](../../reference/class-index).

---

*Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes `tools/verify_names.py`.*
