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
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),
ChunkAccess.upgradeData
and the nullable ChunkAccess.blendingData whose presence is
ChunkAccess.isOldNoiseGeneration (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).
The pool that fills all of that in
is the generation pipeline. 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), 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) — 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.
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).
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).
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), 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),
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.
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).
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:
| 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.
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.
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
BlockEntitys 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). 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.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.