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