Skip to main content

steel_registry/data_components/components/
recursive_items.rs

1//! Item components whose codecs recursively contain item stack templates.
2
3use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::codec::VarInt;
8use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
9use steel_utils::nbt::NbtNumeric as _;
10use steel_utils::serial::{ReadFrom, WriteTo};
11
12use super::Bees;
13use crate::ItemStackTemplate;
14use crate::data_components::registry::ValidatePersistentComponent;
15use crate::data_components::vanilla_components::{BEES, BUNDLE_CONTENTS};
16use crate::item_stack::ItemStack;
17
18macro_rules! impl_template_wrapper_codecs {
19    ($type:ty, $field:ident) => {
20        impl WriteTo for $type {
21            fn write(&self, writer: &mut impl Write) -> Result<()> {
22                self.$field.write(writer)
23            }
24        }
25
26        impl ReadFrom for $type {
27            fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
28                Ok(Self::new(ItemStackTemplate::read(data)?))
29            }
30        }
31
32        impl ToNbtTag for $type {
33            fn to_nbt_tag(self) -> NbtTag {
34                self.$field.to_nbt_tag()
35            }
36        }
37
38        impl FromNbtTag for $type {
39            fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
40                Some(Self::new(ItemStackTemplate::from_nbt_tag(tag)?))
41            }
42        }
43
44        impl HashComponent for $type {
45            fn hash_component(&self, hasher: &mut ComponentHasher) {
46                self.$field.hash_component(hasher);
47            }
48        }
49    };
50}
51
52/// The item template produced after consuming an item.
53#[derive(Debug, Clone, PartialEq)]
54pub struct UseRemainder {
55    convert_into: ItemStackTemplate,
56}
57
58impl UseRemainder {
59    #[must_use]
60    pub const fn new(convert_into: ItemStackTemplate) -> Self {
61        Self { convert_into }
62    }
63
64    #[must_use]
65    pub const fn convert_into(&self) -> &ItemStackTemplate {
66        &self.convert_into
67    }
68
69    /// replaces an empty stack or returns an extra remainder to deliver after unlocking
70    #[must_use]
71    pub fn convert_into_remainder(
72        &self,
73        used_stack: &mut ItemStack,
74        stack_count_before_using: i32,
75        has_infinite_materials: bool,
76    ) -> Option<ItemStack> {
77        if has_infinite_materials || used_stack.count() >= stack_count_before_using {
78            return None;
79        }
80        let remainder = self.convert_into.create();
81        if used_stack.is_empty() {
82            *used_stack = remainder;
83            return None;
84        }
85        Some(remainder)
86    }
87}
88
89impl_template_wrapper_codecs!(UseRemainder, convert_into);
90
91impl ValidatePersistentComponent for UseRemainder {
92    fn validate_persistent(&self) -> Result<()> {
93        self.convert_into.validate_persistent_encoding()
94    }
95}
96
97/// The non-empty projectile templates loaded into a crossbow.
98#[derive(Debug, Default, Clone, PartialEq)]
99pub struct ChargedProjectiles {
100    items: Vec<ItemStackTemplate>,
101}
102
103impl ChargedProjectiles {
104    pub const MAX_SIZE: usize = 1024;
105
106    #[must_use]
107    pub const fn empty() -> Self {
108        Self { items: Vec::new() }
109    }
110
111    pub fn new(items: Vec<ItemStackTemplate>) -> Result<Self> {
112        if items.len() > Self::MAX_SIZE {
113            return Err(Error::other(format!(
114                "Got {} charged projectiles, but maximum is {}",
115                items.len(),
116                Self::MAX_SIZE
117            )));
118        }
119        Ok(Self { items })
120    }
121
122    #[must_use]
123    pub fn items(&self) -> &[ItemStackTemplate] {
124        &self.items
125    }
126}
127
128impl WriteTo for ChargedProjectiles {
129    fn write(&self, writer: &mut impl Write) -> Result<()> {
130        write_template_list(&self.items, Some(Self::MAX_SIZE), writer)
131    }
132}
133
134impl ReadFrom for ChargedProjectiles {
135    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
136        Self::new(read_template_list(data, Some(Self::MAX_SIZE))?)
137    }
138}
139
140impl ToNbtTag for ChargedProjectiles {
141    fn to_nbt_tag(self) -> NbtTag {
142        template_list_nbt(&self.items)
143    }
144}
145
146impl FromNbtTag for ChargedProjectiles {
147    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
148        Self::new(template_list_from_nbt(tag, Some(Self::MAX_SIZE))?).ok()
149    }
150}
151
152impl HashComponent for ChargedProjectiles {
153    fn hash_component(&self, hasher: &mut ComponentHasher) {
154        hash_template_list(&self.items, hasher);
155    }
156}
157
158impl ValidatePersistentComponent for ChargedProjectiles {
159    fn validate_persistent(&self) -> Result<()> {
160        validate_templates(self.items.iter())
161    }
162}
163
164/// The ordered item templates stored in a bundle.
165#[derive(Debug, Default, Clone, PartialEq)]
166pub struct BundleContents {
167    items: Vec<ItemStackTemplate>,
168}
169
170impl BundleContents {
171    #[must_use]
172    pub const fn empty() -> Self {
173        Self { items: Vec::new() }
174    }
175
176    #[must_use]
177    pub const fn new(items: Vec<ItemStackTemplate>) -> Self {
178        Self { items }
179    }
180
181    #[must_use]
182    pub fn items(&self) -> &[ItemStackTemplate] {
183        &self.items
184    }
185
186    /// Validates the checked rational weight used by Vanilla's strict item-stack validation.
187    pub(crate) fn validate_weight(&self) -> Result<()> {
188        self.compute_weight().map(|_| ())
189    }
190
191    fn compute_weight(&self) -> Result<CheckedFraction> {
192        let mut weight = CheckedFraction::ZERO;
193        for item in &self.items {
194            let item_weight = bundle_item_weight(item)?.multiply(item.count())?;
195            weight = weight.add(item_weight)?;
196        }
197        Ok(weight)
198    }
199}
200
201fn bundle_item_weight(item: &ItemStackTemplate) -> Result<CheckedFraction> {
202    if let Some(bundle) = item.get(BUNDLE_CONTENTS) {
203        return bundle.compute_weight()?.add(CheckedFraction::new(1, 16)?);
204    }
205    if item
206        .get(BEES)
207        .is_some_and(|bees: &Bees| !bees.bees().is_empty())
208    {
209        return Ok(CheckedFraction::ONE);
210    }
211    CheckedFraction::new(1, item.max_stack_size())
212}
213
214/// Positive subset of Commons Lang `Fraction` used by `BundleContents`.
215#[derive(Clone, Copy)]
216struct CheckedFraction {
217    numerator: i32,
218    denominator: i32,
219}
220
221impl CheckedFraction {
222    const ZERO: Self = Self {
223        numerator: 0,
224        denominator: 1,
225    };
226    const ONE: Self = Self {
227        numerator: 1,
228        denominator: 1,
229    };
230
231    fn new(numerator: i32, denominator: i32) -> Result<Self> {
232        if numerator < 0 || denominator <= 0 {
233            return Err(Error::other("Invalid bundle weight fraction"));
234        }
235        let divisor = gcd(numerator, denominator);
236        Ok(Self {
237            numerator: numerator / divisor,
238            denominator: denominator / divisor,
239        })
240    }
241
242    /// Mirrors the positive-number branches of Commons Lang `Fraction.addSub`.
243    fn add(self, other: Self) -> Result<Self> {
244        if self.numerator == 0 {
245            return Ok(other);
246        }
247        if other.numerator == 0 {
248            return Ok(self);
249        }
250
251        let denominator_gcd = gcd(self.denominator, other.denominator);
252        if denominator_gcd == 1 {
253            let left = checked_mul(self.numerator, other.denominator)?;
254            let right = checked_mul(other.numerator, self.denominator)?;
255            return Ok(Self {
256                numerator: checked_add(left, right)?,
257                denominator: checked_mul(self.denominator, other.denominator)?,
258            });
259        }
260
261        let left = i64::from(self.numerator) * i64::from(other.denominator / denominator_gcd);
262        let right = i64::from(other.numerator) * i64::from(self.denominator / denominator_gcd);
263        let sum = left + right;
264        let reduction = gcd_i64(sum % i64::from(denominator_gcd), i64::from(denominator_gcd));
265        let numerator = i32::try_from(sum / reduction)
266            .map_err(|_| Error::other("Excessive total bundle weight"))?;
267        let reduction =
268            i32::try_from(reduction).map_err(|_| Error::other("Excessive total bundle weight"))?;
269        let denominator = checked_mul(
270            self.denominator / denominator_gcd,
271            other.denominator / reduction,
272        )?;
273        Ok(Self {
274            numerator,
275            denominator,
276        })
277    }
278
279    /// Mirrors multiplying by Commons Lang `Fraction.getFraction(value, 1)`.
280    fn multiply(self, value: i32) -> Result<Self> {
281        if value < 0 {
282            return Err(Error::other("Invalid bundle item count"));
283        }
284        if self.numerator == 0 || value == 0 {
285            return Ok(Self::ZERO);
286        }
287        let reduction = gcd(value, self.denominator);
288        Self::new(
289            checked_mul(self.numerator, value / reduction)?,
290            self.denominator / reduction,
291        )
292    }
293}
294
295const fn gcd(mut left: i32, mut right: i32) -> i32 {
296    while right != 0 {
297        let remainder = left % right;
298        left = right;
299        right = remainder;
300    }
301    left.abs()
302}
303
304const fn gcd_i64(mut left: i64, mut right: i64) -> i64 {
305    while right != 0 {
306        let remainder = left % right;
307        left = right;
308        right = remainder;
309    }
310    left.abs()
311}
312
313fn checked_mul(left: i32, right: i32) -> Result<i32> {
314    left.checked_mul(right)
315        .ok_or_else(|| Error::other("Excessive total bundle weight"))
316}
317
318fn checked_add(left: i32, right: i32) -> Result<i32> {
319    left.checked_add(right)
320        .ok_or_else(|| Error::other("Excessive total bundle weight"))
321}
322
323impl WriteTo for BundleContents {
324    fn write(&self, writer: &mut impl Write) -> Result<()> {
325        write_template_list(&self.items, None, writer)
326    }
327}
328
329impl ReadFrom for BundleContents {
330    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
331        Ok(Self::new(read_template_list(data, None)?))
332    }
333}
334
335impl ToNbtTag for BundleContents {
336    fn to_nbt_tag(self) -> NbtTag {
337        template_list_nbt(&self.items)
338    }
339}
340
341impl FromNbtTag for BundleContents {
342    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
343        Some(Self::new(template_list_from_nbt(tag, None)?))
344    }
345}
346
347impl HashComponent for BundleContents {
348    fn hash_component(&self, hasher: &mut ComponentHasher) {
349        hash_template_list(&self.items, hasher);
350    }
351}
352
353impl ValidatePersistentComponent for BundleContents {
354    fn validate_persistent(&self) -> Result<()> {
355        validate_templates(self.items.iter())
356    }
357}
358
359/// Up to 256 dense optional container slots.
360#[derive(Debug, Default, Clone, PartialEq)]
361pub struct ItemContainerContents {
362    items: Vec<Option<ItemStackTemplate>>,
363}
364
365impl ItemContainerContents {
366    pub const MAX_SIZE: usize = 256;
367
368    #[must_use]
369    pub const fn empty() -> Self {
370        Self { items: Vec::new() }
371    }
372
373    pub fn new(items: Vec<Option<ItemStackTemplate>>) -> Result<Self> {
374        if items.len() > Self::MAX_SIZE {
375            return Err(Error::other(format!(
376                "Got {} container slots, but maximum is {}",
377                items.len(),
378                Self::MAX_SIZE
379            )));
380        }
381        Ok(Self { items })
382    }
383
384    #[must_use]
385    pub fn items(&self) -> &[Option<ItemStackTemplate>] {
386        &self.items
387    }
388
389    fn from_slots(slots: Vec<ContainerSlot>) -> Option<Self> {
390        if slots.len() > Self::MAX_SIZE {
391            return None;
392        }
393        let size = slots.iter().map(|slot| slot.index + 1).max().unwrap_or(0);
394        let mut items = vec![None; size];
395        for slot in slots {
396            items[slot.index] = Some(slot.item);
397        }
398        Self::new(items).ok()
399    }
400
401    fn slots(&self) -> impl Iterator<Item = (usize, &ItemStackTemplate)> {
402        self.items
403            .iter()
404            .enumerate()
405            .filter_map(|(index, item)| item.as_ref().map(|item| (index, item)))
406    }
407}
408
409impl WriteTo for ItemContainerContents {
410    fn write(&self, writer: &mut impl Write) -> Result<()> {
411        write_count(self.items.len(), Some(Self::MAX_SIZE), writer)?;
412        for item in &self.items {
413            item.is_some().write(writer)?;
414            if let Some(item) = item {
415                item.write(writer)?;
416            }
417        }
418        Ok(())
419    }
420}
421
422impl ReadFrom for ItemContainerContents {
423    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
424        let count = read_count(data, Some(Self::MAX_SIZE))?;
425        let mut items = Vec::with_capacity(count);
426        for _ in 0..count {
427            items.push(if bool::read(data)? {
428                Some(ItemStackTemplate::read(data)?)
429            } else {
430                None
431            });
432        }
433        Self::new(items)
434    }
435}
436
437impl ToNbtTag for ItemContainerContents {
438    fn to_nbt_tag(self) -> NbtTag {
439        let slots = self
440            .slots()
441            .map(|(index, item)| container_slot_nbt(index, item))
442            .collect();
443        if self.items.iter().all(Option::is_none) {
444            NbtTag::List(NbtList::Empty)
445        } else {
446            NbtTag::List(NbtList::Compound(slots))
447        }
448    }
449}
450
451impl FromNbtTag for ItemContainerContents {
452    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
453        let list = tag.list()?;
454        if list.to_owned().as_nbt_tags().is_empty() {
455            return Some(Self::empty());
456        }
457        let compounds = list.compounds()?;
458        if compounds.len() > Self::MAX_SIZE {
459            return None;
460        }
461        let slots = compounds
462            .into_iter()
463            .map(ContainerSlot::from_nbt_compound)
464            .collect::<Option<Vec<_>>>()?;
465        Self::from_slots(slots)
466    }
467}
468
469impl HashComponent for ItemContainerContents {
470    fn hash_component(&self, hasher: &mut ComponentHasher) {
471        hasher.start_list();
472        for (index, item) in self.slots() {
473            let mut slot_hasher = ComponentHasher::new();
474            hash_container_slot(index, item, &mut slot_hasher);
475            hasher.put_raw_bytes(&slot_hasher.finish().to_le_bytes());
476        }
477        hasher.end_list();
478    }
479}
480
481impl ValidatePersistentComponent for ItemContainerContents {
482    fn validate_persistent(&self) -> Result<()> {
483        validate_templates(self.items.iter().filter_map(Option::as_ref))
484    }
485}
486
487#[derive(Debug, Clone, PartialEq)]
488struct ContainerSlot {
489    index: usize,
490    item: ItemStackTemplate,
491}
492
493impl ContainerSlot {
494    fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound<'_, '_>) -> Option<Self> {
495        let index = compound.get("slot")?.codec_i32()?;
496        let index = usize::try_from(index).ok()?;
497        if index >= ItemContainerContents::MAX_SIZE {
498            return None;
499        }
500        Some(Self {
501            index,
502            item: ItemStackTemplate::from_nbt_tag(compound.get("item")?)?,
503        })
504    }
505}
506
507fn container_slot_nbt(index: usize, item: &ItemStackTemplate) -> NbtCompound {
508    let mut compound = NbtCompound::new();
509    compound.insert("slot", index as i32);
510    compound.insert("item", item.to_nbt_tag_ref());
511    compound
512}
513
514fn hash_container_slot(index: usize, item: &ItemStackTemplate, hasher: &mut ComponentHasher) {
515    let mut entries = Vec::with_capacity(2);
516    push_hash_entry(&mut entries, "slot", &(index as i32));
517    push_hash_entry(&mut entries, "item", item);
518    sort_map_entries(&mut entries);
519    hasher.start_map();
520    for entry in &entries {
521        hasher.put_raw_bytes(&entry.key_bytes);
522        hasher.put_raw_bytes(&entry.value_bytes);
523    }
524    hasher.end_map();
525}
526
527/// The non-empty absorbed block item carried by a sulfur cube.
528#[derive(Debug, Clone, PartialEq)]
529pub struct SulfurCubeContent {
530    absorbed_block_item_stack: ItemStackTemplate,
531}
532
533impl SulfurCubeContent {
534    #[must_use]
535    pub const fn new(absorbed_block_item_stack: ItemStackTemplate) -> Self {
536        Self {
537            absorbed_block_item_stack,
538        }
539    }
540
541    #[must_use]
542    pub const fn absorbed_block_item_stack(&self) -> &ItemStackTemplate {
543        &self.absorbed_block_item_stack
544    }
545}
546
547impl_template_wrapper_codecs!(SulfurCubeContent, absorbed_block_item_stack);
548
549impl ValidatePersistentComponent for SulfurCubeContent {
550    fn validate_persistent(&self) -> Result<()> {
551        self.absorbed_block_item_stack
552            .validate_persistent_encoding()
553    }
554}
555
556fn write_template_list(
557    items: &[ItemStackTemplate],
558    max: Option<usize>,
559    writer: &mut impl Write,
560) -> Result<()> {
561    write_count(items.len(), max, writer)?;
562    for item in items {
563        item.write(writer)?;
564    }
565    Ok(())
566}
567
568fn validate_templates<'a>(items: impl IntoIterator<Item = &'a ItemStackTemplate>) -> Result<()> {
569    for item in items {
570        item.validate_persistent_encoding()?;
571    }
572    Ok(())
573}
574
575fn read_template_list(
576    data: &mut Cursor<&[u8]>,
577    max: Option<usize>,
578) -> Result<Vec<ItemStackTemplate>> {
579    let count = read_count(data, max)?;
580    let mut items = Vec::with_capacity(count.min(65_536));
581    for _ in 0..count {
582        items.push(ItemStackTemplate::read(data)?);
583    }
584    Ok(items)
585}
586
587fn write_count(count: usize, max: Option<usize>, writer: &mut impl Write) -> Result<()> {
588    if let Some(max) = max
589        && count > max
590    {
591        return Err(Error::other(format!(
592            "{count} elements exceeded max size of {max}"
593        )));
594    }
595    let count = i32::try_from(count).map_err(|_| Error::other("List is too large"))?;
596    VarInt(count).write(writer)
597}
598
599fn read_count(data: &mut Cursor<&[u8]>, max: Option<usize>) -> Result<usize> {
600    let count = VarInt::read(data)?.0;
601    let count = usize::try_from(count).map_err(|_| Error::other("Negative list length"))?;
602    if let Some(max) = max
603        && count > max
604    {
605        return Err(Error::other(format!(
606            "{count} elements exceeded max size of {max}"
607        )));
608    }
609    Ok(count)
610}
611
612fn template_list_nbt(items: &[ItemStackTemplate]) -> NbtTag {
613    if items.is_empty() {
614        return NbtTag::List(NbtList::Empty);
615    }
616    NbtTag::List(NbtList::Compound(
617        items
618            .iter()
619            .map(|item| match item.to_nbt_tag_ref() {
620                NbtTag::Compound(compound) => compound,
621                _ => unreachable!("item stack template primary codec is a compound"),
622            })
623            .collect(),
624    ))
625}
626
627fn template_list_from_nbt(
628    tag: simdnbt::borrow::NbtTag,
629    max: Option<usize>,
630) -> Option<Vec<ItemStackTemplate>> {
631    let list = tag.list()?;
632    if list.to_owned().as_nbt_tags().is_empty() {
633        return Some(Vec::new());
634    }
635
636    if let Some(values) = list.strings() {
637        if max.is_some_and(|max| values.len() > max) {
638            return None;
639        }
640        return values
641            .iter()
642            .map(|value| ItemStackTemplate::from_nbt_identifier(&value.to_str()))
643            .collect();
644    }
645
646    let compounds = list.compounds()?;
647    if max.is_some_and(|max| compounds.len() > max) {
648        return None;
649    }
650    compounds
651        .into_iter()
652        .map(ItemStackTemplate::from_nbt_compound)
653        .collect()
654}
655
656fn hash_template_list(items: &[ItemStackTemplate], hasher: &mut ComponentHasher) {
657    hasher.start_list();
658    for item in items {
659        hasher.put_component_hash(item);
660    }
661    hasher.end_list();
662}
663
664fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
665    let mut key_hasher = ComponentHasher::new();
666    key.hash_component(&mut key_hasher);
667    let mut value_hasher = ComponentHasher::new();
668    value.hash_component(&mut value_hasher);
669    entries.push(HashEntry::new(key_hasher, value_hasher));
670}
671
672#[cfg(test)]
673mod tests {
674    use std::io::Cursor;
675
676    use simdnbt::borrow::read_tag;
677    use simdnbt::owned::NbtList;
678    use simdnbt::{FromNbtTag, ToNbtTag};
679    use steel_utils::hash::HashComponent;
680    use steel_utils::serial::{ReadFrom, WriteTo};
681
682    use super::{
683        BundleContents, ChargedProjectiles, ItemContainerContents, SulfurCubeContent, UseRemainder,
684    };
685    use crate::data_components::DataComponentPatch;
686    use crate::data_components::components::{BeehiveOccupant, Bees, CustomData, EntityData};
687    use crate::data_components::vanilla_components::{
688        BEES, BUNDLE_CONTENTS, CHARGED_PROJECTILES, CONTAINER, MAX_STACK_SIZE, USE_REMAINDER,
689    };
690    use crate::init_vanilla_registry;
691    use crate::{ItemStackTemplate, REGISTRY, vanilla_entities, vanilla_items};
692
693    fn round_trip<T>(value: T)
694    where
695        T: Clone
696            + std::fmt::Debug
697            + PartialEq
698            + WriteTo
699            + ReadFrom
700            + ToNbtTag
701            + FromNbtTag
702            + HashComponent,
703    {
704        let nbt = value.clone().to_nbt_tag();
705        let mut bytes = Vec::new();
706        nbt.write(&mut bytes);
707        let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).expect("NBT should parse");
708        let decoded = T::from_nbt_tag(borrowed.as_tag()).expect("NBT should decode");
709        assert_eq!(decoded, value);
710        assert_eq!(decoded.compute_hash(), value.compute_hash());
711
712        let mut network = Vec::new();
713        value
714            .write(&mut network)
715            .expect("network value should encode");
716        assert_eq!(
717            T::read(&mut Cursor::new(network.as_slice())).expect("network value should decode"),
718            value
719        );
720    }
721
722    #[test]
723    fn recursive_item_components_round_trip_both_codecs() {
724        init_vanilla_registry();
725        let arrow = ItemStackTemplate::new(&vanilla_items::ARROW);
726        let diamond = ItemStackTemplate::new(&vanilla_items::DIAMOND);
727        round_trip(UseRemainder::new(ItemStackTemplate::new(
728            &vanilla_items::BOWL,
729        )));
730        round_trip(
731            ChargedProjectiles::new(vec![arrow.clone()]).expect("one projectile should fit"),
732        );
733        round_trip(BundleContents::new(vec![diamond.clone()]));
734        round_trip(
735            ItemContainerContents::new(vec![None, Some(diamond)]).expect("two slots should fit"),
736        );
737        round_trip(SulfurCubeContent::new(arrow));
738    }
739
740    #[test]
741    fn sparse_container_persistence_and_dense_network_preserve_vanilla_shapes() {
742        init_vanilla_registry();
743        let contents = ItemContainerContents::new(vec![
744            None,
745            Some(ItemStackTemplate::new(&vanilla_items::DIAMOND)),
746            None,
747        ])
748        .expect("three slots should fit");
749        let nbt = contents.clone().to_nbt_tag();
750        let compounds = nbt.list().and_then(NbtList::compounds).expect("slot list");
751        assert_eq!(compounds.len(), 1);
752        assert_eq!(compounds[0].int("slot"), Some(1));
753
754        let mut network = Vec::new();
755        contents
756            .write(&mut network)
757            .expect("contents should encode");
758        assert_eq!(network[0], 3);
759    }
760
761    #[test]
762    fn recursive_collection_limits_are_enforced() {
763        init_vanilla_registry();
764        let projectile = ItemStackTemplate::new(&vanilla_items::ARROW);
765        assert!(
766            ChargedProjectiles::new(vec![projectile; ChargedProjectiles::MAX_SIZE + 1]).is_err()
767        );
768        assert!(
769            ItemContainerContents::new(vec![None; ItemContainerContents::MAX_SIZE + 1]).is_err()
770        );
771    }
772
773    #[test]
774    fn bundle_weight_matches_nested_bundle_and_beehive_rules() {
775        init_vanilla_registry();
776
777        let mut inner_patch = DataComponentPatch::new();
778        inner_patch.set(
779            BUNDLE_CONTENTS,
780            BundleContents::new(vec![ItemStackTemplate::new(&vanilla_items::STONE)]),
781        );
782        let nested_bundle =
783            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, inner_patch)
784                .expect("nested bundle should persist");
785        let nested_weight = BundleContents::new(vec![nested_bundle])
786            .compute_weight()
787            .expect("small nested bundle weight should compute");
788        assert_eq!(
789            (nested_weight.numerator, nested_weight.denominator),
790            (5, 64)
791        );
792
793        let occupant = BeehiveOccupant::new(
794            EntityData::new(&vanilla_entities::BEE, CustomData::default()),
795            0,
796            0,
797        );
798        let mut beehive_patch = DataComponentPatch::new();
799        beehive_patch.set(BEES, Bees::new(vec![occupant]));
800        let beehive =
801            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::BEEHIVE, 1, beehive_patch)
802                .expect("occupied beehive should persist");
803        let beehive_weight = BundleContents::new(vec![beehive])
804            .compute_weight()
805            .expect("occupied beehive weight should compute");
806        assert_eq!(
807            (beehive_weight.numerator, beehive_weight.denominator),
808            (1, 1)
809        );
810    }
811
812    #[test]
813    fn bundle_weight_rejects_commons_fraction_denominator_overflow() {
814        init_vanilla_registry();
815
816        let items = [97, 89, 83, 79, 73]
817            .into_iter()
818            .map(|max_stack_size| {
819                let mut patch = DataComponentPatch::new();
820                patch.set(MAX_STACK_SIZE, max_stack_size);
821                ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, patch)
822                    .expect("prime max stack size should be persistable")
823            })
824            .collect();
825
826        assert!(BundleContents::new(items).validate_weight().is_err());
827    }
828
829    #[test]
830    fn extracted_item_prototypes_use_recursive_component_values() {
831        init_vanilla_registry();
832        assert_eq!(
833            REGISTRY
834                .items
835                .iter()
836                .filter(|(_, item)| item.components.has(USE_REMAINDER))
837                .count(),
838            7
839        );
840        assert_eq!(
841            REGISTRY
842                .items
843                .iter()
844                .filter(|(_, item)| item.components.has(CHARGED_PROJECTILES))
845                .count(),
846            1
847        );
848        assert_eq!(
849            REGISTRY
850                .items
851                .iter()
852                .filter(|(_, item)| item.components.has(BUNDLE_CONTENTS))
853                .count(),
854            17
855        );
856        assert_eq!(
857            REGISTRY
858                .items
859                .iter()
860                .filter(|(_, item)| item.components.has(CONTAINER))
861                .count(),
862            45
863        );
864
865        assert_eq!(
866            vanilla_items::MILK_BUCKET
867                .components
868                .get(USE_REMAINDER)
869                .map(|remainder| remainder.convert_into().item()),
870            Some(&*vanilla_items::BUCKET)
871        );
872    }
873}