1use steel_registry::data_components::vanilla_components::{
4 BANNER_PATTERNS, BASE_COLOR, DYE, DYED_COLOR, ENCHANTMENTS, FIREWORK_EXPLOSION, FIREWORKS,
5 MAP_POST_PROCESSING, MAX_DAMAGE, POT_DECORATIONS, POTION_CONTENTS, WRITTEN_BOOK_CONTENT,
6};
7use steel_registry::data_components::vanilla_components::{
8 DyedItemColor, FireworkExplosion, FireworkExplosionShape, Fireworks, ItemEnchantments,
9 MapPostProcessing, PotDecorations,
10};
11use steel_registry::recipe::{
12 BookCloningRecipe, CraftingInput, CraftingRecipe, DyeRecipe, FireworkRocketRecipe,
13 FireworkStarFadeRecipe, FireworkStarRecipe, PositionedCraftingInput, ShieldDecorationRecipe,
14 TypedRecipeRef, vanilla_recipe_types,
15};
16use steel_registry::{
17 DyeColor, REGISTRY, RegistryExt as _, TaggedRegistryExt as _, item_stack::ItemStack,
18 vanilla_enchantment_tags, vanilla_items,
19};
20
21use crate::inventory::container::CraftingContainer;
22
23use super::container::Container;
24
25pub type CraftingRecipeRef = TypedRecipeRef<CraftingRecipe, CraftingInput>;
27
28pub fn slot_changed_crafting_grid<R: Container>(crafting: &CraftingContainer, result: &mut R) {
30 result.set_item(
31 0,
32 assemble_for_container(crafting).unwrap_or_else(ItemStack::empty),
33 );
34}
35
36#[must_use]
38pub fn find_recipe(crafting: &CraftingContainer) -> Option<CraftingRecipeRef> {
39 let positioned = crafting.as_positioned_input();
40 REGISTRY
41 .recipes
42 .find_match(&vanilla_recipe_types::CRAFTING, &positioned.input)
43}
44
45#[must_use]
47pub fn assemble_for_container(crafting: &CraftingContainer) -> Option<ItemStack> {
48 let positioned = crafting.as_positioned_input();
49 let recipe = find_recipe(crafting)?;
50 Some(assemble_recipe(recipe, &positioned.input))
51}
52
53#[must_use]
55pub fn assemble_recipe(recipe: CraftingRecipeRef, input: &CraftingInput) -> ItemStack {
56 match recipe.data() {
57 CraftingRecipe::Shaped(recipe) => recipe.result.create(),
58 CraftingRecipe::Shapeless(recipe) => recipe.result.create(),
59 CraftingRecipe::Transmute(recipe) => {
60 let material_count = input
61 .items
62 .iter()
63 .filter(|stack| !stack.is_empty() && recipe.material.test(stack))
64 .count();
65 let extra_count = if recipe.add_material_count_to_result {
66 i32::try_from(material_count).unwrap_or(i32::MAX)
67 } else {
68 0
69 };
70 input
71 .items
72 .iter()
73 .find(|stack| !stack.is_empty() && recipe.input.test(stack))
74 .map_or_else(ItemStack::empty, |source| {
75 recipe.result.apply(
76 recipe.result.count().saturating_add(extra_count),
77 source.components_patch(),
78 )
79 })
80 }
81 CraftingRecipe::Dye(recipe) => assemble_dye(recipe, input),
82 CraftingRecipe::DecoratedPot(recipe) => {
83 let decorations = [
84 input.get(1, 0).item(),
85 input.get(0, 1).item(),
86 input.get(2, 1).item(),
87 input.get(1, 2).item(),
88 ];
89 let Ok(decorations) = PotDecorations::from_ordered(&decorations) else {
90 return ItemStack::empty();
91 };
92 let mut result = recipe.result.create();
93 result.set(POT_DECORATIONS, decorations);
94 result
95 }
96 CraftingRecipe::Imbue(recipe) => {
97 let mut result = recipe.result.create();
98 if let Some(contents) = input.get(1, 1).get(POTION_CONTENTS) {
99 result.set(POTION_CONTENTS, contents.clone());
100 }
101 result
102 }
103 CraftingRecipe::BannerDuplicate(recipe) => input
104 .items
105 .iter()
106 .find(|stack| {
107 recipe.banner.test(stack)
108 && stack
109 .get(BANNER_PATTERNS)
110 .is_some_and(|patterns| !patterns.layers().is_empty())
111 })
112 .map_or_else(ItemStack::empty, |source| {
113 recipe
114 .result
115 .apply(recipe.result.count(), source.components_patch())
116 }),
117 CraftingRecipe::BookCloning(recipe) => assemble_book_cloning(recipe, input),
118 CraftingRecipe::FireworkRocket(recipe) => assemble_firework_rocket(recipe, input),
119 CraftingRecipe::FireworkStar(recipe) => assemble_firework_star(recipe, input),
120 CraftingRecipe::FireworkStarFade(recipe) => assemble_firework_fade(recipe, input),
121 CraftingRecipe::MapExtending(recipe) => {
122 let source = input.get(1, 1);
123 let mut result = recipe
124 .result
125 .apply(recipe.result.count(), source.components_patch());
126 result.set(MAP_POST_PROCESSING, MapPostProcessing::Scale);
127 result
128 }
129 CraftingRecipe::RepairItem(_) => assemble_repair(input),
130 CraftingRecipe::ShieldDecoration(recipe) => assemble_shield(recipe, input),
131 }
132}
133
134#[must_use]
137pub fn get_remaining_items(
138 crafting: &CraftingContainer,
139) -> Option<(Vec<ItemStack>, PositionedCraftingInput)> {
140 let positioned = crafting.as_positioned_input();
141 let recipe = find_recipe(crafting)?;
142 let mut remainders: Vec<_> = positioned
143 .input
144 .items
145 .iter()
146 .map(|stack| {
147 if stack.is_empty() {
148 ItemStack::empty()
149 } else {
150 stack.item().get_crafting_remainder()
151 }
152 })
153 .collect();
154
155 match recipe.data() {
156 CraftingRecipe::BannerDuplicate(_) => {
157 for (slot, stack) in positioned.input.items.iter().enumerate() {
158 if remainders[slot].is_empty()
159 && stack
160 .get(BANNER_PATTERNS)
161 .is_some_and(|patterns| !patterns.layers().is_empty())
162 {
163 remainders[slot] = stack.copy_with_count(1);
164 }
165 }
166 }
167 CraftingRecipe::BookCloning(_) => {
168 for (slot, stack) in positioned.input.items.iter().enumerate() {
169 if remainders[slot].is_empty() && stack.has(WRITTEN_BOOK_CONTENT) {
170 remainders[slot] = stack.copy_with_count(1);
171 break;
172 }
173 }
174 }
175 _ => {}
176 }
177
178 Some((remainders, positioned))
179}
180
181fn assemble_dye(recipe: &DyeRecipe, input: &CraftingInput) -> ItemStack {
182 let mut target = None;
183 let mut dyes = Vec::new();
184 for stack in input.items.iter().filter(|stack| !stack.is_empty()) {
185 if recipe.target.test(stack) {
186 target = Some(stack);
187 } else if recipe.dye.test(stack) {
188 dyes.push(stack.get(DYE).copied().unwrap_or(DyeColor::White));
189 }
190 }
191 let Some(target) = target else {
192 return ItemStack::empty();
193 };
194 if dyes.is_empty() {
195 return ItemStack::empty();
196 }
197 let color = DyedItemColor::apply_dyes(target.get(DYED_COLOR).copied(), &dyes);
198 let mut result = recipe
199 .result
200 .apply(recipe.result.count(), target.components_patch());
201 result.set(DYED_COLOR, color);
202 result
203}
204
205fn assemble_book_cloning(recipe: &BookCloningRecipe, input: &CraftingInput) -> ItemStack {
206 let Some(source) = input
207 .items
208 .iter()
209 .find(|stack| !stack.is_empty() && recipe.source.test(stack))
210 else {
211 return ItemStack::empty();
212 };
213 let Some(content) = source.get(WRITTEN_BOOK_CONTENT) else {
214 return ItemStack::empty();
215 };
216 let material_count = input
217 .items
218 .iter()
219 .filter(|stack| !stack.is_empty() && recipe.material.test(stack))
220 .count();
221 let Ok(extra_count) = i32::try_from(material_count.saturating_sub(1)) else {
222 return ItemStack::empty();
223 };
224 let mut result = recipe.result.apply(
225 recipe.result.count().saturating_add(extra_count),
226 source.components_patch(),
227 );
228 result.set(WRITTEN_BOOK_CONTENT, content.craft_copy());
229 result
230}
231
232fn assemble_firework_rocket(recipe: &FireworkRocketRecipe, input: &CraftingInput) -> ItemStack {
233 let mut flight_duration = 0;
234 let mut explosions = Vec::new();
235 for stack in input.items.iter().filter(|stack| !stack.is_empty()) {
236 if recipe.fuel.test(stack) {
237 flight_duration += 1;
238 } else if recipe.star.test(stack)
239 && let Some(explosion) = stack.get(FIREWORK_EXPLOSION)
240 {
241 explosions.push(explosion.clone());
242 }
243 }
244 let Ok(fireworks) = Fireworks::new(flight_duration, explosions) else {
245 return ItemStack::empty();
246 };
247 let mut result = recipe.result.create();
248 result.set(FIREWORKS, fireworks);
249 result
250}
251
252fn assemble_firework_star(recipe: &FireworkStarRecipe, input: &CraftingInput) -> ItemStack {
253 let mut shape = FireworkExplosionShape::SmallBall;
254 let mut has_twinkle = false;
255 let mut has_trail = false;
256 let mut colors = Vec::new();
257 for stack in input.items.iter().filter(|stack| !stack.is_empty()) {
258 if let Some((found_shape, _)) = recipe
259 .shapes
260 .iter()
261 .find(|(_, ingredient)| ingredient.test(stack))
262 {
263 shape = *found_shape;
264 } else if recipe.twinkle.test(stack) {
265 has_twinkle = true;
266 } else if recipe.trail.test(stack) {
267 has_trail = true;
268 } else if recipe.dye.test(stack) {
269 colors.push(
270 stack
271 .get(DYE)
272 .copied()
273 .unwrap_or(DyeColor::White)
274 .firework_color(),
275 );
276 }
277 }
278 let mut result = recipe.result.create();
279 result.set(
280 FIREWORK_EXPLOSION,
281 FireworkExplosion::new(shape, colors, Vec::new(), has_trail, has_twinkle),
282 );
283 result
284}
285
286fn assemble_firework_fade(recipe: &FireworkStarFadeRecipe, input: &CraftingInput) -> ItemStack {
287 let Some(target) = input
288 .items
289 .iter()
290 .find(|stack| !stack.is_empty() && recipe.target.test(stack))
291 else {
292 return ItemStack::empty();
293 };
294 let fade_colors: Vec<_> = input
295 .items
296 .iter()
297 .filter(|stack| !stack.is_empty() && recipe.dye.test(stack))
298 .map(|stack| {
299 stack
300 .get(DYE)
301 .copied()
302 .unwrap_or(DyeColor::White)
303 .firework_color()
304 })
305 .collect();
306 let mut result = recipe
307 .result
308 .apply(recipe.result.count(), target.components_patch());
309 let explosion = target
310 .get(FIREWORK_EXPLOSION)
311 .cloned()
312 .unwrap_or_default()
313 .with_fade_colors(fade_colors);
314 result.set(FIREWORK_EXPLOSION, explosion);
315 result
316}
317
318fn assemble_repair(input: &CraftingInput) -> ItemStack {
319 let mut inputs = input.items.iter().filter(|stack| !stack.is_empty());
320 let Some(first) = inputs.next() else {
321 return ItemStack::empty();
322 };
323 let Some(second) = inputs.next() else {
324 return ItemStack::empty();
325 };
326 let durability = first.get_max_damage().max(second.get_max_damage());
327 let remaining = (first.get_max_damage() - first.get_damage_value())
328 + (second.get_max_damage() - second.get_damage_value())
329 + durability * 5 / 100;
330 let mut result = ItemStack::new(first.item());
331 result.set(MAX_DAMAGE, durability);
332 result.set_damage_value((durability - remaining).max(0));
333
334 let mut curses = ItemEnchantments::empty();
335 for enchantments in [
336 first.get_enchantments_for_crafting(),
337 second.get_enchantments_for_crafting(),
338 ]
339 .into_iter()
340 .flatten()
341 {
342 for (key, level) in enchantments.iter() {
343 let Some(enchantment) = REGISTRY.enchantments.by_key(key) else {
344 continue;
345 };
346 if REGISTRY.enchantments.is_in_tag(
347 enchantment,
348 &vanilla_enchantment_tags::EnchantmentTag::CURSE,
349 ) {
350 curses.upgrade(key.clone(), *level);
351 }
352 }
353 }
354 result.set(ENCHANTMENTS, curses);
355 result
356}
357
358fn assemble_shield(recipe: &ShieldDecorationRecipe, input: &CraftingInput) -> ItemStack {
359 let Some(banner) = input
360 .items
361 .iter()
362 .find(|stack| !stack.is_empty() && recipe.banner.test(stack))
363 else {
364 return ItemStack::empty();
365 };
366 let Some(target) = input
367 .items
368 .iter()
369 .find(|stack| !stack.is_empty() && recipe.target.test(stack))
370 else {
371 return ItemStack::empty();
372 };
373 let mut result = recipe
374 .result
375 .apply(recipe.result.count(), target.components_patch());
376 if let Some(patterns) = banner.get(BANNER_PATTERNS) {
377 result.set(BANNER_PATTERNS, patterns.clone());
378 }
379 result.set(BASE_COLOR, banner_color(banner));
380 result
381}
382
383fn banner_color(stack: &ItemStack) -> DyeColor {
384 let banners = [
385 (&*vanilla_items::WHITE_BANNER, DyeColor::White),
386 (&*vanilla_items::ORANGE_BANNER, DyeColor::Orange),
387 (&*vanilla_items::MAGENTA_BANNER, DyeColor::Magenta),
388 (&*vanilla_items::LIGHT_BLUE_BANNER, DyeColor::LightBlue),
389 (&*vanilla_items::YELLOW_BANNER, DyeColor::Yellow),
390 (&*vanilla_items::LIME_BANNER, DyeColor::Lime),
391 (&*vanilla_items::PINK_BANNER, DyeColor::Pink),
392 (&*vanilla_items::GRAY_BANNER, DyeColor::Gray),
393 (&*vanilla_items::LIGHT_GRAY_BANNER, DyeColor::LightGray),
394 (&*vanilla_items::CYAN_BANNER, DyeColor::Cyan),
395 (&*vanilla_items::PURPLE_BANNER, DyeColor::Purple),
396 (&*vanilla_items::BLUE_BANNER, DyeColor::Blue),
397 (&*vanilla_items::BROWN_BANNER, DyeColor::Brown),
398 (&*vanilla_items::GREEN_BANNER, DyeColor::Green),
399 (&*vanilla_items::RED_BANNER, DyeColor::Red),
400 (&*vanilla_items::BLACK_BANNER, DyeColor::Black),
401 ];
402 banners
403 .into_iter()
404 .find_map(|(item, color)| (stack.item() == item).then_some(color))
405 .unwrap_or(DyeColor::White)
406}
407
408#[cfg(test)]
409mod tests {
410 use steel_registry::data_components::vanilla_components::DYE;
411 use steel_registry::recipe::{CraftingInput, vanilla_recipe_types};
412 use steel_registry::{DyeColor, REGISTRY, init_vanilla_registry, vanilla_items};
413
414 use super::assemble_recipe;
415 use steel_registry::item_stack::ItemStack;
416
417 #[test]
418 fn external_crafting_interpreter_assembles_a_generated_dye_recipe() {
419 init_vanilla_registry();
420 let input = CraftingInput::new(
421 2,
422 1,
423 vec![
424 ItemStack::new(&vanilla_items::RED_DYE),
425 ItemStack::new(&vanilla_items::BLUE_DYE),
426 ],
427 );
428 let Some(recipe) = REGISTRY
429 .recipes
430 .find_match(&vanilla_recipe_types::CRAFTING, &input)
431 else {
432 panic!("red and blue dye should match the purple dye recipe");
433 };
434
435 let result = assemble_recipe(recipe, &input);
436
437 assert!(result.is(&vanilla_items::PURPLE_DYE));
438 assert_eq!(result.get(DYE), Some(&DyeColor::Purple));
439 }
440}