Skip to main content

mcproto_types/slot/
hashed.rs

1//! Hashed inventory slots used by serverbound container interactions.
2
3use std::io::{Read, Write};
4
5use mcproto_codec::{
6    error::{CodecError, CodecKind},
7    varint::{VarIntRead, VarIntWrite},
8};
9
10use crate::{Boolean, Int, TypeCodec, TypeStructCodec};
11
12use super::{
13    DataComponentType, InvalidItemStack, decode_item_count, decode_item_id, read_length,
14    validate_item_fields, write_length,
15};
16
17/// A data component type and the CRC32C hash of its encoded value.
18///
19/// The protocol defines the hash as an [`Int`] bit pattern. How vanilla
20/// computes this CRC32C value is currently undocumented; this type stores and
21/// transmits an already-computed hash.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TypeStructCodec)]
23#[type_struct_codec(kind = HashedSlot)]
24pub struct HashedDataComponent {
25    /// Type of the component whose value was hashed.
26    pub component_type: DataComponentType,
27    /// CRC32C hash represented by the protocol's signed 32-bit `Int` field.
28    pub data_hash: Int,
29}
30
31/// A non-empty item stack carried by a [`HashedSlot`].
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct HashedItemStack {
34    /// Numeric ID in the `minecraft:item` registry.
35    pub item_id: u32,
36    /// Positive item count.
37    pub count: u32,
38    /// Component types and CRC32C hashes added to the item defaults.
39    pub components_to_add: Vec<HashedDataComponent>,
40    /// Component types removed from the item defaults.
41    pub components_to_remove: Vec<DataComponentType>,
42}
43
44impl HashedItemStack {
45    /// Creates a hashed item stack with no component changes.
46    ///
47    /// The count must be positive and both fields must fit their non-negative
48    /// VarInt wire representations.
49    pub fn new(count: u32, item_id: u32) -> Result<Self, InvalidItemStack> {
50        if count == 0 || count > i32::MAX as u32 {
51            return Err(InvalidItemStack::Count(count));
52        }
53        if item_id > i32::MAX as u32 {
54            return Err(InvalidItemStack::ItemId(item_id));
55        }
56        Ok(Self {
57            item_id,
58            count,
59            components_to_add: Vec::new(),
60            components_to_remove: Vec::new(),
61        })
62    }
63}
64
65/// An empty slot or an item stack represented using component data hashes.
66///
67/// A boolean is written first. `false` represents an empty slot. When it is
68/// `true`, the item ID and count are followed by a prefixed array of component
69/// type/hash pairs and a prefixed array of removed component types. Array order
70/// is preserved exactly.
71///
72/// # Examples
73///
74/// ```
75/// use mcproto_types::{
76///     DataComponentType, HashedDataComponent, HashedItemStack, HashedSlot, Int, TypeCodec,
77/// };
78///
79/// let mut item = HashedItemStack::new(2, 5)?;
80/// item.components_to_add.push(HashedDataComponent {
81///     component_type: DataComponentType::MaxDamage,
82///     data_hash: Int(0x1234_5678),
83/// });
84/// let slot = HashedSlot::Item(item);
85///
86/// let mut encoded = Vec::new();
87/// slot.encode(&mut encoded)?;
88/// let mut input = encoded.as_slice();
89/// assert_eq!(HashedSlot::decode(&mut input)?, slot);
90/// # Ok::<(), Box<dyn std::error::Error>>(())
91/// ```
92///
93/// See the official [Hashed Format] protocol documentation.
94///
95/// [Hashed Format]: https://minecraft.wiki/w/Java_Edition_protocol/Slot_data#Hashed_Format
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
97pub enum HashedSlot {
98    /// Encoded only as a `false` presence boolean.
99    #[default]
100    Empty,
101    /// Encoded as `true` followed by the hashed item-stack fields.
102    Item(HashedItemStack),
103}
104
105impl TypeCodec for HashedSlot {
106    fn encode(&self, writer: &mut impl Write) -> Result<(), CodecError> {
107        let Self::Item(item) = self else {
108            return Boolean(false)
109                .encode(writer)
110                .map_err(|error| error.with_context(CodecKind::HashedSlot));
111        };
112
113        validate_item_fields(item.count, item.item_id, CodecKind::HashedSlot)?;
114        Boolean(true)
115            .encode(writer)
116            .map_err(|error| error.with_context(CodecKind::HashedSlot))?;
117        writer
118            .write_varint(item.item_id as i32)
119            .map_err(|error| error.with_context(CodecKind::HashedSlot))?;
120        writer
121            .write_varint(item.count as i32)
122            .map_err(|error| error.with_context(CodecKind::HashedSlot))?;
123        write_length(writer, item.components_to_add.len(), CodecKind::HashedSlot)?;
124        for component in &item.components_to_add {
125            component.encode(writer)?;
126        }
127        write_length(
128            writer,
129            item.components_to_remove.len(),
130            CodecKind::HashedSlot,
131        )?;
132        for component_type in &item.components_to_remove {
133            component_type
134                .encode(writer)
135                .map_err(|error| error.with_context(CodecKind::HashedSlot))?;
136        }
137        Ok(())
138    }
139
140    fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
141        let has_item = Boolean::decode(reader)
142            .map_err(|error| error.with_context(CodecKind::HashedSlot))?
143            .0;
144        if !has_item {
145            return Ok(Self::Empty);
146        }
147
148        let item_id = reader
149            .read_varint()
150            .map_err(|error| error.with_context(CodecKind::HashedSlot))?;
151        let item_id = decode_item_id(item_id, CodecKind::HashedSlot)?;
152        let count = reader
153            .read_varint()
154            .map_err(|error| error.with_context(CodecKind::HashedSlot))?;
155        let count = decode_item_count(count, CodecKind::HashedSlot)?;
156
157        let add_count = read_length(reader, CodecKind::HashedSlot)?;
158        let mut components_to_add = Vec::with_capacity(add_count.min(1024));
159        for _ in 0..add_count {
160            components_to_add.push(HashedDataComponent::decode(reader)?);
161        }
162
163        let remove_count = read_length(reader, CodecKind::HashedSlot)?;
164        let mut components_to_remove = Vec::with_capacity(remove_count.min(1024));
165        for _ in 0..remove_count {
166            components_to_remove.push(
167                DataComponentType::decode(reader)
168                    .map_err(|error| error.with_context(CodecKind::HashedSlot))?,
169            );
170        }
171
172        Ok(Self::Item(HashedItemStack {
173            item_id,
174            count,
175            components_to_add,
176            components_to_remove,
177        }))
178    }
179}