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    #[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    /// Increases the count by the given amount.
127    pub const fn grow(&mut self, amount: i32) {
128        self.count += amount;
129    }
130
131    /// Decreases the count by the given amount.
132    pub const fn shrink(&mut self, amount: i32) {
133        self.count -= amount;
134    }
135
136    /// Decreases the count by one.
137    pub const fn shrink_one(&mut self) {
138        self.count -= 1;
139    }
140
141    /// Decreases the count by the given amount, unless the user has infinite
142    /// materials.
143    ///
144    /// See: `Player::has_infinite_materials`
145    #[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    /// [Consumes](Self::consume) a single item from this stack.
153    #[inline]
154    pub const fn consume_one(&mut self, has_infinite_materials: bool) {
155        self.consume(1, has_infinite_materials);
156    }
157
158    /// Splits off the given amount and [consumes](Self::consume) it from this stack.
159    #[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    /// Splits off the specified amount from this stack and returns it as a new stack.
168    ///
169    /// If the amount is greater than or equal to the current count, this stack becomes
170    /// empty and the entire contents are returned.
171    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    /// Copies the identity (item type and patch) from another stack.
183    ///
184    /// Used when splitting stacks to preserve components.
185    #[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    /// Returns true if this item can stack (max stack size > 1 and not damaged).
199    /// Damaged items cannot stack.
200    #[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    /// Returns true if this item can take damage.
206    #[must_use]
207    pub fn is_damageable_item(&self) -> bool {
208        self.has(MAX_DAMAGE) && !self.has(UNBREAKABLE) && self.has(DAMAGE)
209    }
210
211    /// Returns true if this item has taken damage.
212    #[must_use]
213    pub fn is_damaged(&self) -> bool {
214        self.is_damageable_item() && self.get_damage_value() > 0
215    }
216
217    /// Gets the current damage value of this item.
218    #[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    /// Sets the damage value of this item.
227    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    /// Gets the maximum damage this item can take before breaking.
233    #[must_use]
234    pub fn get_max_damage(&self) -> i32 {
235        self.get(MAX_DAMAGE).copied().unwrap_or(0)
236    }
237
238    /// Returns true if the item is broken (damage >= max damage).
239    #[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    /// Returns vanilla `ItemStack.nextDamageWillBreak()`.
245    #[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    /// Damages the item and breaks it if durability reaches zero.
251    ///
252    /// Returns `true` if the item broke and should be removed/replaced.
253    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    /// Damages the item using the supplied random source for data-driven
259    /// `minecraft:item_damage` enchantment effects.
260    ///
261    /// Returns `true` if the item broke and should be removed/replaced.
262    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        // TODO: Trigger ITEM_DURABILITY_CHANGED advancement criteria
285
286        self.set_damage_value(new_damage);
287
288        if self.is_broken() {
289            // TODO: Call onEquippedItemBroken callback which:
290            // - Broadcasts entity event (byte 47 for mainhand) for break sound/particles
291            // - Stops location-based effects (removes attribute modifiers)
292            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    /// Returns true if this item has the specified component (by type).
333    #[must_use]
334    pub fn has<T: 'static>(&self, component: DataComponentType<T>) -> bool {
335        self.has_component(&component.key)
336    }
337
338    /// Returns true if this item has the specified component (by key).
339    #[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    /// Checks if two stacks have the same item and components.
354    #[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    /// Validates the complete stack constraints enforced by Vanilla's `ItemStack.validateStrict`.
381    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    /// Returns the equippable component if this item has one.
412    #[must_use]
413    pub fn get_equippable(&self) -> Option<&Equippable> {
414        self.get(EQUIPPABLE)
415    }
416
417    /// Returns the item attribute modifiers component.
418    #[must_use]
419    pub fn get_attribute_modifiers(&self) -> Option<&ItemAttributeModifiers> {
420        self.get(ATTRIBUTE_MODIFIERS)
421    }
422
423    /// Returns the equipment slot this item can be equipped to, if any.
424    #[must_use]
425    pub fn get_equippable_slot(&self) -> Option<EquipmentSlot> {
426        self.get_equippable().map(|e| e.slot)
427    }
428
429    /// Returns true if this item can be equipped in the given slot.
430    #[must_use]
431    pub fn is_equippable_in_slot(&self, slot: EquipmentSlot) -> bool {
432        self.get_equippable_slot() == Some(slot)
433    }
434
435    /// Gets the raw component data by key.
436    #[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    /// Gets the effective value of a component, considering the patch and prototype.
446    /// Returns `None` if the component is not present or has been removed.
447    #[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    /// Gets the effective value of a component, or returns the default value if not present.
454    #[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    /// Sets a component value in this item's patch, overriding the prototype.
464    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    /// Removes a component from this item (marks it as removed in the patch).
475    /// This will hide the component even if it exists in the prototype.
476    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    /// Clears any patch entry for this component (neither set nor removed).
485    /// The prototype value will be visible again.
486    pub fn clear<T: 'static>(&mut self, component: DataComponentType<T>) {
487        self.patch.clear(component);
488    }
489
490    /// Returns a reference to the component patch.
491    #[must_use]
492    pub const fn patch(&self) -> &DataComponentPatch {
493        &self.patch
494    }
495
496    /// Gets the Tool component if present.
497    #[must_use]
498    pub fn get_tool(&self) -> Option<&Tool> {
499        self.get(TOOL)
500    }
501
502    /// Gets the Weapon component if present.
503    #[must_use]
504    pub fn get_weapon(&self) -> Option<&Weapon> {
505        self.get(WEAPON)
506    }
507
508    /// Gets the `AttackRange` component if present.
509    #[must_use]
510    pub fn get_attack_range(&self) -> Option<&AttackRange> {
511        self.get(ATTACK_RANGE)
512    }
513
514    /// Returns vanilla `DataComponents.MINIMUM_ATTACK_CHARGE`, defaulting to 0.
515    #[must_use]
516    pub fn minimum_attack_charge(&self) -> f32 {
517        self.get(MINIMUM_ATTACK_CHARGE).copied().unwrap_or(0.0)
518    }
519
520    /// Gets the vanilla damage type component if present.
521    #[must_use]
522    pub fn get_damage_type(&self) -> Option<DamageTypeRef> {
523        self.get(DAMAGE_TYPE).map(|component| component.damage_type)
524    }
525
526    /// Returns vanilla `ItemStack.canBeHurtBy` for a damage type.
527    #[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    /// Returns vanilla `ItemStack.isValidRepairItem`.
534    #[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    /// Returns whether this item has the vanilla piercing weapon component.
541    #[must_use]
542    pub fn is_piercing_weapon(&self) -> bool {
543        self.has(PIERCING_WEAPON)
544    }
545
546    /// Gets the `PiercingWeapon` component if present.
547    #[must_use]
548    pub fn get_piercing_weapon(&self) -> Option<&PiercingWeapon> {
549        self.get(PIERCING_WEAPON)
550    }
551
552    /// Returns the mining speed for the given block state ID.
553    /// If no Tool component is present, returns 1.0 (hand speed).
554    #[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    /// Returns true if this tool is correct for getting drops from the block.
561    #[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    /// Returns the damage per block for this tool (how much durability is consumed per block mined).
568    /// Returns 0 if no Tool component is present.
569    #[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    /// Returns true if this tool can destroy blocks in creative mode.
575    /// Returns true if no Tool component is present (default behavior).
576    #[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    /// Vanilla `EnchantmentHelper.getEnchantmentsForCrafting`: enchanted books
594    /// expose `STORED_ENCHANTMENTS` to crafting operations, while every other
595    /// item exposes `ENCHANTMENTS`.
596    #[must_use]
597    pub fn get_enchantments_for_crafting(&self) -> Option<&ItemEnchantments> {
598        self.get(self.enchantment_component())
599    }
600
601    /// Vanilla `EnchantmentHelper.getComponentType`.
602    #[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    /// Mirrors Vanilla's component-based `ItemStack.isEnchantable` check.
612    #[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    /// Checks if `ItemStack` has an enchantment
621    #[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    /// Sets the damage/durability as a fraction (0.0 = broken, 1.0 = full).
688    /// If `add` is true, adds to current damage instead of setting.
689    pub const fn set_damage_fraction(&mut self, _fraction: f32, _add: bool) {
690        // TODO: Implement when damage component system is ready
691        // let max_damage = self.get_max_damage();
692        // let damage_value = ((1.0 - fraction) * max_damage as f32) as i32;
693        // self.set_component(DAMAGE, damage_value);
694    }
695
696    /// Enchants this item randomly with enchantments from the given options.
697    pub const fn enchant_randomly<R: rand::Rng>(
698        &mut self,
699        _options: &crate::loot_table::EnchantmentOptions,
700        _rng: &mut R,
701    ) {
702        // TODO: Implement when enchantment registry and system are ready
703        // 1. Get list of valid enchantments from options (tag or list)
704        // 2. Filter to enchantments that can apply to this item
705        // 3. Pick one randomly
706        // 4. Pick a random level for that enchantment
707        // 5. Add to ENCHANTMENTS component
708    }
709
710    /// Enchants this item as if using an enchanting table at the given level.
711    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        // TODO: Implement when enchantment registry and system are ready
718        // This simulates the enchanting table algorithm:
719        // 1. Calculate modified level based on item enchantability
720        // 2. Generate list of possible enchantments for that level
721        // 3. Filter by options (tag or list)
722        // 4. Apply enchantments with proper weights
723    }
724
725    /// Copies components from a source (block entity, attacker, etc.) to this item.
726    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        // TODO: Implement when block entity system is ready
733        // 1. Get the source entity/block entity from context
734        // 2. For each component in `include`, copy it to this item's patch
735    }
736
737    /// Copies block state properties to this item (for blocks like `note_block`).
738    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        // TODO: Implement block state copying
745        // 1. Get block state from context
746        // 2. For each property, store it in the item's BLOCK_STATE component
747    }
748
749    /// Sets components from a JSON string representation.
750    pub const fn set_components_from_json(&mut self, _components: &str) {
751        // TODO: Implement component parsing from JSON
752        // Parse the JSON and set each component in the patch
753    }
754
755    /// Merges custom NBT data into this item's `custom_data` component.
756    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    /// Applies furnace smelting to convert this item (e.g., raw iron -> iron ingot).
770    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    /// Creates an exploration map pointing to a structure.
789    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        // TODO: Implement exploration map creation
797        // 1. Change item to filled_map
798        // 2. Set MAP_DECORATIONS component
799        // 3. Set destination structure tag
800        // This requires world access to find the structure
801    }
802
803    /// Sets the custom name or item name of this item.
804    pub const fn set_name(&mut self, _name: &str, _target: crate::loot_table::NameTarget) {
805        // TODO: Implement name setting
806        // Parse the name as a text component and set CUSTOM_NAME or ITEM_NAME
807    }
808
809    /// Sets the ominous bottle amplifier component.
810    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    /// Sets the potion type for this item.
818    pub const fn set_potion(&mut self, _id: &Identifier) {
819        // TODO: Implement potion type setting
820        // Set the POTION_CONTENTS component with the potion ID
821    }
822
823    /// Sets the suspicious stew effects for this item.
824    pub const fn set_stew_effects<R: rand::Rng>(
825        &mut self,
826        _effects: &[crate::loot_table::StewEffect],
827        _rng: &mut R,
828    ) {
829        // TODO: Implement stew effect setting
830        // Set the SUSPICIOUS_STEW_EFFECTS component
831        // Duration is determined by each effect's NumberProvider
832    }
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    /// Vanilla `ItemStack.enchant` → `Mutable.upgrade`: keeps the higher of existing vs new level.
853    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    /// Changes the item type entirely.
863    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    /// Copies the name from a source entity/block to this item.
871    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        // TODO: Implement when entity/block entity name access is available
877        // Get name from source (block_entity.custom_name or entity.custom_name)
878        // Set as CUSTOM_NAME component
879    }
880
881    /// Sets lore lines on this item.
882    pub const fn set_lore(&mut self, _lore: &[&str], _mode: crate::loot_table::ListOperation) {
883        // TODO: Implement lore setting
884        // Parse lore strings as text components and set LORE component
885        // Apply mode (replace, append, insert, etc.)
886    }
887
888    /// Sets container inventory contents.
889    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        // TODO: Implement container contents setting
896        // Generate items from entries and set as CONTAINER component
897    }
898
899    /// Modifies existing container contents.
900    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        // TODO: Implement container contents modification
907        // Apply modifier functions to existing container contents
908    }
909
910    /// Sets the container's loot table reference.
911    pub const fn set_loot_table(&mut self, _loot_table: &Identifier, _seed: Option<i64>) {
912        // TODO: Implement loot table reference setting
913        // Set CONTAINER_LOOT component with table reference and seed
914    }
915
916    /// Sets attribute modifiers on this item.
917    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        // TODO: Implement attribute modifier setting
924        // Set ATTRIBUTE_MODIFIERS component
925    }
926
927    /// Fills a player head with texture from an entity.
928    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        // TODO: Implement player head texture filling
934        // Get player profile from entity and set PROFILE component
935    }
936
937    /// Copies custom NBT data from a source.
938    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        // TODO: Implement custom data copying
945        // Copy NBT paths from source to item's CUSTOM_DATA component
946    }
947
948    /// Sets banner pattern layers.
949    pub const fn set_banner_pattern(
950        &mut self,
951        _patterns: &[crate::loot_table::BannerPattern],
952        _append: bool,
953    ) {
954        // TODO: Implement banner pattern setting
955        // Set BANNER_PATTERNS component
956    }
957
958    /// Sets firework rocket properties.
959    pub const fn set_fireworks(
960        &mut self,
961        _explosions: Option<&[crate::loot_table::FireworkExplosion]>,
962        _flight_duration: Option<i32>,
963    ) {
964        // TODO: Implement firework setting
965        // Set FIREWORKS component
966    }
967
968    /// Sets firework star explosion properties.
969    pub const fn set_firework_explosion(
970        &mut self,
971        _explosion: &crate::loot_table::FireworkExplosion,
972    ) {
973        // TODO: Implement firework explosion setting
974        // Set FIREWORK_EXPLOSION component
975    }
976
977    /// Sets book cover (title/author for written books).
978    pub const fn set_book_cover(
979        &mut self,
980        _title: Option<&str>,
981        _author: Option<&str>,
982        _generation: Option<i32>,
983    ) {
984        // TODO: Implement book cover setting
985        // Set WRITTEN_BOOK_CONTENT component fields
986    }
987
988    /// Sets written book page contents.
989    pub const fn set_written_book_pages(
990        &mut self,
991        _pages: &[&str],
992        _mode: crate::loot_table::ListOperation,
993    ) {
994        // TODO: Implement written book pages setting
995        // Set WRITTEN_BOOK_CONTENT pages
996    }
997
998    /// Sets writable book page contents.
999    pub const fn set_writable_book_pages(
1000        &mut self,
1001        _pages: &[&str],
1002        _mode: crate::loot_table::ListOperation,
1003    ) {
1004        // TODO: Implement writable book pages setting
1005        // Set WRITABLE_BOOK_CONTENT pages
1006    }
1007
1008    /// Runs vanilla `ToggleTooltips`: each boolean says whether the component is shown.
1009    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    /// Vanilla `ItemStack.getCustomName`: an explicit custom name, or a
1065    /// nonblank written-book title.
1066    #[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    /// Returns the custom name if set, otherwise the effective `ITEM_NAME`
1077    /// component.
1078    ///
1079    /// This does not apply item-class name overrides such as potion contents;
1080    /// callers with access to item behaviors should use their behavior-aware
1081    /// hover-name API.
1082    #[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            // Write item ID as VarInt
1142            VarInt(self.item.id() as i32).write(writer)?;
1143            // Write DataComponentPatch
1144            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        // Read DataComponentPatch
1166        let patch = DataComponentPatch::read(data)?;
1167
1168        Ok(Self::with_count_and_patch(item, count, patch))
1169    }
1170}
1171
1172impl ItemStack {
1173    /// Reads an item stack using the delimited (untrusted) component format.
1174    ///
1175    /// Vanilla uses this for serverbound packets where component data is
1176    /// length-prefixed (e.g., `ServerboundSetCreativeModeSlotPacket`).
1177    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    /// Converts this item stack to an NBT tag for persistent storage.
1207    ///
1208    /// Format (matching vanilla Minecraft):
1209    /// ```text
1210    /// {
1211    ///     id: "minecraft:stone",
1212    ///     count: 64,
1213    ///     components: { ... }  // Only present if patch is non-empty
1214    /// }
1215    /// ```
1216    fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
1217        self.to_nbt_tag_ref()
1218    }
1219}
1220
1221impl ItemStack {
1222    /// Checks that this stack can be encoded by Vanilla's persistent
1223    /// `ItemStack.CODEC` before untrusted network data enters server state.
1224    ///
1225    /// This is an ingress check rather than a type invariant: programmatic
1226    /// component mutation can still create values whose save codec reports
1227    /// and omits invalid fields.
1228    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    /// Converts this item stack to an NBT tag for persistent storage without consuming it.
1242    #[must_use]
1243    pub fn to_nbt_tag_ref(&self) -> simdnbt::owned::NbtTag {
1244        if self.is_empty() {
1245            // Empty stacks are represented as an empty compound
1246            return simdnbt::owned::NbtTag::Compound(NbtCompound::new());
1247        }
1248
1249        let mut compound = NbtCompound::new();
1250
1251        // id: The item identifier
1252        compound.insert("id", self.item.key.to_string());
1253
1254        compound.insert("count", self.count);
1255
1256        // components: The component patch (only if non-empty)
1257        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    /// Parses an item stack from an NBT tag.
1267    ///
1268    /// Accepts the vanilla format:
1269    /// ```text
1270    /// {
1271    ///     id: "minecraft:stone",
1272    ///     count: 64,
1273    ///     components: { ... }
1274    /// }
1275    /// ```
1276    fn from_nbt_tag(tag: BorrowedNbtTag) -> Option<Self> {
1277        let compound = tag.compound()?;
1278
1279        // Get the item ID
1280        let id_str = compound.get("id")?.string()?.to_str();
1281        let id = id_str.parse::<Identifier>().ok()?;
1282
1283        // Look up the item in the registry
1284        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    /// Parses an `ItemStack` from a borrowed `NbtCompoundView`.
1299    ///
1300    /// This is useful for loading items from disk where we have borrowed NBT data
1301    /// and want to avoid the overhead of converting to an owned tag first.
1302    #[must_use]
1303    pub fn from_borrowed_compound(compound: &NbtCompoundView<'_, '_>) -> Option<Self> {
1304        // Get the item ID
1305        let id_str = compound.string("id")?.to_str();
1306        let id = id_str.parse::<Identifier>().ok()?;
1307
1308        // Look up the item in the registry
1309        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}