02a249a379
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>
282 lines
14 KiB
Markdown
282 lines
14 KiB
Markdown
# 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 : <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 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**: 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.
|