steel_registry/recipe/crafting/
matching.rs1use crate::data_components::vanilla_components::{
4 BANNER_PATTERNS, DAMAGE, DYE, MAX_DAMAGE, WRITTEN_BOOK_CONTENT,
5};
6use crate::item_stack::ItemStack;
7use crate::item_stack_template::ItemStackTemplate;
8
9use super::{
10 BannerDuplicateRecipe, BookCloningRecipe, CraftingInput, CraftingRecipe, DecoratedPotRecipe,
11 DyeRecipe, FireworkRocketRecipe, FireworkStarFadeRecipe, FireworkStarRecipe, ImbueRecipe,
12 Ingredient, MapExtendingRecipe, RepairItemRecipe, ShapedRecipe, ShapelessRecipe,
13 ShieldDecorationRecipe, TransmuteRecipe,
14};
15
16const CRAFTING_TABLE_GRID_SIZE: usize = 3;
17const CRAFTING_TABLE_SLOT_COUNT: usize = CRAFTING_TABLE_GRID_SIZE * CRAFTING_TABLE_GRID_SIZE;
18const CRAFTING_TABLE_CENTER: usize = CRAFTING_TABLE_GRID_SIZE / 2;
19const CRAFTING_TABLE_LAST_INDEX: usize = CRAFTING_TABLE_GRID_SIZE - 1;
20const CRAFTING_TABLE_CENTER_SLOT: usize = CRAFTING_TABLE_SLOT_COUNT / 2;
21const DECORATED_POT_INGREDIENT_COUNT: usize = 4;
22const MAX_BANNER_PATTERNS: usize = 6;
23const MIN_FIREWORK_ROCKET_FUEL_COUNT: usize = 1;
24const MAX_FIREWORK_ROCKET_FUEL_COUNT: usize = 3;
25const MAX_MAP_SCALE: u8 = 4;
26
27pub(crate) fn matches(recipe: &CraftingRecipe, input: &CraftingInput) -> bool {
28 match recipe {
29 CraftingRecipe::Shaped(recipe) => shaped(recipe, input),
30 CraftingRecipe::Shapeless(recipe) => shapeless(recipe, input),
31 CraftingRecipe::Transmute(recipe) => transmute(recipe, input),
32 CraftingRecipe::Dye(recipe) => dye(recipe, input),
33 CraftingRecipe::DecoratedPot(recipe) => decorated_pot(recipe, input),
34 CraftingRecipe::Imbue(recipe) => imbue(recipe, input),
35 CraftingRecipe::BannerDuplicate(recipe) => banner_duplicate(recipe, input),
36 CraftingRecipe::BookCloning(recipe) => book_cloning(recipe, input),
37 CraftingRecipe::FireworkRocket(recipe) => firework_rocket(recipe, input),
38 CraftingRecipe::FireworkStar(recipe) => firework_star(recipe, input),
39 CraftingRecipe::FireworkStarFade(recipe) => firework_star_fade(recipe, input),
40 CraftingRecipe::MapExtending(recipe) => map_extending(recipe, input),
41 CraftingRecipe::RepairItem(recipe) => repair_item(recipe, input),
42 CraftingRecipe::ShieldDecoration(recipe) => shield_decoration(recipe, input),
43 }
44}
45
46fn occupied_stacks(input: &CraftingInput) -> impl Iterator<Item = &ItemStack> {
47 input.items.iter().filter(|stack| !stack.is_empty())
48}
49
50fn shaped(recipe: &ShapedRecipe, input: &CraftingInput) -> bool {
51 if input.ingredient_count()
52 != recipe
53 .pattern
54 .iter()
55 .filter(|ingredient| !ingredient.is_empty())
56 .count()
57 || input.width != recipe.width
58 || input.height != recipe.height
59 {
60 return false;
61 }
62 matches_shaped_orientation(recipe, input, false)
63 || (!recipe.symmetrical && matches_shaped_orientation(recipe, input, true))
64}
65
66fn matches_shaped_orientation(
67 recipe: &ShapedRecipe,
68 input: &CraftingInput,
69 mirrored: bool,
70) -> bool {
71 for y in 0..recipe.height {
72 for x in 0..recipe.width {
73 let pattern_x = if mirrored { recipe.width - 1 - x } else { x };
74 if !recipe.pattern[y * recipe.width + pattern_x].test(input.get(x, y)) {
75 return false;
76 }
77 }
78 }
79 true
80}
81
82fn shapeless(recipe: &ShapelessRecipe, input: &CraftingInput) -> bool {
83 if input.ingredient_count() != recipe.ingredients.len() {
84 return false;
85 }
86 let items: Vec<_> = occupied_stacks(input).collect();
87 let mut used = vec![false; items.len()];
88 match_shapeless_ingredients(&recipe.ingredients, &items, &mut used)
89}
90
91fn match_shapeless_ingredients(
92 ingredients: &[Ingredient],
93 items: &[&ItemStack],
94 used: &mut [bool],
95) -> bool {
96 let Some((ingredient, remaining_ingredients)) = ingredients.split_first() else {
97 return true;
98 };
99 for (item_index, item) in items.iter().enumerate() {
100 if used[item_index] || !ingredient.test(item) {
101 continue;
102 }
103 used[item_index] = true;
104 if match_shapeless_ingredients(remaining_ingredients, items, used) {
105 return true;
106 }
107 used[item_index] = false;
108 }
109 false
110}
111
112fn transmute(recipe: &TransmuteRecipe, input: &CraftingInput) -> bool {
113 if input.ingredient_count() < recipe.min_material_count + 1
114 || input.ingredient_count() > recipe.max_material_count + 1
115 {
116 return false;
117 }
118 let mut found_input = None;
119 let mut material_count = 0;
120 for stack in occupied_stacks(input) {
121 if recipe.input.test(stack) {
122 if found_input.is_some() {
123 return false;
124 }
125 found_input = Some(stack);
126 } else if recipe.material.test(stack) {
127 material_count += 1;
128 if material_count > recipe.max_material_count {
129 return false;
130 }
131 } else {
132 return false;
133 }
134 }
135 let Some(found_input) = found_input else {
136 return false;
137 };
138 if !(recipe.min_material_count..=recipe.max_material_count).contains(&material_count) {
139 return false;
140 }
141 let base_count = recipe.result.as_ref().map_or(1, ItemStackTemplate::count);
142 let result_count = if recipe.add_material_count_to_result {
143 base_count + i32::try_from(material_count).unwrap_or(i32::MAX)
144 } else {
145 base_count
146 };
147 if result_count != 1 {
148 return true;
149 }
150 let Some(result) = &recipe.result else {
152 return false;
153 };
154 let result = result.apply(1, found_input.components_patch());
155 !result.is_empty() && !ItemStack::is_same_item_same_components(found_input, &result)
156}
157
158fn dye(recipe: &DyeRecipe, input: &CraftingInput) -> bool {
159 if input.ingredient_count() < 2 {
160 return false;
161 }
162 let mut has_target = false;
163 let mut has_dye = false;
164 for stack in occupied_stacks(input) {
165 if recipe.target.test(stack) {
166 if has_target {
167 return false;
168 }
169 has_target = true;
170 } else if recipe.dye.test(stack) && stack.has(DYE) {
171 has_dye = true;
172 } else {
173 return false;
174 }
175 }
176 has_target && has_dye
177}
178
179fn decorated_pot(recipe: &DecoratedPotRecipe, input: &CraftingInput) -> bool {
180 input.width == CRAFTING_TABLE_GRID_SIZE
181 && input.height == CRAFTING_TABLE_GRID_SIZE
182 && input.ingredient_count() == DECORATED_POT_INGREDIENT_COUNT
183 && recipe.back.test(input.get(CRAFTING_TABLE_CENTER, 0))
184 && recipe.left.test(input.get(0, CRAFTING_TABLE_CENTER))
185 && recipe
186 .right
187 .test(input.get(CRAFTING_TABLE_LAST_INDEX, CRAFTING_TABLE_CENTER))
188 && recipe
189 .front
190 .test(input.get(CRAFTING_TABLE_CENTER, CRAFTING_TABLE_LAST_INDEX))
191}
192
193fn imbue(recipe: &ImbueRecipe, input: &CraftingInput) -> bool {
194 if input.width != CRAFTING_TABLE_GRID_SIZE
195 || input.height != CRAFTING_TABLE_GRID_SIZE
196 || input.ingredient_count() != CRAFTING_TABLE_SLOT_COUNT
197 {
198 return false;
199 }
200 for y in 0..CRAFTING_TABLE_GRID_SIZE {
201 for x in 0..CRAFTING_TABLE_GRID_SIZE {
202 let ingredient = if x == CRAFTING_TABLE_CENTER && y == CRAFTING_TABLE_CENTER {
203 &recipe.source
204 } else {
205 &recipe.material
206 };
207 if !ingredient.test(input.get(x, y)) {
208 return false;
209 }
210 }
211 }
212 true
213}
214
215fn banner_duplicate(recipe: &BannerDuplicateRecipe, input: &CraftingInput) -> bool {
216 if input.ingredient_count() != 2 {
217 return false;
218 }
219 let mut has_target = false;
220 let mut has_source = false;
221 for stack in occupied_stacks(input) {
222 if !recipe.banner.test(stack) {
223 return false;
224 }
225 let pattern_count = stack
226 .get(BANNER_PATTERNS)
227 .map_or(0, |patterns| patterns.layers().len());
228 if pattern_count > MAX_BANNER_PATTERNS {
229 return false;
230 }
231 if pattern_count > 0 {
232 if has_source {
233 return false;
234 }
235 has_source = true;
236 } else {
237 if has_target {
238 return false;
239 }
240 has_target = true;
241 }
242 }
243 has_source && has_target
244}
245
246fn book_cloning(recipe: &BookCloningRecipe, input: &CraftingInput) -> bool {
247 if input.ingredient_count() < 2 {
248 return false;
249 }
250 let mut has_source = false;
251 let mut has_material = false;
252 for stack in occupied_stacks(input) {
253 if recipe.source.test(stack) {
254 let Some(content) = stack.get(WRITTEN_BOOK_CONTENT) else {
255 return false;
256 };
257 if has_source
258 || !(recipe.min_generation..=recipe.max_generation).contains(&content.generation())
259 {
260 return false;
261 }
262 has_source = true;
263 } else if recipe.material.test(stack) {
264 has_material = true;
265 } else {
266 return false;
267 }
268 }
269 has_source && has_material
270}
271
272fn firework_rocket(recipe: &FireworkRocketRecipe, input: &CraftingInput) -> bool {
273 if input.ingredient_count() < 2 {
274 return false;
275 }
276 let mut has_shell = false;
277 let mut fuel_count = 0;
278 for stack in occupied_stacks(input) {
279 if recipe.shell.test(stack) {
280 if has_shell {
281 return false;
282 }
283 has_shell = true;
284 } else if recipe.fuel.test(stack) {
285 fuel_count += 1;
286 if fuel_count > MAX_FIREWORK_ROCKET_FUEL_COUNT {
287 return false;
288 }
289 } else if !recipe.star.test(stack) {
290 return false;
291 }
292 }
293 has_shell && fuel_count >= MIN_FIREWORK_ROCKET_FUEL_COUNT
294}
295
296fn firework_star(recipe: &FireworkStarRecipe, input: &CraftingInput) -> bool {
297 if input.ingredient_count() < 2 {
298 return false;
299 }
300 let mut has_fuel = false;
301 let mut has_dye = false;
302 let mut has_shape = false;
303 let mut has_trail = false;
304 let mut has_twinkle = false;
305 for stack in occupied_stacks(input) {
306 if recipe.twinkle.test(stack) {
307 if has_twinkle {
308 return false;
309 }
310 has_twinkle = true;
311 } else if recipe.trail.test(stack) {
312 if has_trail {
313 return false;
314 }
315 has_trail = true;
316 } else if recipe.fuel.test(stack) {
317 if has_fuel {
318 return false;
319 }
320 has_fuel = true;
321 } else if recipe.dye.test(stack) && stack.has(DYE) {
322 has_dye = true;
323 } else if recipe
324 .shapes
325 .iter()
326 .any(|(_, ingredient)| ingredient.test(stack))
327 {
328 if has_shape {
329 return false;
330 }
331 has_shape = true;
332 } else {
333 return false;
334 }
335 }
336 has_fuel && has_dye
337}
338
339fn firework_star_fade(recipe: &FireworkStarFadeRecipe, input: &CraftingInput) -> bool {
340 if input.ingredient_count() < 2 {
341 return false;
342 }
343 let mut has_target = false;
344 let mut has_dye = false;
345 for stack in occupied_stacks(input) {
346 if recipe.dye.test(stack) && stack.has(DYE) {
347 has_dye = true;
348 } else if recipe.target.test(stack) {
349 if has_target {
350 return false;
351 }
352 has_target = true;
353 } else {
354 return false;
355 }
356 }
357 has_target && has_dye
358}
359
360fn map_extending(recipe: &MapExtendingRecipe, input: &CraftingInput) -> bool {
365 if input.width != CRAFTING_TABLE_GRID_SIZE
366 || input.height != CRAFTING_TABLE_GRID_SIZE
367 || input.ingredient_count() != CRAFTING_TABLE_SLOT_COUNT
368 {
369 return false;
370 }
371 for y in 0..CRAFTING_TABLE_GRID_SIZE {
372 for x in 0..CRAFTING_TABLE_GRID_SIZE {
373 let ingredient = if x == CRAFTING_TABLE_CENTER && y == CRAFTING_TABLE_CENTER {
374 &recipe.map
375 } else {
376 &recipe.material
377 };
378 if !ingredient.test(input.get(x, y)) {
379 return false;
380 }
381 }
382 }
383 let Some(data) = input.map_data(CRAFTING_TABLE_CENTER_SLOT) else {
385 return false;
386 };
387 !data.exploration_map && data.scale < MAX_MAP_SCALE
388}
389
390fn repair_item(_recipe: &RepairItemRecipe, input: &CraftingInput) -> bool {
391 if input.ingredient_count() != 2 {
392 return false;
393 }
394 let mut stacks = occupied_stacks(input);
395 let Some(first) = stacks.next() else {
396 return false;
397 };
398 let Some(second) = stacks.next() else {
399 return false;
400 };
401 first.item() == second.item()
402 && first.count() == 1
403 && second.count() == 1
404 && first.has(MAX_DAMAGE)
405 && second.has(MAX_DAMAGE)
406 && first.has(DAMAGE)
407 && second.has(DAMAGE)
408}
409
410fn shield_decoration(recipe: &ShieldDecorationRecipe, input: &CraftingInput) -> bool {
411 if input.ingredient_count() != 2 {
412 return false;
413 }
414 let mut has_banner = false;
415 let mut has_target = false;
416 for stack in occupied_stacks(input) {
417 if recipe.banner.test(stack) {
418 if has_banner {
419 return false;
420 }
421 has_banner = true;
422 } else if recipe.target.test(stack) {
423 if has_target
424 || stack
425 .get(BANNER_PATTERNS)
426 .is_some_and(|patterns| !patterns.layers().is_empty())
427 {
428 return false;
429 }
430 has_target = true;
431 } else {
432 return false;
433 }
434 }
435 has_banner && has_target
436}
437
438#[cfg(test)]
439mod tests {
440 use crate::item_stack_template::ItemStackTemplate;
441 use crate::recipe::RecipeProperties;
442 use crate::{init_vanilla_registry, vanilla_items};
443
444 use super::*;
445
446 fn asymmetric_shaped_recipe(pattern: Vec<Ingredient>) -> ShapedRecipe {
447 ShapedRecipe::new(
448 RecipeProperties::special(),
449 pattern.len(),
450 1,
451 pattern.into_boxed_slice(),
452 ItemStackTemplate::new(&vanilla_items::PURPLE_DYE),
453 )
454 }
455
456 #[test]
457 fn asymmetric_shaped_recipe_matches_both_orientations() {
458 init_vanilla_registry();
459 let recipe = asymmetric_shaped_recipe(vec![
460 Ingredient::Item(&vanilla_items::RED_DYE),
461 Ingredient::Item(&vanilla_items::BLUE_DYE),
462 ]);
463 let forward = CraftingInput::new(
464 2,
465 1,
466 vec![
467 ItemStack::new(&vanilla_items::RED_DYE),
468 ItemStack::new(&vanilla_items::BLUE_DYE),
469 ],
470 );
471 let mirrored = CraftingInput::new(
472 2,
473 1,
474 vec![
475 ItemStack::new(&vanilla_items::BLUE_DYE),
476 ItemStack::new(&vanilla_items::RED_DYE),
477 ],
478 );
479
480 assert!(shaped(&recipe, &forward));
481 assert!(shaped(&recipe, &mirrored));
482 }
483
484 #[test]
485 fn shaped_recipe_requires_empty_pattern_slots() {
486 init_vanilla_registry();
487 let recipe = asymmetric_shaped_recipe(vec![
488 Ingredient::Item(&vanilla_items::RED_DYE),
489 Ingredient::Empty,
490 Ingredient::Item(&vanilla_items::BLUE_DYE),
491 ]);
492 let valid = CraftingInput::new(
493 3,
494 1,
495 vec![
496 ItemStack::new(&vanilla_items::RED_DYE),
497 ItemStack::empty(),
498 ItemStack::new(&vanilla_items::BLUE_DYE),
499 ],
500 );
501 let shifted = CraftingInput::new(
502 3,
503 1,
504 vec![
505 ItemStack::new(&vanilla_items::RED_DYE),
506 ItemStack::new(&vanilla_items::BLUE_DYE),
507 ItemStack::empty(),
508 ],
509 );
510
511 assert!(shaped(&recipe, &valid));
512 assert!(!shaped(&recipe, &shifted));
513 }
514
515 #[test]
516 fn firework_rocket_requires_one_to_three_fuel_items() {
517 init_vanilla_registry();
518 let recipe = FireworkRocketRecipe {
519 properties: RecipeProperties::special(),
520 shell: Ingredient::Item(&vanilla_items::PAPER),
521 fuel: Ingredient::Item(&vanilla_items::GUNPOWDER),
522 star: Ingredient::Item(&vanilla_items::FIREWORK_STAR),
523 result: ItemStackTemplate::new(&vanilla_items::FIREWORK_ROCKET),
524 };
525 let input_with_fuel = |fuel_count| {
526 let mut items = vec![ItemStack::new(&vanilla_items::PAPER)];
527 items.extend(
528 std::iter::repeat_with(|| ItemStack::new(&vanilla_items::GUNPOWDER))
529 .take(fuel_count),
530 );
531 CraftingInput::new(items.len(), 1, items)
532 };
533
534 assert!(!firework_rocket(&recipe, &input_with_fuel(0)));
535 assert!(firework_rocket(
536 &recipe,
537 &input_with_fuel(MIN_FIREWORK_ROCKET_FUEL_COUNT)
538 ));
539 assert!(firework_rocket(
540 &recipe,
541 &input_with_fuel(MAX_FIREWORK_ROCKET_FUEL_COUNT)
542 ));
543 assert!(!firework_rocket(
544 &recipe,
545 &input_with_fuel(MAX_FIREWORK_ROCKET_FUEL_COUNT + 1)
546 ));
547 }
548}