# 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](../versions/1.8.md) · [1.14](../versions/1.14.md) · [1.17](../versions/1.17.md) · [1.18](../versions/1.18.md) · data-type primitives in [01-data-types.md](../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_light` defs, 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](https://minecraft.wiki/w/Java_Edition_protocol/Chunk_format) and [Packets](https://minecraft.wiki/w/Java_Edition_protocol/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//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))`. > **`trustEdges` lifetime.** 1.18 keeps the `trustEdges` boolean from the old `LIGHT_UPDATE` (`WorldPacketRewriter1_18.java:81,183`; minecraft-data 1.18 `packet_map_chunk`). It was **removed in 1.21.x** — minecraft-data `1.21.1/packet_map_chunk` has no `trustEdges` field. Use the per-version `packet_map_chunk` to 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 are `ySectionCount + 2` light layers (one extra below the world and one above, for edge lighting). ViaVersion's empty-light fallback sets the mask over `currentWorldSectionHeight() + 2` bits (`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 have `blockCount = 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). ### 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 `VarInt` element-count followed by that many `i64` (little-endian-within-long bit indexing per the BitSet spec). ViaVersion writes them with `Types.LONG_ARRAY_PRIMITIVE` (`WorldPacketRewriter1_18.java:184-187`); minecraft-data types them `array(countType: varint, type: i64)`. The mask bit positions correspond to the `ySectionCount + 2` light 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 2048` then the bytes — `Types.BYTE_ARRAY_PRIMITIVE`, `WorldPacketRewriter1_18.java:189-194`); minecraft-data types it `array(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 │ │ ← format SELECTED BY bitsPerEntry (see below) │ │ ← 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 for `BLOCKS`**, **3 for `BIOMES`** (`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,133` and the comment at 132) and the reader bumps any `1 ≤ bpe < 4` up 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](https://minecraft.wiki/w/Java_Edition_protocol/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) ⇒ use `globalPaletteBits`". 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-125` pack entries with **no padding**, so an entry **can straddle a long boundary** (`startIndex != endIndex` branch 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) = 256` longs. - Blocks, bpe = 8: `valuesPerLong = 8`, `longCount = ceil(4096/8) = 512` longs. - Blocks, bpe = 15 (direct): `valuesPerLong = 4`, `longCount = ceil(4096/4) = 1024` longs. - Biomes, bpe = 1: `valuesPerLong = 64`, `longCount = ceil(64/64) = 1` long. - Biomes, bpe = 3: `valuesPerLong = 21`, `longCount = ceil(64/21) = 4` longs. --- ## 3. Evolution — what changed at each step Diff source: `data/pc//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_entity` packets / 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](../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](../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 Light` packet** (client now receives chunk and light as two packets). The chunk packet no longer carries the per-section light arrays. - **`heightmaps` NBT added** to the chunk (`MOTION_BLOCKING`, `WORLD_SURFACE`, etc. — packed long arrays inside an NBT compound), supporting the new lighting engine. - `bitMap` widened **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_light` masks 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](../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 from `packet_map_chunk`). The world-height growth to come needed a clean full-column model. - **`bitMap` became a Long-array BitSet** (`array(varint, i64)`) instead of a VarInt, because section counts could exceed 32. The light masks in `update_light` likewise became Long-array BitSets, and `trustEdges` + explicit `skyLight`/`blockLight` array-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](../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 of `0` (`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_UPDATE` body 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 `chunkBlockEntity` structs (`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](../versions/1.18.md) §"Chunk packet restructure". ### 3.6 — after 1.18 (context) - **1.21.x**: `trustEdges` removed from the chunk packet (minecraft-data `1.21.1/packet_map_chunk` has no `trustEdges`); packet ID shifted to `0x27`. 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 fluidCount` is added to each Chunk Section (ViaVersion `ChunkSection.getFluidCount` "Available with versions 26.1+", `ChunkSection.java:67-73`). --- ## 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.