packets/: packet model + catalogs + wire-format deep-dives

Control-state catalogs (handshake/status/login/config), categorized Play
catalog (~182 packets), and deep-dives on the four hard formats: chunk data
(paletted containers + light), entity metadata (type registry), slot/structured
components, command graph (Brigadier). Sourced from minecraft-data + ViaVersion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude-timemachine
2026-06-19 15:15:26 +02:00
parent a3d5f64ef5
commit 90b711d12a
8 changed files with 2159 additions and 0 deletions
+297
View File
@@ -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 (164)
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 : <typed> // 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. <!-- UNCONFIRMED: exact snapshot commit when Mojang changed the wire type -->
---
## 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 7683 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. <!-- 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.21.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). <!-- UNCONFIRMED: exact NBT name-prefix removal commit -->
- **`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.
<!-- UNCONFIRMED -->