steel_registry/data_components/components/
recipes.rs1use 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#[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}