Files
claude-timemachine 02a249a379 verify pass 2: resolve markers via ViaBackwards/ViaRewind/ViaLegacy
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>
2026-06-19 19:13:19 +02:00

334 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<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.181.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).
**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 4344). This type is registered in `Protocol1_21_11To26_1` — i.e. protocol 26.1 is the version following MC 1.21.11. For 1.181.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** (015); 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.181.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 │
│ <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: 48 ; biomes: 13)
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 **48** | bpe **≥ 9** → global |
| **Biomes** | bpe `0` | bpe **13** | 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.91.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/<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_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. Confirmed via `ChunkSectionType26_1.java` (reads two `readShort()` calls) and `Protocol1_21_11To26_1.java` (uses `ChunkType26_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.