Skip to main content

steel_registry/data_components/components/
recipes.rs

1//! Vanilla `minecraft:recipes` item component.
2
3use std::io::{Cursor, Error, Result, Write};
4use std::str::FromStr;
5
6use simdnbt::owned::{NbtList, NbtTag, read_tag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::Identifier;
9use steel_utils::hash::{ComponentHasher, HashComponent};
10use steel_utils::nbt::vanilla_nbt_heap_size;
11use steel_utils::serial::{ReadFrom, WriteTo};
12
13const DEFAULT_NBT_QUOTA: u64 = 2_097_152;
14
15/// Ordered recipe resource keys carried by knowledge books.
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
17pub struct Recipes {
18    keys: Vec<Identifier>,
19}
20
21impl Recipes {
22    #[must_use]
23    pub const fn empty() -> Self {
24        Self { keys: Vec::new() }
25    }
26
27    #[must_use]
28    pub const fn new(keys: Vec<Identifier>) -> Self {
29        Self { keys }
30    }
31
32    #[must_use]
33    pub fn keys(&self) -> &[Identifier] {
34        &self.keys
35    }
36
37    fn to_nbt_tag_ref(&self) -> NbtTag {
38        if self.keys.is_empty() {
39            return NbtTag::List(NbtList::Empty);
40        }
41        NbtTag::List(NbtList::String(
42            self.keys.iter().map(|key| key.to_string().into()).collect(),
43        ))
44    }
45
46    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
47        let list = tag.list()?;
48        let values = list.as_nbt_tags();
49        if values.is_empty() {
50            return Some(Self::empty());
51        }
52        let keys = values
53            .iter()
54            .map(|key| Identifier::from_str(&key.string()?.to_string()).ok())
55            .collect::<Option<Vec<_>>>()?;
56        Some(Self::new(keys))
57    }
58}
59
60impl WriteTo for Recipes {
61    fn write(&self, writer: &mut impl Write) -> Result<()> {
62        let mut encoded = Vec::new();
63        self.to_nbt_tag_ref().write(&mut encoded);
64        writer.write_all(&encoded)
65    }
66}
67
68impl ReadFrom for Recipes {
69    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
70        let tag =
71            read_tag(data).map_err(|error| Error::other(format!("Invalid NBT: {error:?}")))?;
72        let Some(heap_size) = vanilla_nbt_heap_size(&tag) else {
73            return Err(Error::other("NBT contains malformed modified UTF-8"));
74        };
75        if heap_size > DEFAULT_NBT_QUOTA {
76            return Err(Error::other(format!(
77                "NBT exceeds Vanilla's {DEFAULT_NBT_QUOTA}-byte heap quota"
78            )));
79        }
80        Self::from_owned_nbt(&tag)
81            .ok_or_else(|| Error::other("Recipes network value is not a list of recipe keys"))
82    }
83}
84
85impl ToNbtTag for Recipes {
86    fn to_nbt_tag(self) -> NbtTag {
87        self.to_nbt_tag_ref()
88    }
89}
90
91impl FromNbtTag for Recipes {
92    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
93        Self::from_owned_nbt(&tag.to_owned())
94    }
95}
96
97impl HashComponent for Recipes {
98    fn hash_component(&self, hasher: &mut ComponentHasher) {
99        hasher.start_list();
100        for key in &self.keys {
101            hasher.put_component_hash(key);
102        }
103        hasher.end_list();
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use std::io::Cursor;
110
111    use simdnbt::borrow::read_tag;
112    use simdnbt::owned::{NbtList, NbtTag};
113    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
114    use steel_utils::Identifier;
115    use steel_utils::hash::HashComponent as _;
116    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
117
118    use super::Recipes;
119    use crate::data_components::vanilla_components::RECIPES;
120    use crate::init_vanilla_registry;
121    use crate::{REGISTRY, RegistryExt};
122
123    fn parse(tag: NbtTag) -> Option<Recipes> {
124        let mut bytes = Vec::new();
125        tag.write(&mut bytes);
126        let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
127        Recipes::from_nbt_tag(borrowed.as_tag())
128    }
129
130    #[test]
131    fn recipe_keys_round_trip_persistent_and_derived_network_codecs() {
132        let recipes = Recipes::new(vec![
133            Identifier::vanilla_static("oak_planks"),
134            Identifier::new_static("steel", "example"),
135        ]);
136        let nbt = recipes.clone().to_nbt_tag();
137        assert_eq!(parse(nbt.clone()), Some(recipes.clone()));
138        assert_eq!(recipes.compute_hash(), nbt.compute_hash());
139
140        let mut network = Vec::new();
141        recipes.write(&mut network).expect("recipes should encode");
142        assert_eq!(
143            Recipes::read(&mut Cursor::new(network.as_slice())).expect("recipes should decode"),
144            recipes
145        );
146    }
147
148    #[test]
149    fn empty_and_abbreviated_recipe_key_lists_match_identifier_codec_rules() {
150        let empty = Recipes::empty();
151        assert_eq!(empty.clone().to_nbt_tag(), NbtTag::List(NbtList::Empty));
152        assert_eq!(
153            parse(NbtTag::List(NbtList::String(Vec::new()))),
154            Some(empty)
155        );
156        assert_eq!(
157            parse(NbtTag::List(NbtList::String(vec!["stick".into()])))
158                .expect("abbreviated key should decode")
159                .keys(),
160            &[Identifier::vanilla_static("stick")]
161        );
162    }
163
164    #[test]
165    fn recipe_key_codec_does_not_require_a_registered_recipe() {
166        let unknown = Identifier::new_static("steel", "not_registered");
167        let recipes = Recipes::new(vec![unknown.clone()]);
168        assert_eq!(
169            parse(recipes.clone().to_nbt_tag())
170                .expect("resource keys are registry-independent")
171                .keys(),
172            &[unknown]
173        );
174    }
175
176    #[test]
177    fn extracted_knowledge_book_keeps_its_empty_recipe_list() {
178        init_vanilla_registry();
179        let knowledge_book = REGISTRY
180            .items
181            .by_key(&Identifier::vanilla_static("knowledge_book"))
182            .expect("knowledge book should be registered");
183        assert_eq!(
184            knowledge_book.components.get(RECIPES),
185            Some(Recipes::empty())
186        );
187    }
188}