Files
minecraft_protocol/packets/format-slot-and-components.md
T
claude-timemachine 90b711d12a 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>
2026-06-19 15:15:26 +02:00

298 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 -->