1use 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#[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#[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#[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 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#[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 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 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#[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#[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}