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

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

classwhat it decidesthread
ChunkAccesseverything a chunk has whatever its shape: position, height, sections, heightmaps, block entities, structures, the two volatile flagsabstract — whichever thread owns the shape below
ProtoChunka chunk under construction: status, entities as NBT, the light engine it reports towritten on the worker pool, one writer at a time
LevelChunka chunk that is part of a Level: block entities, tickers, tick containers, the full-status supplierthe Server thread — on the client, the Render thread
ImposterProtoChunkwhat a still-generating neighbour sees when the chunk it asked for is already livebuilt 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
LevelChunkSection16×16×16: two palette containers and four counters that let a whole section be skippedwhichever thread holds its permit
PalettedContainerthe mapping from 4,096 (or 64) entries to values, and when to widen itone writer at a time — a second is detected, blocks, and both threads die; reads are lock-free
Strategywhich palette and which bit width each entry count deserves, for block states and for biomesimmutable, shared by every container in the level
Heightmapthe top of each of 256 columns, for one definition of topwith 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 statesbiomes
1 (0 bits)Strategy.ZERO_BITS → SingleValuePalette + ZeroBitStoragethe same
2 … 8 (1–3 bits)Strategy.FOUR_BITS_LINEAR → LinearPalette, already 4 bitsStrategy.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 bitsConfiguration.Global already
17 … 256 (5–8 bits)Strategy.FIVE_BITS_HASHMAP … Strategy.EIGHT_BITS_HASHMAP → HashMapPaletteConfiguration.Global — there is no hashed tier for biomes
moreConfiguration.Global → GlobalPalette, the registry’s own IdMapConfiguration.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.

typeopaque meansusagesavedsent
Heightmap.Types.WORLD_SURFACE_WGnot airHeightmap.Usage.WORLDGENproto onlyno
Heightmap.Types.WORLD_SURFACEnot airHeightmap.Usage.CLIENTyesyes
Heightmap.Types.OCEAN_FLOOR_WGin BlockTags.BLOCKS_MOTION_IN_HEIGHTMAPHeightmap.Usage.WORLDGENproto onlyno
Heightmap.Types.OCEAN_FLOORin the same tagHeightmap.Usage.LIVE_WORLDyesno
Heightmap.Types.MOTION_BLOCKINGin that tag, or holds fluidHeightmap.Usage.CLIENTyesyes
Heightmap.Types.MOTION_BLOCKING_NO_LEAVESin BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, or holds fluidHeightmap.Usage.CLIENTyesyes

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 orderwhat happenswhen it is skipped
1the section is fetched and its emptiness rememberedair into an all-air section returns null immediately
2LevelChunkSection.setBlockState writes the palette entry and moves all four counters—
3the old state is compared to the newidentical state returns null, and nothing below runs
4all four heightmaps take Heightmap.update—
5if the section’s emptiness flipped: LevelLightEngine.updateSectionStatus and ChunkSource.onSectionEmptinessChangedwhen it did not flip
6if LightEngine.hasDifferentLightProperties: ChunkSkyLightSources.update, then LevelLightEngine.checkBlockwhen opacity and emission are unchanged and neither state uses a shape for light occlusion
7the old block entity is dropped, preceded on the server by BlockEntity.preRemoveSideEffectswhen 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
8BlockBehaviour.BlockStateBase.affectNeighborsAfterRemovalwhen 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
9the section is re-read — if step 8 changed the block again, the call returns null—
10BlockBehaviour.BlockStateBase.onPlaceon the client, or under Block.UPDATE_SKIP_ON_PLACE
11the new block entity is created or re-validated, and its ticker reboundwhen the new state has no block entity, or step 10 changed the block
12LevelChunk.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.