diff --git a/README.md b/README.md index 4080299..d9f1eab 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A from-scratch, version-aware study of the Minecraft: Java Edition network proto | [07-version-differences.md](07-version-differences.md) | Protocol-number table 1.7.10→latest, the major breaking changes per era, and how ViaVersion translates between them | | [proxy-forwarding/](proxy-forwarding/) | What a proxy does; online vs offline mode; BungeeCord legacy forwarding; Velocity modern forwarding; BungeeGuard; Forge/FML | | [versions/](versions/INDEX.md) | **Per-version deep-dives** — one doc per release line (1.7.10→26.2), what changed in each protocol bump, sourced from release notes + ViaVersion commits + wiki | +| [packets/](packets/README.md) | **Packet reference** — the packet model, complete control-state catalogs, the categorized Play catalog (~182 packets), + wire-format deep-dives: chunk data (paletted containers), entity metadata, slot/components, command graph | | [references.md](references.md) | Every source: spec pages + which reference repo to read for what | | [PLAN.md](PLAN.md) | How this repo was built — orchestration + per-agent research spec | diff --git a/packets/README.md b/packets/README.md new file mode 100644 index 0000000..ba16676 --- /dev/null +++ b/packets/README.md @@ -0,0 +1,111 @@ +# Packets — Model & Navigation + +> **Scope:** Java Edition, 1.21.1 baseline (protocol 773) unless noted. +> **Sources:** `minecraft-data/data/pc/1.21.1/protocol.json` · minecraft.wiki/w/Java_Edition_protocol/Packets + +--- + +## 1. Packet Framing Recap + +Every packet is **length-prefixed over TCP**. Two modes exist: + +**Uncompressed** (before `Set Compression` arrives): + +``` +[Length: VarInt] [Packet ID: VarInt] [Payload: bytes...] +``` + +**Compressed** (after `Set Compression`; applies for rest of session): + +``` +[Packet Length: VarInt] [Data Length: VarInt] [Packet ID + Payload: zlib or raw] +``` + +`Data Length = 0` means the inner payload was not compressed (below threshold). When `Data Length > 0` it is the uncompressed size of `(Packet ID + Payload)`. + +Full framing spec with code citations: [../00-overview.md](../00-overview.md) §2. + +--- + +## 2. Packet ID Scoping + +**Packet IDs are scoped by BOTH connection state AND traffic direction.** + +The same byte value `0x00` identifies a completely different packet in each slot: + +| State | Direction | 0x00 packet | +|---------------|-------------|------------------------| +| Handshaking | Serverbound | Handshake | +| Status | Serverbound | Status Request | +| Status | Clientbound | Status Response | +| Login | Serverbound | Login Start | +| Login | Clientbound | Disconnect (Login) | +| Configuration | Serverbound | Client Information | +| Configuration | Clientbound | Cookie Request | +| Play | Serverbound | Confirm Teleportation | +| Play | Clientbound | Bundle Delimiter | + +A parser **must** track the current state and which peer sent the packet before it can decode the ID. State transitions are driven by specific packets (Handshake → Status/Login, Login Acknowledged → Configuration, Finish Configuration → Play). + +--- + +## 3. ID Instability Across Versions + +Packet IDs within a state/direction namespace are **not stable between protocol versions**. When Mojang adds or removes packets, every subsequent ID in that list shifts. + +Examples: +- Configuration state did not exist before 1.20.2 (protocol 764). All its IDs are new. +- In 1.20.5 (protocol 766), Cookie/Transfer/Store-Cookie/Resource-Pack-Pop/Resource-Pack-Push were added to Configuration, shifting existing IDs upward. +- Login gained `Cookie Request` (CB 0x05) and `Cookie Response` (SB 0x04) in 1.20.5. +- `Login Acknowledged` (SB 0x03) was added in 1.20.2. + +For per-version ID tables, see [../versions/](../versions/) and the authoritative machine-readable source below. + +--- + +## 4. Authoritative Per-Version Packet Definitions + +`minecraft-data` provides a complete, machine-readable protocol schema for every supported version: + +``` +/tmp/mcproto-refs/minecraft-data/data/pc//protocol.json +``` + +Structure per state/direction: + +```json +{ + "": { + "toClient": { + "types": { + "packet": ["container", [ + { "name": "name", "type": ["mapper", { "type": "varint", "mappings": { "0x00": "..." } }] }, + { "name": "params", "type": ["switch", { "compareTo": "name", "fields": { "...": "packet_..." } }] } + ]], + "packet_": ["container", [ ... field defs ... ]] + } + } + } +} +``` + +The `mappings` object is the ID→name table. The `packet_` types give field-level detail. + +--- + +## 5. Navigation + +| Document | Contents | +|----------|----------| +| [catalog-control-states.md](catalog-control-states.md) | Complete packet tables for Handshaking, Status, Login, Configuration (1.21.1) | +| [catalog-play.md](catalog-play.md) | Play-state packet index (high count — separate file) | +| [format-chunk-data.md](format-chunk-data.md) | Chunk Data & Update Light packet payload format | +| [format-entity-metadata.md](format-entity-metadata.md) | Entity metadata encoding, type IDs, value formats | +| [format-slot-and-components.md](format-slot-and-components.md) | Item slot encoding; data component system (1.20.5+) | +| [format-command-graph.md](format-command-graph.md) | Declare Commands packet graph encoding | +| [../versions/](../versions/) | Per-version delta notes; ID shift history | +| [../00-overview.md](../00-overview.md) | Transport, framing, compression, encryption | +| [../03-handshake.md](../03-handshake.md) | Handshake packet deep-dive | +| [../04-status-ping.md](../04-status-ping.md) | Status/ping flow detail | +| [../05-login-encryption.md](../05-login-encryption.md) | Login + encryption flow detail | +| [../06-configuration.md](../06-configuration.md) | Configuration state detail | diff --git a/packets/catalog-control-states.md b/packets/catalog-control-states.md new file mode 100644 index 0000000..10c57a3 --- /dev/null +++ b/packets/catalog-control-states.md @@ -0,0 +1,179 @@ +# Packet Catalog — Control States + +Complete packet tables for the small, fully-enumerable states: **Handshaking, Status, Login, Configuration**. + +> **Baseline:** 1.21.1 (protocol 773). Version deltas noted inline. +> **Sources:** +> - `minecraft-data/data/pc/1.21.1/protocol.json` — IDs and field types (machine-readable ground truth) +> - ViaVersion `ClientboundLoginPackets.java`, `ServerboundLoginPackets.java` — enum ordinal cross-check +> - ViaVersion `ClientboundConfigurationPackets1_20_2.java`, `1_20_5.java`, `ServerboundConfigurationPackets1_20_2.java`, `1_20_5.java` — version delta cross-check +> - minecraft.wiki/w/Java_Edition_protocol/Packets — field names and semantic descriptions +> +> **See also:** [../03-handshake.md](../03-handshake.md) · [../04-status-ping.md](../04-status-ping.md) · [../05-login-encryption.md](../05-login-encryption.md) · [../06-configuration.md](../06-configuration.md) + +--- + +## Handshaking State + +The handshaking state has **no clientbound packets**. There is exactly one modern serverbound packet; the legacy ping byte is a legacy artifact that predates the formal state machine. + +### Serverbound + +| ID | Name | Direction | Field summary | +|--------|------------------------|-----------|---------------| +| `0x00` | Handshake | C→S | `protocolVersion` (VarInt), `serverHost` (String ≤255), `serverPort` (u16), `nextState` (VarInt: 1=Status, 2=Login, 3=Transfer¹) | +| `0xFE` | Legacy Server List Ping | C→S | `payload` (u8, always `0x01`); pre-1.7 clients only — modern servers recognize and respond with legacy format² | + +¹ `nextState=3` (Transfer) added in 1.20.5 (protocol 766). +² `0xFE` is outside the normal VarInt packet-ID space; servers detect it by the raw byte value. + +Source: `minecraft-data/data/pc/1.21.1/protocol.json` → `handshaking.toServer.types` mappings `{"0x00": "set_protocol", "0xfe": "legacy_server_list_ping"}`. + +--- + +## Status State + +Two packets in each direction. IDs are stable since 1.7. + +### Serverbound + +| ID | Name | Direction | Field summary | +|--------|----------------|-----------|---------------| +| `0x00` | Status Request | C→S | No fields; triggers Status Response | +| `0x01` | Ping Request | C→S | `time` (i64, ms timestamp) | + +### Clientbound + +| ID | Name | Direction | Field summary | +|--------|-----------------|-----------|---------------| +| `0x00` | Status Response | S→C | `response` (String; JSON with version, players, description, favicon) | +| `0x01` | Pong Response | S→C | `time` (i64; echo of Ping Request's timestamp) | + +Source: `minecraft-data/data/pc/1.21.1/protocol.json` → `status.toServer` / `status.toClient` mappings. + +--- + +## Login State + +Login was extended in **1.20.2** (Login Acknowledged) and **1.20.5** (Cookie Request/Response). + +### Serverbound + +| ID | Name | Direction | Since | Field summary | +|--------|------------------------|-----------|---------|---------------| +| `0x00` | Login Start | C→S | 1.7 | `username` (String ≤16), `playerUUID` (UUID) | +| `0x01` | Encryption Response | C→S | 1.7 | `sharedSecret` (ByteArray, VarInt-length-prefixed; RSA-encrypted), `verifyToken` (ByteArray, VarInt-length-prefixed; RSA-encrypted) | +| `0x02` | Login Plugin Response | C→S | 1.13 | `messageId` (VarInt; echoes request), `data` (optional restBuffer; absent if unhandled) | +| `0x03` | Login Acknowledged | C→S | 1.20.2 | No fields; transitions server to Configuration state | +| `0x04` | Cookie Response | C→S | 1.20.5 | `key` (String; resource location), `value` (optional ByteArray ≤5120 bytes) | + +### Clientbound + +| ID | Name | Direction | Since | Field summary | +|--------|------------------------|-----------|---------|---------------| +| `0x00` | Disconnect (Login) | S→C | 1.7 | `reason` (String; JSON text component) | +| `0x01` | Encryption Request | S→C | 1.7 | `serverId` (String, always `""` in modern protocol¹), `publicKey` (ByteArray; DER-encoded RSA), `verifyToken` (ByteArray; random nonce), `shouldAuthenticate` (bool; false = offline-mode) | +| `0x02` | Login Success | S→C | 1.7 | `uuid` (UUID), `username` (String ≤16), `properties` (Array of {name:String, value:String, signature:Optional\}), `strictErrorHandling` (bool; added 1.20.5²) | +| `0x03` | Set Compression | S→C | 1.8 | `threshold` (VarInt; packets ≥ this size get compressed; −1 = disable) | +| `0x04` | Login Plugin Request | S→C | 1.13 | `messageId` (VarInt; unique per request), `channel` (String; resource location), `data` (restBuffer; plugin-defined payload) | +| `0x05` | Cookie Request (Login) | S→C | 1.20.5 | `cookie` (String; resource location key) | + +¹ Server ID was used for session-server auth in very old versions; now always empty string. +² `strictErrorHandling` field added in 1.20.5 — older clients receive a shorter Login Success. + +Source: `minecraft-data/data/pc/1.21.1/protocol.json` → `login.toServer` / `login.toClient` mappings. +ViaVersion cross-check: `ClientboundLoginPackets.java` ordinals 0–5 match; `ServerboundLoginPackets.java` ordinals 0–4 match. + +--- + +## Configuration State + +**Added in 1.20.2 (protocol 764).** Not present in 1.20 or earlier. + +The Configuration state runs between Login Acknowledged and Finish Configuration. Its purpose: push registry data, feature flags, resource packs, and tags before the client enters Play. The state can be re-entered from Play (server sends `Start Configuration`). + +The packet set expanded again in **1.20.5 (protocol 766)**: Cookie, Transfer, Store Cookie, resource-pack split (push/pop vs. unified), Select Known Packs, Custom Report Details, Server Links were added, shifting most IDs. + +### Serverbound + +| ID | Name | Direction | Since | Field summary | +|--------|------------------------------|-----------|---------|---------------| +| `0x00` | Client Information | C→S | 1.20.2 | `locale` (String ≤16), `viewDistance` (i8), `chatFlags` (VarInt), `chatColors` (bool), `skinParts` (u8 bitmask), `mainHand` (VarInt: 0=left,1=right), `enableTextFiltering` (bool), `allowServerListings` (bool) | +| `0x01` | Cookie Response | C→S | 1.20.5 | `key` (String; resource location), `value` (optional ByteArray ≤5120 bytes) | +| `0x02` | Plugin Message | C→S | 1.20.2 | `channel` (String; resource location), `data` (restBuffer; `minecraft:brand` → UTF-8 VarInt-prefixed client brand string) | +| `0x03` | Acknowledge Finish Configuration | C→S | 1.20.2 | No fields; transitions to Play state | +| `0x04` | Keep Alive | C→S | 1.20.2 | `keepAliveId` (i64; echoes server's value) | +| `0x05` | Pong | C→S | 1.20.2 | `id` (i32; echoes server's Ping id) | +| `0x06` | Resource Pack Response | C→S | 1.20.2 | `uuid` (UUID; 1.20.5+¹), `result` (VarInt: 0=Success,1=Declined,2=Failed,3=Accepted,4=Downloaded,5=InvalidUrl,6=FailedReload,7=Discarded) | +| `0x07` | Select Known Packs | C→S | 1.20.5 | `packs` (Array of {namespace:String, id:String, version:String}) | +| `0x08` | Custom Report Details | C→S | 1.21 | `details` (Array of {key:String, value:String}; debug crash info)² | +| `0x09` | Server Links Response | C→S | 1.21 | `links` (Array; echoes known-type or custom-text link list)² | + +¹ In 1.20.2–1.20.4 the resource pack response had no UUID field; UUID was added when resource-pack-push/pop split was introduced in 1.20.5. +² IDs `0x08` and `0x09` are present in 1.21.1 per `protocol.json` (`custom_report_details`, `server_links`) but absent from earlier ViaVersion 1.20.5 enum — confirm exact introduction version. + + + +**ID shift note (1.20.2 → 1.20.5 serverbound):** + +| ID (1.20.2) | Name | ID (1.20.5+) | +|-------------|------------------|--------------| +| `0x00` | Client Information | `0x00` (no change) | +| `0x01` | Plugin Message | `0x02` (+1) | +| `0x02` | Finish Configuration | `0x03` (+1) | +| `0x03` | Keep Alive | `0x04` (+1) | +| `0x04` | Pong | `0x05` (+1) | +| `0x05` | Resource Pack | `0x06` (+1) | +| — | Cookie Response | `0x01` (new) | +| — | Select Known Packs | `0x07` (new) | + +Source: `minecraft-data/data/pc/1.21.1/protocol.json` → `configuration.toServer`; `ViaVersion/.../ServerboundConfigurationPackets1_20_2.java` vs `ServerboundConfigurationPackets1_20_5.java`. + +--- + +### Clientbound + +| ID | Name | Direction | Since | Field summary | +|--------|------------------------|-----------|---------|---------------| +| `0x00` | Cookie Request | S→C | 1.20.5 | `cookie` (String; resource location key to retrieve) | +| `0x01` | Plugin Message | S→C | 1.20.2 | `channel` (String), `data` (restBuffer; `minecraft:brand` sends server brand) | +| `0x02` | Disconnect | S→C | 1.20.2 | `reason` (anonymousNbt; text component as NBT, not JSON string) | +| `0x03` | Finish Configuration | S→C | 1.20.2 | No fields; server signals config complete, waits for SB Acknowledge | +| `0x04` | Keep Alive | S→C | 1.20.2 | `keepAliveId` (i64; client must echo within ~30s or be kicked) | +| `0x05` | Ping | S→C | 1.20.2 | `id` (i32; client echoes in Pong) | +| `0x06` | Reset Chat | S→C | 1.20.2 | No fields; clears client chat session state | +| `0x07` | Registry Data | S→C | 1.20.2 | `id` (String; registry resource location), `entries` (Array of {key:String, value:Optional\}); one packet per registry | +| `0x08` | Remove Resource Pack | S→C | 1.20.5 | `uuid` (optional UUID; absent = remove all) | +| `0x09` | Add Resource Pack | S→C | 1.20.5 | `uuid` (UUID), `url` (String), `hash` (String, SHA-1 hex), `forced` (bool), `promptMessage` (Optional\ text component) | +| `0x0A` | Store Cookie | S→C | 1.20.5 | `key` (String; resource location), `value` (ByteArray ≤5120 bytes) | +| `0x0B` | Transfer | S→C | 1.20.5 | `host` (String), `port` (VarInt); redirects client to another server | +| `0x0C` | Feature Flags | S→C | 1.20.2 | `features` (Array\; resource location list of active experimental features) | +| `0x0D` | Update Tags | S→C | 1.20.2 | `tags` (Array of {tagType:String, tags:Array of {tagName:String, entries:Array\}}) | +| `0x0E` | Select Known Packs | S→C | 1.20.5 | `packs` (Array of {namespace:String, id:String, version:String}); client replies SB Select Known Packs | +| `0x0F` | Custom Report Details | S→C | 1.21 | `details` (Array of {key:String, value:String}); appears in crash reports¹ | +| `0x10` | Server Links | S→C | 1.21 | `links` (Array of {hasKnownType:bool, knownType:ServerLinkType (if known), unknownType:NbtComponent (if custom), url:String})¹ | + +¹ `0x0F` and `0x10` present in 1.21.1 `protocol.json`; not in ViaVersion 1.20.5 CB enum (15 entries, 0x00–0x0E). Likely introduced 1.21.0 or 1.21.1. + + + +**ID shift note (1.20.2 → 1.20.5 clientbound):** + +| ID (1.20.2) | Name | ID (1.20.5+) | +|-------------|-------------------|--------------| +| `0x00` | Plugin Message | `0x01` (+1) | +| `0x01` | Disconnect | `0x02` (+1) | +| `0x02` | Finish Configuration | `0x03` (+1) | +| `0x03` | Keep Alive | `0x04` (+1) | +| `0x04` | Ping | `0x05` (+1) | +| `0x05` | Registry Data | `0x07` (+2) | +| `0x06` | Resource Pack | split: `0x08`/`0x09` | +| `0x07` | Feature Flags | `0x0C` (+5) | +| `0x08` | Update Tags | `0x0D` (+5) | +| — | Cookie Request | `0x00` (new) | +| — | Reset Chat | `0x06` (new) | +| — | Store Cookie | `0x0A` (new) | +| — | Transfer | `0x0B` (new) | +| — | Select Known Packs | `0x0E` (new) | + +Source: `minecraft-data/data/pc/1.21.1/protocol.json` → `configuration.toClient`; `ViaVersion/.../ClientboundConfigurationPackets1_20_2.java` vs `ClientboundConfigurationPackets1_20_5.java`. diff --git a/packets/catalog-play.md b/packets/catalog-play.md new file mode 100644 index 0000000..a498a95 --- /dev/null +++ b/packets/catalog-play.md @@ -0,0 +1,453 @@ +# Play-State Packet Catalog — Java Edition 1.21.1 (protocol 767) + +**Version lock**: all IDs on this page are 1.21.1 (protocol 767). Play-state IDs shift between versions — sometimes heavily. See [`../versions/1.21.md`](../versions/1.21.md) for the delta from 1.20.x, and [`../versions/INDEX.md`](../versions/INDEX.md) for the full version map. + +**Sources**: +- `minecraft-data`: `/tmp/mcproto-refs/minecraft-data/data/pc/1.21.1/protocol.json` (packet IDs + field types, authoritative for 1.21.1) +- `ViaVersion CB`: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_20_5to1_21/packet/ClientboundPackets1_21.java` (ordered enum — ordinal = wire ID) +- `ViaVersion SB`: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_20_3to1_20_5/packet/ServerboundPackets1_20_5.java` (serverbound IDs unchanged from 1.20.5→1.21.1; no new SB enum file in the `v1_20_5to1_21` package) + +**Totals** (minecraft-data count): **124 clientbound**, **58 serverbound** = 182 play-state packets. + +**Notation**: +- `CB` = Server→Client (clientbound) +- `SB` = Client→Server (serverbound) +- `→ format-*.md` = that packet's binary layout has its own deep-dive doc in this directory +- `` = claim not verified against a second source + +--- + +## 1. Connection / System + +Core session-maintenance and state-transition packets. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x00 | `BUNDLE_DELIMITER` | `bundle_delimiter` | Frame boundary: server wraps a group of CB packets between two 0x00 delimiters; client applies them atomically (added 1.19.4) | +| 0x26 | `KEEP_ALIVE` | `keep_alive` | Heartbeat: server sends random `i64` ID; client must echo it within timeout or be kicked | +| 0x35 | `PING` | `ping` | Echoes an `i32` ID back from client's `PONG` (not the status-ping; used for latency measurement mid-session) | +| 0x36 | `PONG_RESPONSE` | `ping_response` | Server response to client's `PING_REQUEST` (0x21 SB); carries `i64` payload | +| 0x1D | `DISCONNECT` | `kick_disconnect` | Kick with chat-component reason (Text component, NBT-encoded in 1.20.3+) | +| 0x2B | `LOGIN` | `login` | **Join Game** — first play-state packet after configuration; see detail below | +| 0x47 | `RESPAWN` | `respawn` | Sent on dimension change or respawn; see detail below | +| 0x53 | `SET_CARRIED_ITEM` | `held_item_slot` | Sets the client's active hotbar slot (0–8) | +| 0x69 | `START_CONFIGURATION` | `start_configuration` | Transitions back to Configuration state for resource-pack / registry updates (1.20.2+) | +| 0x19 | `CUSTOM_PAYLOAD` | `custom_payload` | Plugin channel message (`Identifier` channel + byte blob) | +| 0x16 | `COOKIE_REQUEST` | `cookie_request` | Server requests a previously stored cookie value from the client (1.20.5+) | +| 0x6B | `STORE_COOKIE` | `store_cookie` | Server pushes a key/value cookie to the client for later retrieval (1.20.5+) | +| 0x73 | `TRANSFER` | `transfer` | Transfers the client to a different server (host + port); 1.20.5+ | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x18 | `KEEP_ALIVE` | `keep_alive` | Echo of server's `i64` keep-alive ID | +| 0x27 | `PONG` | `pong` | Echo of server's `PING` `i32` ID | +| 0x21 | `PING_REQUEST` | `ping_request` | Client-initiated latency probe; carries `i64` payload; server echoes via `PONG_RESPONSE` | +| 0x00 | `ACCEPT_TELEPORTATION` | `teleport_confirm` | Client acks a `PLAYER_POSITION` teleport by echoing its `varint teleportId` | +| 0x0C | `CONFIGURATION_ACKNOWLEDGED` | `configuration_acknowledged` | Client acks `START_CONFIGURATION`, triggering transition back to Configuration state | +| 0x11 | `COOKIE_RESPONSE` | `cookie_response` | Response to `COOKIE_REQUEST`; carries key + optional value | +| 0x12 | `CUSTOM_PAYLOAD` | `custom_payload` | Plugin channel message (SB direction) | +| 0x2B | `RESOURCE_PACK` | `resource_pack_receive` | Client reports resource-pack download status (accepted/declined/downloaded/failed) | + +--- + +### Detail: LOGIN (0x2B CB) — Join Game + +First play-state packet sent by the server (after Configuration → Play transition). Sets up the client's world context. + +Fields (from `protocol.json` `packet_login`): + +| Field | Type | Notes | +|-------|------|-------| +| `entityId` | i32 | Client's entity ID | +| `isHardcore` | bool | | +| `worldNames` | varint-prefixed string[] | All dimension names known to the server | +| `maxPlayers` | varint | (informational, not enforced client-side) | +| `viewDistance` | varint | Server's configured view distance | +| `simulationDistance` | varint | | +| `reducedDebugInfo` | bool | Hides debug overlay | +| `enableRespawnScreen` | bool | | +| `doLimitedCrafting` | bool | | +| `worldState` | SpawnInfo | Composite: dimension type ID, dimension name, hashed seed, gamemode, prev gamemode, debug/flat flags, death location (optional), portal cooldown | +| `enforcesSecureChat` | bool | If true, server requires signed chat messages | + +### Detail: RESPAWN (0x47 CB) + +Sent on dimension switch, death respawn, or `/respawn`. Resets world context without a full Login. + +Fields (`packet_respawn`): + +| Field | Type | Notes | +|-------|------|-------| +| `worldState` | SpawnInfo | Same composite type as Login | +| `copyMetadata` | u8 | Bitmask: bit 0 = keep attributes, bit 1 = keep entity data | + +--- + +## 2. Entity + +Spawning, movement, metadata, equipment, effects, and removal. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x01 | `ADD_ENTITY` | `spawn_entity` | Spawn any non-XP entity; carries entity ID (varint), UUID, type (varint), position (f64×3), angles, data (varint, type-dependent), velocity | +| 0x02 | `ADD_EXPERIENCE_ORB` | `spawn_entity_experience_orb` | Spawn XP orb; entity ID + position + count (i16) | +| 0x03 | `ANIMATE` | `animation` | Play entity animation (swing arm, take damage, leave bed, etc.); entity ID + animation byte | +| 0x2E | `MOVE_ENTITY_POS` | `rel_entity_move` | Delta position only (fixed-point i16×3); entity moved < 8 blocks | +| 0x2F | `MOVE_ENTITY_POS_ROT` | `entity_move_look` | Delta position + rotation | +| 0x30 | `MOVE_ENTITY_ROT` | `entity_look` | Rotation only | +| 0x48 | `ROTATE_HEAD` | `entity_head_rotation` | Yaw of entity head (separate from body) | +| 0x31 | `MOVE_VEHICLE` | `vehicle_move` | Absolute position + rotation for mounted vehicle | +| 0x70 | `TELEPORT_ENTITY` | `entity_teleport` | Absolute position + rotation for entities beyond delta range (>8 blocks) | +| 0x5A | `SET_ENTITY_MOTION` | `entity_velocity` | Velocity in fixed-point i16 units (1/8000 m/tick) | +| 0x58 | `SET_ENTITY_DATA` | `entity_metadata` | Entity metadata key-value stream; **→ `format-entity-metadata.md`** | +| 0x5B | `SET_EQUIPMENT` | `entity_equipment` | Equipment slots (main/off hand, 4 armor slots); slot bitmask + Slot data | +| 0x76 | `UPDATE_MOB_EFFECT` | `entity_effect` | Apply/update a potion effect (entity ID, effect ID, amplifier, duration, flags) | +| 0x43 | `REMOVE_MOB_EFFECT` | `remove_entity_effect` | Remove a specific effect from entity | +| 0x59 | `SET_ENTITY_LINK` | `attach_entity` | Attach entity A to entity B (leash); entity ID + vehicle ID (-1 to detach) | +| 0x5F | `SET_PASSENGERS` | `set_passengers` | Set which entities are riding on a vehicle; vehicle entity ID + array of passenger IDs | +| 0x42 | `REMOVE_ENTITIES` | `entity_destroy` | Remove one or more entities by ID (varint array) | +| 0x1F | `ENTITY_EVENT` | `entity_status` | Entity-specific status byte (e.g. death animation, tipped arrow hit, villager anger) | +| 0x1A | `DAMAGE_EVENT` | `damage_event` | Entity took damage: entity ID, damage type, source entity/position (1.19.4+) | +| 0x24 | `HURT_ANIMATION` | `hurt_animation` | Visual-only hurt swing (entity ID + yaw); added 1.19.4 | +| 0x75 | `UPDATE_ATTRIBUTES` | `entity_update_attributes` | Update base attribute values (movement speed, attack damage, etc.) | +| 0x6F | `TAKE_ITEM_ENTITY` | `collect` | Entity picked up an item (item entity ID + collector entity ID + pickup count) | +| 0x3F | `PLAYER_LOOK_AT` | `face_player` | Make entity/player face a position or another entity | +| 0x79 | `PROJECTILE_POWER` | `set_projectile_power` | Projectile acceleration vector | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x1A | `MOVE_PLAYER_POS` | `position` | Player position update (`f64` x/y/z + `bool` onGround) | +| 0x1B | `MOVE_PLAYER_POS_ROT` | `position_look` | Position + yaw/pitch | +| 0x1C | `MOVE_PLAYER_ROT` | `look` | Rotation only | +| 0x1D | `MOVE_PLAYER_STATUS_ONLY` | `flying` | Ground status only (used when no other movement to report) | +| 0x1E | `MOVE_VEHICLE` | `vehicle_move` | Absolute position + rotation while controlling a vehicle | +| 0x16 | `INTERACT` | `use_entity` | Interact with entity: entity ID + action (attack/interact/interact-at) + hand + position (for interact-at) | +| 0x24 | `PLAYER_ACTION` | `block_dig` | Block dig/place actions (start dig, cancel, finish, drop item, swap hands, shoot arrow) + location + face + sequence | +| 0x25 | `PLAYER_COMMAND` | `entity_action` | Player-state commands: sneak start/stop, sprint start/stop, leave bed, start/stop elytra, horse jump | +| 0x36 | `SWING` | `arm_animation` | Arm swing animation (main or off hand) | +| 0x37 | `TELEPORT_TO_ENTITY` | `spectate` | Spectator teleport to entity UUID | +| 0x26 | `PLAYER_INPUT` | `steer_vehicle` | Vehicle steering input (sideways/forward + flags for jump/unmount) | +| 0x1F | `PADDLE_BOAT` | `steer_boat` | Boat paddle state (left + right `bool`) | +| 0x20 | `PICK_ITEM` | `pick_item` | Pick Block (middle-click); carries hotbar slot index | +| 0x15 | `ENTITY_TAG_QUERY` | `query_entity_nbt` | Debug: request entity NBT (debug build) | + +--- + +## 3. World + +Chunks, blocks, lighting, world events, and world border. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x27 | `LEVEL_CHUNK_WITH_LIGHT` | `map_chunk` | Full chunk column + light data bundled; **→ `format-chunk-data.md`** | +| 0x2A | `LIGHT_UPDATE` | `update_light` | Update lighting only (no block data); same sky/block light structure as above; **→ `format-chunk-data.md`** | +| 0x0C | `CHUNK_BATCH_FINISHED` | `chunk_batch_finished` | Signals end of chunk batch; carries batch size (varint); client responds with `CHUNK_BATCH_RECEIVED` (1.20.2+) | +| 0x0D | `CHUNK_BATCH_START` | `chunk_batch_start` | Signals start of chunk batch (1.20.2+) | +| 0x0E | `CHUNKS_BIOMES` | `chunk_biomes` | Update biome data for existing chunks without re-sending block data (1.20+) | +| 0x21 | `FORGET_LEVEL_CHUNK` | `unload_chunk` | Unload a chunk column; `i32` chunk X + Z | +| 0x54 | `SET_CHUNK_CACHE_CENTER` | `update_view_position` | Shift the client's chunk center (sent on player movement between chunks) | +| 0x55 | `SET_CHUNK_CACHE_RADIUS` | `update_view_distance` | Update render distance | +| 0x09 | `BLOCK_UPDATE` | `block_change` | Single block change: `Position` + block state ID (varint) | +| 0x49 | `SECTION_BLOCKS_UPDATE` | `multi_block_change` | Multiple block changes in one chunk section; section coords + array of `(localPos << 12 | stateId)` i64 | +| 0x07 | `BLOCK_ENTITY_DATA` | `tile_entity_data` | Update a block entity's NBT (chest, furnace, sign, etc.) | +| 0x08 | `BLOCK_EVENT` | `block_action` | Block action event (e.g. chest open/close sound, note block pitch, piston extension); location + action byte + action param + block type | +| 0x06 | `BLOCK_DESTRUCTION` | `block_break_animation` | Show block breaking animation progress (0–9) for a given entity ID at a position | +| 0x05 | `BLOCK_CHANGED_ACK` | `acknowledge_player_digging` | Server acknowledges a block-dig sequence number; carries sequence ID + position + block state + status + successful bool | +| 0x28 | `LEVEL_EVENT` | `world_event` | World event/sound (door open, explosion sound, portal, block break sound, etc.); event ID + position + data + global flag | +| 0x20 | `EXPLODE` | `explosion` | Explosion: position (f64), radius (f32), array of affected block offsets, player knockback velocity, interaction type, particles, sound | +| 0x22 | `GAME_EVENT` | `game_state_change` | Game state notifications: start/stop rain, change gamemode, win game, demo events, bed/respawn prompts, guardian elder effect, etc. Carries `u8` reason + `f32` value | +| 0x56 | `SET_DEFAULT_SPAWN_POSITION` | `spawn_position` | World spawn point (used for compass direction) | +| 0x25 | `INITIALIZE_BORDER` | `initialize_world_border` | Full world border state (center, size, lerp target, lerp time, warning blocks/time) | +| 0x4D | `SET_BORDER_CENTER` | `world_border_center` | Update border center | +| 0x4E | `SET_BORDER_LERP_SIZE` | `world_border_lerp_size` | Animate border size change | +| 0x4F | `SET_BORDER_SIZE` | `world_border_size` | Set border size instantly | +| 0x50 | `SET_BORDER_WARNING_DELAY` | `world_border_warning_delay` | Warning time (seconds) | +| 0x51 | `SET_BORDER_WARNING_DISTANCE` | `world_border_warning_reach` | Warning distance (blocks) | +| 0x64 | `SET_TIME` | `update_time` | World age (i64, always incrementing) + time of day (i64, 0–24000; negative = frozen) | +| 0x62 | `SET_SIMULATION_DISTANCE` | `simulation_distance` | Server's entity simulation radius | +| 0x71 | `TICKING_STATE` | `set_ticking_state` | Tick rate + whether frozen (1.20.3+) | +| 0x72 | `TICKING_STEP` | `step_tick` | Advance frozen world by N ticks (1.20.3+) | +| 0x0B | `CHANGE_DIFFICULTY` | `difficulty` | Current difficulty + locked flag | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x08 | `CHUNK_BATCH_RECEIVED` | `chunk_batch_received` | Client reports desired chunks/tick back to server after batch (throttle feedback) | +| 0x01 | `BLOCK_ENTITY_TAG_QUERY` | `query_block_nbt` | Debug: request block entity NBT at position | +| 0x02 | `CHANGE_DIFFICULTY` | `set_difficulty` | Op changes world difficulty (op-only) | +| 0x19 | `LOCK_DIFFICULTY` | `lock_difficulty` | Op locks/unlocks difficulty | +| 0x38 | `USE_ITEM_ON` | `block_place` | Place/use block: hand + block position + face + cursor position + head-inside-block flag + sequence | +| 0x39 | `USE_ITEM` | `use_item` | Use item in hand (right-click in air): hand + sequence | +| 0x17 | `JIGSAW_GENERATE` | `generate_structure` | Generate jigsaw structure (op/debug) | +| 0x30 | `SET_COMMAND_BLOCK` | `update_command_block` | Update command block: position + command + mode + flags | +| 0x31 | `SET_COMMAND_MINECART` | `update_command_block_minecart` | Update command block in minecart | +| 0x33 | `SET_JIGSAW_BLOCK` | `update_jigsaw_block` | Update jigsaw block settings | +| 0x34 | `SET_STRUCTURE_BLOCK` | `update_structure_block` | Update structure block settings | +| 0x35 | `SIGN_UPDATE` | `update_sign` | Submit sign text (front/back): position + is-front-side + 4 lines | + +--- + +## 4. Inventory / Container + +Opening, populating, and interacting with containers and the player inventory. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x33 | `OPEN_SCREEN` | `open_window` | Open a container UI; window ID + menu type (varint) + title (NBT text) | +| 0x23 | `HORSE_SCREEN_OPEN` | `open_horse_window` | Open horse inventory UI (separate from generic containers) | +| 0x12 | `CONTAINER_CLOSE` | `close_window` | Server closes a container (window ID) | +| 0x13 | `CONTAINER_SET_CONTENT` | `window_items` | Full slot sync for open container; window ID + `stateId` + Slot array + carried item; **Slot type → `format-slot-and-components.md`** | +| 0x15 | `CONTAINER_SET_SLOT` | `set_slot` | Update single slot; window ID + `stateId` + slot index (i16) + Slot; **→ `format-slot-and-components.md`** | +| 0x14 | `CONTAINER_SET_DATA` | `craft_progress_bar` | Update a container "property" (furnace progress, enchant seed, etc.); window ID + property ID (i16) + value (i16) | +| 0x32 | `OPEN_BOOK` | `open_book` | Open a book UI (hand: main/off) | +| 0x34 | `OPEN_SIGN_EDITOR` | `open_sign_entity` | Open sign editor UI for a position; bool for front/back side | +| 0x37 | `PLACE_GHOST_RECIPE` | `craft_recipe_response` | Show recipe in crafting grid (response to `PLACE_RECIPE` SB) | +| 0x17 | `COOLDOWN` | `set_cooldown` | Start item use cooldown (item ID + ticks) | +| 0x2D | `MERCHANT_OFFERS` | `trade_list` | Villager trade list (window ID + trades array + villager level/XP + regular villager + can restock) | +| 0x2C | `MAP_ITEM_DATA` | `map` | Map item data (map ID + scale + locked + icons + patch data) | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x0F | `CONTAINER_CLOSE` | `close_window` | Client closed a container (window ID) | +| 0x0E | `CONTAINER_CLICK` | `window_click` | Inventory click; window ID + `stateId` + slot (i16) + button + mode + changed slots array + cursor Slot | +| 0x0D | `CONTAINER_BUTTON_CLICK` | `enchant_item` | Click a container button (enchanting table slot select, trade button, etc.) | +| 0x10 | `CONTAINER_SLOT_STATE_CHANGED` | `set_slot_state` | Toggle a slot state (e.g. toggle a crafting grid slot on/off — added 1.20.2) | +| 0x22 | `PLACE_RECIPE` | `craft_recipe_request` | Request to auto-fill a recipe in crafting grid; window ID + recipe ID + make-all flag | +| 0x32 | `SET_CREATIVE_MODE_SLOT` | `set_creative_slot` | Creative-mode direct slot set; slot (i16) + Slot data | +| 0x29 | `RECIPE_BOOK_SEEN_RECIPE` | `displayed_recipe` | Mark a recipe as seen in the recipe book | +| 0x28 | `RECIPE_BOOK_CHANGE_SETTINGS` | `recipe_book` | Toggle recipe book filter/open state | +| 0x2A | `RENAME_ITEM` | `name_item` | Submit anvil rename string | +| 0x2E | `SET_BEACON` | `set_beacon_effect` | Set beacon effect (primary + secondary effect IDs) | +| 0x2D | `SELECT_TRADE` | `select_trade` | Select villager trade by index | +| 0x14 | `EDIT_BOOK` | `edit_book` | Submit book contents (hand + pages array + optional title) | + +--- + +## 5. Player + +Position synchronization, health, XP, abilities, and player list. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x40 | `PLAYER_POSITION` | `position` | **Synchronize player position**: server-authoritative teleport; fields: x/y/z (f64), yaw/pitch (f32), flags (PositionUpdateRelatives bitmask — absolute vs relative per axis), `teleportId` (varint); client must confirm with `ACCEPT_TELEPORTATION`; see field detail below | +| 0x38 | `PLAYER_ABILITIES` | `abilities` | Player ability flags (invulnerable, flying, allow-fly, creative-mode) + fly speed + field-of-view modifier | +| 0x5D | `SET_HEALTH` | `update_health` | Health (f32), food (varint), saturation (f32) | +| 0x5C | `SET_EXPERIENCE` | `experience` | XP bar progress (f32 0–1) + total XP levels (varint) + total XP points (varint) | +| 0x3E | `PLAYER_INFO_UPDATE` | `player_info` | Add/update player list entries; bitmask of actions (add player, initialize chat, update gamemode, update listed, update latency, update display name) + array of UUID + per-action data | +| 0x3D | `PLAYER_INFO_REMOVE` | `player_remove` | Remove player list entries by UUID array | +| 0x3A | `PLAYER_COMBAT_END` | `end_combat_event` | Player exited combat (duration + entity ID) | +| 0x3B | `PLAYER_COMBAT_ENTER` | `enter_combat_event` | Player entered combat | +| 0x3C | `PLAYER_COMBAT_KILL` | `death_combat_event` | Player killed in combat (player entity ID + death message) | +| 0x39 | `PLAYER_CHAT` | `player_chat` | Signed player chat message; see field detail below | +| 0x09 | `CLIENT_COMMAND` (mismap) | — | See SB below | +| 0x52 | `SET_CAMERA` | `camera` | Set which entity the player's camera follows | +| 0x31 | `MOVE_VEHICLE` | `vehicle_move` | Sync vehicle position to client | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x09 | `CLIENT_COMMAND` | `client_command` | Perform client action: 0 = respawn, 1 = open inventory stats | +| 0x0A | `CLIENT_INFORMATION` | `settings` | Client settings: locale, view distance, chat mode, colors, skin parts, main hand, filter text, allow server listings | +| 0x23 | `PLAYER_ABILITIES` | `abilities` | Client reports flying flag (bit 1); other flags ignored by server | +| 0x2F | `SET_CARRIED_ITEM` | `held_item_slot` | Change held hotbar slot (0–8) | +| 0x0B | `COMMAND_SUGGESTION` | `tab_complete` | Tab-complete request; `transactionId` (varint) + partial command string | +| 0x13 | `DEBUG_SAMPLE_SUBSCRIPTION` | `debug_sample_subscription` | Subscribe to debug performance samples (tick duration, ping, etc.) | + +--- + +### Detail: PLAYER_POSITION (0x40 CB) — Synchronize Player Position + +The server asserts the player's canonical position. Client must respond with `ACCEPT_TELEPORTATION` (0x00 SB) carrying the same `teleportId` before sending movement packets will be trusted again. + +Fields (from `protocol.json` `packet_position`): + +| Field | Type | Notes | +|-------|------|-------| +| `x` | f64 | | +| `y` | f64 | | +| `z` | f64 | | +| `yaw` | f32 | | +| `pitch` | f32 | | +| `flags` | `PositionUpdateRelatives` (u8 bitmask) | Bit 0=X, 1=Y, 2=Z, 3=pitch, 4=yaw: if set, the value is *relative* to current | +| `teleportId` | varint | Client echoes in `ACCEPT_TELEPORTATION` | + +--- + +## 6. UI / HUD + +Chat messages (all three kinds), titles, scoreboard, boss bar, tab list. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x6C | `SYSTEM_CHAT` | `system_chat` | System message (NBT text component + `isActionBar` bool); used for `/say`, `/tell`, server announcements | +| 0x39 | `PLAYER_CHAT` | `player_chat` | Signed player chat; carries sender UUID, message, timestamp, signature (256-byte optional), chat type decoration reference, optional unsigned fallback; see detail below | +| 0x1E | `DISGUISED_CHAT` | `profileless_chat` | System-style chat that mimics a player message without a UUID (no signature); added 1.19.3 | +| 0x1C | `DELETE_CHAT` | `hide_message` | Delete a previously sent signed chat message by signature hash | +| 0x65 | `SET_TITLE_TEXT` | `set_title_text` | Show title (NBT text) | +| 0x63 | `SET_SUBTITLE_TEXT` | `set_title_subtitle` | Show subtitle (NBT text) | +| 0x4C | `SET_ACTION_BAR_TEXT` | `action_bar` | Action bar message (NBT text) | +| 0x66 | `SET_TITLES_ANIMATION` | `set_title_time` | Title timing: fade-in, stay, fade-out (all i32 ticks) | +| 0x0F | `CLEAR_TITLES` | `clear_titles` | Clear title/subtitle/action bar; `bool` reset (resets timing) | +| 0x0A | `BOSS_EVENT` | `boss_bar` | Boss bar operation: ADD, REMOVE, UPDATE_HEALTH, UPDATE_TITLE, UPDATE_STYLE, UPDATE_FLAGS; UUID + per-action data | +| 0x6D | `TAB_LIST` | `playerlist_header` | Tab list header + footer (both NBT text) | +| 0x5E | `SET_OBJECTIVE` | `scoreboard_objective` | Create/remove/update scoreboard objective; name + mode (0=create,1=remove,2=update) + display name + type | +| 0x61 | `SET_SCORE` | `scoreboard_score` | Set an entity's score for an objective; entity name + objective name + score (varint) + optional display name + optional number format | +| 0x44 | `RESET_SCORE` | `reset_score` | Remove a score entry; entity name + optional objective name (1.20.3+) | +| 0x57 | `SET_DISPLAY_OBJECTIVE` | `scoreboard_display_objective` | Set which objective appears in a display slot (sidebar, list, below-name) | +| 0x60 | `SET_PLAYER_TEAM` | `teams` | Scoreboard team operations: CREATE, REMOVE, INFO_UPDATE, ADD_PLAYERS, REMOVE_PLAYERS | +| 0x18 | `CUSTOM_CHAT_COMPLETIONS` | `chat_suggestions` | Add/remove/set chat autocomplete suggestions (1.19.1+) | +| 0x0B | `CHANGE_DIFFICULTY` (mismap) | `difficulty` | See World § above | +| 0x1B | `DEBUG_SAMPLE` | `debug_sample` | Performance debug sample push (tick durations, etc.) | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x06 | `CHAT` | `chat_message` | Player chat message; text + timestamp + salt + optional signature + ack bitset | +| 0x04 | `CHAT_COMMAND` | `chat_command` | Run command (unsigned); command string | +| 0x05 | `CHAT_COMMAND_SIGNED` | `chat_command_signed` | Run command with signed arguments (1.19.1+) | +| 0x07 | `CHAT_SESSION_UPDATE` | `chat_session_update` | Update session signing key (public key + expiry + signature) | +| 0x03 | `CHAT_ACK` | `message_acknowledgement` | Acknowledge received signed chat messages (cumulative offset into message history) | + +--- + +### Detail: PLAYER_CHAT (0x39 CB) — Signed Chat + +Fields (`protocol.json` `packet_player_chat`): + +| Field | Type | Notes | +|-------|------|-------| +| `senderUuid` | UUID | | +| `index` | varint | Position in sender's message sequence | +| `signature` | optional byte[256] | RSA-signed message hash; absent for unsigned messages | +| `plainMessage` | string | Raw text | +| `timestamp` | i64 | Unix ms | +| `salt` | i64 | | +| `previousMessages` | previousMessages | Compact acknowledgment of prior messages (used for signature chaining) | +| `unsignedChatContent` | optional NBT | Decorated/filtered version if different from plain | +| `filterType` | varint | 0=pass-through, 1=fully-filtered, 2=partially-filtered | +| `filterTypeMask` | conditional | Bitmask when `filterType=2` | +| `type` | ChatTypesHolder | Reference into registry for formatting (chat, say-command, etc.) | +| `networkName` | NBT | Sender's display name | +| `networkTargetName` | optional NBT | Target for directed messages (whisper, /tell) | + +--- + +## 7. Commands / Recipes + +The command graph, tab-complete, and recipe synchronization. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x11 | `COMMANDS` | `declare_commands` | Full command graph as a node tree; **→ `format-command-graph.md`** | +| 0x10 | `COMMAND_SUGGESTIONS` | `tab_complete` | Tab-complete response; `transactionId` + match start + length + array of (text, optional tooltip) | +| 0x77 | `UPDATE_RECIPES` | `declare_recipes` | Full recipe list (crafting, smelting, stonecutting, etc.) | +| 0x41 | `RECIPE` | `unlock_recipes` | Recipe book state: action (init/add/remove) + recipe IDs shown + recipe IDs highlighted + display settings | +| 0x37 | `PLACE_GHOST_RECIPE` | `craft_recipe_response` | (Repeat from Inventory §) Display recipe in crafting grid | +| 0x4A | `SELECT_ADVANCEMENTS_TAB` | `select_advancement_tab` | Open advancement tab (optional identifier) | +| 0x74 | `UPDATE_ADVANCEMENTS` | `advancements` | Advancement data: reset flag + added map + removed IDs + progress map | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x0B | `COMMAND_SUGGESTION` | `tab_complete` | Tab-complete request (see Player §5 SB) | +| 0x22 | `PLACE_RECIPE` | `craft_recipe_request` | (Repeat from Inventory §) | +| 0x28 | `RECIPE_BOOK_CHANGE_SETTINGS` | `recipe_book` | Recipe book toggle | +| 0x29 | `RECIPE_BOOK_SEEN_RECIPE` | `displayed_recipe` | Mark recipe seen | +| 0x2C | `SEEN_ADVANCEMENTS` | `advancement_tab` | Open/close an advancement tab | + +--- + +## 8. Sound / Particle + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x68 | `SOUND` | `sound_effect` | Named sound at fixed position (sound ID varint, category, x/y/z as fixed-point i32, volume f32, pitch f32, seed i64) | +| 0x67 | `SOUND_ENTITY` | `entity_sound_effect` | Same but position follows an entity ID | +| 0x6A | `STOP_SOUND` | `stop_sound` | Stop sounds matching a category (optional) and/or sound ID (optional) | +| 0x29 | `LEVEL_PARTICLES` | `world_particles` | Spawn particle effect: particle type + data + position (f64) + offset (f32×3) + max speed + count + long-range flag | +| 0x28 | `LEVEL_EVENT` | `world_event` | (See World §) Also carries many sound/effect IDs | + +### Serverbound + +_(No SB packets for sound/particle — client never requests specific sounds.)_ + +--- + +## 9. Misc + +Tags, resource packs, server info, and debug/telemetry. + +### Clientbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x78 | `UPDATE_TAGS` | `tags` | Registry tag lists (block, item, fluid, entity type, etc.); map of registry → array of (tag ID + value IDs) | +| 0x46 | `RESOURCE_PACK_PUSH` | `add_resource_pack` | Push a resource pack to client: UUID + URL + hash + forced + optional prompt message (1.20.3: UUID-keyed; 1.20.5: stacks) | +| 0x45 | `RESOURCE_PACK_POP` | `remove_resource_pack` | Remove a previously pushed resource pack by UUID (optional; clear all if absent) | +| 0x4B | `SERVER_DATA` | `server_data` | Server icon (optional PNG bytes) + enforces-secure-chat flag (sent once at login) | +| 0x6E | `TAG_QUERY` | `nbt_query_response` | Response to block/entity NBT query; transaction ID + NBT | +| 0x04 | `AWARD_STATS` | `statistics` | Statistics update (category + statistic + value; varint array) | +| 0x2C | `MAP_ITEM_DATA` | `map` | (See Inventory §) | +| 0x3C | `PLAYER_COMBAT_KILL` | `death_combat_event` | (See Player §) | +| 0x7A | `CUSTOM_REPORT_DETAILS` | `custom_report_details` | Key-value map sent to crash reports (1.20.5+) | +| 0x7B | `SERVER_LINKS` | `server_links` | List of labeled URLs (bug report, feedback, support, etc.) shown in pause menu (1.20.5+) | + +### Serverbound + +| ID | ViaVersion name | mc-data name | Purpose | +|----|-----------------|--------------|---------| +| 0x2B | `RESOURCE_PACK` | `resource_pack_receive` | Client status for a resource pack UUID: accepted/declined/success/failed | + +--- + +## Cross-links + +- **Slot type** (used in container packets): [`format-slot-and-components.md`](format-slot-and-components.md) — Slot encoding, item component NBT, 1.20.5 data-driven component format +- **Entity metadata**: [`format-entity-metadata.md`](format-entity-metadata.md) — per-type metadata keys, value encoding +- **Chunk data + lighting**: [`format-chunk-data.md`](format-chunk-data.md) — section encoding, palette, heightmaps, block entity list, sky/block light arrays +- **Command graph**: [`format-command-graph.md`](format-command-graph.md) — node types, flags, parser IDs, redirect mechanism +- **Version deltas**: [`../versions/1.21.md`](../versions/1.21.md) — what changed 1.20.5→1.21; [`../versions/INDEX.md`](../versions/INDEX.md) — full version map + +> **Note**: The `format-*.md` cross-link targets do not exist yet in this directory — they are planned deep-dive docs. Links will resolve when those docs are written. + +--- + +## Version caveat + +Play-state packet IDs change **frequently** between versions. Examples of large shifts: + +| Version range | Notable shift | +|---|---| +| 1.19 → 1.19.1 | Chat signing added; PLAYER_CHAT restructured | +| 1.20.2 | Configuration state added; START_CONFIGURATION 0x69 is new | +| 1.20.3 | NBT chat components replace string; SET_SCORE/RESET_SCORE split | +| 1.20.5 | Item components (data-driven Slot); cookies; resource-pack UUID stacking; SERVER_LINKS; CUSTOM_REPORT_DETAILS | +| 1.21 (767) | No major new play packets vs 1.20.5; PROJECTILE_POWER (0x79) added | + +Always cross-check IDs against `minecraft-data/data/pc//protocol.json` or the ViaVersion `ClientboundPackets` enum for the target version. diff --git a/packets/format-chunk-data.md b/packets/format-chunk-data.md new file mode 100644 index 0000000..55b8938 --- /dev/null +++ b/packets/format-chunk-data.md @@ -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//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. diff --git a/packets/format-command-graph.md b/packets/format-command-graph.md new file mode 100644 index 0000000..b1822cc --- /dev/null +++ b/packets/format-command-graph.md @@ -0,0 +1,442 @@ +# Commands (Declare Commands) Packet + +Clientbound · Play state +Added: **1.13** (protocol 393) + +Sends the server's Brigadier command graph to the client so it can populate tab-completion and validate syntax client-side before sending the command. + +Packet IDs by version: +| Version | Packet ID | +|---------|-----------| +| 1.13 | `0x11` | +| 1.19 | `0x0F` | +| 1.21.1 | `0x11` | + +Sources: +- `minecraft-data/data/pc/1.13/protocol.json` — top-level `types.command_node` + `play.toClient` packet list +- `minecraft-data/data/pc/1.19/protocol.json` — same, post-VarInt-parser change +- `minecraft-data/data/pc/1.21.1/protocol.json` — same, 1.21.1 parser table +- `ViaVersion/common/src/main/java/com/viaversion/viaversion/rewriter/CommandRewriter.java` — `registerDeclareCommands` (String era) and `handle1_19` / `registerDeclareCommands1_19` (VarInt era) +- `ViaVersion/…/protocols/v1_18_2to1_19/Protocol1_18_2To1_19.java` lines 178–210 — transition handler (reads `Types.STRING`, writes `Types.VAR_INT`) + +See also: [../versions/1.13.md](../versions/1.13.md) · [../versions/1.19.md](../versions/1.19.md) + +--- + +## Packet Layout + +``` +Count VarInt number of nodes in the flat node array +Nodes[] command_node[] one record per node (see below) +Root Index VarInt index into Nodes[] of the root node +``` + +The node array is **flat** — nodes reference each other by index, not by nesting. The root node is always type 0 (root); its index is given by `Root Index` at the end of the packet. + +--- + +## Node Record (`command_node`) + +Fields are conditional on the `flags` byte: + +``` +Flags byte (bitfield) +Children VarInt count, then VarInt[] of child node indices +Redirect Node VarInt — only if flags bit 3 (0x08) set +Name String — only for literal (type 1) and argument (type 2) nodes +Parser ID String|VarInt — only for argument nodes (type 2); encoding changed in 1.19 +Properties (variable) — only for argument nodes; depends on Parser ID +Suggestions String/Identifier — only if flags bit 4 (0x10) set +``` + +### Flags Byte (bitfield) + +The byte is parsed from **LSB to MSB**: + +``` +Bit(s) Mask Field +------ ----- ------------------------------------------- +0–1 0x03 node type: 0 = root, 1 = literal, 2 = argument +2 0x04 is_executable — this node itself is a valid end-of-input +3 0x08 has_redirect — a Redirect Node index follows Children +4 0x10 has_suggestions — a Suggestions Type identifier follows Properties + (argument nodes only; ignored on other types) +5–7 0xE0 unused +``` + +Note: `minecraft-data` names these `command_node_type` (bits 0–1), `has_command` (bit 2), `has_redirect_node` (bit 3), `has_custom_suggestions` (bit 4). + +### Children + +``` +ChildCount VarInt number of children +Children[] VarInt[] indices into the flat node array +``` + +### Redirect Node + +Present only when `flags & 0x08 != 0`. A single VarInt index into the flat node array. Used for command aliases — e.g. `/tp` and `/teleport` can share the same subgraph via redirect. + +### Name + +Present for node types **literal** (1) and **argument** (2). Not present for root (0). A length-prefixed UTF-8 String (max 32767 chars per the standard String encoding). + +### Parser ID + Properties + +Present only for argument nodes (type 2). The parser encoding **changed in 1.19**. + +--- + +## Parser Encoding: String (1.13) vs VarInt (1.19+) + +### Pre-1.19 (1.13 through 1.18.x) + +Parser is a plain **String** (identifier, e.g. `"brigadier:string"`, `"minecraft:entity"`). + +``` +Parser String e.g. "brigadier:integer" +``` + +Source: `minecraft-data/data/pc/1.13/protocol.json` `types.command_node` extraNodeData type=2 → `parser: "string"`. +ViaVersion `CommandRewriter.registerDeclareCommands()` reads `Types.STRING` for the parser name. + +### 1.19+ (protocol 759 onwards) + +Parser is a **VarInt** registry index (a compact enum of all known parser names). + +``` +Parser VarInt e.g. 5 = "brigadier:string" +``` + +Source: `minecraft-data/data/pc/1.19/protocol.json` `types.command_node` extraNodeData type=2 → `parser: ["mapper", {"type": "varint", ...}]`. +ViaVersion `Protocol1_18_2To1_19.java` lines 194–200: reads `Types.STRING`, looks up `MAPPINGS.getArgumentTypeMappings().mappedId(argumentType)`, writes `Types.VAR_INT`. +ViaVersion `CommandRewriter.handle1_19()` line 131: reads `Types.VAR_INT` and resolves it to a name via `argumentType(argumentTypeId)`. + +The property format **did not change** — only the parser identifier encoding changed. Properties are still parsed by the same per-parser logic after resolving the VarInt to a name. + +--- + +## Common Parsers and Their Properties + +All parsers with no listed properties send nothing (`void`). + +### Brigadier parsers (no namespace qualifier needed for these 6) + +| Parser name | VarInt ID (1.19+) | Properties | +|---------------------|:-----------------:|------------| +| `brigadier:bool` | 0 | none | +| `brigadier:float` | 1 | flags byte (bit 0 = min present, bit 1 = max present), then optional f32 min, optional f32 max | +| `brigadier:double` | 2 | flags byte (bit 0 = min present, bit 1 = max present), then optional f64 min, optional f64 max | +| `brigadier:integer` | 3 | flags byte (bit 0 = min present, bit 1 = max present), then optional i32 min, optional i32 max | +| `brigadier:long` | 4 | flags byte (bit 0 = min present, bit 1 = max present), then optional i64 min, optional i64 max | +| `brigadier:string` | 5 | VarInt mode: 0 = SINGLE_WORD, 1 = QUOTABLE_PHRASE, 2 = GREEDY_PHRASE | + +Numeric range flags detail (same pattern for float/double/integer/long): +``` +Bit 0 (0x01) — minimum bound present → read min value +Bit 1 (0x02) — maximum bound present → read max value +Bits 2–7 — unused +``` + +### Minecraft parsers (selected; full table below) + +| Parser name | VarInt ID (1.19+) | Properties | +|------------------------------|:-----------------:|------------| +| `minecraft:entity` | 6 | flags byte: bit 0 (0x01) = only allow entities (not players), bit 1 (0x02) = only allow players | +| `minecraft:game_profile` | 7 | none | +| `minecraft:block_pos` | 8 | none | +| `minecraft:column_pos` | 9 | none | +| `minecraft:vec3` | 10 | none | +| `minecraft:vec2` | 11 | none | +| `minecraft:block_state` | 12 | none | +| `minecraft:block_predicate` | 13 | none | +| `minecraft:item_stack` | 14 | none | +| `minecraft:item_predicate` | 15 | none | +| `minecraft:color` | 16 | none | +| `minecraft:component` | 17 | none | +| `minecraft:message` | 18 (1.19) / 19 (1.21.1) | none | +| `minecraft:nbt` | 19 (1.19) / 20 (1.21.1) | none | +| `minecraft:nbt_path` | 21 (1.19) / 22 (1.21.1) | none | +| `minecraft:objective` | 22 (1.19) / 23 (1.21.1) | none | +| `minecraft:scoreboard_slot` | 28 (1.19) / 29 (1.21.1) | none | +| `minecraft:score_holder` | 29 (1.19) / 30 (1.21.1) | flags byte: bit 0 (0x01) = allow multiple | +| `minecraft:swizzle` | 30 (1.19) | none | +| `minecraft:team` | 31 (1.19) | none | +| `minecraft:resource_location`| 33 (1.19) | none | +| `minecraft:resource_or_tag` | 43 (1.19) | String registry name | +| `minecraft:resource` | 44 (1.19) | String registry name | +| `minecraft:uuid` | 47 (1.19) / 53 (1.21.1) | none | + +Note: IDs shift across versions as new parsers are inserted. The VarInt ID is version-specific. Use the `minecraft-data` protocol.json for the authoritative table for a given protocol version. + +#### `minecraft:entity` flags detail + +``` +Bit 0 (0x01) — onlyAllowEntities (non-player entities only) +Bit 1 (0x02) — onlyAllowPlayers +Bits 2–7 — unused +``` +Both bits 0 and 1 can be 0 simultaneously (allow any selector). + +#### `minecraft:score_holder` flags detail + +``` +Bit 0 (0x01) — allowMultiple (accept `*` and selectors matching multiple holders) +Bits 1–7 — unused +``` + +### Full 1.19 VarInt Parser Table + +``` + 0 brigadier:bool + 1 brigadier:float + 2 brigadier:double + 3 brigadier:integer + 4 brigadier:long + 5 brigadier:string + 6 minecraft:entity + 7 minecraft:game_profile + 8 minecraft:block_pos + 9 minecraft:column_pos +10 minecraft:vec3 +11 minecraft:vec2 +12 minecraft:block_state +13 minecraft:block_predicate +14 minecraft:item_stack +15 minecraft:item_predicate +16 minecraft:color +17 minecraft:component +18 minecraft:message +19 minecraft:nbt +20 minecraft:nbt_tag +21 minecraft:nbt_path +22 minecraft:objective +23 minecraft:objective_criteria +24 minecraft:operation +25 minecraft:particle +26 minecraft:angle +27 minecraft:rotation +28 minecraft:scoreboard_slot +29 minecraft:score_holder +30 minecraft:swizzle +31 minecraft:team +32 minecraft:item_slot +33 minecraft:resource_location +34 minecraft:mob_effect +35 minecraft:function +36 minecraft:entity_anchor +37 minecraft:int_range +38 minecraft:float_range +39 minecraft:item_enchantment +40 minecraft:entity_summon +41 minecraft:dimension +42 minecraft:time +43 minecraft:resource_or_tag +44 minecraft:resource +45 minecraft:template_mirror +46 minecraft:template_rotation +47 minecraft:uuid +``` + +Source: `minecraft-data/data/pc/1.19/protocol.json` `types.command_node` parser mapper mappings. + +### Full 1.21.1 VarInt Parser Table (additions highlighted) + +``` + 0 brigadier:bool + 1 brigadier:float + 2 brigadier:double + 3 brigadier:integer + 4 brigadier:long + 5 brigadier:string + 6 minecraft:entity + 7 minecraft:game_profile + 8 minecraft:block_pos + 9 minecraft:column_pos +10 minecraft:vec3 +11 minecraft:vec2 +12 minecraft:block_state +13 minecraft:block_predicate +14 minecraft:item_stack +15 minecraft:item_predicate +16 minecraft:color +17 minecraft:component +18 minecraft:style ← new in 1.21.x era +19 minecraft:message +20 minecraft:nbt +21 minecraft:nbt_tag +22 minecraft:nbt_path +23 minecraft:objective +24 minecraft:objective_criteria +25 minecraft:operation +26 minecraft:particle +27 minecraft:angle +28 minecraft:rotation +29 minecraft:scoreboard_slot +30 minecraft:score_holder +31 minecraft:swizzle +32 minecraft:team +33 minecraft:item_slot +34 minecraft:item_slots ← new +35 minecraft:resource_location +36 minecraft:function +37 minecraft:entity_anchor +38 minecraft:int_range +39 minecraft:float_range +40 minecraft:dimension +41 minecraft:gamemode ← new +42 minecraft:time +43 minecraft:resource_or_tag +44 minecraft:resource_or_tag_key ← new +45 minecraft:resource +46 minecraft:resource_key ← new +47 minecraft:template_mirror +48 minecraft:template_rotation +49 minecraft:heightmap ← new +50 minecraft:loot_table ← new +51 minecraft:loot_predicate ← new +52 minecraft:loot_modifier ← new +53 minecraft:uuid +``` + +Source: `minecraft-data/data/pc/1.21.1/protocol.json` `types.command_node` parser mapper mappings. + +--- + +## Suggestions Type + +When flags bit 4 (`0x10`) is set, an extra field follows Properties: + +``` +Suggestions String an Identifier naming the suggestion provider +``` + +Known values: +- `minecraft:ask_server` — client sends a Tab-Complete request to the server for this argument +- `minecraft:all_recipes` — client fills from the known recipe list +- `minecraft:available_sounds` — client fills from the sound registry +- `minecraft:summonable_entities` — client fills from entity type registry + + + +--- + +## Example: `/teleport ` + +A minimal `/teleport ` command with one argument. Three nodes total: + +``` +Node 0: type=root, executable=false, children=[1] +Node 1: type=literal, executable=false, name="teleport", children=[2] +Node 2: type=argument, executable=true, name="target", + parser="minecraft:entity" (VarInt 6 in 1.19+), + properties=flags(0x02 = onlyAllowPlayers), + children=[] + +Root Index = 0 +``` + +Wire layout (hex, showing field-by-field): +``` +Count 03 (3 nodes) + +Node 0: + flags 00 (type=root, not executable, no redirect) + childCount 01 (1 child) + children 01 (index 1) + [no name — root node] + +Node 1: + flags 04 (type=literal 0x01 | executable=false; wait — not executable here, so 0x01) + -- actually flags = 0x01 (type=literal, not executable) + childCount 01 + children 02 + name 08 74 65 6c 65 70 6f 72 74 ("teleport", length-prefixed string) + +Node 2: + flags 06 (type=argument 0x02 | is_executable 0x04) + childCount 00 + name 06 74 61 72 67 65 74 ("target") + parser 06 (VarInt 6 = minecraft:entity, in 1.19+) + properties 02 (onlyAllowPlayers flag) + +Root Index 00 +``` + +ASCII graph: +``` +[ROOT node=0] + | + v +[LITERAL "teleport" node=1] + | + v +[ARGUMENT "target" node=2] parser=minecraft:entity executable +``` + +--- + +## Redirect Example + +`/tp` can alias `/teleport` by using a redirect: + +``` +Node 3: type=literal, executable=false, flags=0x09 (literal | has_redirect), + name="tp", children=[], redirect=1 +``` + +`flags = 0x01 | 0x08 = 0x09`. The redirect points to node 1 (the `teleport` literal), so the client treats `/tp` as if it navigated into the `teleport` subgraph. + +--- + +## Node-Record Pseudocode + +```python +def read_command_node(buf): + flags = buf.read_byte() + node_type = flags & 0x03 # 0=root, 1=literal, 2=argument + is_executable = bool(flags & 0x04) + has_redirect = bool(flags & 0x08) + has_suggestions = bool(flags & 0x10) + + child_count = buf.read_varint() + children = [buf.read_varint() for _ in range(child_count)] + + redirect = buf.read_varint() if has_redirect else None + + name = None + if node_type in (1, 2): # literal or argument + name = buf.read_string() + + parser = None + properties = None + if node_type == 2: # argument + # Pre-1.19: parser = buf.read_string() + # 1.19+: parser = PARSER_TABLE[buf.read_varint()] + parser = read_parser_id(buf) + properties = read_parser_properties(buf, parser) + + suggestions = None + if has_suggestions: # argument nodes only in practice + suggestions = buf.read_string() # identifier + + return CommandNode(node_type, is_executable, children, redirect, + name, parser, properties, suggestions) +``` + +--- + +## Version History + +| Version | Protocol | Change | +|------------|----------|--------| +| 1.13 | 393 | Packet added; parser = String identifier | +| 1.14 | 477 | New argument parsers added (e.g. `minecraft:uuid`, `minecraft:nbt_compound_tag`) | +| 1.19 | 759 | Parser identifier changed from String to VarInt registry index; new parsers added | +| 1.19.3 | 761 | Additional parser entries added | +| 1.21.1 | 767 | `minecraft:style`, `minecraft:item_slots`, `minecraft:gamemode`, `minecraft:resource_or_tag_key`, `minecraft:resource_key`, `minecraft:heightmap`, `minecraft:loot_*` parsers added | + +The `minecraft-data` `types.command_node.extraNodeData[type=2].parser` field is `"string"` for 1.13–1.18, and `["mapper", {"type": "varint", ...}]` for 1.19+. +ViaVersion `Protocol1_18_2To1_19.java:194–200` is the canonical protocol transition point: it reads `Types.STRING` from the 1.18 upstream and writes `Types.VAR_INT` to the 1.19 client. + + diff --git a/packets/format-entity-metadata.md b/packets/format-entity-metadata.md new file mode 100644 index 0000000..2522674 --- /dev/null +++ b/packets/format-entity-metadata.md @@ -0,0 +1,343 @@ +# Entity Metadata Wire Format + +> **Scope:** Java Edition entity metadata (a.k.a. "entity data") as carried by `Set Entity Metadata` and (historically) inside spawn packets. +> **Primary sources:** +> - `/tmp/mcproto-refs/minecraft-data/data/pc/*/protocol.json` — `entityMetadata`, `entityMetadataEntry`, `entityMetadataItem` type defs +> - `/tmp/mcproto-refs/ViaVersion/api/src/main/java/com/viaversion/viaversion/api/type/types/entitydata/OldEntityDataType.java` — legacy frame reader +> - `/tmp/mcproto-refs/ViaVersion/api/src/main/java/com/viaversion/viaversion/api/type/types/entitydata/ModernEntityDataType.java` — modern frame reader +> - `/tmp/mcproto-refs/ViaVersion/api/src/main/java/com/viaversion/viaversion/api/minecraft/entitydata/types/EntityDataTypes*.java` — per-version type registries +> - Cross-links: [../versions/1.9.md](../versions/1.9.md) · [../versions/1.14.md](../versions/1.14.md) · [../01-data-types.md](../01-data-types.md) (VarInt / NBT / Slot) + +--- + +## 1. Overview + +Entity metadata is an indexed key/value list sent by the server to describe the current state of an entity: health flags, custom names, riding status, arm poses, and hundreds of entity-class-specific attributes. It travels in two contexts: + +1. **`Set Entity Metadata` (Play CB)** — differential update for an already-spawned entity. Only the changed indices are sent. +2. **Spawn packets (historical, ≤ 1.8)** — the full metadata list was embedded in `Spawn Mob` and `Spawn Object`. + +The format has two incompatible eras: + +| Era | Versions | Index encoding | Type encoding | Terminator | +|-----|----------|----------------|---------------|------------| +| **Legacy** | ≤ 1.8 (protocol ≤ 47) | low 5 bits of a single byte | high 3 bits of the same byte | `0x7F` (`127`) | +| **Modern** | ≥ 1.9 (protocol ≥ 107) | separate unsigned byte | separate VarInt | `0xFF` (`255`) | + +--- + +## 2. Modern Format (1.9+) + +Source: `ModernEntityDataType.java:34–38`; `minecraft-data` `data/pc/1.9/protocol.json` (and all later versions). + +### 2.1 Frame structure + +``` +EntityMetadata = (Entry)* Terminator + +Entry = { + Index : u8 -- unsigned byte; 0x00–0xFE are valid indices + Type : VarInt -- type ID from the per-version registry (see §3) + Value : +} + +Terminator = 0xFF -- single byte; signals end of list +``` + +Read loop (pseudocode matching `ModernEntityDataType.java`): + +``` +loop: + index = readUnsignedByte() + if index == 0xFF: break + typeId = readVarInt() + type = registry.byId(typeId) + value = type.read(buffer) + entries.add(Entry(index, type, value)) +``` + +**Key points:** +- Index and Type are always present together; neither is omitted. +- The list is unordered within the packet (indices need not be ascending). +- An empty metadata list is a legal single byte: `FF`. +- The `Type` VarInt is always small (≤ 2 bytes on the wire) but must be read as a full VarInt. + +> **Note on minecraft-data 1.9–1.12.2:** `minecraft-data` records the `type` field as `i8` (signed byte) in those versions rather than `varint`. This is an artifact of minecraft-data's protocol description — since values 0–12 all fit in one byte, both readings are wire-compatible. ViaVersion's `ModernEntityDataType` reads it as VarInt from 1.9 onward, and the Minecraft wiki specifies VarInt. The `varint` encoding is authoritative; `i8` in minecraft-data is a pragmatic approximation. The minecraft-data source confirms the change to `varint` in its 1.13 JSON: `/tmp/mcproto-refs/minecraft-data/data/pc/1.13/protocol.json`. + +--- + +## 3. Type Registry — 1.21.1 + +Source: `EntityDataTypes1_21.java` (lines 33–63) + `minecraft-data` `data/pc/1.21.1/protocol.json` `entityMetadataEntry` type mapper. + +| ID | Name | Wire encoding | +|----|------|---------------| +| 0 | Byte | `i8` — signed byte | +| 1 | VarInt | VarInt | +| 2 | VarLong | VarLong (added 1.19.3; see §5) | +| 3 | Float | `f32` — big-endian IEEE 754 single | +| 4 | String | VarInt length + UTF-8 bytes | +| 5 | Chat (TextComponent) | Anonymous NBT tag (1.20.3+); `string` (JSON text) in 1.9–1.20.2 | +| 6 | Optional Chat | `bool` present-flag + Chat (if present) | +| 7 | Slot | Item stack — see [../01-data-types.md](../01-data-types.md) §Slot; encoding changed at 1.9, 1.13, 1.13.2, 1.20.2, 1.20.5 | +| 8 | Boolean | `bool` — single byte `0x00`/`0x01` | +| 9 | Rotations | `f32` pitch + `f32` yaw + `f32` roll (3 × 4 bytes) | +| 10 | Position | 64-bit packed `Position` — `[x:26][z:26][y:12]` since 1.14; `[x:26][y:12][z:26]` in 1.9–1.13 | +| 11 | Optional Position | `bool` present-flag + Position (if present) | +| 12 | Direction | VarInt enum: `0`=Down `1`=Up `2`=North `3`=South `4`=West `5`=East | +| 13 | Optional UUID | `bool` present-flag + 128-bit UUID big-endian (if present) | +| 14 | Block State | VarInt flat block-state ID (0 = air) | +| 15 | Optional Block State | VarInt: `0` = absent; non-zero = block-state ID | +| 16 | NBT | Anonymous compound NBT (no leading name) — see [../01-data-types.md](../01-data-types.md) §NBT | +| 17 | Particle | Typed `Particle` compound (added 1.13) | +| 18 | Particles | VarInt count + Particle\[\] (added 1.20.5) | +| 19 | Villager Data | VarInt villagerType + VarInt villagerProfession + VarInt level (added 1.14) | +| 20 | Optional VarInt | VarInt: `0` = absent; value = stored\_value + 1 | +| 21 | Pose | VarInt enum — see §4.3 (added 1.14) | +| 22 | Cat Variant | VarInt registry ID (added 1.19) | +| 23 | Wolf Variant | `registryEntryHolder`: VarInt registry ID or inline `WolfVariant` struct (added 1.21.2; was plain VarInt in 1.21) | +| 24 | Frog Variant | VarInt registry ID (added 1.19) | +| 25 | Optional Global Position | `bool` present-flag + dimension String + Position (if present) | +| 26 | Painting Variant | `registryEntryHolder`: VarInt registry ID or inline `PaintingVariant` struct (added 1.21; struct encoding differs 1.21 vs 1.21.2+) | +| 27 | Sniffer State | VarInt enum (added 1.20) | +| 28 | Armadillo State | VarInt enum (added 1.20.5) | +| 29 | Vector3 | `f32` x + `f32` y + `f32` z (added 1.20) | +| 30 | Quaternion | `f32` x + `f32` y + `f32` z + `f32` w (added 1.20) | + +**Total: 31 type IDs (0–30) in 1.21.x.** +Source: `EntityDataTypes1_21.java` constructor argument `super(31)`. + +### 3.1 `registryEntryHolder` encoding (types 23, 26) + +Used since 1.21 for Wolf Variant and Painting Variant. + +``` +RegistryEntryHolder = { + hasInlineData : bool + if hasInlineData: + inlineValue : + else: + registryId : VarInt -- numeric ID within the server's registry +} +``` + +`WolfVariant` inline struct (1.21): wildTexture String + tameTexture String + angryTexture String + biome IDSet. +`PaintingVariant` inline struct (1.21): i32 width + i32 height + String assetId + Optional Anonymous NBT title + Optional Anonymous NBT author. +Source: `EntityMetadataWolfVariant` / `EntityMetadataPaintingVariant` in `minecraft-data` `data/pc/1.21.1/protocol.json`. + +--- + +## 4. Type Registry History (selected versions) + +The per-version type classes in ViaVersion are authoritative. Below are the salient snapshots. + +### 4.1 1.9–1.12.2 (EntityDataTypes1_9 / EntityDataTypes1_12) + +Source: `EntityDataTypes1_9.java` (ordinal = ID), `EntityDataTypes1_12.java`. + +| ID | 1.9 | 1.12 (added) | +|----|-----|--------------| +| 0 | Byte | same | +| 1 | VarInt | same | +| 2 | Float | same | +| 3 | String | same | +| 4 | Component (JSON text) | same | +| 5 | Slot (`ITEM1_8`) | same | +| 6 | Boolean | same | +| 7 | Rotations | same | +| 8 | Position (`BLOCK_POSITION1_8`) | same | +| 9 | Optional Position | same | +| 10 | Direction | same | +| 11 | Optional UUID | same | +| 12 | Optional Block State | same | +| 13 | — | NBT (`NAMED_COMPOUND_TAG`) | + +Note: no VarLong, no Particle, no Villager Data, no Pose in this era. + +### 4.2 1.13–1.13.2 (EntityDataTypes1_13 / EntityDataTypes1_13_2) + +Source: `EntityDataTypes1_13.java`, `EntityDataTypes1_13_2.java`. + +Key changes: +- **Type field becomes VarInt** (was `i8`-compatible byte in 1.9–1.12.2; explicitly `varint` in minecraft-data from 1.13). +- ID 5 becomes `Optional Component` (was absent; Component shifts from id 4 to id 4, Optional Component added at 5; ids 5–13 shift up by 1). +- Particle added at ID 15. +- Slot encoding updated to `ITEM1_13` / `ITEM1_13_2`. + +Full 1.13.2 registry (16 entries, IDs 0–15): +`Byte · VarInt · Float · String · Component · OptComponent · Slot · Boolean · Rotations · Position · OptPosition · Direction · OptUUID · OptBlockState · NBT · Particle` + +### 4.3 1.14 (EntityDataTypes1_14) + +Source: `EntityDataTypes1_14.java`. + +Added IDs (shifts all subsequent IDs by +1 relative to 1.12, due to Optional Component insertion in 1.13 and now Villager Data + Opt VarInt + Pose): + +| New ID | Type | Notes | +|--------|------|-------| +| 16 | Villager Data | VarInt×3: villagerType, villagerProfession, level | +| 17 | Optional VarInt | 0 = absent; otherwise stored\_value + 1 | +| 18 | Pose | VarInt enum | + +**Pose values (1.14):** `0`=Standing `1`=FallFlying `2`=Sleeping `3`=Swimming `4`=SpinAttack `5`=Sneaking _(1.14 original list)_ + +Position encoding also changes: `BLOCK_POSITION1_14` (`[x:26][z:26][y:12]` bit layout) replaces `BLOCK_POSITION1_8`. See [../versions/1.14.md](../versions/1.14.md) §Position. + +### 4.4 1.19 (EntityDataTypes1_19) + +Source: `EntityDataTypes1_19.java`. + +Added IDs 19–22: +- 19: Cat Variant (VarInt) +- 20: Frog Variant (VarInt) +- 21: Optional Global Position (`bool` + dimension String + Position) +- 22: Painting Variant (VarInt) + +### 4.5 1.19.3 (EntityDataTypes1_19_3) + +Source: `EntityDataTypes1_19_3.java`; confirmed by `minecraft-data` `data/pc/1.19.3/protocol.json`. + +**VarLong inserted at ID 2.** All existing IDs from 2 onward shift up by 1 (Float moves to 3, String to 4, etc.). This was the largest single-version numbering shift since 1.13. + +Registry after insertion (24 entries): +`Byte(0) · VarInt(1) · VarLong(2) · Float(3) · String(4) · Component(5) · OptComponent(6) · Slot(7) · Boolean(8) · Rotations(9) · Position(10) · OptPosition(11) · Direction(12) · OptUUID(13) · OptBlockState(14) · NBT(15) · Particle(16) · VillagerData(17) · OptVarInt(18) · Pose(19) · CatVariant(20) · FrogVariant(21) · OptGlobalPos(22) · PaintingVariant(23)` + +### 4.6 1.19.4 (EntityDataTypes1_19_4) + +Source: `EntityDataTypes1_19_4.java`. + +Added: +- ID 14: Block State (non-optional, explicit; previously only Optional Block State existed — no bare Block State type ID was present in 1.14–1.19.3) +- ID 25: Sniffer State (VarInt enum, new in 1.20 — but the type ID slot is first defined in ViaVersion's 1.19.4 class) +- ID 26: Vector3F (`f32`×3) +- ID 27: Quaternion (`f32`×4) + +### 4.7 1.20.5 (EntityDataTypes1_20_5) and 1.21 (EntityDataTypes1_21) + +Source: `EntityDataTypes1_20_5.java`, `EntityDataTypes1_21.java`. + +Changes vs 1.19.4: +- **Particles (plural) added at ID 18.** Particle (singular) stays at 17. All subsequent IDs shift +1. +- **Chat/Component now uses anonymous NBT tag** (`TRUSTED_TAG`) instead of JSON string. Source: `Types.TRUSTED_TAG` / `Types.TRUSTED_OPTIONAL_TAG` in both files. +- **Wolf Variant (ID 23)**: 1.21 uses `WolfVariant.TYPE` (inline struct holder); 1.20.5 used plain VarInt. +- **Armadillo State (ID 28)** added. +- **Wolf Variant, Painting Variant** use `registryEntryHolder` shape from 1.21.2 onward. + +Full 1.21.x registry: see §3 above (31 entries, IDs 0–30). + +--- + +## 5. Evolution Timeline + +| Version | Change | +|---------|--------| +| ≤ 1.8 | Legacy single-byte packed format (§6); terminator `0x7F` | +| **1.9** | Modern format: separate u8 index + VarInt type + 0xFF terminator; 13 type IDs | +| 1.9 | Dual-hand: `HAND_ACTIVE` (Byte, index 5 on LivingEntity) added; off-hand Slot carried in Slot type; old Block/Int types removed | +| 1.12 | NBT type added (ID 13); total 14 type IDs | +| **1.13** | Type field officially becomes VarInt (was byte-range compatible); Optional Component added (ID 5); Particle added (ID 15); Slot encoding updated; 16 type IDs | +| **1.14** | Villager Data, Optional VarInt, Pose added (IDs 16–18); Position bit-layout changed (`[x:26][z:26][y:12]`) | +| 1.19 | Cat Variant, Frog Variant, Optional Global Position, Painting Variant added (IDs 19–22); 23 type IDs | +| **1.19.3** | VarLong inserted at ID 2 — all subsequent IDs +1; 24 type IDs | +| 1.19.4 | Block State (explicit non-optional) ID 14 split from Optional Block State; Sniffer State, Vector3, Quaternion added; 28 type IDs | +| 1.20.5 | Particles (plural) added at ID 18 (+1 shift); Chat type switches from JSON string to anonymous NBT; Armadillo State added; Wolf Variant expanded; 31 type IDs | +| 1.21 | Wolf Variant becomes `registryEntryHolder`; Painting Variant becomes `registryEntryHolder` | +| 1.21.2 | Painting Variant inline struct encoding updated | + +--- + +## 6. Legacy Format (≤ 1.8) + +Source: `OldEntityDataType.java:36–50`; `minecraft-data` `data/pc/1.8/protocol.json` `entityMetadata`. + +### 6.1 Frame structure + +``` +EntityMetadata = (Entry)* Terminator + +Entry = { + header : u8 -- bits [7:5] = type ID (3 bits); bits [4:0] = index (5 bits) + Value : +} + +Terminator = 0x7F -- single byte (127); index field would be 31, type 3 = Float, but 0x7F is the sentinel +``` + +The header byte encoding: + +``` +header = (typeId << 5) | (index & 0x1F) +``` + +This limits index to 0–31 (5 bits) and type to 0–7 (3 bits). + +### 6.2 1.8 type registry (8 entries) + +Source: `EntityDataTypes1_8.java` (ordinal = ID); `minecraft-data` `data/pc/1.8/protocol.json` `entityMetadataItem`. + +| ID | Name | Wire encoding | +|----|------|---------------| +| 0 | Byte | `i8` | +| 1 | Short | `i16` big-endian | +| 2 | Int | `i32` big-endian | +| 3 | Float | `f32` IEEE 754 | +| 4 | String | VarInt length + UTF-8 | +| 5 | Slot | `ITEM1_8` — item id (i16) + count (i8) + damage (i16) + NBT | +| 6 | Block Position | `i32` x + `i32` y + `i32` z (three separate ints, NOT the packed Position) | +| 7 | Rotations | `f32` pitch + `f32` yaw + `f32` roll | + +**Key differences from 1.9+:** +- No Boolean type (boolean flags used Byte 0/1). +- No Optional types (UUID, Position, Block State). +- Short (i16) and Int (i32) exist; both removed in 1.9 (replaced by VarInt). +- Block Position is three plain `i32` fields, not a packed 64-bit value. +- No Component / NBT / Direction / Particle. + +--- + +## 7. Index Assignments Are Per-Entity-Class and Per-Version + +**The type registry (§3) defines only the wire encoding per type ID. Which type ID and which index value mean what for a given entity class is entirely version-specific and entity-class-specific.** + +Examples (from `EntityDataIndex1_9.java`): + +| Entity class | Index (1.8) | Index (1.9) | Type (1.8) | Type (1.9) | Meaning | +|---|---|---|---|---|---| +| Entity | 0 | 0 | Byte | Byte | Status flags (on fire, crouching, …) | +| Entity | 1 | 1 | Short | VarInt | Air supply | +| Entity | 2 | 2 | String | String | Custom name | +| LivingEntity | 6 | 6 | Float | Float | Health | +| LivingEntity | 7 | 7 | Int | VarInt | Potion effect colour | +| Player | 10 | 12 | Byte | Byte | Skin flags | +| LivingEntity | — | 5 | — | Byte | Hand state (new in 1.9, dual-hand) | + +Per-version index tables live in the ViaVersion EntityDataIndex files under +`/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_8to1_9/data/EntityDataIndex1_9.java` +and in `EntityTypes*` classes for later versions. + +For 1.21.1 entity data index assignments, refer to [../versions/1.21.md](../versions/1.21.md) or the Minecraft wiki [Entity format](https://minecraft.wiki/w/Entity_format) page which tabulates per-class indices. + +--- + +## 8. Carrying Packet + +In 1.21.1 (Play state, client-bound): + +``` +Set Entity Metadata (0x52) = { + entityId : VarInt + metadata : EntityMetadata -- zero or more entries + 0xFF +} +``` + +Source: `minecraft-data` `data/pc/1.21.1/protocol.json` `play.toClient.types.packet_entity_metadata`. + +Historically (≤ 1.8) the full metadata list was also embedded in `Spawn Mob` (CB 0x0F) and `Spawn Object` (CB 0x0E). + +--- + +## 9. Open Items + +- `` — Whether `Block State` (ID 14, non-optional) already existed in 1.14–1.19.2 or was truly new in 1.19.4: ViaVersion's 1.14 class omits it (only `optionalBlockState` at 13) but 1.19.4 explicitly has both 14 and 15. This implies the split was at 1.19.4, but no direct minecraft-data diff or wiki changelog entry in the available sources explicitly names this change. +- The `optional_global_pos` value in 1.21.1 (`minecraft-data` encodes it as `['option', 'string']`) — the string appears to be the dimension name key only, not a full ResourceLocation + Position pair as ViaVersion's `OPTIONAL_GLOBAL_POSITION` type implies. +- Pose enum values beyond the 1.14 original (e.g. `Croaking`, `UsingTongue`, `Roaring`, `Sniffing`, `Emerging`, `Digging` added for 1.17–1.20 mobs) — not enumerated here; the authoritative list is at `minecraft.wiki/w/Entity_format#Pose`. diff --git a/packets/format-slot-and-components.md b/packets/format-slot-and-components.md new file mode 100644 index 0000000..3f7074e --- /dev/null +++ b/packets/format-slot-and-components.md @@ -0,0 +1,297 @@ +# Slot (Item Stack) Wire Format and Component Evolution + +> Cross-references: [`../01-data-types.md`](../01-data-types.md) · [`../versions/1.13.md`](../versions/1.13.md) · [`../versions/1.20.md`](../versions/1.20.md) + +A **Slot** is the on-wire representation of an item stack. It appears in dozens of +play-phase packets (Set Slot, Window Items, Creative Inventory Action, Entity +Equipment, …). Its layout has changed three times at major protocol boundaries. + +--- + +## Era 1 — Pre-1.13 (≤ protocol 340) + +**Source:** `minecraft-data/data/pc/1.12.2/protocol.json` — `types.slot`; `ViaVersion` `ItemType1_8.java` + +``` +Slot { + blockId : i16 // item numeric ID; -1 = empty slot + if blockId != -1 { + itemCount : i8 // stack size (1–64) + itemDamage : i16 // damage / metadata value + nbtData : NBT // TAG_Compound or 0x00 byte (no tag) + } +} +``` + +### Notes + +- `blockId` was the numeric item registry ID assigned by Mojang and exposed via + `ids.json`. Negative values (`blockId < 0`, conventionally `-1`) signal an + empty slot. +- `itemDamage` (also called *metadata* or *aux*) served double duty: + - For tools/weapons: the amount of durability consumed. + - For items like wool, dye, or spawn eggs: a sub-type discriminator + (e.g. wool damage=14 → red wool). + - Together, `(blockId, itemDamage)` formed the full **item identity**. +- `nbtData` is a **named compound tag** serialised directly into the packet + without a length prefix. The "named" form means the stream contains: + `0x0A` (TAG_Compound type byte) · UTF-16BE length-prefixed name (empty string + `0x00 0x00` for anonymous root) · tag payload. + A leading `0x00` (TAG_End type byte) signals no tag. + This is **uncompressed** network NBT — not gzip-wrapped. + The `optionalNbt` minecraft-data abstract type maps to this idiom; its value + is `"native"` meaning the codec is supplied by the runtime + (`NamedCompoundTagType`, ViaVersion `api/.../misc/NamedCompoundTagType.java:62`). + +--- + +## Era 2 — 1.13 / The Flattening (protocol 393) + +**Source:** `minecraft-data/data/pc/1.13/protocol.json` — `types.slot`; `ViaVersion` `ItemType1_13.java` + +``` +Slot { + itemId : i16 // flat numeric ID; -1 = empty + if itemId != -1 { + itemCount : i8 + nbtData : NBT // same named-compound encoding as pre-1.13 + } +} +``` + +### What changed + +- **The Flattening** (MC-Java 1.13) merged the `(id, damage)` identity pair into + a single flat item ID. Each old `(blockId, damage)` pair that was a distinct + item became its own registry entry (e.g. the 16 wool colours became 16 separate + item IDs). The `itemDamage` field was **removed from the Slot wire format**. +- The sentinel is still `i16 == -1` (not a boolean). +- NBT encoding unchanged. + +> **ViaVersion evidence** — `ItemType1_13.java:39`: `short id = buffer.readShort();` +> then `item.setAmount(buffer.readByte())` and `Types.NAMED_COMPOUND_TAG.read(buffer)`. +> No damage field read. + +--- + +## Era 3 — 1.13.2 → 1.20.4 (protocols 404 – 765) + +**Source:** `minecraft-data/data/pc/1.13.2/protocol.json` — `types.slot`; `ViaVersion` `ItemType1_13_2.java`, `ItemType1_20_2.java` + +``` +Slot { + present : bool // false = empty slot + if present { + itemId : VarInt // flat numeric ID + itemCount : i8 // stack size + nbtData : NBT // named compound tag or 0x00 + } +} +``` + +### What changed + +- The empty-slot sentinel flipped from `i16 == -1` to a dedicated **boolean** + `present` byte (`0x00` / `0x01`). +- `itemId` widened from `i16` to **VarInt** in the same change (1.13.2-pre1). +- `itemCount` remained `i8` throughout this era. +- 1.20.2 (protocol 764) switched the `nbtData` from *named* to *anonymous* NBT + (no name prefix in the stream; `Types.COMPOUND_TAG` vs `Types.NAMED_COMPOUND_TAG` + in ViaVersion `ItemType1_20_2.java:47`) — but the field is otherwise identical. + +> **ViaVersion evidence** — `ItemType1_13_2.java:39`: `boolean present = buffer.readBoolean();` +> then `Types.VAR_INT.readPrimitive(buffer)` + `buffer.readByte()` + `Types.NAMED_COMPOUND_TAG.read(buffer)`. + +This era spans 1.13.2 through 1.20.3 with only NBT-encoding tweaks. The logical +layout (present + id + count + tag) was stable for ~5 years. + +--- + +## Era 4 — 1.20.5+ Structured Components (protocol 766+) + +**Sources:** +- `minecraft-data/data/pc/1.20.5/protocol.json` — `types.{Slot,SlotComponent,SlotComponentType}` +- `minecraft-data/data/pc/1.21.1/protocol.json` — same keys +- `ViaVersion` `ItemType1_20_5.java`, `StructuredDataType.java`, `StructuredDataKey.java` + +### Wire layout + +``` +Slot { + itemCount : VarInt // 0 = empty slot + if itemCount > 0 { + itemId : VarInt + numComponentsToAdd : VarInt + numComponentsToRemove : VarInt + + // Add array — length = numComponentsToAdd + components[] { + componentType : VarInt // SlotComponentType id + data : // depends on componentType + } + + // Remove array — length = numComponentsToRemove + removeComponents[] { + componentType : VarInt // marks this component absent + } + } +} +``` + +### What changed + +- The `present` boolean + `nbtData` tag are **gone**. Empty is now signalled by + `itemCount == 0`. +- The single freeform NBT `tag` key was replaced by **typed structured components**: + a registry of named data shapes. Each component is identified by a VarInt ID and + has its own typed encoding. +- `numComponentsToAdd` / `numComponentsToRemove` are both typically zero for items + that carry only default properties; a non-zero count means the item overrides or + explicitly clears default component values. +- The "remove" array carries only the component type IDs (no data), marking those + components explicitly absent (overrides the item type's defaults). + +### `itemCount` type discrepancy + +`minecraft-data` 1.20.5 records `itemCount` as `i8`. ViaVersion's `ItemType1_20_5.java:51` +reads it as `VAR_INT`. ViaVersion is tested against live servers; the minecraft-data +entry is believed to be an artifact of the dataset's snapshot timing. +From 1.21.2 onwards minecraft-data also records `varint`, confirming the ViaVersion +reading. + +--- + +## SlotComponentType Registry + +The component type ID is a **VarInt** whose mapping to named components is +version-specific. The tables below show the 1.20.5 and 1.21.1 registries. + +**1.20.5** (`minecraft-data/data/pc/1.20.5/protocol.json` → `types.SlotComponentType.mappings`) +**1.21.1** (`minecraft-data/data/pc/1.21.1/protocol.json` → `types.SlotComponentType.mappings`) + +| ID (1.20.5) | ID (1.21.1) | Component name | Wire encoding (Add payload) | +|:-----------:|:-----------:|----------------|----------------------------| +| 0 | 0 | `custom_data` | anonymousNbt (TAG_Compound) | +| 1 | 1 | `max_stack_size` | VarInt | +| 2 | 2 | `max_damage` | VarInt | +| 3 | 3 | `damage` | VarInt | +| 4 | 4 | `unbreakable` | bool (show tooltip flag) | +| 5 | 5 | `custom_name` | anonymousNbt (text component) | +| 6 | 6 | `item_name` | anonymousNbt (text component) | +| 7 | 7 | `lore` | VarInt count + anonymousNbt[] | +| 8 | 8 | `rarity` | VarInt (0=common 1=uncommon 2=rare 3=epic) | +| 9 | 9 | `enchantments` | VarInt count + {id:VarInt, level:VarInt}[] + bool showTooltip | +| 10 | 10 | `can_place_on` | VarInt count + ItemBlockPredicate[] + bool showTooltip | +| 11 | 11 | `can_break` | VarInt count + ItemBlockPredicate[] + bool showTooltip | +| 12 | 12 | `attribute_modifiers` | VarInt count + {typeId, uuid¹, name, value:f64, operation, slot}[] + bool showTooltip | +| 13 | 13 | `custom_model_data` | VarInt | +| 14 | 14 | `hide_additional_tooltip` | void (presence-only) | +| 15 | 15 | `hide_tooltip` | void | +| 16 | 16 | `repair_cost` | VarInt | +| 17 | 17 | `creative_slot_lock` | void | +| 18 | 18 | `enchantment_glint_override` | bool | +| 19 | 19 | `intangible_projectile` | anonymousNbt | +| 20 | 20 | `food` | {nutrition:VarInt, saturationModifier:f32, canAlwaysEat:bool, secondsToEat:f32, usingConvertsTo:Slot, effects:[]} | +| 21 | 21 | `fire_resistant` | void | +| 22 | 22 | `tool` | {rules:[], defaultMiningSpeed:f32, damagePerBlock:VarInt} | +| 23 | 23 | `stored_enchantments` | same as `enchantments` | +| 24 | 24 | `dyed_color` | {color:i32, showTooltip:bool} | +| 25 | 25 | `map_color` | i32 (RGB) | +| 26 | 26 | `map_id` | VarInt | +| 27 | 27 | `map_decorations` | anonymousNbt | +| 28 | 28 | `map_post_processing` | VarInt | +| 29 | 29 | `charged_projectiles` | VarInt count + Slot[] | +| 30 | 30 | `bundle_contents` | VarInt count + Slot[] | +| 31 | 31 | `potion_contents` | {potionId:VarInt?, customColor:i32?, customEffects:[], customName:string?} | +| 32 | 32 | `suspicious_stew_effects` | VarInt count + {effect:VarInt, duration:VarInt}[] | +| 33 | 33 | `writable_book_content` | VarInt count + ItemBookPage[] | +| 34 | 34 | `written_book_content` | {rawTitle, filteredTitle?, author, generation:VarInt, pages[], resolved:bool} | +| 35 | 35 | `trim` | {material:RegistryEntry, pattern:RegistryEntry, showInTooltip:bool} | +| 36 | 36 | `debug_stick_state` | anonymousNbt | +| 37 | 37 | `entity_data` | anonymousNbt | +| 38 | 38 | `bucket_entity_data` | anonymousNbt | +| 39 | 39 | `block_entity_data` | anonymousNbt | +| 40 | 40 | `instrument` | RegistryEntryHolder (inline or id ref) | +| 41 | 41 | `ominous_bottle_amplifier` | VarInt | +| 42 | — | `recipes` *(1.20.5 only)* | anonymousNbt | +| — | 42 | `jukebox_playable` *(1.21+ only)* | {hasHolder:bool, song, showInTooltip:bool} | +| 43 | 42 | `jukebox_playable` / `recipes` | *(see above — IDs shift by 1 after id 41 in 1.21)* | +| 44 | 43 | `lodestone_tracker` | {globalPosition:optional {dimension:string, position:Position}, tracked:bool} | +| 45 | 44 | `firework_explosion` | ItemFireworkExplosion | +| 46 | 45 | `fireworks` | {flightDuration:VarInt, explosions:[]} | +| 47 | 46 | `profile` | {name:string?, uuid:UUID?, properties:[{name, value, signature?}]} | +| 48 | 47 | `note_block_sound` | string (resource location) | +| 49 | 48 | `banner_patterns` | VarInt count + BannerPatternLayer[] | +| 50 | 49 | `base_color` | VarInt (DyeColor) | +| 51 | 50 | `pot_decorations` | VarInt count + VarInt[] (item ids) | +| 52 | 51 | `container` | VarInt count + Slot[] | +| 53 | 52 | `block_state` | VarInt count + {property:string, value:string}[] | +| 54 | 53 | `bees` | VarInt count + {nbtData:anonymousNbt, ticksInHive:VarInt, minTicksInHive:VarInt}[] | +| 55 | 54 | `lock` | anonymousNbt | +| 56 | 55 | `container_loot` | anonymousNbt (CompoundTag with loot table + seed) | + +¹ `attribute_modifiers` in 1.20.5 includes a UUID field that was removed in 1.21. + +### ID shift: 1.20.5 → 1.21 + +In 1.20.5 `recipes` occupies id 42 and `jukebox_playable` is absent (it was +added in MC 1.21). In 1.21+, `jukebox_playable` is inserted at id 42 and +`recipes` shifts to 43. All subsequent IDs shift by +1. **Parsers must never +hardcode component IDs across versions** — always consult the per-version registry. + +--- + +## NBT → Components: `custom_data` and backward compatibility + +Before 1.20.5, all item metadata lived in the free-form NBT `tag` compound. +ViaVersion's `StructuredDataConverter` (path: +`common/.../protocols/v1_20_3to1_20_5/rewriter/StructuredDataConverter.java`) +translates the old NBT keys into the appropriate components during downgrade: + +- `tag.display.Name` → `custom_name` component +- `tag.display.Lore` → `lore` component +- `tag.Enchantments` → `enchantments` component +- `tag.Unbreakable` → `unbreakable` component +- `tag.CustomModelData` → `custom_model_data` component +- `tag.AttributeModifiers` → `attribute_modifiers` component +- `tag.CanPlaceOn` → `can_place_on` component +- `tag.CanDestroy` → `can_break` component +- Data not expressible in any known component is preserved in `custom_data` + under a `VV|DataComponents` backup key. + +The `StructuredDataConverter` constants for the old hide-flags bitfield (`HIDE_ENCHANTMENTS=1`, +`HIDE_ATTRIBUTES=2`, `HIDE_UNBREAKABLE=4`, …) appear at lines 76–83 of that file, +confirming how the `HideFlags` NBT int mapped to the old tooltip-suppression system +that components replace with per-component `showTooltip` booleans. + +--- + +## Summary of format evolution + +| Era | Versions | Empty sentinel | Item ID | Count | Sub-type | Metadata | +|-----|----------|---------------|---------|-------|----------|----------| +| Pre-1.13 | ≤ 1.12.x | `i16 < 0` | `i16` | `i8` | `i16` damage | freeform NBT | +| 1.13 | 1.13 – 1.13.1 | `i16 < 0` | `i16` (flat) | `i8` | — | freeform NBT | +| 1.13.2 – 1.20.4 | 1.13.2 – 1.20.3 | `bool false` | `VarInt` | `i8` | — | freeform NBT | +| 1.20.5+ | 1.20.5+ | `VarInt == 0` | `VarInt` | `VarInt` | — | typed components | + +--- + +## Open items + +- **`itemCount` i8 vs VarInt in 1.20.5**: minecraft-data records `i8`; ViaVersion + `ItemType1_20_5.java` reads `VAR_INT`. The ViaVersion implementation is tested + against live traffic and is treated as authoritative here, but the exact protocol + snapshot where Mojang switched is unconfirmed. +- **1.20.2 NBT format**: ViaVersion `ItemType1_20_2.java` uses `Types.COMPOUND_TAG` + (anonymous, no name prefix) while earlier versions used `Types.NAMED_COMPOUND_TAG`. + The exact protocol version of that sub-change within the 1.13.2–1.20.4 era is + confirmed as 1.20.2 by the class name but not independently verified against + protocol.json (1.20.2 and 1.20.3 files share the same `present+id+count+nbt` + shape in minecraft-data). +- **`jukebox_playable` wire shape in 1.20.5**: present in the component type enum + at id 42 per minecraft-data 1.20.5? The diff above shows `recipes` at 42 in 1.20.5 + and `jukebox_playable` absent — consistent with Mojang adding `jukebox_playable` + in the 1.21 pre-releases. If a 1.20.5 release-candidate snapshot introduced it + mid-stream, that is not captured in the current minecraft-data snapshot. +