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:
@@ -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 : <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 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. <!-- 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). <!-- 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 -->
|
||||
Reference in New Issue
Block a user