Closed most remaining UNCONFIRMED items + caught corrections: 1.7 data[]=proto5, 1.8 multi-block nibble order (z/x), stance y+1.62, 1.15 wolf idx18=begging, 1.18 below-zero sections NOT stripped (claim was wrong), CONTAINER_SLOT_STATE 1.20.3, + pinned first-appeared versions (report details/server links=1.21, fluidCount=26.1, block-state id14=1.19.4, itemCount VarInt=1.20.5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
28 KiB
Packet deep-dive — Chunk Data wire format & its evolution
The single hardest wire format in the Java protocol. This doc nails the current (1.18+) "Chunk Data and Update Light" packet byte-for-byte — including the paletted container, the format's crux — then walks the evolution back through 1.17, 1.14, 1.9, and 1.8.
Cross-links: 1.8 · 1.14 · 1.17 · 1.18 · data-type primitives in 01-data-types.md (NBT §11, BitSet §16, VarInt, Long Array).
Primary sources (cited inline as path:line):
- minecraft-data
data/pc/{1.8,1.14,1.17,1.18,1.21.1}/protocol.json—packet_map_chunk/packet_update_lightdefs, diffed for evolution. - ViaVersion (the authoritative paletted-container + section impl):
api/.../type/types/chunk/ChunkType1_18.java,ChunkSectionType1_18.java,PaletteType1_18.java,ChunkType1_8.java;api/.../minecraft/chunks/PaletteType.java,ChunkSection.java;util/CompactArrayUtil.java;api/.../type/types/block/BlockEntityType1_18.java;common/.../protocols/v1_17_1to1_18/rewriter/WorldPacketRewriter1_18.java,.../storage/ChunkLightStorage.java. - minecraft.wiki Java Edition protocol / Chunk format and Packets (fetched 2026-06-19).
| Version | Protocol # | minecraft-data dir | Chunk packet name |
|---|---|---|---|
| 1.8.x | 47 | 1.8 |
packet_map_chunk (0x21) — light embedded |
| 1.14.x | 477 | 1.14 |
packet_map_chunk (0x22) + separate packet_update_light |
| 1.17.x | 755 | 1.17 |
packet_map_chunk + packet_update_light (masks now Long-array BitSets) |
| 1.18.x | 757 | 1.18 |
LEVEL_CHUNK_WITH_LIGHT (0x22) — light re-merged |
| 1.21.1 | 767 | 1.21.1 |
level_chunk_with_light (0x27) — trustEdges dropped |
Protocol numbers from data/pc/<ver>/version.json.
1. Current packet — "Chunk Data and Update Light" (1.18+)
Clientbound, Play state. Named LEVEL_CHUNK_WITH_LIGHT in ViaVersion / packet_map_chunk in minecraft-data. Packet ID is version-dependent (1.18: 0x22; 1.21.1: 0x27 — data/pc/1.21.1/protocol.json play.toClient.types.packet).
Three logical blocks in one packet: (A) Chunk Data, (B) Block Entities, (C) Light Data. The 1.18 merge is exactly: take the old LEVEL_CHUNK (A + B) and append the old LIGHT_UPDATE body (C) with no separator.
1.0 Top-level layout
LEVEL_CHUNK_WITH_LIGHT (clientbound, Play)
┌──────────────────────────────────────────────────────────────────────────┐
│ Int chunkX │ ── A
│ Int chunkZ │
│ NBT heightmaps (named compound; 1.21.1+ anonymous compound) │
│ VarInt dataSize (byte length of the Data field below) │
│ Byte[dataSize] data = ySectionCount × Chunk Section (see §1.2) │
│ VarInt blockEntityCount │ ── B
│ BlockEntity[] blockEntities (count above; struct in §1.3) │
│ ── light (was the separate LIGHT_UPDATE packet pre-1.18) ── │ ── C
│ Boolean trustEdges (PRESENT 1.18–1.20; REMOVED 1.21.1+) │
│ BitSet skyLightMask │
│ BitSet blockLightMask │
│ BitSet emptySkyLightMask │
│ BitSet emptyBlockLightMask │
│ VarInt skyLightArrayCount │
│ ByteArray[] skyLightArrays (each: VarInt len(=2048) + 2048 bytes) │
│ VarInt blockLightArrayCount │
│ ByteArray[] blockLightArrays(each: VarInt len(=2048) + 2048 bytes) │
└──────────────────────────────────────────────────────────────────────────┘
Field order/types confirmed: ChunkType1_18.java:48-86 (read/write of chunkX, chunkZ, heightMap, dataSize, sections, blockEntityCount, blockEntities) and WorldPacketRewriter1_18.java:166-196 (the appended light block). minecraft-data packet_map_chunk for 1.18 lists exactly: x i32, z i32, heightmaps nbt, chunkData buffer(varint), blockEntities array(chunkBlockEntity), trustEdges bool, skyLightMask i64[], blockLightMask i64[], emptySkyLightMask i64[], emptyBlockLightMask i64[], skyLight array(array(u8)), blockLight array(array(u8)).
trustEdgeslifetime. 1.18 keeps thetrustEdgesboolean from the oldLIGHT_UPDATE(WorldPacketRewriter1_18.java:81,183; minecraft-data 1.18packet_map_chunk). It was removed in 1.21.x — minecraft-data1.21.1/packet_map_chunkhas notrustEdgesfield. Use the per-versionpacket_map_chunkto be exact.
1.1 ySectionCount — how many sections in data
data is a flat concatenation of Chunk Sections, one per 16-block-tall Y layer, with no count prefix of its own — the receiver must already know how many to read. The count is the dimension's section count, derived from the dimension-type NBT (height >> 4):
| Dimension | World Y | Block height | Sections (height>>4) |
|---|---|---|---|
| Overworld (1.18+) | −64 … 319 | 384 | 24 |
| Pre-1.18 overworld | 0 … 255 | 256 | 16 |
| Nether / End | 0 … 255 | 256 | 16 |
ViaVersion reads height from the registry NBT in LOGIN/RESPAWN and stores height >> 4 as currentWorldSectionHeight(), then passes it to new ChunkType1_18(ySectionCount, …) so decoding is dimension-aware, not hardcoded to 24 (ChunkType1_18.java:41-46,55-59; tracker plumbing in versions/1.18.md §"World height in the dimension type NBT"). Every section is always serialised, even empty ones (single-value air palette) — there is no longer a presence bitmask (see §3.4).
Light arrays index a larger range than
ySectionCount: there areySectionCount + 2light layers (one extra below the world and one above, for edge lighting). ViaVersion's empty-light fallback sets the mask overcurrentWorldSectionHeight() + 2bits (WorldPacketRewriter1_18.java:173-174). For the overworld that is 26 light layers around 24 block sections.
1.2 Chunk Section (the unit inside data)
Chunk Section
┌────────────────────────────────────────────────────────────┐
│ Short blockCount (non-air block count) │
│ PalettedContainer blockStates (4096 entries = 16×16×16) │
│ PalettedContainer biomes (64 entries = 4×4×4) ◄ NEW 1.18│
└────────────────────────────────────────────────────────────┘
Source: ChunkSectionType1_18.java:49-63 — reads Short non-air count, then the BLOCKS paletted container, then the BIOMES paletted container. Sizes: ChunkSection.java:32 SIZE = 16*16*16 = 4096; ChunkSection.java:37 BIOME_SIZE = 4*4*4 = 64.
blockCount— number of non-air blocks; the client uses it to skip rendering / mark a section empty. Air-only sections haveblockCount = 0.blockStates— paletted container over 4096 entries (one per block in the 16³ section). Entry = a block-state global-registry ID, not a block ID. Cell index ordering is YZX:index = (y<<8) | (z<<4) | x(ChunkSection.java:39).biomes— paletted container over 64 entries (4×4×4 sub-grid; one biome per 4³ cell). Entry = a biome registry ID. This container did not exist before 1.18 — biomes lived in a flat chunk-level array (§3).
26.1+: A second Short fluidCount field is added after blockCount in each Chunk Section. Confirmed: ViaVersion/.../api/type/types/chunk/ChunkSectionType26_1.java reads buffer.readShort() for nonAirBlocksCount then buffer.readShort() for fluidCount (line 43–44). This type is registered in Protocol1_21_11To26_1 — i.e. protocol 26.1 is the version following MC 1.21.11. For 1.18–1.21.11 there is exactly ONE Short (blockCount/nonAirBlocksCount) before the block-states container.
1.3 Block Entity (the array after data)
VarInt blockEntityCount
repeated:
┌──────────────────────────────────────────────────┐
│ Byte packedXZ = ((x & 15) << 4) | (z & 15) │
│ Short y (absolute world Y, signed i16) │
│ VarInt type (block-entity registry type ID) │
│ NBT data (optional NBT; may be TAG_End) │
└──────────────────────────────────────────────────┘
Source: BlockEntityType1_18.java:38-53 (byte xz, short y, varint typeId, named-compound NBT) and ChunkType1_18.java:61-65. minecraft-data top-level type chunkBlockEntity for 1.18 decomposes packedXZ as a bitfield {x: u4 (high nibble), z: u4 (low nibble)}, then y i16, type varint, nbtData optionalNbt. x/z are section-local (0–15); the section is identified by the packet's chunkX/chunkZ. ViaVersion packs it as (byte)((x & 15) << 4 | (z & 15)) when up-converting (WorldPacketRewriter1_18.java:127).
This packed form is new in 1.18 — pre-1.18 block entities were raw NBT compounds carrying their own x/y/z/id fields (§3.3, §4).
1.4 Light Data (block C)
Re-merged into this packet in 1.18 (was the standalone LIGHT_UPDATE since 1.14). Layout (WorldPacketRewriter1_18.java:182-196, write side):
Boolean trustEdges (1.18–1.20 only; gone 1.21.1+)
BitSet skyLightMask \
BitSet blockLightMask | each = VarInt length + that many i64 (longs)
BitSet emptySkyLightMask | → see 01-data-types.md §16 (prefixed BitSet)
BitSet emptyBlockLightMask /
VarInt skyLightArrayCount
repeated skyLightArrayCount×: ByteArray (VarInt length = 2048, then 2048 bytes)
VarInt blockLightArrayCount
repeated blockLightArrayCount×: ByteArray (VarInt length = 2048, then 2048 bytes)
- Each BitSet is wire-encoded as a Long Array: a
VarIntelement-count followed by that manyi64(little-endian-within-long bit indexing per the BitSet spec). ViaVersion writes them withTypes.LONG_ARRAY_PRIMITIVE(WorldPacketRewriter1_18.java:184-187); minecraft-data types themarray(countType: varint, type: i64). The mask bit positions correspond to theySectionCount + 2light layers (bit 0 = the layer below the world). - Sky/Block Light masks mark which light layers are present in the following arrays; Empty masks mark layers that are explicitly all-zero (so no array is sent for them). A layer's bit set in the mask ⇒ one 2048-byte array follows, in mask-bit order.
- Each light array is exactly 2048 bytes = 4096 nibbles = one 4-bit light level per block in a 16³ layer. The wire form is still a length-prefixed byte array (
VarInt 2048then the bytes —Types.BYTE_ARRAY_PRIMITIVE,WorldPacketRewriter1_18.java:189-194); minecraft-data types itarray(varint, array(varint, u8)).
2. PALETTED CONTAINER — the crux (1.18 impl, format introduced 1.9)
A paletted container compresses a fixed number of entries (4096 for blocks, 64 for biomes) by choosing, per container, the smallest practical bits-per-entry and an optional local palette. Authoritative impl: PaletteType1_18.java:43-129 (read/write); thresholds in PaletteType.java:25-27.
PalettedContainer
┌──────────────────────────────────────────────────────────────────────┐
│ Unsigned Byte bitsPerEntry │
│ <palette> ← format SELECTED BY bitsPerEntry (see below) │
│ <data> ← Long Array: VarInt length + that many i64 │
└──────────────────────────────────────────────────────────────────────┘
bitsPerEntry is read as a single byte (PaletteType1_18.java:45). The three formats:
2.1 Single-valued palette — bitsPerEntry == 0
The entire container is one value (e.g. a whole section of air, or a whole biome).
Byte 0
VarInt value ← the one global-registry ID for every entry
VarInt dataArrayLen = 0 (empty data array — NO long data)
Read: PaletteType1_18.java:47-53 — on bitsPerEntry == 0, read one VarInt id, then readValues returns immediately because bitsPerValue == 0. Write: lines 92-100 + 116-121 — write byte 0, the single id, then VarInt 0 for the (empty) data-array length. Note the empty long array still has its VarInt length prefix (= 0).
2.2 Indirect palette
A local palette lists the global IDs actually used; the data array stores indices into that local palette.
Byte bitsPerEntry (blocks: 4–8 ; biomes: 1–3)
VarInt paletteLength
VarInt[paletteLength] palette ← global IDs; data entries index THIS array
VarInt dataArrayLen
i64[dataArrayLen] data ← packed paletteLength-indices, bitsPerEntry wide
Read: PaletteType1_18.java:62-73 — when bitsPerValue != globalPaletteBits, read paletteLength then that many VarInt ids into the local palette, then the packed values (interpreted via setPaletteIndexAt). Write: lines 105-113.
Bits-per-entry ranges (the load-bearing numbers):
| Container | Single | Indirect | Direct (global) |
|---|---|---|---|
| Block states | bpe 0 |
bpe 4–8 | bpe ≥ 9 → global |
| Biomes | bpe 0 |
bpe 1–3 | bpe ≥ 4 → global |
- The indirect ceiling is
PaletteType.highestBitsPerValue(): 8 forBLOCKS, 3 forBIOMES(PaletteType.java:26-27—BLOCKS(ChunkSection.SIZE, 8),BIOMES(ChunkSection.BIOME_SIZE, 3)). Above the ceiling the reader switches to the global/direct palette:if (bitsPerValue < 0 || bitsPerValue > type.highestBitsPerValue()) bitsPerValue = globalPaletteBits(PaletteType1_18.java:55-56). - Block-palette floor = 4 bits. Linear (indirect) block palettes of 1/2/3 bits "can't be read by the client", so the writer clamps to 4 (
PaletteType1_18.java:57-59,133and the comment at 132) and the reader bumps any1 ≤ bpe < 4up to 4 (:57-59). Biomes have no floor — they legitimately use 1, 2, 3. - The wiki gives the same ranges from the client's view: blocks "Indirect (BPE 4-8)", "Direct (BPE ≥15)"; biomes "Indirect (1-3)", "Direct (≥7)" (Chunk format, fetched 2026-06-19). The wiki's "≥15"/"≥7" are the actual global-palette bit widths vanilla emits (≈
ceil(log2(registry_size))), whereas ViaVersion's threshold is "anything above the indirect ceiling (8 / 3) ⇒ useglobalPaletteBits". Both describe the same switch: indirect tops out at 8 (blocks) / 3 (biomes); everything wider is direct.
2.3 Direct palette — bitsPerEntry == globalPaletteBits
No local palette. Data entries are global-registry IDs packed directly.
Byte bitsPerEntry = globalPaletteBits (blocks ~15, biomes ~6/7)
VarInt dataArrayLen
i64[dataArrayLen] data ← packed global IDs, bitsPerEntry wide
Read: PaletteType1_18.java:68-70 — when bitsPerValue == globalPaletteBits, skip the palette and read values straight via setIdAt. globalPaletteBits is passed into the type at construction. ViaVersion computes it as ceilLog2(blockStateMappings.mappedSize()) for blocks and ceilLog2(tracker.biomesSent()) for biomes (WorldPacketRewriter1_18.java:159-161) — i.e. enough bits to index the whole registry.
2.4 Data-array bit packing — no entry spans two longs
This is the detail people get wrong. In the modern (1.16+) format entries are packed so that each entry stays inside one 64-bit long; leftover high bits at the top of each long are padding (zero), not the start of the next entry.
valuesPerLong = floor(64 / bitsPerEntry) ← truncating division
longCount = ceil(entries / valuesPerLong) ← entries = 4096 (blocks) or 64 (biomes)
within a long: entry0 in the LOW bits, then entry1 above it, …; top (64 mod bpe) bits = padding
Source: CompactArrayUtil.createCompactArrayWithPadding:55-73 (write) and iterateCompactArrayWithPadding:75-89 (read) — valuesPerLong = (char)(64 / bitsPerEntry), size = (entries + valuesPerLong - 1) / valuesPerLong, each value shifted by bitIndex += bitsPerEntry and the loop bounded by min(i + valuesPerLong, entries) so it never crosses into the next long. PaletteType1_18.readValues:77-89 computes the expected long count the same way and only iterates if values.length == expectedLength. The wiki states it identically: entries "cannot span across multiple longs; instead, padding is inserted… starting from the most significant bits", "tightly packed within the long, with the first entry on the least significant bits".
Contrast — the OLD tightly-packed format (pre-1.16).
CompactArrayUtil.createCompactArray:91-107/iterateCompactArray:109-125pack entries with no padding, so an entry can straddle a long boundary (startIndex != endIndexbranch at:101-104/:119-122). 1.9–1.15 used this; 1.16 switched to the padded form above. A decoder must pick the packing by version — same palette framing, different long math.
Worked sizes (modern padded format):
- Blocks, bpe = 4:
valuesPerLong = 16,longCount = ceil(4096/16) = 256longs. - Blocks, bpe = 8:
valuesPerLong = 8,longCount = ceil(4096/8) = 512longs. - Blocks, bpe = 15 (direct):
valuesPerLong = 4,longCount = ceil(4096/4) = 1024longs. - Biomes, bpe = 1:
valuesPerLong = 64,longCount = ceil(64/64) = 1long. - Biomes, bpe = 3:
valuesPerLong = 21,longCount = ceil(64/21) = 4longs.
3. Evolution — what changed at each step
Diff source: data/pc/<ver>/protocol.json packet_map_chunk (+ packet_update_light). The five steps below are the structural inflection points.
3.1 — 1.8 (proto 47): per-section palette is born; presence bitmask; light embedded
packet_map_chunk (1.8) = { x i32, z i32, groundUp bool, bitMap u16, chunkData buffer(varint) }. Inside chunkData (decoded by ChunkType1_8.deserialize:88-124):
for each of 16 sections, IF (bitMap >> i) & 1: block data (per-section palette)
for each present section: block light (2048 bytes)
if NORMAL dimension, for each present section: sky light (2048 bytes)
if groundUp (full chunk): biome array = 256 bytes (one per X,Z column)
Key facts of this era:
- Per-section block palette introduced (the ancestor of today's paletted container) — sections carry their own palette + packed data.
bitMap(u16) selects which of the 16 sections are present (ChunkType1_8.read:58, decode loop:95-98).groundUp/full-chunk (:57) means "replace the whole column, biomes included".- Light is INSIDE the chunk packet, interleaved per section, block-light then sky-light (
deserialize:100-112). There is no separate light packet yet. - Biomes = a flat 256-byte array (one biome byte per horizontal column, 2-D), only on full chunks (
:115-120). No per-section, no 3-D biomes. - No heightmaps; no block-entity array (block entities came via separate
update_block_entitypackets / chunk-bulk).
Source: ChunkType1_8.java:53-160; minecraft-data 1.8/packet_map_chunk (groundUp bool, bitMap u16).
Cross-link: versions/1.8.md.
3.2 — 1.9 (proto 107+): block-entity NBT array appended
1.9.3+ appends a block-entities array to the chunk: count + that many full NBT compounds, each self-describing via its own x/y/z/id tags. This is why pre-1.18 the block entity is "just NBT" (contrast §1.3's packed struct). The 1.9 line also moved block IDs to the global block-state registry that the palette indexes.
(minecraft-data carries the array on packet_map_chunk from 1.14 onward in the dirs we diffed; the 1.9-era addition is documented in versions/1.9.md. The flat-packed long arithmetic of this era is the createCompactArray/iterateCompactArray "entries CAN span longs" form — see §2.4.)
3.3 — 1.14 (proto 477): light SPLIT OUT; heightmaps NBT added
packet_map_chunk (1.14) = { x i32, z i32, groundUp bool, bitMap varint, heightmaps nbt, chunkData buffer(varint), blockEntities array(nbt) }. New packet_update_light = { chunkX varint, chunkZ varint, skyLightMask varint, blockLightMask varint, emptySkyLightMask varint, emptyBlockLightMask varint, data restBuffer }.
Changes vs 1.8/1.9:
- Light removed from the chunk packet and moved to a separate
Update Lightpacket (client now receives chunk and light as two packets). The chunk packet no longer carries the per-section light arrays. heightmapsNBT added to the chunk (MOTION_BLOCKING,WORLD_SURFACE, etc. — packed long arrays inside an NBT compound), supporting the new lighting engine.bitMapwidened u16 → VarInt (still a section-presence bitmask).- Block entities are an explicit
array(nbt)field on the packet (full NBT each). - In 1.14 the
update_lightmasks are still single VarInts (≤32 sections fit in 32 bits).
Source: minecraft-data 1.14/packet_map_chunk + 1.14/packet_update_light.
Cross-link: versions/1.14.md.
3.4 — 1.17 (proto 755): full-chunks-only; masks become Long-array BitSets; biomes go 3-D-flat
packet_map_chunk (1.17) = { x i32, z i32, bitMap i64[], heightmaps nbt, biomes array(varint), chunkData buffer(varint), blockEntities array(nbt) }. packet_update_light (1.17) = { chunkX, chunkZ, trustEdges bool, skyLightMask i64[], blockLightMask i64[], emptySkyLightMask i64[], emptyBlockLightMask i64[], skyLight array(array(u8)), blockLight array(array(u8)) }.
Changes vs 1.14:
groundUp/ partial-chunk dropped — 1.17 only sends full chunks; there is no longer a "groundUp" boolean (the field is gone frompacket_map_chunk). The world-height growth to come needed a clean full-column model.bitMapbecame a Long-array BitSet (array(varint, i64)) instead of a VarInt, because section counts could exceed 32. The light masks inupdate_lightlikewise became Long-array BitSets, andtrustEdges+ explicitskyLight/blockLightarray-of-arrays appeared (the form §1.4 later inherits).- Biomes promoted to a chunk-level
VarInt[]= 1024 entries (4×4×4 per section × 16 sections) — full 3-D biomes, but still a flat array at chunk level, not per-section. ViaVersion reads this flat array and re-slices it into per-section palettes when up-converting to 1.18 (WorldPacketRewriter1_18.java:131,146-155). - World height still 0…255 (16 sections).
Source: minecraft-data 1.17/packet_map_chunk + 1.17/packet_update_light; ChunkType1_17 (read) referenced from WorldPacketRewriter1_18.java:107.
Cross-link: versions/1.17.md.
3.5 — 1.18 (proto 757): height −64…319 (24 sections); biomes → per-section container; light RE-MERGED
packet_map_chunk (1.18) drops bitMap and the chunk-level biomes array, and appends the whole update_light body: { x i32, z i32, heightmaps nbt, chunkData buffer(varint), blockEntities array(chunkBlockEntity), trustEdges bool, skyLightMask i64[], blockLightMask i64[], emptySkyLightMask i64[], emptyBlockLightMask i64[], skyLight array(array(u8)), blockLight array(array(u8)) }.
Changes vs 1.17 (the heavy ones):
- World height −64…319 ⇒ 24 sections (was 16). Section count is dimension-driven (
height >> 4), not constant (§1.1). - Section presence bitmask REMOVED — every section is serialised, empty ones using the single-value air palette. ViaVersion synthesises an air section for previously-absent ones when up-converting:
new ChunkSectionImpl()+ a single-id palette of0(WorldPacketRewriter1_18.java:135-144). - Biomes moved INTO each section as a paletted container (4×4×4, §1.2/§2). The flat chunk-level
biomes VarInt[]is gone; ViaVersion fills the per-section biome palettes from the old flat array (WorldPacketRewriter1_18.java:146-155). - Light RE-MERGED into "Chunk Data and Update Light" — the separate
LIGHT_UPDATEbody is appended directly (§1.4). Proxies bridging 1.17↔1.18 must buffer one packet until the other arrives (ChunkLightStorage.java;WorldPacketRewriter1_18.java:66-103,163-196). - Block entities packed into
chunkBlockEntitystructs (packedXZ/y/type/optNBT) instead of full self-describing NBT (§1.3).
Source: minecraft-data 1.17 vs 1.18 packet_map_chunk; ViaVersion ChunkType1_18.java, ChunkSectionType1_18.java, WorldPacketRewriter1_18.java. Full prose in versions/1.18.md §"Chunk packet restructure".
3.6 — after 1.18 (context)
- 1.21.x:
trustEdgesremoved from the chunk packet (minecraft-data1.21.1/packet_map_chunkhas notrustEdges); packet ID shifted to0x27. heightmaps switched to anonymous (network/unnamed) NBT (anonymousNbt). - 1.16: the data-array packing changed from "entries-can-span-longs" to the padded "no entry spans two longs" form (§2.4) — a silent but decoder-breaking change with no field-list delta.
- 26.1+: a second
Short fluidCountis added to each Chunk Section. Confirmed viaChunkSectionType26_1.java(reads tworeadShort()calls) andProtocol1_21_11To26_1.java(usesChunkType26_1). Source:ViaVersion/.../api/type/types/chunk/ChunkSectionType26_1.java; see §1.2 for detailed layout.
4. Quick comparison matrix
| Aspect | 1.8 (47) | 1.14 (477) | 1.17 (755) | 1.18+ (757) |
|---|---|---|---|---|
Sections in data |
16, bitmask-selected | 16, bitmask-selected | 16, full-chunk only | 24 (dim-driven), all present |
| Section presence | bitMap u16 |
bitMap VarInt |
bitMap i64[] BitSet |
no mask (air palette) |
| Block palette | per-section (spanning longs) | per-section | per-section | per-section, padded longs (§2.4) |
| Biomes | 256-byte 2-D array | 256-byte (full chunk) | flat VarInt[1024] 3-D | per-section paletted container |
| Heightmaps | none | NBT added | NBT | NBT (anon NBT in 1.21+) |
| Block entities | none (separate pkts) | NBT array | NBT array | packed chunkBlockEntity |
| Light | embedded in chunk | separate Update Light |
separate Update Light |
re-merged into chunk pkt |
| Light masks | n/a (interleaved) | single VarInt | i64[] BitSet | i64[] BitSet |
5. Paletted-container cheat-sheet (decoder pseudocode, 1.18+)
read_paletted_container(buf, entries, indirectCeiling, globalBits): # blocks: 4096,8,~15 biomes: 64,3,~6
bpe = buf.read_u8()
if bpe == 0: # single-valued
value = buf.read_varint()
_len = buf.read_varint() # == 0, empty data array
return uniform(entries, value)
if bpe > indirectCeiling: # direct / global
bpe = globalBits
palette = None # data holds global IDs
else: # indirect
if blocks and bpe < 4: bpe = 4 # block-palette floor (biomes: no floor)
n = buf.read_varint(); palette = [buf.read_varint() for _ in range(n)]
longs = buf.read_varint(); data = [buf.read_i64() for _ in range(longs)]
# unpack: valuesPerLong = 64 // bpe ; entry i is in long (i // valuesPerLong),
# at bit ((i % valuesPerLong) * bpe), masked to bpe bits. NO entry spans two longs.
return [ palette[idx] if palette else idx for idx in unpack(data, bpe, entries) ]
Matches PaletteType1_18.read:43-89 + CompactArrayUtil.iterateCompactArrayWithPadding:75-89 exactly.