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};
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#[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 #[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#[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#[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 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#[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 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 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#[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#[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}