pumpkinplus/modules/recipes/recipe.rs
1//! Recipe system for PumpkinPlus.
2//!
3//! Each recipe category is implemented as a module implementing the [`Recipe`] trait.
4//! Recipes are registered with the server in bulk via [`Recipe::register`].
5//!
6//! ## Supported Recipe Types
7//!
8//! | Type | Pumpkin API Status | Description |
9//! |-------------|--------------------|--------------------------------------|
10//! | `shaped` | ✅ Available | Crafting recipes with a fixed layout |
11//! | `shapeless` | ✅ Available | Crafting recipes with loose items |
12//! | `cooking` | ✅ Available | Furnace, smoker, campfire, blast |
13//! | `potion` | ⛔ Unavailable | Potion brewing recipes |
14
15use std::time::Instant;
16use tracing::info;
17
18/// A trait representing a collection of custom recipes that can be registered.
19///
20/// Types implementing this trait provide one or more recipes to be added to the
21/// server when [`Recipe::register`] is called. All recipes are returned in
22/// bulk to allow for efficient registration.
23///
24/// # Example
25///
26/// ```rust,ignore
27/// pub struct MyRecipes;
28///
29/// impl Recipe for MyRecipes {
30/// fn recipes(&self) -> Vec<RecipeKind> {
31/// vec![
32/// RecipeKind::Shaped {
33/// // ...
34/// },
35/// ]
36/// }
37/// }
38/// ```
39pub trait Recipe {
40 /// Returns the shaped crafting recipes to be registered.
41 ///
42 /// Each entry describes a recipe with a fixed grid pattern. Override this
43 /// to provide shaped recipes. Defaults to an empty vector.
44 fn shaped(&self) -> Vec<ShapedRecipe> {
45 vec![]
46 }
47
48 /// Returns the shapeless crafting recipes to be registered.
49 ///
50 /// Each entry describes a recipe where ingredients can be placed in any
51 /// slot of the crafting grid. Override this to provide shapeless recipes.
52 /// Defaults to an empty vector.
53 fn shapeless(&self) -> Vec<ShapelessRecipe> {
54 vec![]
55 }
56
57 /// Returns the cooking recipes to be registered.
58 ///
59 /// Covers furnace, smoker, blast furnace, and campfire recipes. Override
60 /// this to provide cooking recipes. Defaults to an empty vector.
61 fn cooking(&self) -> Vec<CookingRecipe> {
62 vec![]
63 }
64
65 /// Returns `true` if there is at least one recipe to register.
66 fn has_recipes(&self) -> bool {
67 !self.shaped().is_empty() || !self.shapeless().is_empty() || !self.cooking().is_empty()
68 }
69
70 /// Registers all recipes returned by the trait methods.
71 ///
72 /// Logs the count and time taken. If no recipes are present this is a
73 /// no-op.
74 fn register(&self) {
75 if !self.has_recipes() {
76 return;
77 }
78
79 let start = Instant::now();
80
81 let shaped = self.shaped();
82 let shapeless = self.shapeless();
83 let cooking = self.cooking();
84
85 let total = shaped.len() + shapeless.len() + cooking.len();
86
87 // TODO: Wire to pumpkin_plugin_api::recipe once re-exported upstream.
88 // https://github.com/Pumpkin-MC/Pumpkin/issues/XXX
89 //
90 // Example desired call:
91 // context.register_shaped_recipes(&shaped);
92 // context.register_shapeless_recipes(&shapeless);
93 // context.register_cooking_recipes(&cooking);
94
95 let elapsed = start.elapsed().as_millis();
96 info!(
97 "Registered: {} recipe(s) ({} shaped, {} shapeless, {} cooking) | Took {}ms",
98 total,
99 shaped.len(),
100 shapeless.len(),
101 cooking.len(),
102 elapsed
103 );
104 }
105}
106
107/// A shaped crafting recipe.
108///
109/// Patterns use single-character keys mapped to [`Ingredient`] entries.
110/// Spaces represent empty slots.
111#[derive(Debug, Clone)]
112pub struct ShapedRecipe {
113 /// Unique recipe identifier (e.g. `"pumpkinplus:diamond_horse_armor"`).
114 pub id: String,
115 /// Grid height (1–3).
116 pub height: u8,
117 /// Grid width (1–3).
118 pub width: u8,
119 /// Pattern rows. Each string must be exactly `width` characters.
120 /// Use a space `' '` for an empty slot.
121 pub pattern: Vec<String>,
122 /// Mapping from pattern characters to ingredients.
123 pub keys: Vec<(char, Ingredient)>,
124 /// The result item.
125 pub result: ItemStack,
126}
127
128/// A shapeless crafting recipe.
129///
130/// Ingredients can be placed in any slot of the crafting grid.
131#[derive(Debug, Clone)]
132pub struct ShapelessRecipe {
133 /// Unique recipe identifier.
134 pub id: String,
135 /// List of ingredients (may include duplicates for multi-count slots).
136 pub ingredients: Vec<Ingredient>,
137 /// The result item.
138 pub result: ItemStack,
139}
140
141/// A furnace / smoker / blast furnace / campfire recipe.
142#[derive(Debug, Clone)]
143pub struct CookingRecipe {
144 /// Unique recipe identifier.
145 pub id: String,
146 /// The input ingredient.
147 pub ingredient: Ingredient,
148 /// The result item.
149 pub result: ItemStack,
150 /// Base cooking time in ticks (e.g. 200 for furnace).
151 pub cook_time: u32,
152 /// Experience granted when the item is removed.
153 pub experience: f32,
154 /// Which cooking block this applies to.
155 pub kind: CookingKind,
156}
157
158/// Variant of cooking recipe.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum CookingKind {
161 /// Standard furnace.
162 Smelting,
163 /// Blast furnace (2× speed).
164 Blasting,
165 /// Smoker (2× speed).
166 Smoking,
167 /// Campfire (3× duration).
168 Campfire,
169}
170
171/// An ingredient accepted by a recipe.
172///
173/// Mirrors the Pumpkin WIT `RecipeIngredient` type.
174#[derive(Debug, Clone)]
175pub enum Ingredient {
176 /// Accept an exact item by its identifier (e.g. `"minecraft:diamond"`).
177 Item { id: String },
178 /// Accept any item in a tag (e.g. `"minecraft:logs"`).
179 Tag { id: String },
180}
181
182/// A stack of items produced by a recipe.
183///
184/// Mirrors the Pumpkin WIT `ItemStack` type.
185#[derive(Debug, Clone)]
186pub struct ItemStack {
187 /// Item identifier (e.g. `"minecraft:diamond_horse_armor"`).
188 pub id: String,
189 /// Number of items in the stack.
190 pub count: u8,
191}
192
193impl Default for ItemStack {
194 fn default() -> Self {
195 Self {
196 id: String::new(),
197 count: 1,
198 }
199 }
200}