Skip to main content

steel_registry/recipe/
types.rs

1//! Typed recipe discriminators and recipe entries.
2
3use std::fmt::{self, Debug, Formatter};
4use std::marker::PhantomData;
5
6use rustc_hash::FxHashMap;
7use steel_utils::{DowncastType, DowncastTypeKey, ErasedType, Identifier};
8
9use super::RecipeProperties;
10
11/// Passive data stored by a recipe.
12pub trait RecipeData: ErasedType + Debug + Send + Sync + 'static {
13    fn properties(&self) -> Option<&RecipeProperties> {
14        None
15    }
16}
17
18/// Immutable input snapshot used while matching a recipe type.
19pub trait RecipeInput: DowncastType + Debug + Send + Sync + 'static {
20    #[must_use]
21    fn is_empty(&self) -> bool;
22}
23
24/// Matching behavior implemented by one recipe data type for its input snapshot.
25pub trait RecipeMatches<I: RecipeInput>: RecipeData {
26    /// Returns whether this recipe accepts the provided input.
27    #[must_use]
28    fn matches(&self, input: &I) -> bool;
29}
30
31/// Type-erased registered recipe discriminator.
32#[derive(Debug, PartialEq, Eq)]
33pub struct RecipeTypeEntry {
34    pub key: Identifier,
35    data_type_key: DowncastTypeKey,
36    input_type_key: DowncastTypeKey,
37}
38
39impl RecipeTypeEntry {
40    #[must_use]
41    pub const fn data_type_key(&self) -> DowncastTypeKey {
42        self.data_type_key
43    }
44
45    #[must_use]
46    pub const fn input_type_key(&self) -> DowncastTypeKey {
47        self.input_type_key
48    }
49}
50
51pub type RecipeTypeEntryRef = &'static RecipeTypeEntry;
52
53/// Typed handle for an operational recipe type.
54pub struct RecipeType<D: RecipeMatches<I> + DowncastType, I: RecipeInput> {
55    entry: RecipeTypeEntry,
56    _marker: PhantomData<fn(&D, &I)>,
57}
58
59impl<D: RecipeMatches<I> + DowncastType, I: RecipeInput> RecipeType<D, I> {
60    #[must_use]
61    pub const fn new(key: Identifier) -> Self {
62        Self {
63            entry: RecipeTypeEntry {
64                key,
65                data_type_key: D::TYPE_KEY,
66                input_type_key: I::TYPE_KEY,
67            },
68            _marker: PhantomData,
69        }
70    }
71
72    #[must_use]
73    pub const fn entry(&'static self) -> RecipeTypeEntryRef {
74        &self.entry
75    }
76
77    #[must_use]
78    pub const fn key(&self) -> &Identifier {
79        &self.entry.key
80    }
81}
82
83impl<D: RecipeMatches<I> + DowncastType, I: RecipeInput> Debug for RecipeType<D, I> {
84    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
85        formatter
86            .debug_struct("RecipeType")
87            .field("key", &self.entry.key)
88            .field("data_type_key", &self.entry.data_type_key)
89            .field("input_type_key", &self.entry.input_type_key)
90            .finish_non_exhaustive()
91    }
92}
93
94/// A keyed recipe with concrete passive data and input types.
95pub struct Recipe<D: RecipeMatches<I> + DowncastType, I: RecipeInput> {
96    key: Identifier,
97    recipe_type: &'static RecipeType<D, I>,
98    data: D,
99}
100
101impl<D: RecipeMatches<I> + DowncastType, I: RecipeInput> Recipe<D, I> {
102    #[must_use]
103    pub const fn new(key: Identifier, recipe_type: &'static RecipeType<D, I>, data: D) -> Self {
104        Self {
105            key,
106            recipe_type,
107            data,
108        }
109    }
110
111    #[must_use]
112    pub const fn key(&self) -> &Identifier {
113        &self.key
114    }
115
116    #[must_use]
117    pub const fn recipe_type(&self) -> &'static RecipeType<D, I> {
118        self.recipe_type
119    }
120
121    #[must_use]
122    pub const fn data(&self) -> &D {
123        &self.data
124    }
125}
126
127impl<D: RecipeMatches<I> + DowncastType, I: RecipeInput> Debug for Recipe<D, I> {
128    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
129        formatter
130            .debug_struct("Recipe")
131            .field("key", &self.key)
132            .field("recipe_type", &self.recipe_type.key())
133            .field("data", &self.data)
134            .finish()
135    }
136}
137
138pub(crate) trait ErasedRecipe: Debug + Send + Sync {
139    fn key(&self) -> &Identifier;
140    fn recipe_type(&self) -> RecipeTypeEntryRef;
141    fn data(&self) -> &dyn RecipeData;
142}
143
144impl<D: RecipeMatches<I> + DowncastType, I: RecipeInput> ErasedRecipe for Recipe<D, I> {
145    fn key(&self) -> &Identifier {
146        self.key()
147    }
148
149    fn recipe_type(&self) -> RecipeTypeEntryRef {
150        self.recipe_type.entry()
151    }
152
153    fn data(&self) -> &dyn RecipeData {
154        &self.data
155    }
156}
157
158/// Registry for operational recipe types.
159pub struct RecipeTypeRegistry {
160    types: Vec<RecipeTypeEntryRef>,
161    by_key: FxHashMap<Identifier, usize>,
162    allows_registering: bool,
163}
164
165impl RecipeTypeRegistry {
166    #[must_use]
167    pub fn new() -> Self {
168        Self {
169            types: Vec::new(),
170            by_key: FxHashMap::default(),
171            allows_registering: true,
172        }
173    }
174
175    pub fn register<D: RecipeMatches<I> + DowncastType, I: RecipeInput>(
176        &mut self,
177        recipe_type: &'static RecipeType<D, I>,
178    ) {
179        self.register_entry(recipe_type.entry());
180    }
181
182    fn register_entry(&mut self, entry: RecipeTypeEntryRef) {
183        assert!(
184            self.allows_registering,
185            "Cannot register recipe types after the registry has been frozen"
186        );
187        assert!(
188            !self.by_key.contains_key(&entry.key),
189            "Cannot register duplicate recipe type key: {}",
190            entry.key
191        );
192        let id = self.types.len();
193        self.types.push(entry);
194        self.by_key.insert(entry.key.clone(), id);
195    }
196
197    pub const fn freeze(&mut self) {
198        self.allows_registering = false;
199    }
200
201    #[must_use]
202    pub fn by_key(&self, key: &Identifier) -> Option<RecipeTypeEntryRef> {
203        self.by_key
204            .get(key)
205            .and_then(|id| self.types.get(*id))
206            .copied()
207    }
208
209    #[must_use]
210    pub fn contains<D: RecipeMatches<I> + DowncastType, I: RecipeInput>(
211        &self,
212        recipe_type: &'static RecipeType<D, I>,
213    ) -> bool {
214        self.by_key(recipe_type.key())
215            .is_some_and(|registered| std::ptr::eq(registered, recipe_type.entry()))
216    }
217
218    pub fn iter(&self) -> impl Iterator<Item = RecipeTypeEntryRef> + '_ {
219        self.types.iter().copied()
220    }
221
222    #[must_use]
223    pub const fn len(&self) -> usize {
224        self.types.len()
225    }
226
227    #[must_use]
228    pub const fn is_empty(&self) -> bool {
229        self.types.is_empty()
230    }
231}
232
233impl Default for RecipeTypeRegistry {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239/// Vanilla operational recipe types.
240pub mod vanilla_recipe_types {
241    use steel_utils::Identifier;
242
243    use super::{RecipeType, RecipeTypeRegistry};
244    use crate::recipe::{
245        CookingRecipe, CraftingInput, CraftingRecipe, SingleItemRecipeInput, SmithingRecipe,
246        SmithingRecipeInput, StonecuttingRecipe,
247    };
248
249    const fn cooking_type(key: &'static str) -> RecipeType<CookingRecipe, SingleItemRecipeInput> {
250        RecipeType::new(Identifier::vanilla_static(key))
251    }
252
253    pub static CRAFTING: RecipeType<CraftingRecipe, CraftingInput> =
254        RecipeType::new(Identifier::vanilla_static("crafting"));
255    pub static SMELTING: RecipeType<CookingRecipe, SingleItemRecipeInput> =
256        cooking_type("smelting");
257    pub static BLASTING: RecipeType<CookingRecipe, SingleItemRecipeInput> =
258        cooking_type("blasting");
259    pub static SMOKING: RecipeType<CookingRecipe, SingleItemRecipeInput> = cooking_type("smoking");
260    pub static CAMPFIRE_COOKING: RecipeType<CookingRecipe, SingleItemRecipeInput> =
261        cooking_type("campfire_cooking");
262    pub static STONECUTTING: RecipeType<StonecuttingRecipe, SingleItemRecipeInput> =
263        RecipeType::new(Identifier::vanilla_static("stonecutting"));
264    pub static SMITHING: RecipeType<SmithingRecipe, SmithingRecipeInput> =
265        RecipeType::new(Identifier::vanilla_static("smithing"));
266
267    pub(crate) fn register(registry: &mut RecipeTypeRegistry) {
268        registry.register(&CRAFTING);
269        registry.register(&SMELTING);
270        registry.register(&BLASTING);
271        registry.register(&SMOKING);
272        registry.register(&CAMPFIRE_COOKING);
273        registry.register(&STONECUTTING);
274        registry.register(&SMITHING);
275    }
276}