1use std::{
4 borrow::Cow,
5 io::{Cursor, Result, Write},
6};
7
8use rand::RngExt;
9
10use steel_utils::{
11 DowncastType, Identifier,
12 codec::VarInt,
13 java,
14 random::{Random, xoroshiro::Xoroshiro},
15 serial::{ReadFrom, WriteTo},
16};
17use text_components::TextComponent;
18
19use crate::{
20 REGISTRY, RegistryEntry, RegistryExt,
21 damage_type::DamageTypeRef,
22 data_components::{
23 Component, ComponentData, ComponentPatchEntry, CustomData, DataComponentMap,
24 DataComponentPatch, DataComponentType,
25 vanilla_components::{
26 ATTACK_RANGE, ATTRIBUTE_MODIFIERS, AttackRange, BUNDLE_CONTENTS, CHARGED_PROJECTILES,
27 CONTAINER, CUSTOM_DATA, CUSTOM_NAME, DAMAGE, DAMAGE_RESISTANT, DAMAGE_TYPE,
28 ENCHANTABLE, ENCHANTMENTS, EQUIPPABLE, Equippable, ITEM_NAME, ItemAttributeModifiers,
29 ItemEnchantments, MAX_DAMAGE, MAX_STACK_SIZE, MINIMUM_ATTACK_CHARGE,
30 OMINOUS_BOTTLE_AMPLIFIER, OminousBottleAmplifier, PIERCING_WEAPON, PiercingWeapon,
31 REPAIRABLE, STORED_ENCHANTMENTS, TOOL, Tool, UNBREAKABLE, WEAPON, WRITTEN_BOOK_CONTENT,
32 Weapon,
33 },
34 },
35 enchantment_effect::EnchantmentEffectComponent,
36 equipment::EquipmentSlot,
37 item_stack_template::ItemStackTemplate,
38 items::{Item, ItemRef},
39 vanilla_items,
40};
41
42#[derive(Debug, Clone, PartialEq)]
44pub struct ItemStack {
45 pub item: ItemRef,
47 pub count: i32,
49 patch: DataComponentPatch,
51}
52
53impl Default for ItemStack {
54 fn default() -> Self {
55 Self::empty()
56 }
57}
58
59impl ItemStack {
60 #[must_use]
62 pub fn empty() -> Self {
63 Self {
64 item: &vanilla_items::AIR,
65 count: 0,
66 patch: DataComponentPatch::new(),
67 }
68 }
69
70 #[must_use]
72 pub fn new(item: ItemRef) -> Self {
73 Self::with_count(item, 1)
74 }
75
76 #[must_use]
78 pub fn with_count(item: ItemRef, count: i32) -> Self {
79 Self {
80 item,
81 count,
82 patch: DataComponentPatch::new(),
83 }
84 }
85
86 #[must_use]
88 pub fn with_count_and_patch(item: ItemRef, count: i32, mut patch: DataComponentPatch) -> Self {
89 patch.sanitize_against(&item.components);
90 Self { item, count, patch }
91 }
92
93 #[must_use]
94 const fn prototype(&self) -> &'static DataComponentMap {
95 &self.item.components
96 }
97
98 #[must_use]
99 pub fn is_empty(&self) -> bool {
100 self.item == &*vanilla_items::AIR || self.count <= 0
101 }
102
103 #[must_use]
104 pub fn item(&self) -> ItemRef {
105 if self.is_empty() {
106 &vanilla_items::AIR
107 } else {
108 self.item
109 }
110 }
111
112 #[must_use]
113 pub fn count(&self) -> i32 {
114 if self.is_empty() { 0 } else { self.count }
115 }
116
117 #[must_use]
119 pub fn components_patch(&self) -> &DataComponentPatch {
120 if self.is_empty() {
121 DataComponentPatch::empty()
122 } else {
123 &self.patch
124 }
125 }
126
127 pub const fn set_count(&mut self, count: i32) {
128 self.count = count;
129 }
130
131 pub const fn grow(&mut self, amount: i32) {
133 self.count += amount;
134 }
135
136 pub const fn shrink(&mut self, amount: i32) {
138 self.count -= amount;
139 }
140
141 pub const fn shrink_one(&mut self) {
143 self.count -= 1;
144 }
145
146 #[inline]
151 pub const fn consume(&mut self, amount: i32, has_infinite_materials: bool) {
152 if !has_infinite_materials {
153 self.shrink(amount);
154 }
155 }
156
157 #[inline]
159 pub const fn consume_one(&mut self, has_infinite_materials: bool) {
160 self.consume(1, has_infinite_materials);
161 }
162
163 #[inline]
165 #[must_use]
166 pub fn consume_and_return(&mut self, amount: i32, has_infinite_materials: bool) -> Self {
167 let split = self.copy_with_count(amount);
168 self.consume(amount, has_infinite_materials);
169 split
170 }
171
172 pub fn split(&mut self, amount: i32) -> Self {
177 let take = amount.min(self.count);
178 let result = Self {
179 item: self.item,
180 count: take,
181 patch: self.patch.clone(),
182 };
183 self.shrink(take);
184 result
185 }
186
187 #[must_use]
191 pub fn copy_with_count(&self, count: i32) -> Self {
192 if self.is_empty() {
193 Self::empty()
194 } else {
195 Self {
196 item: self.item,
197 count,
198 patch: self.patch.clone(),
199 }
200 }
201 }
202
203 #[must_use]
206 pub fn is_stackable(&self) -> bool {
207 self.max_stack_size() > 1 && (!self.is_damageable_item() || !self.is_damaged())
208 }
209
210 #[must_use]
212 pub fn is_damageable_item(&self) -> bool {
213 self.has(MAX_DAMAGE) && !self.has(UNBREAKABLE) && self.has(DAMAGE)
214 }
215
216 #[must_use]
218 pub fn is_damaged(&self) -> bool {
219 self.is_damageable_item() && self.get_damage_value() > 0
220 }
221
222 #[must_use]
224 pub fn get_damage_value(&self) -> i32 {
225 self.get(DAMAGE)
226 .copied()
227 .unwrap_or(0)
228 .clamp(0, self.get_max_damage())
229 }
230
231 pub fn set_damage_value(&mut self, value: i32) {
233 let clamped = value.clamp(0, self.get_max_damage());
234 self.set(DAMAGE, clamped);
235 }
236
237 #[must_use]
239 pub fn get_max_damage(&self) -> i32 {
240 self.get(MAX_DAMAGE).copied().unwrap_or(0)
241 }
242
243 #[must_use]
245 pub fn is_broken(&self) -> bool {
246 self.is_damageable_item() && self.get_damage_value() >= self.get_max_damage()
247 }
248
249 #[must_use]
250 pub fn next_damage_will_break(&self) -> bool {
251 self.is_damageable_item() && self.get_damage_value() >= self.get_max_damage() - 1
252 }
253
254 pub fn hurt_and_break(&mut self, amount: i32, has_infinite_materials: bool) -> bool {
258 let mut random = Xoroshiro::from_seed_unmixed(rand::rng().random());
259 self.hurt_and_break_with_random(amount, has_infinite_materials, &mut random)
260 }
261
262 pub fn hurt_and_break_with_random(
267 &mut self,
268 amount: i32,
269 has_infinite_materials: bool,
270 random: &mut impl Random,
271 ) -> bool {
272 if !self.is_damageable_item() {
273 return false;
274 }
275
276 if has_infinite_materials {
277 return false;
278 }
279
280 let effective_amount = self.process_durability_change(amount, random);
281
282 if effective_amount == 0 {
283 return false;
284 }
285
286 let new_damage = self.get_damage_value() + effective_amount;
287
288 self.set_damage_value(new_damage);
291
292 if self.is_broken() {
293 self.shrink_one();
297 return true;
298 }
299
300 false
301 }
302
303 fn process_durability_change(&self, amount: i32, random: &mut impl Random) -> i32 {
304 if amount <= 0 {
305 return amount;
306 }
307
308 let Some(enchantments) = self.get_enchantments() else {
309 return amount;
310 };
311
312 let mut value = amount as f32;
313 for (key, level) in enchantments.iter() {
314 if *level == 0 {
315 continue;
316 }
317 let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
318 continue;
319 };
320
321 for effect in enchantment.effects.item_damage {
322 if effect.requirements.is_some_and(|requirements| {
323 requirements.matches_item_context(self.item()) != Some(true)
324 }) {
325 continue;
326 }
327 value = effect
328 .effect
329 .process_with_random(*level as i32, random, value);
330 }
331 }
332
333 value as i32
334 }
335
336 #[must_use]
338 pub fn has<T: 'static>(&self, component: DataComponentType<T>) -> bool {
339 self.has_component(&component.key)
340 }
341
342 #[must_use]
346 pub fn has_component(&self, key: &Identifier) -> bool {
347 if self.is_empty() {
348 return false;
349 }
350 match self.patch.get_entry(key) {
351 Some(ComponentPatchEntry::Set(_)) => true,
352 Some(ComponentPatchEntry::Removed) => false,
353 None => self.prototype().get_raw(key).is_some(),
354 }
355 }
356
357 #[must_use]
358 pub fn is_same_item(a: &Self, b: &Self) -> bool {
359 a.item().key == b.item().key
360 }
361
362 #[must_use]
364 pub fn is_same_item_same_components(a: &Self, b: &Self) -> bool {
365 if !Self::is_same_item(a, b) {
366 return false;
367 }
368 if a.is_empty() && b.is_empty() {
369 return true;
370 }
371 a.components_equal(b)
372 }
373
374 #[must_use]
375 pub fn matches(a: &Self, b: &Self) -> bool {
376 a.count() == b.count() && Self::is_same_item_same_components(a, b)
377 }
378
379 #[must_use]
380 pub fn is(&self, item: ItemRef) -> bool {
381 self.item().key == item.key
382 }
383
384 #[must_use]
385 pub fn max_stack_size(&self) -> i32 {
386 self.get(MAX_STACK_SIZE).copied().unwrap_or(1)
387 }
388
389 pub fn validate_strict(&self) -> Result<()> {
391 let max_stack_size = self.max_stack_size();
392 if self.has(MAX_DAMAGE) && max_stack_size > 1 {
393 return Err(std::io::Error::other(
394 "Item cannot be both damageable and stackable",
395 ));
396 }
397
398 if let Some(container) = self.get(CONTAINER) {
399 validate_contained_item_sizes(container.items().iter().flatten())?;
400 }
401
402 if let Some(bundle) = self.get(BUNDLE_CONTENTS) {
403 validate_contained_item_sizes(bundle.items())?;
404 bundle.validate_weight()?;
405 }
406
407 if let Some(projectiles) = self.get(CHARGED_PROJECTILES) {
408 validate_contained_item_sizes(projectiles.items())?;
409 }
410
411 if self.count > max_stack_size {
412 return Err(std::io::Error::other(format!(
413 "Item stack with stack size of {} was larger than maximum: {max_stack_size}",
414 self.count
415 )));
416 }
417 Ok(())
418 }
419
420 #[must_use]
422 pub fn get_equippable(&self) -> Option<&Equippable> {
423 self.get(EQUIPPABLE)
424 }
425
426 #[must_use]
428 pub fn get_attribute_modifiers(&self) -> Option<&ItemAttributeModifiers> {
429 self.get(ATTRIBUTE_MODIFIERS)
430 }
431
432 #[must_use]
434 pub fn get_equippable_slot(&self) -> Option<EquipmentSlot> {
435 self.get_equippable().map(|e| e.slot)
436 }
437
438 #[must_use]
440 pub fn is_equippable_in_slot(&self, slot: EquipmentSlot) -> bool {
441 self.get_equippable_slot() == Some(slot)
442 }
443
444 #[must_use]
446 pub fn get_effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData> {
447 if self.is_empty() {
448 return None;
449 }
450 match self.patch.get_entry(key) {
451 Some(ComponentPatchEntry::Set(data)) => Some(data),
452 Some(ComponentPatchEntry::Removed) => None,
453 None => self.prototype().get_raw(key),
454 }
455 }
456
457 #[must_use]
460 pub fn get<T: Component + DowncastType>(&self, component: DataComponentType<T>) -> Option<&T> {
461 let data = self.get_effective_value_raw(&component.key)?;
462 data.downcast_ref::<T>()
463 }
464
465 #[must_use]
467 pub fn get_or_default<T: Component + DowncastType + Clone>(
468 &self,
469 component: DataComponentType<T>,
470 default: T,
471 ) -> T {
472 self.get(component).cloned().unwrap_or(default)
473 }
474
475 pub fn set<T: Component + DowncastType>(&mut self, component: DataComponentType<T>, value: T) {
477 let value = ComponentData::new(value);
478 let is_default = self.prototype().get_raw(&component.key) == Some(&value);
479 if is_default {
480 self.patch.clear(component);
481 } else {
482 self.patch.set_component_data(component.key, value);
483 }
484 }
485
486 pub fn remove<T: 'static>(&mut self, component: DataComponentType<T>) {
489 if self.prototype().get_raw(&component.key).is_some() {
490 self.patch.remove(component);
491 } else {
492 self.patch.clear(component);
493 }
494 }
495
496 pub fn clear<T: 'static>(&mut self, component: DataComponentType<T>) {
499 self.patch.clear(component);
500 }
501
502 #[must_use]
504 pub fn patch(&self) -> &DataComponentPatch {
505 self.components_patch()
506 }
507
508 #[must_use]
510 pub fn get_tool(&self) -> Option<&Tool> {
511 self.get(TOOL)
512 }
513
514 #[must_use]
516 pub fn get_weapon(&self) -> Option<&Weapon> {
517 self.get(WEAPON)
518 }
519
520 #[must_use]
522 pub fn get_attack_range(&self) -> Option<&AttackRange> {
523 self.get(ATTACK_RANGE)
524 }
525
526 #[must_use]
528 pub fn minimum_attack_charge(&self) -> f32 {
529 self.get(MINIMUM_ATTACK_CHARGE).copied().unwrap_or(0.0)
530 }
531
532 #[must_use]
534 pub fn get_damage_type(&self) -> Option<DamageTypeRef> {
535 self.get(DAMAGE_TYPE).map(|component| component.damage_type)
536 }
537
538 #[must_use]
540 pub fn can_be_hurt_by(&self, damage_type: DamageTypeRef) -> bool {
541 self.get(DAMAGE_RESISTANT)
542 .is_none_or(|resistance| !resistance.is_resistant_to(damage_type))
543 }
544
545 #[must_use]
546 pub fn is_valid_repair_item(&self, repair_item: &Self) -> bool {
547 self.get(REPAIRABLE)
548 .is_some_and(|repairable| repairable.is_valid_repair_item(repair_item))
549 }
550
551 #[must_use]
553 pub fn is_piercing_weapon(&self) -> bool {
554 self.has(PIERCING_WEAPON)
555 }
556
557 #[must_use]
559 pub fn get_piercing_weapon(&self) -> Option<&PiercingWeapon> {
560 self.get(PIERCING_WEAPON)
561 }
562
563 #[must_use]
566 pub fn get_destroy_speed(&self, block_state_id: steel_utils::BlockStateId) -> f32 {
567 self.get_tool()
568 .map_or(1.0, |tool| tool.get_mining_speed(block_state_id))
569 }
570
571 #[must_use]
573 pub fn is_correct_tool_for_drops(&self, block_state_id: steel_utils::BlockStateId) -> bool {
574 self.get_tool()
575 .is_some_and(|tool| tool.is_correct_for_drops(block_state_id))
576 }
577
578 #[must_use]
581 pub fn get_tool_damage_per_block(&self) -> i32 {
582 self.get_tool().map_or(0, |tool| tool.damage_per_block)
583 }
584
585 #[must_use]
588 pub fn can_destroy_blocks_in_creative(&self) -> bool {
589 self.get_tool()
590 .is_none_or(|tool| tool.can_destroy_blocks_in_creative)
591 }
592
593 #[must_use]
594 pub fn get_enchantment_level(&self, enchantment: &Identifier) -> i32 {
595 self.get_enchantments()
596 .map_or(0, |e| e.get_level(enchantment) as i32)
597 }
598
599 #[must_use]
600 pub fn get_enchantments(&self) -> Option<&ItemEnchantments> {
601 self.get(ENCHANTMENTS)
602 }
603
604 #[must_use]
608 pub fn get_enchantments_for_crafting(&self) -> Option<&ItemEnchantments> {
609 self.get(self.enchantment_component())
610 }
611
612 #[must_use]
613 fn enchantment_component(&self) -> DataComponentType<ItemEnchantments> {
614 if self.is(&vanilla_items::ENCHANTED_BOOK) {
615 STORED_ENCHANTMENTS
616 } else {
617 ENCHANTMENTS
618 }
619 }
620
621 #[must_use]
623 pub fn is_enchantable(&self) -> bool {
624 self.has(ENCHANTABLE)
625 && self
626 .get(ENCHANTMENTS)
627 .is_some_and(ItemEnchantments::is_empty)
628 }
629
630 #[must_use]
632 pub fn is_enchanted(&self) -> bool {
633 self.get_enchantments().is_some_and(|e| !e.is_empty())
634 }
635
636 #[must_use]
637 pub fn has_enchantment_effect(&self, component: EnchantmentEffectComponent) -> bool {
638 let Some(enchantments) = self.get_enchantments() else {
639 return false;
640 };
641
642 for (key, level) in enchantments.iter() {
643 if *level == 0 {
644 continue;
645 }
646 let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
647 continue;
648 };
649 if enchantment.effects.has(component) {
650 return true;
651 }
652 }
653
654 false
655 }
656
657 #[must_use]
658 pub fn apply_unconditional_enchantment_value_effects(
659 &self,
660 component: EnchantmentEffectComponent,
661 input: f32,
662 ) -> f32 {
663 let Some(enchantments) = self.get_enchantments() else {
664 return input;
665 };
666
667 let mut value = input;
668 for (key, level) in enchantments.iter() {
669 if *level == 0 {
670 continue;
671 }
672 let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
673 continue;
674 };
675 let level = *level as i32;
676
677 for effect in enchantment.effects.value_effects(component) {
678 if !effect.is_unconditional() {
679 continue;
680 }
681 if let Some(updated) = effect.effect.process_without_random(level, value) {
682 value = updated;
683 }
684 }
685
686 let Some(effect) = enchantment.effects.single_value_effect(component) else {
687 continue;
688 };
689 if let Some(updated) = effect.process_without_random(level, value) {
690 value = updated;
691 }
692 }
693
694 value
695 }
696
697 pub const fn set_damage_fraction(&mut self, _fraction: f32, _add: bool) {
700 }
705
706 pub const fn enchant_randomly<R: rand::Rng>(
708 &mut self,
709 _options: &crate::loot_table::EnchantmentOptions,
710 _rng: &mut R,
711 ) {
712 }
719
720 pub const fn enchant_with_levels<R: rand::Rng>(
722 &mut self,
723 _level: i32,
724 _options: &crate::loot_table::EnchantmentOptions,
725 _rng: &mut R,
726 ) {
727 }
734
735 pub const fn copy_components<R: rand::Rng>(
737 &mut self,
738 _source: crate::loot_table::CopySource,
739 _include: &[Identifier],
740 _ctx: &crate::loot_table::LootContext<'_, R>,
741 ) {
742 }
746
747 pub const fn copy_block_state<R: rand::Rng>(
749 &mut self,
750 _block: &Identifier,
751 _properties: &[&str],
752 _ctx: &crate::loot_table::LootContext<'_, R>,
753 ) {
754 }
758
759 pub const fn set_components_from_json(&mut self, _components: &str) {
761 }
764
765 pub fn set_custom_data(&mut self, value: &CustomData) {
767 let merged = self
768 .get(CUSTOM_DATA)
769 .cloned()
770 .unwrap_or_default()
771 .merged_with(value);
772 if merged.is_empty() {
773 self.remove(CUSTOM_DATA);
774 } else {
775 self.set(CUSTOM_DATA, merged);
776 }
777 }
778
779 pub fn apply_furnace_smelt(&mut self, use_input_count: bool) {
781 let input = crate::recipe::SingleItemRecipeInput::new(self.copy_with_count(self.count()));
782 let Some(recipe) = REGISTRY
783 .recipes
784 .find_match(&crate::recipe::vanilla_recipe_types::SMELTING, &input)
785 else {
786 return;
787 };
788 let count = if use_input_count { self.count() } else { 1 };
789 let mut result = recipe.data().result.create();
790 result.set_count(
791 count
792 .saturating_mul(result.count())
793 .min(result.max_stack_size()),
794 );
795 *self = result;
796 }
797
798 pub const fn create_exploration_map(
800 &mut self,
801 _destination: &Identifier,
802 _decoration: &Identifier,
803 _zoom: i32,
804 _skip_existing_chunks: bool,
805 ) {
806 }
812
813 pub const fn set_name(&mut self, _name: &str, _target: crate::loot_table::NameTarget) {
815 }
818
819 pub fn set_ominous_bottle_amplifier(&mut self, amplifier: i32) {
821 self.set(
822 OMINOUS_BOTTLE_AMPLIFIER,
823 OminousBottleAmplifier::new(amplifier),
824 );
825 }
826
827 pub const fn set_potion(&mut self, _id: &Identifier) {
829 }
832
833 pub const fn set_stew_effects<R: rand::Rng>(
835 &mut self,
836 _effects: &[crate::loot_table::StewEffect],
837 _rng: &mut R,
838 ) {
839 }
843
844 pub fn set_enchantments(&mut self, enchantments: &[(Identifier, u32)], add: bool) {
845 let mut current = self
846 .get(self.enchantment_component())
847 .cloned()
848 .unwrap_or_else(ItemEnchantments::empty);
849
850 for (key, level) in enchantments {
851 if add {
852 let existing = current.get_level(key);
853 current.set(key.clone(), existing + *level);
854 } else {
855 current.set(key.clone(), *level);
856 }
857 }
858
859 self.set(self.enchantment_component(), current);
860 }
861
862 pub fn upgrade_enchantment(&mut self, enchantment: Identifier, level: u32) {
864 let mut current = self
865 .get(self.enchantment_component())
866 .cloned()
867 .unwrap_or_else(ItemEnchantments::empty);
868 current.upgrade(enchantment, level);
869 self.set(self.enchantment_component(), current);
870 }
871
872 pub fn set_item(&mut self, new_item: &Identifier) {
874 if let Some(item_ref) = REGISTRY.items.by_key(new_item) {
875 self.item = item_ref;
876 self.patch.sanitize_against(&item_ref.components);
877 }
878 }
879
880 pub const fn copy_name<R: rand::Rng>(
882 &mut self,
883 _source: crate::loot_table::CopySource,
884 _ctx: &crate::loot_table::LootContext<'_, R>,
885 ) {
886 }
890
891 pub const fn set_lore(&mut self, _lore: &[&str], _mode: crate::loot_table::ListOperation) {
893 }
897
898 pub const fn set_contents<R: rand::Rng>(
900 &mut self,
901 _entries: &[crate::loot_table::LootEntry],
902 _component_type: &Identifier,
903 _ctx: &mut crate::loot_table::LootContext<'_, R>,
904 ) {
905 }
908
909 pub const fn modify_contents<R: rand::Rng>(
911 &mut self,
912 _modifier: &[crate::loot_table::ConditionalLootFunction],
913 _component_type: &Identifier,
914 _ctx: &mut crate::loot_table::LootContext<'_, R>,
915 ) {
916 }
919
920 pub const fn set_loot_table(&mut self, _loot_table: &Identifier, _seed: Option<i64>) {
922 }
925
926 pub const fn set_attributes<R: rand::Rng>(
928 &mut self,
929 _modifiers: &[crate::loot_table::AttributeModifier],
930 _replace: bool,
931 _rng: &mut R,
932 ) {
933 }
936
937 pub const fn fill_player_head<R: rand::Rng>(
939 &mut self,
940 _entity: crate::loot_table::LootContextEntity,
941 _ctx: &crate::loot_table::LootContext<'_, R>,
942 ) {
943 }
946
947 pub const fn copy_custom_data<R: rand::Rng>(
949 &mut self,
950 _source: crate::loot_table::CopySource,
951 _operations: &[crate::loot_table::CopyDataOperation],
952 _ctx: &crate::loot_table::LootContext<'_, R>,
953 ) {
954 }
957
958 pub const fn set_banner_pattern(
960 &mut self,
961 _patterns: &[crate::loot_table::BannerPattern],
962 _append: bool,
963 ) {
964 }
967
968 pub const fn set_fireworks(
970 &mut self,
971 _explosions: Option<&[crate::loot_table::FireworkExplosion]>,
972 _flight_duration: Option<i32>,
973 ) {
974 }
977
978 pub const fn set_firework_explosion(
980 &mut self,
981 _explosion: &crate::loot_table::FireworkExplosion,
982 ) {
983 }
986
987 pub const fn set_book_cover(
989 &mut self,
990 _title: Option<&str>,
991 _author: Option<&str>,
992 _generation: Option<i32>,
993 ) {
994 }
997
998 pub const fn set_written_book_pages(
1000 &mut self,
1001 _pages: &[&str],
1002 _mode: crate::loot_table::ListOperation,
1003 ) {
1004 }
1007
1008 pub const fn set_writable_book_pages(
1010 &mut self,
1011 _pages: &[&str],
1012 _mode: crate::loot_table::ListOperation,
1013 ) {
1014 }
1017
1018 pub fn toggle_tooltips(&mut self, toggles: &[(Identifier, bool)]) {
1020 use crate::data_components::vanilla_components::{TOOLTIP_DISPLAY, TooltipDisplay};
1021
1022 let mut display = self
1023 .get(TOOLTIP_DISPLAY)
1024 .cloned()
1025 .unwrap_or(TooltipDisplay::DEFAULT);
1026 for (component, shown) in toggles {
1027 display = display.with_hidden_key(component.clone(), !shown);
1028 }
1029 self.set(TOOLTIP_DISPLAY, display);
1030 }
1031
1032 #[must_use]
1033 pub fn components_equal(&self, other: &Self) -> bool {
1034 let mut all_keys = rustc_hash::FxHashSet::default();
1035
1036 for key in self.prototype().keys() {
1037 if !self.patch.is_removed(key) {
1038 all_keys.insert(key);
1039 }
1040 }
1041 for (key, entry) in self.patch.iter() {
1042 if matches!(entry, ComponentPatchEntry::Set(_)) {
1043 all_keys.insert(key);
1044 }
1045 }
1046 for key in other.prototype().keys() {
1047 if !other.patch.is_removed(key) {
1048 all_keys.insert(key);
1049 }
1050 }
1051 for (key, entry) in other.patch.iter() {
1052 if matches!(entry, ComponentPatchEntry::Set(_)) {
1053 all_keys.insert(key);
1054 }
1055 }
1056 for key in all_keys {
1057 let val_a = self.get_effective_value_raw(key);
1058 let val_b = other.get_effective_value_raw(key);
1059
1060 match (val_a, val_b) {
1061 (Some(a), Some(b)) => {
1062 if a != b {
1063 return false;
1064 }
1065 }
1066 (None, None) => {}
1067 _ => return false,
1068 }
1069 }
1070
1071 true
1072 }
1073
1074 #[must_use]
1077 pub fn custom_name(&self) -> Option<Cow<'_, TextComponent>> {
1078 if let Some(name) = self.get(CUSTOM_NAME) {
1079 return Some(Cow::Borrowed(name));
1080 }
1081
1082 let title = self.get(WRITTEN_BOOK_CONTENT)?.title().raw();
1083 (!java::is_blank(title)).then(|| Cow::Owned(TextComponent::plain(title.to_owned())))
1084 }
1085
1086 #[must_use]
1093 pub fn custom_or_component_name(&self) -> Cow<'_, TextComponent> {
1094 self.custom_name()
1095 .or_else(|| self.get(ITEM_NAME).map(Cow::Borrowed))
1096 .unwrap_or(Cow::Borrowed(&EMPTY_NAME))
1097 }
1098}
1099
1100static EMPTY_NAME: TextComponent = TextComponent::new();
1101
1102fn validate_contained_item_sizes<'a>(
1103 items: impl IntoIterator<Item = &'a ItemStackTemplate>,
1104) -> Result<()> {
1105 for item in items {
1106 let max_stack_size = item.max_stack_size();
1107 if item.count() > max_stack_size {
1108 return Err(std::io::Error::other(format!(
1109 "Item stack with count of {} was larger than maximum: {max_stack_size}",
1110 item.count()
1111 )));
1112 }
1113 }
1114 Ok(())
1115}
1116
1117impl From<&ItemStack> for ItemStack {
1118 fn from(stack: &Self) -> Self {
1119 stack.to_owned()
1120 }
1121}
1122
1123impl From<ItemRef> for ItemStack {
1124 fn from(item: ItemRef) -> Self {
1125 Self::new(item)
1126 }
1127}
1128
1129impl From<&'static std::sync::LazyLock<Item>> for ItemStack {
1130 fn from(item: &'static std::sync::LazyLock<Item>) -> Self {
1131 Self::new(item)
1132 }
1133}
1134
1135impl std::fmt::Display for ItemStack {
1136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1137 if self.is_empty() {
1138 write!(f, "Empty")
1139 } else {
1140 write!(f, "{} {}", self.count, self.item.key)
1141 }
1142 }
1143}
1144
1145impl WriteTo for ItemStack {
1146 fn write(&self, writer: &mut impl Write) -> Result<()> {
1147 if self.is_empty() {
1148 VarInt(0).write(writer)?;
1149 } else {
1150 VarInt(self.count).write(writer)?;
1151 VarInt(self.item.id() as i32).write(writer)?;
1153 self.patch.write(writer)?;
1155 }
1156 Ok(())
1157 }
1158}
1159
1160impl ReadFrom for ItemStack {
1161 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
1162 let count = VarInt::read(data)?.0;
1163 if count <= 0 {
1164 return Ok(Self::empty());
1165 }
1166
1167 let item_id = VarInt::read(data)?.0;
1168 let item_id = usize::try_from(item_id)
1169 .map_err(|_| std::io::Error::other(format!("Negative item id: {item_id}")))?;
1170 let item = REGISTRY
1171 .items
1172 .by_id(item_id)
1173 .ok_or_else(|| std::io::Error::other(format!("Unknown item id: {item_id}")))?;
1174
1175 let patch = DataComponentPatch::read(data)?;
1177
1178 Ok(Self::with_count_and_patch(item, count, patch))
1179 }
1180}
1181
1182impl ItemStack {
1183 pub fn read_untrusted(data: &mut Cursor<&[u8]>) -> Result<Self> {
1188 let count = VarInt::read(data)?.0;
1189 if count <= 0 {
1190 return Ok(Self::empty());
1191 }
1192
1193 let item_id = VarInt::read(data)?.0;
1194 let item_id = usize::try_from(item_id)
1195 .map_err(|_| std::io::Error::other(format!("Negative item id: {item_id}")))?;
1196 let item = REGISTRY
1197 .items
1198 .by_id(item_id)
1199 .ok_or_else(|| std::io::Error::other(format!("Unknown item id: {item_id}")))?;
1200 let patch = DataComponentPatch::read_delimited(data)?;
1201
1202 let stack = Self::with_count_and_patch(item, count, patch);
1203 stack.validate_persistent_encoding()?;
1204 Ok(stack)
1205 }
1206}
1207
1208use simdnbt::{
1209 FromNbtTag, ToNbtTag,
1210 borrow::{NbtCompound as NbtCompoundView, NbtTag as BorrowedNbtTag},
1211 owned::NbtCompound,
1212};
1213use steel_utils::nbt::NbtNumeric as _;
1214
1215impl ToNbtTag for ItemStack {
1216 fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
1227 self.to_nbt_tag_ref()
1228 }
1229}
1230
1231impl ItemStack {
1232 pub fn validate_persistent_encoding(&self) -> Result<()> {
1239 if self.is_empty() {
1240 return Ok(());
1241 }
1242 if !(1..=99).contains(&self.count) {
1243 return Err(std::io::Error::other(format!(
1244 "Item stack count {} is outside the persistent range 1..=99",
1245 self.count
1246 )));
1247 }
1248 self.patch.try_to_nbt_tag_ref().map(|_| ())
1249 }
1250
1251 #[must_use]
1253 pub fn to_nbt_tag_ref(&self) -> simdnbt::owned::NbtTag {
1254 if self.is_empty() {
1255 return simdnbt::owned::NbtTag::Compound(NbtCompound::new());
1257 }
1258
1259 let mut compound = NbtCompound::new();
1260
1261 compound.insert("id", self.item.key.to_string());
1263
1264 compound.insert("count", self.count);
1265
1266 if !self.patch.is_empty() {
1268 compound.insert("components", self.patch.to_nbt_tag_ref());
1269 }
1270
1271 simdnbt::owned::NbtTag::Compound(compound)
1272 }
1273}
1274
1275impl FromNbtTag for ItemStack {
1276 fn from_nbt_tag(tag: BorrowedNbtTag) -> Option<Self> {
1287 let compound = tag.compound()?;
1288
1289 let id_str = compound.get("id")?.string()?.to_str();
1291 let id = id_str.parse::<Identifier>().ok()?;
1292
1293 let item = REGISTRY.items.by_key(&id)?;
1295
1296 let count = decode_persistent_count(compound.get("count"))?;
1297
1298 let patch = match compound.get("components") {
1299 Some(tag) => DataComponentPatch::from_nbt_tag(tag)?,
1300 None => DataComponentPatch::new(),
1301 };
1302
1303 Some(Self::with_count_and_patch(item, count, patch))
1304 }
1305}
1306
1307impl ItemStack {
1308 #[must_use]
1313 pub fn from_borrowed_compound(compound: &NbtCompoundView<'_, '_>) -> Option<Self> {
1314 let id_str = compound.string("id")?.to_str();
1316 let id = id_str.parse::<Identifier>().ok()?;
1317
1318 let item = REGISTRY.items.by_key(&id)?;
1320
1321 let count = decode_persistent_count(compound.get("count"))?;
1322
1323 let patch = match compound.get("components") {
1324 Some(tag) => DataComponentPatch::from_nbt_tag(tag)?,
1325 None => DataComponentPatch::new(),
1326 };
1327
1328 Some(Self::with_count_and_patch(item, count, patch))
1329 }
1330}
1331
1332fn decode_persistent_count(tag: Option<BorrowedNbtTag<'_, '_>>) -> Option<i32> {
1333 let count = match tag {
1334 Some(tag) => tag.codec_i32()?,
1335 None => 1,
1336 };
1337 (1..=99).contains(&count).then_some(count)
1338}
1339
1340#[cfg(test)]
1341mod consume_tests {
1342 use super::ItemStack;
1343 use crate::{init_vanilla_registry, vanilla_items};
1344
1345 #[test]
1346 fn consume_shrinks_only_without_infinite_materials() {
1347 init_vanilla_registry();
1348
1349 let mut survival = ItemStack::with_count(&vanilla_items::GLOWSTONE, 2);
1350 survival.consume_one(false);
1351 assert_eq!(survival.count(), 1);
1352
1353 let mut creative = ItemStack::with_count(&vanilla_items::GLOWSTONE, 2);
1354 creative.consume_one(true);
1355 assert_eq!(creative.count(), 2);
1356 }
1357
1358 #[test]
1359 fn consume_and_return_splits_before_shrinking() {
1360 init_vanilla_registry();
1361
1362 let mut survival = ItemStack::with_count(&vanilla_items::GLOWSTONE, 2);
1363 let taken = survival.consume_and_return(1, false);
1364 assert_eq!(taken.count(), 1);
1365 assert_eq!(survival.count(), 1);
1366
1367 let mut creative = ItemStack::with_count(&vanilla_items::GLOWSTONE, 2);
1368 let taken = creative.consume_and_return(1, true);
1369 assert_eq!(taken.count(), 1);
1370 assert_eq!(creative.count(), 2);
1371 }
1372}
1373
1374#[cfg(test)]
1375mod enchantment_tests {
1376 use super::ItemStack;
1377 use crate::{init_vanilla_registry, vanilla_enchantments, vanilla_items};
1378
1379 #[test]
1380 fn stored_book_enchantments_are_not_active_item_enchantments() {
1381 init_vanilla_registry();
1382 let mut book = ItemStack::new(&vanilla_items::ENCHANTED_BOOK);
1383 book.upgrade_enchantment(vanilla_enchantments::SHARPNESS.key.clone(), 3);
1384
1385 assert_eq!(
1386 book.get_enchantment_level(&vanilla_enchantments::SHARPNESS.key),
1387 0
1388 );
1389 assert_eq!(
1390 book.get_enchantments_for_crafting().map(|enchantments| {
1391 enchantments.get_level(&vanilla_enchantments::SHARPNESS.key)
1392 }),
1393 Some(3)
1394 );
1395 }
1396}
1397
1398#[cfg(test)]
1399mod name_tests {
1400 use text_components::TextComponent;
1401
1402 use super::ItemStack;
1403 use crate::data_components::components::{Filterable, WrittenBookContent};
1404 use crate::data_components::vanilla_components::{CUSTOM_NAME, WRITTEN_BOOK_CONTENT};
1405 use crate::init_vanilla_registry;
1406 use crate::vanilla_items;
1407
1408 fn written_book(raw_title: &str, filtered_title: Option<&str>) -> ItemStack {
1409 let content = WrittenBookContent::new(
1410 Filterable::new(raw_title.to_owned(), filtered_title.map(ToOwned::to_owned)),
1411 "Author".to_owned(),
1412 0,
1413 Vec::new(),
1414 true,
1415 );
1416 let Ok(content) = content else {
1417 panic!("test written-book content should be valid");
1418 };
1419 let mut book = ItemStack::new(&vanilla_items::WRITTEN_BOOK);
1420 book.set(WRITTEN_BOOK_CONTENT, content);
1421 book
1422 }
1423
1424 #[test]
1425 fn written_book_raw_title_is_its_custom_and_hover_name() {
1426 init_vanilla_registry();
1427 let book = written_book("Raw title", Some("Filtered title"));
1428 let expected = TextComponent::plain("Raw title");
1429
1430 assert_eq!(book.custom_name().as_deref(), Some(&expected));
1431 assert_eq!(book.custom_or_component_name().as_ref(), &expected);
1432 }
1433
1434 #[test]
1435 fn explicit_custom_name_takes_precedence_over_written_book_title() {
1436 init_vanilla_registry();
1437 let mut book = written_book("Book title", None);
1438 let explicit = TextComponent::plain("Explicit name");
1439 book.set(CUSTOM_NAME, explicit.clone());
1440
1441 assert_eq!(book.custom_name().as_deref(), Some(&explicit));
1442 assert_eq!(book.custom_or_component_name().as_ref(), &explicit);
1443 }
1444
1445 #[test]
1446 fn written_book_title_uses_java_blank_rules() {
1447 init_vanilla_registry();
1448 let blank = written_book("\u{00a0}\u{202f}", None);
1449 assert!(blank.custom_name().is_none());
1450 let Some(default_name) = blank.get(crate::data_components::vanilla_components::ITEM_NAME)
1451 else {
1452 panic!("written book should have a default item name");
1453 };
1454 assert_eq!(blank.custom_or_component_name().as_ref(), default_name);
1455
1456 let next_line = written_book("\u{0085}", None);
1457 assert_eq!(
1458 next_line.custom_name().as_deref(),
1459 Some(&TextComponent::plain("\u{0085}"))
1460 );
1461 }
1462}
1463
1464#[cfg(test)]
1465mod durability_tests {
1466 use steel_utils::random::xoroshiro::Xoroshiro;
1467
1468 use super::ItemStack;
1469 use crate::data_components::vanilla_components::{ENCHANTMENTS, ItemEnchantments};
1470 use crate::init_vanilla_registry;
1471 use crate::{vanilla_enchantments, vanilla_items};
1472
1473 fn with_unbreaking(item: crate::items::ItemRef, level: u32) -> ItemStack {
1474 let mut stack = ItemStack::new(item);
1475 let mut enchantments = ItemEnchantments::empty();
1476 enchantments.set(vanilla_enchantments::UNBREAKING.key.clone(), level);
1477 stack.set(ENCHANTMENTS, enchantments);
1478 stack
1479 }
1480
1481 #[test]
1482 fn item_damage_uses_generated_unbreaking_tool_requirements() {
1483 init_vanilla_registry();
1484 let mut armor = with_unbreaking(&vanilla_items::DIAMOND_CHESTPLATE, 3);
1485 let mut tool = with_unbreaking(&vanilla_items::DIAMOND_PICKAXE, 3);
1486 let mut armor_random = Xoroshiro::from_seed_unmixed(42);
1487 let mut tool_random = Xoroshiro::from_seed_unmixed(42);
1488
1489 assert!(!armor.hurt_and_break_with_random(100, false, &mut armor_random));
1490 assert!(!tool.hurt_and_break_with_random(100, false, &mut tool_random));
1491 assert_eq!(armor.get_damage_value(), 68);
1492 assert_eq!(tool.get_damage_value(), 18);
1493
1494 let effects = vanilla_enchantments::UNBREAKING.effects.item_damage;
1495 assert_eq!(
1496 effects[0].requirements.and_then(|requirements| {
1497 requirements.matches_item_context(&vanilla_items::DIAMOND_CHESTPLATE)
1498 }),
1499 Some(true)
1500 );
1501 assert_eq!(
1502 effects[1].requirements.and_then(|requirements| {
1503 requirements.matches_item_context(&vanilla_items::DIAMOND_CHESTPLATE)
1504 }),
1505 Some(false)
1506 );
1507 assert_eq!(
1508 effects[0].requirements.and_then(|requirements| {
1509 requirements.matches_item_context(&vanilla_items::DIAMOND_PICKAXE)
1510 }),
1511 Some(false)
1512 );
1513 assert_eq!(
1514 effects[1].requirements.and_then(|requirements| {
1515 requirements.matches_item_context(&vanilla_items::DIAMOND_PICKAXE)
1516 }),
1517 Some(true)
1518 );
1519 }
1520
1521 #[test]
1522 fn match_tool_supports_generated_direct_item_sets() {
1523 init_vanilla_registry();
1524 let requirements = vanilla_enchantments::INFINITY.effects.ammo_use[0]
1525 .requirements
1526 .expect("Infinity ammo use should have a match_tool requirement");
1527
1528 assert_eq!(
1529 requirements.matches_item_context(&vanilla_items::ARROW),
1530 Some(true)
1531 );
1532 assert_eq!(
1533 requirements.matches_item_context(&vanilla_items::SPECTRAL_ARROW),
1534 Some(false)
1535 );
1536 }
1537}
1538
1539#[cfg(test)]
1540mod persistence_tests {
1541 use std::io::Cursor;
1542
1543 use simdnbt::FromNbtTag;
1544 use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
1545 use simdnbt::owned::{NbtCompound, NbtTag};
1546 use steel_utils::codec::VarInt;
1547 use steel_utils::serial::WriteTo;
1548
1549 use super::ItemStack;
1550 use crate::data_components::vanilla_components::{
1551 CUSTOM_DATA, DEATH_PROTECTION, JUKEBOX_PLAYABLE, LORE, MAX_DAMAGE, MAX_STACK_SIZE,
1552 TOOLTIP_DISPLAY,
1553 };
1554 use crate::data_components::{CustomData, JukeboxPlayable};
1555 use crate::init_vanilla_registry;
1556 use crate::{REGISTRY, RegistryEntry, RegistryExt, vanilla_items, vanilla_jukebox_songs};
1557
1558 fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
1559 let mut bytes = Vec::new();
1560 tag.write(&mut bytes);
1561 let borrowed =
1562 read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
1563 visitor(borrowed.as_tag())
1564 }
1565
1566 fn parse_stack(compound: NbtCompound) -> Option<ItemStack> {
1567 with_borrowed_tag(NbtTag::Compound(compound), ItemStack::from_nbt_tag)
1568 }
1569
1570 fn stone_stack_nbt() -> NbtCompound {
1571 let mut compound = NbtCompound::new();
1572 compound.insert("id", "minecraft:stone");
1573 compound
1574 }
1575
1576 fn untrusted_stack_bytes(
1577 count: i32,
1578 component: Option<(&steel_utils::Identifier, Vec<u8>)>,
1579 ) -> Vec<u8> {
1580 let mut bytes = Vec::new();
1581 VarInt(count)
1582 .write(&mut bytes)
1583 .expect("test stack count should encode");
1584 VarInt(vanilla_items::STONE.id() as i32)
1585 .write(&mut bytes)
1586 .expect("test item id should encode");
1587
1588 if let Some((component, value)) = component {
1589 VarInt(1)
1590 .write(&mut bytes)
1591 .expect("added component count should encode");
1592 VarInt(0)
1593 .write(&mut bytes)
1594 .expect("removed component count should encode");
1595 let component_id = REGISTRY
1596 .data_components
1597 .id_from_key(component)
1598 .expect("test component should be registered");
1599 VarInt(component_id as i32)
1600 .write(&mut bytes)
1601 .expect("component id should encode");
1602 VarInt(value.len() as i32)
1603 .write(&mut bytes)
1604 .expect("component length should encode");
1605 bytes.extend_from_slice(&value);
1606 } else {
1607 VarInt(0)
1608 .write(&mut bytes)
1609 .expect("added component count should encode");
1610 VarInt(0)
1611 .write(&mut bytes)
1612 .expect("removed component count should encode");
1613 }
1614 bytes
1615 }
1616
1617 #[test]
1618 fn persistent_item_count_uses_vanilla_integer_codec() {
1619 init_vanilla_registry();
1620 let mut compound = stone_stack_nbt();
1621 compound.insert("count", 5.9_f64);
1622 assert_eq!(parse_stack(compound).map(|stack| stack.count()), Some(5));
1623
1624 let mut compound = stone_stack_nbt();
1625 compound.insert("count", 100);
1626 assert!(parse_stack(compound).is_none());
1627
1628 let mut compound = stone_stack_nbt();
1629 compound.insert("count", "5");
1630 assert!(parse_stack(compound).is_none());
1631 }
1632
1633 #[test]
1634 fn malformed_present_component_patch_rejects_the_item_stack() {
1635 init_vanilla_registry();
1636 let mut components = NbtCompound::new();
1637 components.insert("minecraft:max_stack_size", 0);
1638 let mut compound = stone_stack_nbt();
1639 compound.insert("components", components);
1640
1641 assert!(parse_stack(compound).is_none());
1642 }
1643
1644 #[test]
1645 fn shrunk_to_empty_stack_exposes_no_components() {
1646 init_vanilla_registry();
1647 let mut totem = ItemStack::new(&vanilla_items::TOTEM_OF_UNDYING);
1648 assert!(totem.has(DEATH_PROTECTION));
1649
1650 totem.shrink(1);
1651
1652 assert!(!totem.has(DEATH_PROTECTION));
1653 assert!(totem.get(DEATH_PROTECTION).is_none());
1654 }
1655
1656 #[test]
1657 fn component_patches_stay_sanitized_against_the_item_prototype() {
1658 init_vanilla_registry();
1659 let mut patch = crate::data_components::DataComponentPatch::new();
1660 patch.set(MAX_STACK_SIZE, 64);
1661 patch.remove(CUSTOM_DATA);
1662 let mut stack = ItemStack::with_count_and_patch(&vanilla_items::STONE, 1, patch);
1663 assert!(stack.components_patch().is_empty());
1664
1665 stack.set(MAX_STACK_SIZE, 16);
1666 assert_eq!(stack.components_patch().len(), 1);
1667 stack.set(MAX_STACK_SIZE, 64);
1668 assert!(stack.components_patch().is_empty());
1669
1670 stack.remove(MAX_STACK_SIZE);
1671 assert!(stack.components_patch().is_removed(&MAX_STACK_SIZE.key));
1672 stack.set(MAX_STACK_SIZE, 64);
1673 assert!(stack.components_patch().is_empty());
1674
1675 stack.remove(CUSTOM_DATA);
1676 assert!(stack.components_patch().is_empty());
1677
1678 stack.set(MAX_STACK_SIZE, 16);
1679 stack.set_item(&vanilla_items::ENDER_PEARL.key);
1680 assert_eq!(stack.max_stack_size(), 16);
1681 assert!(stack.components_patch().is_empty());
1682 }
1683
1684 #[test]
1685 fn strict_validation_ignores_components_of_empty_stacks() {
1686 init_vanilla_registry();
1687 let mut patch = crate::data_components::DataComponentPatch::new();
1688 patch.set(MAX_DAMAGE, 1);
1689 let stack = ItemStack::with_count_and_patch(&vanilla_items::STONE, 0, patch);
1690
1691 assert!(stack.is_empty());
1692 assert!(stack.validate_strict().is_ok());
1693 }
1694
1695 #[test]
1696 fn default_count_is_always_present_in_persistent_encoding() {
1697 init_vanilla_registry();
1698 let stack = ItemStack::new(&vanilla_items::STONE);
1699 let NbtTag::Compound(compound) = stack.to_nbt_tag_ref() else {
1700 panic!("item stack should encode as a compound");
1701 };
1702
1703 assert_eq!(compound.get("count"), Some(&NbtTag::Int(1)));
1704 }
1705
1706 #[test]
1707 fn untrusted_stack_rejects_direct_jukebox_holders() {
1708 init_vanilla_registry();
1709 let mut component_bytes = Vec::new();
1710 VarInt(0)
1711 .write(&mut component_bytes)
1712 .expect("direct holder discriminator should encode");
1713 let bytes = untrusted_stack_bytes(1, Some((&JUKEBOX_PLAYABLE.key, component_bytes)));
1714
1715 assert!(ItemStack::read_untrusted(&mut Cursor::new(bytes.as_slice())).is_err());
1716 }
1717
1718 #[test]
1719 fn untrusted_stack_accepts_persistable_registry_holders() {
1720 init_vanilla_registry();
1721 let reference = JukeboxPlayable::new(&vanilla_jukebox_songs::CAT);
1722 let mut component_bytes = Vec::new();
1723 reference
1724 .write(&mut component_bytes)
1725 .expect("registry holder should have a network representation");
1726 let bytes = untrusted_stack_bytes(1, Some((&JUKEBOX_PLAYABLE.key, component_bytes)));
1727
1728 let stack = ItemStack::read_untrusted(&mut Cursor::new(bytes.as_slice()))
1729 .expect("persistable untrusted stack should decode");
1730 assert_eq!(stack.get(JUKEBOX_PLAYABLE), Some(&reference));
1731 }
1732
1733 #[test]
1734 fn untrusted_stack_uses_persistent_count_range() {
1735 init_vanilla_registry();
1736 let bytes = untrusted_stack_bytes(100, None);
1737
1738 assert!(ItemStack::read_untrusted(&mut Cursor::new(bytes.as_slice())).is_err());
1739 }
1740
1741 #[test]
1742 fn untrusted_stack_validates_component_persistent_constraints() {
1743 init_vanilla_registry();
1744 let mut component_bytes = Vec::new();
1745 VarInt(0)
1746 .write(&mut component_bytes)
1747 .expect("max stack size should encode on the network");
1748 let bytes = untrusted_stack_bytes(
1749 1,
1750 Some((
1751 &crate::data_components::vanilla_components::MAX_STACK_SIZE.key,
1752 component_bytes,
1753 )),
1754 );
1755
1756 assert!(ItemStack::read_untrusted(&mut Cursor::new(bytes.as_slice())).is_err());
1757 }
1758
1759 #[test]
1760 fn save_omits_invalid_component_value_but_keeps_present_patch_field() {
1761 init_vanilla_registry();
1762 let mut stack = ItemStack::new(&vanilla_items::STONE);
1763 stack.set(MAX_STACK_SIZE, 0);
1764
1765 assert!(stack.validate_persistent_encoding().is_err());
1766 let NbtTag::Compound(compound) = stack.to_nbt_tag_ref() else {
1767 panic!("item stack should still encode as a compound");
1768 };
1769 assert_eq!(
1770 compound.string("id").map(|value| value.to_str()),
1771 Some("minecraft:stone".into())
1772 );
1773 assert!(
1774 compound
1775 .compound("components")
1776 .is_some_and(simdnbt::owned::NbtCompound::is_empty)
1777 );
1778 }
1779
1780 #[test]
1781 fn toggle_tooltips_updates_the_typed_display_component() {
1782 init_vanilla_registry();
1783 let mut stack = ItemStack::new(&vanilla_items::STONE);
1784
1785 stack.toggle_tooltips(&[(LORE.key, false)]);
1786 let display = stack
1787 .get(TOOLTIP_DISPLAY)
1788 .expect("tooltip display should be set");
1789 assert!(!display.shows(LORE));
1790
1791 stack.toggle_tooltips(&[(LORE.key, true)]);
1792 assert!(
1793 stack
1794 .get(TOOLTIP_DISPLAY)
1795 .expect("tooltip display should remain set")
1796 .shows(LORE)
1797 );
1798 }
1799
1800 #[test]
1801 fn set_custom_data_recursively_merges_and_removes_empty_values() {
1802 init_vanilla_registry();
1803 let mut stack = ItemStack::new(&vanilla_items::STONE);
1804 let empty = CustomData::default();
1805 stack.set_custom_data(&empty);
1806 assert!(stack.get(CUSTOM_DATA).is_none());
1807
1808 let mut nested = NbtCompound::new();
1809 nested.insert("kept", 1);
1810 nested.insert("changed", 1);
1811 let mut first = NbtCompound::new();
1812 first.insert("nested", nested);
1813 stack.set_custom_data(
1814 &CustomData::try_from_compound(first).expect("first value should be valid"),
1815 );
1816
1817 let mut nested = NbtCompound::new();
1818 nested.insert("changed", 2);
1819 let mut second = NbtCompound::new();
1820 second.insert("nested", nested);
1821 stack.set_custom_data(
1822 &CustomData::try_from_compound(second).expect("second value should be valid"),
1823 );
1824
1825 let nested = stack
1826 .get(CUSTOM_DATA)
1827 .and_then(|data| data.as_compound().compound("nested"))
1828 .expect("nested custom data should remain");
1829 assert_eq!(nested.int("kept"), Some(1));
1830 assert_eq!(nested.int("changed"), Some(2));
1831 }
1832}