Skip to main content

steel_registry/recipe/crafting/
mod.rs

1//! Passive data and immutable input snapshots for crafting recipes.
2
3mod matching;
4
5use steel_utils::{DowncastType, DowncastTypeKey};
6
7use crate::data_components::vanilla_components::FireworkExplosionShape;
8use crate::item_stack::ItemStack;
9use crate::item_stack_template::ItemStackTemplate;
10
11use super::{Ingredient, RecipeData, RecipeInput, RecipeMatches, RecipeProperties};
12
13/// A shaped recipe pattern.
14#[derive(Debug)]
15pub struct ShapedRecipe {
16    pub properties: RecipeProperties,
17    pub width: usize,
18    pub height: usize,
19    pub pattern: Box<[Ingredient]>,
20    pub result: ItemStackTemplate,
21    /// Whether the pattern is horizontally symmetric.
22    pub symmetrical: bool,
23}
24
25impl ShapedRecipe {
26    #[must_use]
27    pub fn new(
28        mut properties: RecipeProperties,
29        width: usize,
30        height: usize,
31        pattern: Box<[Ingredient]>,
32        result: ItemStackTemplate,
33    ) -> Self {
34        let symmetrical = Self::compute_symmetrical(width, &pattern);
35        if properties.placement.is_none() {
36            properties.placement = Some(super::PlacementInfo::from_optional_ingredients(&pattern));
37        }
38        Self {
39            properties,
40            width,
41            height,
42            pattern,
43            result,
44            symmetrical,
45        }
46    }
47
48    #[must_use]
49    pub fn with_placement(mut self, placement: super::PlacementInfo) -> Self {
50        self.properties.placement = Some(placement);
51        self
52    }
53
54    fn compute_symmetrical(width: usize, pattern: &[Ingredient]) -> bool {
55        if width == 0 {
56            return true;
57        }
58        let height = pattern.len() / width;
59        for y in 0..height {
60            for x in 0..width / 2 {
61                if !pattern[y * width + x].eq_ingredient(&pattern[y * width + (width - 1 - x)]) {
62                    return false;
63                }
64            }
65        }
66        true
67    }
68
69    #[must_use]
70    pub const fn fits_in(&self, width: usize, height: usize) -> bool {
71        self.width <= width && self.height <= height
72    }
73}
74
75/// A shapeless collection of ingredients.
76#[derive(Debug)]
77pub struct ShapelessRecipe {
78    pub properties: RecipeProperties,
79    pub ingredients: Box<[Ingredient]>,
80    pub result: ItemStackTemplate,
81}
82
83impl ShapelessRecipe {
84    #[must_use]
85    pub fn new(
86        mut properties: RecipeProperties,
87        ingredients: Box<[Ingredient]>,
88        result: ItemStackTemplate,
89    ) -> Self {
90        if properties.placement.is_none() {
91            properties.placement = Some(super::PlacementInfo::from_ingredients(&ingredients));
92        }
93        Self {
94            properties,
95            ingredients,
96            result,
97        }
98    }
99
100    #[must_use]
101    pub fn with_placement(mut self, placement: super::PlacementInfo) -> Self {
102        self.properties.placement = Some(placement);
103        self
104    }
105
106    #[must_use]
107    pub const fn fits_in(&self, width: usize, height: usize) -> bool {
108        self.ingredients.len() <= width * height
109    }
110}
111
112#[derive(Debug)]
113pub struct TransmuteRecipe {
114    pub properties: RecipeProperties,
115    pub input: Ingredient,
116    pub material: Ingredient,
117    pub min_material_count: usize,
118    pub max_material_count: usize,
119    pub result: ItemStackTemplate,
120    pub add_material_count_to_result: bool,
121}
122
123#[derive(Debug)]
124pub struct DyeRecipe {
125    pub properties: RecipeProperties,
126    pub target: Ingredient,
127    pub dye: Ingredient,
128    pub result: ItemStackTemplate,
129}
130
131#[derive(Debug)]
132pub struct DecoratedPotRecipe {
133    pub properties: RecipeProperties,
134    pub back: Ingredient,
135    pub left: Ingredient,
136    pub right: Ingredient,
137    pub front: Ingredient,
138    pub result: ItemStackTemplate,
139}
140
141#[derive(Debug)]
142pub struct ImbueRecipe {
143    pub properties: RecipeProperties,
144    pub source: Ingredient,
145    pub material: Ingredient,
146    pub result: ItemStackTemplate,
147}
148
149#[derive(Debug)]
150pub struct BannerDuplicateRecipe {
151    pub properties: RecipeProperties,
152    pub banner: Ingredient,
153    pub result: ItemStackTemplate,
154}
155
156#[derive(Debug)]
157pub struct BookCloningRecipe {
158    pub properties: RecipeProperties,
159    pub source: Ingredient,
160    pub material: Ingredient,
161    pub min_generation: i32,
162    pub max_generation: i32,
163    pub result: ItemStackTemplate,
164}
165
166#[derive(Debug)]
167pub struct FireworkRocketRecipe {
168    pub properties: RecipeProperties,
169    pub shell: Ingredient,
170    pub fuel: Ingredient,
171    pub star: Ingredient,
172    pub result: ItemStackTemplate,
173}
174
175#[derive(Debug)]
176pub struct FireworkStarRecipe {
177    pub properties: RecipeProperties,
178    pub shapes: Box<[(FireworkExplosionShape, Ingredient)]>,
179    pub trail: Ingredient,
180    pub twinkle: Ingredient,
181    pub fuel: Ingredient,
182    pub dye: Ingredient,
183    pub result: ItemStackTemplate,
184}
185
186#[derive(Debug)]
187pub struct FireworkStarFadeRecipe {
188    pub properties: RecipeProperties,
189    pub target: Ingredient,
190    pub dye: Ingredient,
191    pub result: ItemStackTemplate,
192}
193
194#[derive(Debug)]
195pub struct MapExtendingRecipe {
196    pub properties: RecipeProperties,
197    pub map: Ingredient,
198    pub material: Ingredient,
199    pub result: ItemStackTemplate,
200}
201
202#[derive(Debug)]
203pub struct RepairItemRecipe {
204    pub properties: RecipeProperties,
205}
206
207#[derive(Debug)]
208pub struct ShieldDecorationRecipe {
209    pub properties: RecipeProperties,
210    pub banner: Ingredient,
211    pub target: Ingredient,
212    pub result: ItemStackTemplate,
213}
214
215/// Every vanilla serializer whose operational type is `minecraft:crafting`.
216#[derive(Debug)]
217pub enum CraftingRecipe {
218    Shaped(ShapedRecipe),
219    Shapeless(ShapelessRecipe),
220    Transmute(TransmuteRecipe),
221    Dye(DyeRecipe),
222    DecoratedPot(DecoratedPotRecipe),
223    Imbue(ImbueRecipe),
224    BannerDuplicate(BannerDuplicateRecipe),
225    BookCloning(BookCloningRecipe),
226    FireworkRocket(FireworkRocketRecipe),
227    FireworkStar(FireworkStarRecipe),
228    FireworkStarFade(FireworkStarFadeRecipe),
229    MapExtending(MapExtendingRecipe),
230    RepairItem(RepairItemRecipe),
231    ShieldDecoration(ShieldDecorationRecipe),
232}
233
234impl CraftingRecipe {
235    #[must_use]
236    pub const fn properties(&self) -> &RecipeProperties {
237        match self {
238            Self::Shaped(recipe) => &recipe.properties,
239            Self::Shapeless(recipe) => &recipe.properties,
240            Self::Transmute(recipe) => &recipe.properties,
241            Self::Dye(recipe) => &recipe.properties,
242            Self::DecoratedPot(recipe) => &recipe.properties,
243            Self::Imbue(recipe) => &recipe.properties,
244            Self::BannerDuplicate(recipe) => &recipe.properties,
245            Self::BookCloning(recipe) => &recipe.properties,
246            Self::FireworkRocket(recipe) => &recipe.properties,
247            Self::FireworkStar(recipe) => &recipe.properties,
248            Self::FireworkStarFade(recipe) => &recipe.properties,
249            Self::MapExtending(recipe) => &recipe.properties,
250            Self::RepairItem(recipe) => &recipe.properties,
251            Self::ShieldDecoration(recipe) => &recipe.properties,
252        }
253    }
254
255    #[must_use]
256    pub const fn result(&self) -> Option<&ItemStackTemplate> {
257        match self {
258            Self::Shaped(recipe) => Some(&recipe.result),
259            Self::Shapeless(recipe) => Some(&recipe.result),
260            Self::Transmute(recipe) => Some(&recipe.result),
261            Self::Dye(recipe) => Some(&recipe.result),
262            Self::DecoratedPot(recipe) => Some(&recipe.result),
263            Self::Imbue(recipe) => Some(&recipe.result),
264            Self::BannerDuplicate(recipe) => Some(&recipe.result),
265            Self::BookCloning(recipe) => Some(&recipe.result),
266            Self::FireworkRocket(recipe) => Some(&recipe.result),
267            Self::FireworkStar(recipe) => Some(&recipe.result),
268            Self::FireworkStarFade(recipe) => Some(&recipe.result),
269            Self::MapExtending(recipe) => Some(&recipe.result),
270            Self::RepairItem(_) => None,
271            Self::ShieldDecoration(recipe) => Some(&recipe.result),
272        }
273    }
274}
275
276// SAFETY: This Steel-owned key uniquely identifies the unified vanilla crafting data enum.
277unsafe impl DowncastType for CraftingRecipe {
278    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:recipe_data/crafting");
279}
280
281impl RecipeData for CraftingRecipe {
282    fn properties(&self) -> Option<&RecipeProperties> {
283        Some(self.properties())
284    }
285}
286
287/// Saved-map facts needed by the vanilla map-extending matcher.
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub struct MapRecipeData {
290    pub exploration_map: bool,
291    pub scale: u8,
292}
293
294/// Positioned, immutable crafting-grid snapshot.
295#[derive(Debug, Clone)]
296pub struct CraftingInput {
297    pub width: usize,
298    pub height: usize,
299    pub items: Vec<ItemStack>,
300    ingredient_count: usize,
301    map_data: Vec<Option<MapRecipeData>>,
302}
303
304impl CraftingInput {
305    pub const EMPTY: Self = Self {
306        width: 0,
307        height: 0,
308        items: Vec::new(),
309        ingredient_count: 0,
310        map_data: Vec::new(),
311    };
312
313    #[must_use]
314    pub fn new(width: usize, height: usize, items: Vec<ItemStack>) -> Self {
315        debug_assert_eq!(items.len(), width * height);
316        let ingredient_count = items.iter().filter(|stack| !stack.is_empty()).count();
317        let map_data = vec![None; items.len()];
318        Self {
319            width,
320            height,
321            items,
322            ingredient_count,
323            map_data,
324        }
325    }
326
327    #[must_use]
328    pub fn with_map_data(mut self, slot: usize, data: MapRecipeData) -> Self {
329        if let Some(map_data) = self.map_data.get_mut(slot) {
330            *map_data = Some(data);
331        }
332        self
333    }
334
335    #[must_use]
336    pub fn map_data(&self, slot: usize) -> Option<MapRecipeData> {
337        self.map_data.get(slot).copied().flatten()
338    }
339
340    #[must_use]
341    pub fn positioned(
342        width: usize,
343        height: usize,
344        items: Vec<ItemStack>,
345    ) -> PositionedCraftingInput {
346        if width == 0 || height == 0 {
347            return PositionedCraftingInput::EMPTY;
348        }
349
350        let mut left = width;
351        let mut right = 0;
352        let mut top = height;
353        let mut bottom = 0;
354        for y in 0..height {
355            for x in 0..width {
356                if !items[y * width + x].is_empty() {
357                    left = left.min(x);
358                    right = right.max(x);
359                    top = top.min(y);
360                    bottom = bottom.max(y);
361                }
362            }
363        }
364        if left > right || top > bottom {
365            return PositionedCraftingInput::EMPTY;
366        }
367
368        let new_width = right - left + 1;
369        let new_height = bottom - top + 1;
370        if new_width == width && new_height == height {
371            return PositionedCraftingInput {
372                input: Self::new(width, height, items),
373                left,
374                top,
375            };
376        }
377
378        let mut positioned = Vec::with_capacity(new_width * new_height);
379        for y in 0..new_height {
380            for x in 0..new_width {
381                positioned.push(items[(x + left) + (y + top) * width].clone());
382            }
383        }
384        PositionedCraftingInput {
385            input: Self::new(new_width, new_height, positioned),
386            left,
387            top,
388        }
389    }
390
391    #[must_use]
392    pub fn get(&self, x: usize, y: usize) -> &ItemStack {
393        &self.items[y * self.width + x]
394    }
395
396    #[must_use]
397    pub const fn ingredient_count(&self) -> usize {
398        self.ingredient_count
399    }
400}
401
402// SAFETY: This Steel-owned key uniquely identifies a crafting matching snapshot.
403unsafe impl DowncastType for CraftingInput {
404    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:recipe_input/crafting");
405}
406
407impl RecipeInput for CraftingInput {
408    fn is_empty(&self) -> bool {
409        self.ingredient_count == 0
410    }
411}
412
413impl RecipeMatches<CraftingInput> for CraftingRecipe {
414    fn matches(&self, input: &CraftingInput) -> bool {
415        matching::matches(self, input)
416    }
417}
418
419/// Positioned input plus its offset in the original crafting grid.
420#[derive(Debug, Clone)]
421pub struct PositionedCraftingInput {
422    pub input: CraftingInput,
423    pub left: usize,
424    pub top: usize,
425}
426
427impl PositionedCraftingInput {
428    pub const EMPTY: Self = Self {
429        input: CraftingInput::EMPTY,
430        left: 0,
431        top: 0,
432    };
433
434    #[must_use]
435    pub const fn to_grid_slot(&self, x: usize, y: usize, grid_width: usize) -> usize {
436        (x + self.left) + (y + self.top) * grid_width
437    }
438}