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