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