Skip to main content

steel_registry/
item_stack.rs

1//! Item stack implementation.
2
3use 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/// A stack of items with a count and component modifications.
43#[derive(Debug, Clone, PartialEq)]
44pub struct ItemStack {
45    /// The item type. AIR represents an empty stack.
46    pub item: ItemRef,
47    /// The number of items in this stack.
48    pub count: i32,
49    /// Modifications to the prototype components.
50    patch: DataComponentPatch,
51}
52
53impl Default for ItemStack {
54    fn default() -> Self {
55        Self::empty()
56    }
57}
58
59impl ItemStack {
60    /// Creates an empty item stack (using AIR).
61    #[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    /// Creates a new item stack with count 1.
71    #[must_use]
72    pub fn new(item: ItemRef) -> Self {
73        Self::with_count(item, 1)
74    }
75
76    /// Creates a new item stack with the specified count.
77    #[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    /// Creates a new item stack with the specified count and component patch.
87    #[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    /// Returns the component patch. Empty stacks have an empty patch.
118    #[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    /// Increases the count by the given amount.
132    pub const fn grow(&mut self, amount: i32) {
133        self.count += amount;
134    }
135
136    /// Decreases the count by the given amount.
137    pub const fn shrink(&mut self, amount: i32) {
138        self.count -= amount;
139    }
140
141    /// Decreases the count by one.
142    pub const fn shrink_one(&mut self) {
143        self.count -= 1;
144    }
145
146    /// Decreases the count by the given amount, unless the user has infinite
147    /// materials.
148    ///
149    /// See: `Player::has_infinite_materials`
150    #[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    /// [Consumes](Self::consume) a single item from this stack.
158    #[inline]
159    pub const fn consume_one(&mut self, has_infinite_materials: bool) {
160        self.consume(1, has_infinite_materials);
161    }
162
163    /// Splits off the given amount and [consumes](Self::consume) it from this stack.
164    #[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    /// Splits off the specified amount from this stack and returns it as a new stack.
173    ///
174    /// If the amount is greater than or equal to the current count, this stack becomes
175    /// empty and the entire contents are returned.
176    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    /// Copies the identity (item type and patch) from another stack.
188    ///
189    /// Used when splitting stacks to preserve components.
190    #[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    /// Returns true if this item can stack (max stack size > 1 and not damaged).
204    /// Damaged items cannot stack.
205    #[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    /// Returns true if this item can take damage.
211    #[must_use]
212    pub fn is_damageable_item(&self) -> bool {
213        self.has(MAX_DAMAGE) && !self.has(UNBREAKABLE) && self.has(DAMAGE)
214    }
215
216    /// Returns true if this item has taken damage.
217    #[must_use]
218    pub fn is_damaged(&self) -> bool {
219        self.is_damageable_item() && self.get_damage_value() > 0
220    }
221
222    /// Gets the current damage value of this item.
223    #[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    /// Sets the damage value of this item.
232    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    /// Gets the maximum damage this item can take before breaking.
238    #[must_use]
239    pub fn get_max_damage(&self) -> i32 {
240        self.get(MAX_DAMAGE).copied().unwrap_or(0)
241    }
242
243    /// Returns true if the item is broken (damage >= max damage).
244    #[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    /// Damages the item and breaks it if durability reaches zero.
255    ///
256    /// Returns `true` if the item broke and should be removed/replaced.
257    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    /// Damages the item using the supplied random source for data-driven
263    /// `minecraft:item_damage` enchantment effects.
264    ///
265    /// Returns `true` if the item broke and should be removed/replaced.
266    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        // TODO: Trigger ITEM_DURABILITY_CHANGED advancement criteria
289
290        self.set_damage_value(new_damage);
291
292        if self.is_broken() {
293            // TODO: Call onEquippedItemBroken callback which:
294            // - Broadcasts entity event (byte 47 for mainhand) for break sound/particles
295            // - Stops location-based effects (removes attribute modifiers)
296            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    /// Returns true if this item has the specified component (by type).
337    #[must_use]
338    pub fn has<T: 'static>(&self, component: DataComponentType<T>) -> bool {
339        self.has_component(&component.key)
340    }
341
342    /// Returns true if this item has the specified component (by key).
343    ///
344    /// Empty stacks have no components.
345    #[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    /// Checks if two stacks have the same item and components.
363    #[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    /// Validates the complete stack constraints enforced by Vanilla's `ItemStack.validateStrict`.
390    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    /// Returns the equippable component if this item has one.
421    #[must_use]
422    pub fn get_equippable(&self) -> Option<&Equippable> {
423        self.get(EQUIPPABLE)
424    }
425
426    /// Returns the item attribute modifiers component.
427    #[must_use]
428    pub fn get_attribute_modifiers(&self) -> Option<&ItemAttributeModifiers> {
429        self.get(ATTRIBUTE_MODIFIERS)
430    }
431
432    /// Returns the equipment slot this item can be equipped to, if any.
433    #[must_use]
434    pub fn get_equippable_slot(&self) -> Option<EquipmentSlot> {
435        self.get_equippable().map(|e| e.slot)
436    }
437
438    /// Returns true if this item can be equipped in the given slot.
439    #[must_use]
440    pub fn is_equippable_in_slot(&self, slot: EquipmentSlot) -> bool {
441        self.get_equippable_slot() == Some(slot)
442    }
443
444    /// Gets the raw component data by key. Empty stacks have no components.
445    #[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    /// Gets the effective value of a component, considering the patch and prototype.
458    /// Returns `None` if the component is not present or has been removed.
459    #[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    /// Gets the effective value of a component, or returns the default value if not present.
466    #[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    /// Sets a component value in this item's patch, overriding the prototype.
476    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    /// Removes a component from this item (marks it as removed in the patch).
487    /// This will hide the component even if it exists in the prototype.
488    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    /// Clears any patch entry for this component (neither set nor removed).
497    /// The prototype value will be visible again.
498    pub fn clear<T: 'static>(&mut self, component: DataComponentType<T>) {
499        self.patch.clear(component);
500    }
501
502    /// Returns a reference to the component patch. Empty stacks have an empty patch.
503    #[must_use]
504    pub fn patch(&self) -> &DataComponentPatch {
505        self.components_patch()
506    }
507
508    /// Gets the Tool component if present.
509    #[must_use]
510    pub fn get_tool(&self) -> Option<&Tool> {
511        self.get(TOOL)
512    }
513
514    /// Gets the Weapon component if present.
515    #[must_use]
516    pub fn get_weapon(&self) -> Option<&Weapon> {
517        self.get(WEAPON)
518    }
519
520    /// Gets the `AttackRange` component if present.
521    #[must_use]
522    pub fn get_attack_range(&self) -> Option<&AttackRange> {
523        self.get(ATTACK_RANGE)
524    }
525
526    /// Returns vanilla `DataComponents.MINIMUM_ATTACK_CHARGE`, defaulting to 0.
527    #[must_use]
528    pub fn minimum_attack_charge(&self) -> f32 {
529        self.get(MINIMUM_ATTACK_CHARGE).copied().unwrap_or(0.0)
530    }
531
532    /// Gets the vanilla damage type component if present.
533    #[must_use]
534    pub fn get_damage_type(&self) -> Option<DamageTypeRef> {
535        self.get(DAMAGE_TYPE).map(|component| component.damage_type)
536    }
537
538    /// Returns vanilla `ItemStack.canBeHurtBy` for a damage type.
539    #[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    /// Returns whether this item has the vanilla piercing weapon component.
552    #[must_use]
553    pub fn is_piercing_weapon(&self) -> bool {
554        self.has(PIERCING_WEAPON)
555    }
556
557    /// Gets the `PiercingWeapon` component if present.
558    #[must_use]
559    pub fn get_piercing_weapon(&self) -> Option<&PiercingWeapon> {
560        self.get(PIERCING_WEAPON)
561    }
562
563    /// Returns the mining speed for the given block state ID.
564    /// If no Tool component is present, returns 1.0 (hand speed).
565    #[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    /// Returns true if this tool is correct for getting drops from the block.
572    #[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    /// Returns the damage per block for this tool (how much durability is consumed per block mined).
579    /// Returns 0 if no Tool component is present.
580    #[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    /// Returns true if this tool can destroy blocks in creative mode.
586    /// Returns true if no Tool component is present (default behavior).
587    #[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    /// Vanilla `EnchantmentHelper.getEnchantmentsForCrafting`: enchanted books
605    /// expose `STORED_ENCHANTMENTS` to crafting operations, while every other
606    /// item exposes `ENCHANTMENTS`.
607    #[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    /// Mirrors Vanilla's component-based `ItemStack.isEnchantable` check.
622    #[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    /// Checks if `ItemStack` has an enchantment, not including enchanted books
631    #[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    /// Sets the damage/durability as a fraction (0.0 = broken, 1.0 = full).
698    /// If `add` is true, adds to current damage instead of setting.
699    pub const fn set_damage_fraction(&mut self, _fraction: f32, _add: bool) {
700        // TODO: Implement when damage component system is ready
701        // let max_damage = self.get_max_damage();
702        // let damage_value = ((1.0 - fraction) * max_damage as f32) as i32;
703        // self.set_component(DAMAGE, damage_value);
704    }
705
706    /// Enchants this item randomly with enchantments from the given options.
707    pub const fn enchant_randomly<R: rand::Rng>(
708        &mut self,
709        _options: &crate::loot_table::EnchantmentOptions,
710        _rng: &mut R,
711    ) {
712        // TODO: Implement when enchantment registry and system are ready
713        // 1. Get list of valid enchantments from options (tag or list)
714        // 2. Filter to enchantments that can apply to this item
715        // 3. Pick one randomly
716        // 4. Pick a random level for that enchantment
717        // 5. Add to ENCHANTMENTS component
718    }
719
720    /// Enchants this item as if using an enchanting table at the given level.
721    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        // TODO: Implement when enchantment registry and system are ready
728        // This simulates the enchanting table algorithm:
729        // 1. Calculate modified level based on item enchantability
730        // 2. Generate list of possible enchantments for that level
731        // 3. Filter by options (tag or list)
732        // 4. Apply enchantments with proper weights
733    }
734
735    /// Copies components from a source (block entity, attacker, etc.) to this item.
736    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        // TODO: Implement when block entity system is ready
743        // 1. Get the source entity/block entity from context
744        // 2. For each component in `include`, copy it to this item's patch
745    }
746
747    /// Copies block state properties to this item (for blocks like `note_block`).
748    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        // TODO: Implement block state copying
755        // 1. Get block state from context
756        // 2. For each property, store it in the item's BLOCK_STATE component
757    }
758
759    /// Sets components from a JSON string representation.
760    pub const fn set_components_from_json(&mut self, _components: &str) {
761        // TODO: Implement component parsing from JSON
762        // Parse the JSON and set each component in the patch
763    }
764
765    /// Merges custom NBT data into this item's `custom_data` component.
766    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    /// Applies furnace smelting to convert this item (e.g., raw iron -> iron ingot).
780    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    /// Creates an exploration map pointing to a structure.
799    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        // TODO: Implement exploration map creation
807        // 1. Change item to filled_map
808        // 2. Set MAP_DECORATIONS component
809        // 3. Set destination structure tag
810        // This requires world access to find the structure
811    }
812
813    /// Sets the custom name or item name of this item.
814    pub const fn set_name(&mut self, _name: &str, _target: crate::loot_table::NameTarget) {
815        // TODO: Implement name setting
816        // Parse the name as a text component and set CUSTOM_NAME or ITEM_NAME
817    }
818
819    /// Sets the ominous bottle amplifier component.
820    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    /// Sets the potion type for this item.
828    pub const fn set_potion(&mut self, _id: &Identifier) {
829        // TODO: Implement potion type setting
830        // Set the POTION_CONTENTS component with the potion ID
831    }
832
833    /// Sets the suspicious stew effects for this item.
834    pub const fn set_stew_effects<R: rand::Rng>(
835        &mut self,
836        _effects: &[crate::loot_table::StewEffect],
837        _rng: &mut R,
838    ) {
839        // TODO: Implement stew effect setting
840        // Set the SUSPICIOUS_STEW_EFFECTS component
841        // Duration is determined by each effect's NumberProvider
842    }
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    /// Vanilla `ItemStack.enchant` → `Mutable.upgrade`: keeps the higher of existing vs new level.
863    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    /// Changes the item type entirely.
873    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    /// Copies the name from a source entity/block to this item.
881    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        // TODO: Implement when entity/block entity name access is available
887        // Get name from source (block_entity.custom_name or entity.custom_name)
888        // Set as CUSTOM_NAME component
889    }
890
891    /// Sets lore lines on this item.
892    pub const fn set_lore(&mut self, _lore: &[&str], _mode: crate::loot_table::ListOperation) {
893        // TODO: Implement lore setting
894        // Parse lore strings as text components and set LORE component
895        // Apply mode (replace, append, insert, etc.)
896    }
897
898    /// Sets container inventory contents.
899    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        // TODO: Implement container contents setting
906        // Generate items from entries and set as CONTAINER component
907    }
908
909    /// Modifies existing container contents.
910    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        // TODO: Implement container contents modification
917        // Apply modifier functions to existing container contents
918    }
919
920    /// Sets the container's loot table reference.
921    pub const fn set_loot_table(&mut self, _loot_table: &Identifier, _seed: Option<i64>) {
922        // TODO: Implement loot table reference setting
923        // Set CONTAINER_LOOT component with table reference and seed
924    }
925
926    /// Sets attribute modifiers on this item.
927    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        // TODO: Implement attribute modifier setting
934        // Set ATTRIBUTE_MODIFIERS component
935    }
936
937    /// Fills a player head with texture from an entity.
938    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        // TODO: Implement player head texture filling
944        // Get player profile from entity and set PROFILE component
945    }
946
947    /// Copies custom NBT data from a source.
948    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        // TODO: Implement custom data copying
955        // Copy NBT paths from source to item's CUSTOM_DATA component
956    }
957
958    /// Sets banner pattern layers.
959    pub const fn set_banner_pattern(
960        &mut self,
961        _patterns: &[crate::loot_table::BannerPattern],
962        _append: bool,
963    ) {
964        // TODO: Implement banner pattern setting
965        // Set BANNER_PATTERNS component
966    }
967
968    /// Sets firework rocket properties.
969    pub const fn set_fireworks(
970        &mut self,
971        _explosions: Option<&[crate::loot_table::FireworkExplosion]>,
972        _flight_duration: Option<i32>,
973    ) {
974        // TODO: Implement firework setting
975        // Set FIREWORKS component
976    }
977
978    /// Sets firework star explosion properties.
979    pub const fn set_firework_explosion(
980        &mut self,
981        _explosion: &crate::loot_table::FireworkExplosion,
982    ) {
983        // TODO: Implement firework explosion setting
984        // Set FIREWORK_EXPLOSION component
985    }
986
987    /// Sets book cover (title/author for written books).
988    pub const fn set_book_cover(
989        &mut self,
990        _title: Option<&str>,
991        _author: Option<&str>,
992        _generation: Option<i32>,
993    ) {
994        // TODO: Implement book cover setting
995        // Set WRITTEN_BOOK_CONTENT component fields
996    }
997
998    /// Sets written book page contents.
999    pub const fn set_written_book_pages(
1000        &mut self,
1001        _pages: &[&str],
1002        _mode: crate::loot_table::ListOperation,
1003    ) {
1004        // TODO: Implement written book pages setting
1005        // Set WRITTEN_BOOK_CONTENT pages
1006    }
1007
1008    /// Sets writable book page contents.
1009    pub const fn set_writable_book_pages(
1010        &mut self,
1011        _pages: &[&str],
1012        _mode: crate::loot_table::ListOperation,
1013    ) {
1014        // TODO: Implement writable book pages setting
1015        // Set WRITABLE_BOOK_CONTENT pages
1016    }
1017
1018    /// Runs vanilla `ToggleTooltips`: each boolean says whether the component is shown.
1019    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    /// Vanilla `ItemStack.getCustomName`: an explicit custom name, or a
1075    /// nonblank written-book title.
1076    #[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    /// Returns the custom name if set, otherwise the effective `ITEM_NAME`
1087    /// component.
1088    ///
1089    /// This does not apply item-class name overrides such as potion contents;
1090    /// callers with access to item behaviors should use their behavior-aware
1091    /// hover-name API.
1092    #[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            // Write item ID as VarInt
1152            VarInt(self.item.id() as i32).write(writer)?;
1153            // Write DataComponentPatch
1154            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        // Read DataComponentPatch
1176        let patch = DataComponentPatch::read(data)?;
1177
1178        Ok(Self::with_count_and_patch(item, count, patch))
1179    }
1180}
1181
1182impl ItemStack {
1183    /// Reads an item stack using the delimited (untrusted) component format.
1184    ///
1185    /// Vanilla uses this for serverbound packets where component data is
1186    /// length-prefixed (e.g., `ServerboundSetCreativeModeSlotPacket`).
1187    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    /// Converts this item stack to an NBT tag for persistent storage.
1217    ///
1218    /// Format (matching vanilla Minecraft):
1219    /// ```text
1220    /// {
1221    ///     id: "minecraft:stone",
1222    ///     count: 64,
1223    ///     components: { ... }  // Only present if patch is non-empty
1224    /// }
1225    /// ```
1226    fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
1227        self.to_nbt_tag_ref()
1228    }
1229}
1230
1231impl ItemStack {
1232    /// Checks that this stack can be encoded by Vanilla's persistent
1233    /// `ItemStack.CODEC` before untrusted network data enters server state.
1234    ///
1235    /// This is an ingress check rather than a type invariant: programmatic
1236    /// component mutation can still create values whose save codec reports
1237    /// and omits invalid fields.
1238    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    /// Converts this item stack to an NBT tag for persistent storage without consuming it.
1252    #[must_use]
1253    pub fn to_nbt_tag_ref(&self) -> simdnbt::owned::NbtTag {
1254        if self.is_empty() {
1255            // Empty stacks are represented as an empty compound
1256            return simdnbt::owned::NbtTag::Compound(NbtCompound::new());
1257        }
1258
1259        let mut compound = NbtCompound::new();
1260
1261        // id: The item identifier
1262        compound.insert("id", self.item.key.to_string());
1263
1264        compound.insert("count", self.count);
1265
1266        // components: The component patch (only if non-empty)
1267        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    /// Parses an item stack from an NBT tag.
1277    ///
1278    /// Accepts the vanilla format:
1279    /// ```text
1280    /// {
1281    ///     id: "minecraft:stone",
1282    ///     count: 64,
1283    ///     components: { ... }
1284    /// }
1285    /// ```
1286    fn from_nbt_tag(tag: BorrowedNbtTag) -> Option<Self> {
1287        let compound = tag.compound()?;
1288
1289        // Get the item ID
1290        let id_str = compound.get("id")?.string()?.to_str();
1291        let id = id_str.parse::<Identifier>().ok()?;
1292
1293        // Look up the item in the registry
1294        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    /// Parses an `ItemStack` from a borrowed `NbtCompoundView`.
1309    ///
1310    /// This is useful for loading items from disk where we have borrowed NBT data
1311    /// and want to avoid the overhead of converting to an owned tag first.
1312    #[must_use]
1313    pub fn from_borrowed_compound(compound: &NbtCompoundView<'_, '_>) -> Option<Self> {
1314        // Get the item ID
1315        let id_str = compound.string("id")?.to_str();
1316        let id = id_str.parse::<Identifier>().ok()?;
1317
1318        // Look up the item in the registry
1319        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}