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