packets/: packet model + catalogs + wire-format deep-dives
Control-state catalogs (handshake/status/login/config), categorized Play catalog (~182 packets), and deep-dives on the four hard formats: chunk data (paletted containers + light), entity metadata (type registry), slot/structured components, command graph (Brigadier). Sourced from minecraft-data + ViaVersion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
# 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.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).
|
||||
|
||||
<!-- UNCONFIRMED: the latest wiki (1.21.9-era) shows a second `Short fluidCount` field after `blockCount` in the Chunk Section — ViaVersion exposes ChunkSection.getFluidCount() "Available with versions 26.1+" (ChunkSection.java:67-73). For 1.18–1.21.x there is exactly ONE Short (blockCount) 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 `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: 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/<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 (ViaVersion `ChunkSection.getFluidCount` "Available with versions 26.1+", `ChunkSection.java:67-73`). <!-- UNCONFIRMED for exact protocol/version number; flagged in §1.2 -->
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user