Skip to main content

mcproto_types/
slot.rs

1//! Inventory slots, structured data components, and recipe slot displays.
2//!
3//! The implementation follows the current Java Edition protocol [Slot Data]
4//! and [Slot Display] structures. Submodules live in `slot/`; this file is the
5//! module root so no `slot/mod.rs` is used.
6//!
7//! [Slot Data]: https://minecraft.wiki/w/Java_Edition_protocol/Slot_data
8//! [Slot Display]: https://minecraft.wiki/w/Java_Edition_protocol/Recipes#Slot_Display_structure
9
10use std::io::{Read, Write};
11
12use mcproto_codec::{
13    error::{CodecError, CodecKind, CodecOperation, InvalidEncodingReason},
14    varint::{VarIntRead, VarIntWrite},
15};
16
17use crate::TypeCodec;
18
19#[path = "slot/components.rs"]
20pub mod components;
21#[path = "slot/display.rs"]
22pub mod display;
23#[path = "slot/hashed.rs"]
24pub mod hashed;
25#[path = "slot/types.rs"]
26pub mod types;
27
28pub use components::*;
29pub use display::*;
30pub use hashed::*;
31pub use types::*;
32
33// Keep the established `slot::*` paths while using the canonical profile
34// implementation owned by the profile module.
35pub use crate::profile::{PartialProfile, ResolvableProfile, ResolvableProfileData, SkinModel};
36
37/// A non-empty item stack carried by a [`Slot`].
38#[derive(Debug, Clone, PartialEq)]
39pub struct ItemStack {
40    /// Positive item count.
41    pub count: u32,
42    /// Numeric ID in the `minecraft:item` registry.
43    pub item_id: u32,
44    /// Typed component values added to or replacing item defaults.
45    pub components_to_add: Vec<DataComponent>,
46    /// Component types removed from item defaults.
47    pub components_to_remove: Vec<DataComponentType>,
48}
49
50impl ItemStack {
51    /// Creates an item stack, rejecting values that cannot be represented by a
52    /// positive protocol VarInt.
53    pub fn new(count: u32, item_id: u32) -> Result<Self, InvalidItemStack> {
54        if count == 0 || count > i32::MAX as u32 {
55            return Err(InvalidItemStack::Count(count));
56        }
57        if item_id > i32::MAX as u32 {
58            return Err(InvalidItemStack::ItemId(item_id));
59        }
60        Ok(Self {
61            count,
62            item_id,
63            components_to_add: Vec::new(),
64            components_to_remove: Vec::new(),
65        })
66    }
67}
68
69/// An empty inventory slot or a complete non-empty item stack.
70///
71/// The item count is encoded first. Zero denotes [`Slot::Empty`]; a positive
72/// count is followed by the item registry ID, component-add count,
73/// component-remove count, typed added components, and removed component IDs.
74///
75/// # Examples
76///
77/// ```
78/// use mcproto_types::{
79///     DataComponent, MaxDamageComponent, Slot, ItemStack, TypeCodec, VarInt,
80/// };
81///
82/// let mut item = ItemStack::new(2, 5)?;
83/// item.components_to_add.push(DataComponent::MaxDamage(
84///     MaxDamageComponent { max_damage: VarInt(100) },
85/// ));
86/// let slot = Slot::Item(item);
87/// let mut encoded = Vec::new();
88/// slot.encode(&mut encoded)?;
89///
90/// let mut input = encoded.as_slice();
91/// assert_eq!(Slot::decode(&mut input)?, slot);
92/// # Ok::<(), Box<dyn std::error::Error>>(())
93/// ```
94#[derive(Debug, Clone, PartialEq, Default)]
95pub enum Slot {
96    /// Encoded as an item count of zero with no following fields.
97    #[default]
98    Empty,
99    /// A positive item count followed by item and component-patch data.
100    Item(ItemStack),
101}
102
103impl TypeCodec for Slot {
104    fn encode(&self, writer: &mut impl Write) -> Result<(), CodecError> {
105        let Self::Item(item) = self else {
106            return writer
107                .write_varint(0)
108                .map_err(|error| error.with_context(CodecKind::Slot));
109        };
110        validate_item(item)?;
111        writer
112            .write_varint(item.count as i32)
113            .map_err(|error| error.with_context(CodecKind::Slot))?;
114        writer
115            .write_varint(item.item_id as i32)
116            .map_err(|error| error.with_context(CodecKind::Slot))?;
117        write_length(writer, item.components_to_add.len(), CodecKind::Slot)?;
118        write_length(writer, item.components_to_remove.len(), CodecKind::Slot)?;
119        for component in &item.components_to_add {
120            component
121                .encode(writer)
122                .map_err(|error| error.with_context(CodecKind::Slot))?;
123        }
124        for component_type in &item.components_to_remove {
125            component_type
126                .encode(writer)
127                .map_err(|error| error.with_context(CodecKind::Slot))?;
128        }
129        Ok(())
130    }
131
132    fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
133        let count = reader
134            .read_varint()
135            .map_err(|error| error.with_context(CodecKind::Slot))?;
136        if count == 0 {
137            return Ok(Self::Empty);
138        }
139        let count = decode_item_count(count, CodecKind::Slot)?;
140        let item_id = reader
141            .read_varint()
142            .map_err(|error| error.with_context(CodecKind::Slot))?;
143        let item_id = decode_item_id(item_id, CodecKind::Slot)?;
144        let add_count = read_length(reader, CodecKind::Slot)?;
145        let remove_count = read_length(reader, CodecKind::Slot)?;
146        let mut components_to_add = Vec::with_capacity(add_count.min(1024));
147        for _ in 0..add_count {
148            components_to_add.push(
149                DataComponent::decode(reader)
150                    .map_err(|error| error.with_context(CodecKind::Slot))?,
151            );
152        }
153        let mut components_to_remove = Vec::with_capacity(remove_count.min(1024));
154        for _ in 0..remove_count {
155            components_to_remove.push(
156                DataComponentType::decode(reader)
157                    .map_err(|error| error.with_context(CodecKind::Slot))?,
158            );
159        }
160        Ok(Self::Item(ItemStack {
161            count,
162            item_id,
163            components_to_add,
164            components_to_remove,
165        }))
166    }
167}
168
169fn validate_item(item: &ItemStack) -> Result<(), CodecError> {
170    validate_item_fields(item.count, item.item_id, CodecKind::Slot)
171}
172
173pub(super) fn validate_item_fields(
174    count: u32,
175    item_id: u32,
176    kind: CodecKind,
177) -> Result<(), CodecError> {
178    if count == 0 || count > i32::MAX as u32 {
179        return Err(CodecError::invalid_encoding_for_operation(
180            kind,
181            CodecOperation::Write,
182            0,
183            InvalidEncodingReason::InvalidSlotCount {
184                value: i64::from(count),
185            },
186        ));
187    }
188    if item_id > i32::MAX as u32 {
189        return Err(CodecError::invalid_encoding_for_operation(
190            kind,
191            CodecOperation::Write,
192            0,
193            InvalidEncodingReason::InvalidRegistryId {
194                value: item_id as i32,
195                max: i32::MAX,
196            },
197        ));
198    }
199    Ok(())
200}
201
202pub(super) fn write_length(
203    writer: &mut impl Write,
204    length: usize,
205    kind: CodecKind,
206) -> Result<(), CodecError> {
207    let length = i32::try_from(length).map_err(|_| {
208        CodecError::invalid_encoding_for_operation(
209            kind,
210            CodecOperation::Write,
211            0,
212            InvalidEncodingReason::LengthOutOfRange {
213                max: i32::MAX as usize,
214                actual: length,
215            },
216        )
217    })?;
218    writer
219        .write_varint(length)
220        .map_err(|error| error.with_context(kind))
221}
222
223pub(super) fn read_length(reader: &mut impl Read, kind: CodecKind) -> Result<usize, CodecError> {
224    let value = reader
225        .read_varint()
226        .map_err(|error| error.with_context(kind))?;
227    usize::try_from(value).map_err(|_| {
228        CodecError::invalid_encoding(kind, 0, InvalidEncodingReason::NegativeLength { value })
229    })
230}
231
232pub(super) fn decode_item_count(value: i32, kind: CodecKind) -> Result<u32, CodecError> {
233    if value <= 0 {
234        return Err(CodecError::invalid_encoding(
235            kind,
236            0,
237            InvalidEncodingReason::InvalidSlotCount {
238                value: i64::from(value),
239            },
240        ));
241    }
242    Ok(value as u32)
243}
244
245pub(super) fn decode_item_id(value: i32, kind: CodecKind) -> Result<u32, CodecError> {
246    if value < 0 {
247        return Err(CodecError::invalid_encoding(
248            kind,
249            0,
250            InvalidEncodingReason::InvalidRegistryId {
251                value,
252                max: i32::MAX,
253            },
254        ));
255    }
256    Ok(value as u32)
257}
258
259/// Error returned when constructing an invalid non-empty item stack.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum InvalidItemStack {
262    /// The item count is zero or exceeds a positive VarInt.
263    Count(u32),
264    /// The item registry ID exceeds a non-negative VarInt.
265    ItemId(u32),
266}
267
268impl std::fmt::Display for InvalidItemStack {
269    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        match self {
271            Self::Count(value) => write!(formatter, "invalid item count: {value}"),
272            Self::ItemId(value) => write!(formatter, "invalid item registry ID: {value}"),
273        }
274    }
275}
276
277impl std::error::Error for InvalidItemStack {}