Files
minecraft_protocol/packets/format-slot-and-components.md
claude-timemachine 02a249a379 verify pass 2: resolve markers via ViaBackwards/ViaRewind/ViaLegacy
Closed most remaining UNCONFIRMED items + caught corrections: 1.7 data[]=proto5,
1.8 multi-block nibble order (z/x), stance y+1.62, 1.15 wolf idx18=begging,
1.18 below-zero sections NOT stripped (claim was wrong), CONTAINER_SLOT_STATE
1.20.3, + pinned first-appeared versions (report details/server links=1.21,
fluidCount=26.1, block-state id14=1.19.4, itemCount VarInt=1.20.5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:13:19 +02:00

282 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` — confirmed: `Types.VAR_INT.readPrimitive(buffer)` is the first read in the non-empty branch. ViaVersion is tested against live servers and is authoritative; the minecraft-data `i8` entry is a snapshot artifact from before the 1.20.5 release finalised the wire format. `minecraft-data` 1.21.1 independently records `varint`, confirming that VarInt is correct from **1.20.5** onward. Source: `ViaVersion/.../api/type/types/item/ItemType1_20_5.java:51` + `minecraft-data/data/pc/1.21.1/protocol.json` `types.Slot`.
---
## 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**: confirmed VarInt from **1.20.5**`ItemType1_20_5.java:51` reads `Types.VAR_INT`; `ItemType1_20_2.java:47` reads `readByte()` (i8). `minecraft-data` 1.20.5's `i8` entry is a pre-release snapshot artifact; corrected to `varint` in `minecraft-data` 1.21.1. Source: `ViaVersion/.../api/type/types/item/ItemType1_20_5.java` and `ItemType1_20_2.java`.
- **1.20.2 NBT format (name-prefix removal)**: confirmed at **1.20.2**`ItemType1_20_2.java:47` uses `Types.COMPOUND_TAG` (anonymous); `ItemType1_13_2.java:46` uses `Types.NAMED_COMPOUND_TAG`. There is no `ItemType1_20_3` or `ItemType1_20_4` (those versions reuse `ItemType1_20_2`), so the change landed exactly at the 1.20.2 protocol boundary. Source: `ViaVersion/.../api/type/types/item/ItemType1_20_2.java` vs `ItemType1_13_2.java`.
- **`jukebox_playable` in 1.20.5**: confirmed absent — `minecraft-data` 1.20.5 `SlotComponentType` mappings show `recipes` at id 42; no `jukebox_playable` entry. The 1.21 pre-releases introduced it. The component table in this doc correctly reflects this split.