Skip to main content

steel_registry/
item_stack_template.rs

1//! Non-empty item stack templates used by recursive Vanilla codecs.
2
3use std::cell::Cell;
4use std::io::{Cursor, Error, Result, Write};
5use std::str::FromStr;
6
7use simdnbt::owned::{NbtCompound, NbtTag};
8use simdnbt::{FromNbtTag, ToNbtTag};
9use steel_utils::codec::VarInt;
10use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
11use steel_utils::nbt::NbtNumeric as _;
12use steel_utils::serial::{ReadFrom, WriteTo};
13use steel_utils::{DowncastType, Identifier};
14use text_components::{EncodedNbt, interactivity::HoverEvent};
15
16use crate::data_components::vanilla_components::MAX_STACK_SIZE;
17use crate::data_components::{
18    Component, ComponentData, ComponentPatchEntry, DataComponentPatch, DataComponentType,
19};
20use crate::item_stack::ItemStack;
21use crate::items::ItemRef;
22use crate::{REGISTRY, RegistryEntry, RegistryExt, vanilla_items};
23
24// Vanilla's stream codec has no explicit guard, but deeper values already fail
25// its persistent NBT codec. Use that 512-level limit to avoid stack exhaustion.
26const MAX_TEMPLATE_CODEC_DEPTH: usize = 512;
27
28thread_local! {
29    static TEMPLATE_CODEC_DEPTH: Cell<usize> = const { Cell::new(0) };
30}
31
32struct TemplateDepthGuard;
33
34impl TemplateDepthGuard {
35    fn enter() -> Result<Self> {
36        TEMPLATE_CODEC_DEPTH.with(|depth| {
37            let current = depth.get();
38            if current >= MAX_TEMPLATE_CODEC_DEPTH {
39                return Err(Error::other(format!(
40                    "Item stack template nesting exceeds {MAX_TEMPLATE_CODEC_DEPTH}"
41                )));
42            }
43            depth.set(current + 1);
44            Ok(Self)
45        })
46    }
47}
48
49impl Drop for TemplateDepthGuard {
50    fn drop(&mut self) {
51        TEMPLATE_CODEC_DEPTH.with(|depth| depth.set(depth.get() - 1));
52    }
53}
54
55/// A persistable, non-empty item identity, count, and component patch.
56#[derive(Debug, Clone, PartialEq)]
57pub struct ItemStackTemplate {
58    item: ItemRef,
59    count: i32,
60    components: DataComponentPatch,
61    components_hash: Option<i32>,
62}
63
64impl ItemStackTemplate {
65    pub const MIN_COUNT: i32 = 1;
66    pub const MAX_COUNT: i32 = 99;
67
68    /// Creates the common count-one template with no component changes.
69    #[must_use]
70    pub fn new(item: ItemRef) -> Self {
71        assert!(
72            item != &*vanilla_items::AIR,
73            "Item stack template item must be non-empty"
74        );
75        Self {
76            item,
77            count: 1,
78            components: DataComponentPatch::new(),
79            components_hash: Some(empty_map_hash()),
80        }
81    }
82
83    /// Creates a template with a validated persistent count and no component changes.
84    #[must_use]
85    pub fn with_count(item: ItemRef, count: i32) -> Self {
86        assert!(
87            (Self::MIN_COUNT..=Self::MAX_COUNT).contains(&count),
88            "Item stack template count {count} is outside the persistent range {}..={}",
89            Self::MIN_COUNT,
90            Self::MAX_COUNT
91        );
92        let mut template = Self::new(item);
93        template.count = count;
94        template
95    }
96
97    /// Creates a template after validating its complete persistent codec shape.
98    pub fn try_with_count_and_patch(
99        item: ItemRef,
100        count: i32,
101        components: DataComponentPatch,
102    ) -> Result<Self> {
103        if item == &*vanilla_items::AIR {
104            return Err(Error::other("Item stack template item must be non-empty"));
105        }
106        if !(Self::MIN_COUNT..=Self::MAX_COUNT).contains(&count) {
107            return Err(Error::other(format!(
108                "Item stack template count {count} is outside the persistent range {}..={}",
109                Self::MIN_COUNT,
110                Self::MAX_COUNT
111            )));
112        }
113        components.try_to_nbt_tag_ref()?;
114        let components_hash = components.compute_persistent_hash()?;
115        Ok(Self {
116            item,
117            count,
118            components,
119            components_hash: Some(components_hash),
120        })
121    }
122
123    /// Constructs a template from build-time validated Vanilla extractor data.
124    ///
125    /// This avoids persistent codec validation during registry bootstrap because
126    /// registry-aware component codecs cannot consult `REGISTRY` until it has
127    /// been completely built and published.
128    pub(crate) fn from_extracted(
129        item: ItemRef,
130        count: i32,
131        components: DataComponentPatch,
132        components_hash: i32,
133    ) -> Self {
134        assert!(
135            item != &*vanilla_items::AIR,
136            "Extracted recipe result must be non-empty"
137        );
138        assert!(
139            (Self::MIN_COUNT..=Self::MAX_COUNT).contains(&count),
140            "Extracted recipe result count is outside the persistent range"
141        );
142        Self {
143            item,
144            count,
145            components,
146            components_hash: Some(components_hash),
147        }
148    }
149
150    /// Copies a non-empty stack into its immutable template representation.
151    pub fn from_stack(stack: &ItemStack) -> Result<Self> {
152        if stack.is_empty() {
153            return Err(Error::other("Stack must be non-empty"));
154        }
155        Self::try_with_count_and_patch(stack.item, stack.count, stack.components_patch().clone())
156    }
157
158    pub(crate) fn validate_persistent_encoding(&self) -> Result<()> {
159        if self.item == &*vanilla_items::AIR
160            || !(Self::MIN_COUNT..=Self::MAX_COUNT).contains(&self.count)
161        {
162            return Err(Error::other("Item stack template is not persistable"));
163        }
164        self.components.try_to_nbt_tag_ref().map(|_| ())
165    }
166
167    #[must_use]
168    pub const fn item(&self) -> ItemRef {
169        self.item
170    }
171
172    #[must_use]
173    pub const fn count(&self) -> i32 {
174        self.count
175    }
176
177    #[must_use]
178    pub const fn components(&self) -> &DataComponentPatch {
179        &self.components
180    }
181
182    #[must_use]
183    pub fn create(&self) -> ItemStack {
184        let result =
185            ItemStack::with_count_and_patch(self.item, self.count, self.components.clone());
186        if let Err(error) = result.validate_strict() {
187            log::warn!("Can't create item stack with properties {self:?}, error: {error}");
188            return ItemStack::empty();
189        }
190        result
191    }
192
193    /// Creates this template over an existing component patch, matching
194    /// Vanilla's `ItemStackTemplate.apply` precedence rules.
195    #[must_use]
196    pub fn apply(&self, count: i32, additional_components: &DataComponentPatch) -> ItemStack {
197        let mut components = additional_components.clone();
198        components.apply(&self.components);
199        let result = ItemStack::with_count_and_patch(self.item, count, components);
200        if let Err(error) = result.validate_strict() {
201            log::warn!("Can't create item stack with properties {self:?}, error: {error}");
202            return ItemStack::empty();
203        }
204        result
205    }
206
207    /// Creates the Vanilla hover event for this item template.
208    pub fn to_hover_event(&self) -> Result<HoverEvent> {
209        let components = if self.components.is_empty() {
210            None
211        } else {
212            Some(EncodedNbt::encode(&self.components)?)
213        };
214        Ok(HoverEvent::show_item(
215            self.item.key.to_string(),
216            Some(self.count),
217            components,
218        ))
219    }
220
221    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
222        let mut compound = NbtCompound::new();
223        compound.insert("id", self.item.key.to_string());
224        if self.count != 1 {
225            compound.insert("count", self.count);
226        }
227        if !self.components.is_empty() {
228            compound.insert("components", self.components.to_nbt_tag_ref());
229        }
230        NbtTag::Compound(compound)
231    }
232
233    pub(crate) fn from_nbt_identifier(value: &str) -> Option<Self> {
234        let _depth = TemplateDepthGuard::enter().ok()?;
235        let key = Identifier::from_str(value).ok()?;
236        let item = REGISTRY.items.by_key(&key)?;
237        (item != &*vanilla_items::AIR).then(|| Self::new(item))
238    }
239
240    pub(crate) fn from_nbt_compound(
241        compound: simdnbt::borrow::NbtCompound<'_, '_>,
242    ) -> Option<Self> {
243        let _depth = TemplateDepthGuard::enter().ok()?;
244        let key = Identifier::from_str(&compound.get("id")?.string()?.to_str()).ok()?;
245        let item = REGISTRY.items.by_key(&key)?;
246        let count = match compound.get("count") {
247            Some(count) => count.codec_i32()?,
248            None => 1,
249        };
250        let components = match compound.get("components") {
251            Some(components) => DataComponentPatch::from_nbt_tag(components)?,
252            None => DataComponentPatch::new(),
253        };
254        Self::try_with_count_and_patch(item, count, components).ok()
255    }
256
257    fn from_stream(item: ItemRef, count: i32, components: DataComponentPatch) -> Result<Self> {
258        if item == &*vanilla_items::AIR || count == 0 {
259            return Err(Error::other("Item stack template must be non-empty"));
260        }
261        let components_hash = components.compute_persistent_hash().ok();
262        Ok(Self {
263            item,
264            count,
265            components,
266            components_hash,
267        })
268    }
269
270    /// Gets the effective raw component value from the patch or item prototype.
271    #[must_use]
272    pub fn get_effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData> {
273        match self.components.get_entry(key) {
274            Some(ComponentPatchEntry::Set(value)) => Some(value),
275            Some(ComponentPatchEntry::Removed) => None,
276            None => self.item.components.get_raw(key),
277        }
278    }
279
280    /// Gets an effective typed component value from the patch or item prototype.
281    #[must_use]
282    pub fn get<T: Component + DowncastType>(&self, component: DataComponentType<T>) -> Option<&T> {
283        self.get_effective_value_raw(&component.key)
284            .and_then(ComponentData::downcast_ref::<T>)
285    }
286
287    pub(crate) fn max_stack_size(&self) -> i32 {
288        self.get(MAX_STACK_SIZE).copied().unwrap_or(1)
289    }
290}
291
292impl WriteTo for ItemStackTemplate {
293    fn write(&self, writer: &mut impl Write) -> Result<()> {
294        let _depth = TemplateDepthGuard::enter()?;
295        let item_id = i32::try_from(self.item.id())
296            .map_err(|_| Error::other(format!("Item id is too large: {}", self.item.id())))?;
297        VarInt(item_id).write(writer)?;
298        VarInt(self.count).write(writer)?;
299        self.components.write(writer)
300    }
301}
302
303impl ReadFrom for ItemStackTemplate {
304    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
305        let _depth = TemplateDepthGuard::enter()?;
306        let item_id = VarInt::read(data)?.0;
307        let item_id = usize::try_from(item_id)
308            .map_err(|_| Error::other(format!("Negative item id: {item_id}")))?;
309        let item = REGISTRY
310            .items
311            .by_id(item_id)
312            .ok_or_else(|| Error::other(format!("Unknown item id: {item_id}")))?;
313        let count = VarInt::read(data)?.0;
314        let components = DataComponentPatch::read(data)?;
315        Self::from_stream(item, count, components)
316    }
317}
318
319impl ToNbtTag for ItemStackTemplate {
320    fn to_nbt_tag(self) -> NbtTag {
321        self.to_nbt_tag_ref()
322    }
323}
324
325impl FromNbtTag for ItemStackTemplate {
326    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
327        if let Some(value) = tag.string() {
328            return Self::from_nbt_identifier(&value.to_str());
329        }
330        Self::from_nbt_compound(tag.compound()?)
331    }
332}
333
334impl HashComponent for ItemStackTemplate {
335    fn hash_component(&self, hasher: &mut ComponentHasher) {
336        let mut entries = Vec::with_capacity(3);
337        push_hash_entry(&mut entries, "id", self.item.key.to_string().compute_hash());
338        if self.count != 1 {
339            push_hash_entry(&mut entries, "count", self.count.compute_hash());
340        }
341        if !self.components.is_empty() {
342            let Some(components_hash) = self.components_hash else {
343                panic!("stream-only item stack template must validate before persistent hashing");
344            };
345            push_hash_entry(&mut entries, "components", components_hash);
346        }
347        sort_map_entries(&mut entries);
348        hasher.start_map();
349        for entry in &entries {
350            hasher.put_raw_bytes(&entry.key_bytes);
351            hasher.put_raw_bytes(&entry.value_bytes);
352        }
353        hasher.end_map();
354    }
355}
356
357fn empty_map_hash() -> i32 {
358    let mut hasher = ComponentHasher::new();
359    hasher.start_map();
360    hasher.end_map();
361    hasher.finish()
362}
363
364fn push_hash_entry(entries: &mut Vec<HashEntry>, key: &str, value_hash: i32) {
365    let key_hash = key.compute_hash() as u32;
366    let value_hash = value_hash as u32;
367    entries.push(HashEntry {
368        key_hash: i64::from(key_hash),
369        value_hash: i64::from(value_hash),
370        key_bytes: key_hash.to_le_bytes(),
371        value_bytes: value_hash.to_le_bytes(),
372    });
373}
374
375#[cfg(test)]
376mod tests {
377    use std::io::Cursor;
378
379    use simdnbt::borrow::read_tag;
380    use simdnbt::owned::{NbtCompound, NbtTag};
381    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
382    use steel_utils::hash::HashComponent as _;
383    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
384
385    use super::ItemStackTemplate;
386    use crate::RegistryEntry as _;
387    use crate::data_components::components::{
388        BundleContents, ChargedProjectiles, ItemContainerContents,
389    };
390    use crate::data_components::vanilla_components::{
391        BUNDLE_CONTENTS, CHARGED_PROJECTILES, CONTAINER, CUSTOM_NAME, ENCHANTMENT_GLINT_OVERRIDE,
392        MAX_DAMAGE, MAX_STACK_SIZE,
393    };
394    use crate::data_components::{ComponentData, DataComponentPatch};
395    use crate::init_vanilla_registry;
396    use crate::vanilla_items;
397    use crate::{REGISTRY, RegistryExt as _};
398    use text_components::{Modifier as _, TextComponent};
399
400    fn parse(tag: NbtTag) -> Option<ItemStackTemplate> {
401        let mut bytes = Vec::new();
402        tag.write(&mut bytes);
403        let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
404        ItemStackTemplate::from_nbt_tag(borrowed.as_tag())
405    }
406
407    #[test]
408    fn item_only_alternative_decodes_and_primary_codec_omits_defaults() {
409        init_vanilla_registry();
410        let template = ItemStackTemplate::new(&vanilla_items::STICK);
411        let mut expected = NbtCompound::new();
412        expected.insert("id", "minecraft:stick");
413        let expected = NbtTag::Compound(expected);
414        assert_eq!(template.clone().to_nbt_tag(), expected);
415        assert_eq!(template.compute_hash(), expected.compute_hash());
416        assert_eq!(parse(NbtTag::String("stick".into())), Some(template));
417    }
418
419    #[test]
420    fn complete_templates_round_trip_both_codecs() {
421        init_vanilla_registry();
422        let mut patch = DataComponentPatch::new();
423        patch.set(ENCHANTMENT_GLINT_OVERRIDE, true);
424        let template =
425            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::DIAMOND, 3, patch)
426                .expect("valid template should construct");
427        assert_eq!(parse(template.clone().to_nbt_tag()), Some(template.clone()));
428
429        let mut network = Vec::new();
430        template
431            .write(&mut network)
432            .expect("template should encode");
433        let decoded = ItemStackTemplate::read(&mut Cursor::new(network.as_slice()))
434            .expect("template should decode");
435        assert_eq!(decoded, template);
436        assert_eq!(decoded.compute_hash(), template.compute_hash());
437    }
438
439    #[test]
440    fn hover_events_embed_the_typed_component_patch_codec_output() {
441        init_vanilla_registry();
442        let mut patch = DataComponentPatch::new();
443        patch.set(CUSTOM_NAME, TextComponent::plain("Stone"));
444        let template = ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 2, patch)
445            .expect("valid template should construct");
446        let component = TextComponent::plain("item").hover_event(
447            template
448                .to_hover_event()
449                .expect("valid template should encode for a hover event"),
450        );
451
452        let NbtTag::Compound(component) = component.to_codec_nbt() else {
453            panic!("hover component should encode as a compound");
454        };
455        let components = component
456            .get("hover_event")
457            .and_then(NbtTag::compound)
458            .and_then(|hover| hover.get("components"))
459            .and_then(NbtTag::compound)
460            .expect("hover event should contain a component patch");
461        assert_eq!(
462            components.get("minecraft:custom_name"),
463            Some(&NbtTag::String("Stone".into()))
464        );
465    }
466
467    #[test]
468    fn template_invariants_reject_empty_items_and_unpersistable_counts() {
469        init_vanilla_registry();
470        for count in [-1, 0, 100] {
471            assert!(
472                ItemStackTemplate::try_with_count_and_patch(
473                    &vanilla_items::STICK,
474                    count,
475                    DataComponentPatch::new(),
476                )
477                .is_err()
478            );
479        }
480        assert!(
481            ItemStackTemplate::try_with_count_and_patch(
482                &vanilla_items::AIR,
483                1,
484                DataComponentPatch::new(),
485            )
486            .is_err()
487        );
488    }
489
490    #[test]
491    fn stream_codec_accepts_nonzero_counts_outside_persistent_range() {
492        init_vanilla_registry();
493        for count in [-1, 100] {
494            let mut encoded = Vec::new();
495            steel_utils::codec::VarInt(vanilla_items::STICK.id() as i32)
496                .write(&mut encoded)
497                .expect("item id should encode");
498            steel_utils::codec::VarInt(count)
499                .write(&mut encoded)
500                .expect("count should encode");
501            DataComponentPatch::new()
502                .write(&mut encoded)
503                .expect("patch should encode");
504            let decoded = ItemStackTemplate::read(&mut Cursor::new(encoded.as_slice()))
505                .expect("nonzero stream count should decode");
506            assert_eq!(decoded.count(), count);
507        }
508    }
509
510    #[test]
511    fn containing_component_hash_rejects_stream_only_nested_patch() {
512        init_vanilla_registry();
513        let mut patch = DataComponentPatch::new();
514        patch.set(MAX_STACK_SIZE, 0);
515        let template = ItemStackTemplate::from_stream(&vanilla_items::STONE, 1, patch)
516            .expect("stream codec should admit the nested patch");
517        let bundle = BundleContents::new(vec![template]);
518        let entry = REGISTRY
519            .data_components
520            .by_key(BUNDLE_CONTENTS.key())
521            .expect("bundle_contents should be registered");
522        assert!(entry.compute_hash(&ComponentData::new(bundle)).is_err());
523    }
524
525    #[test]
526    fn item_only_air_alternative_returns_codec_failure() {
527        init_vanilla_registry();
528
529        assert!(parse(NbtTag::String("minecraft:air".into())).is_none());
530    }
531
532    #[test]
533    fn create_rejects_invalid_effective_stack_constraints() {
534        init_vanilla_registry();
535
536        let mut oversized_patch = DataComponentPatch::new();
537        oversized_patch.set(MAX_STACK_SIZE, 1);
538        let oversized =
539            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 2, oversized_patch)
540                .expect("template codec permits counts above the effective stack maximum");
541        assert!(oversized.create().is_empty());
542
543        let mut damageable_patch = DataComponentPatch::new();
544        damageable_patch.set(MAX_DAMAGE, 1);
545        let damageable =
546            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, damageable_patch)
547                .expect("individually valid components should construct a template");
548        assert!(damageable.create().is_empty());
549
550        assert!(
551            !ItemStackTemplate::new(&vanilla_items::STONE)
552                .create()
553                .is_empty()
554        );
555    }
556
557    #[test]
558    fn create_rejects_oversized_recursive_contents() {
559        init_vanilla_registry();
560
561        let mut container_patch = DataComponentPatch::new();
562        container_patch.set(
563            CONTAINER,
564            ItemContainerContents::new(vec![Some(oversized_stone_template())])
565                .expect("one container slot should be valid"),
566        );
567        let container =
568            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, container_patch)
569                .expect("container component should persist");
570        assert!(container.create().is_empty());
571
572        let mut bundle_patch = DataComponentPatch::new();
573        bundle_patch.set(
574            BUNDLE_CONTENTS,
575            BundleContents::new(vec![oversized_stone_template()]),
576        );
577        let bundle =
578            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, bundle_patch)
579                .expect("bundle component should persist");
580        assert!(bundle.create().is_empty());
581
582        let mut projectile_patch = DataComponentPatch::new();
583        projectile_patch.set(
584            CHARGED_PROJECTILES,
585            ChargedProjectiles::new(vec![oversized_stone_template()])
586                .expect("one charged projectile should be valid"),
587        );
588        let projectiles =
589            ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, projectile_patch)
590                .expect("charged-projectiles component should persist");
591        assert!(projectiles.create().is_empty());
592    }
593
594    #[test]
595    fn create_rejects_excessive_bundle_weight_arithmetic() {
596        init_vanilla_registry();
597
598        let items = [97, 89, 83, 79, 73]
599            .into_iter()
600            .map(|max_stack_size| {
601                let mut patch = DataComponentPatch::new();
602                patch.set(MAX_STACK_SIZE, max_stack_size);
603                ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, patch)
604                    .expect("prime max stack size should be persistable")
605            })
606            .collect();
607        let mut patch = DataComponentPatch::new();
608        patch.set(BUNDLE_CONTENTS, BundleContents::new(items));
609        let template = ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 1, patch)
610            .expect("bundle with individually valid entries should persist");
611
612        assert!(template.create().is_empty());
613    }
614
615    fn oversized_stone_template() -> ItemStackTemplate {
616        let mut patch = DataComponentPatch::new();
617        patch.set(MAX_STACK_SIZE, 1);
618        ItemStackTemplate::try_with_count_and_patch(&vanilla_items::STONE, 2, patch)
619            .expect("template codec permits counts above the effective stack maximum")
620    }
621}