Skip to main content

yog_formats/
recipe.rs

1//! The `.yrecipe` format: a recipe of the game's kinds (`shaped`,
2//! `shapeless`, cooking, `stonecutting`, `smithing`, brewing) or of the mod's own
3//! (`mymod:infusion`, with named `slots` and `values`), in Yog Data. Items
4//! are references (`@item:minecraft:stick`), tags `@tag:item:c:dusts`.
5
6use yog_data::schema::{Field, StructShape, read_both};
7use yog_data::with::RefTo;
8use yog_data::{
9    ItemId, ItemIdOrTag, OneOrTag, Ordered, Reader, Schema, Seg, Shape, Value, ValueRef,
10};
11
12use yog_registry::recipe_kinds::ids;
13/// The game's recipe types (the registry's kinds).
14pub use yog_registry::recipe_kinds::ids::ALL as TYPES;
15
16/// What every recipe has.
17#[derive(Schema)]
18pub struct Common {
19    /// The file format; 1.
20    pub format: Option<u32>,
21    /// Its id (`mymod:ruby_block`); the file's name in the mod's namespace by default.
22    pub id: Option<String>,
23    /// The item stack it makes.
24    pub result: ItemId,
25    /// How many (1–64).
26    #[data(default = "one")]
27    pub count: u32,
28    /// Recipes of a group share a recipe-book slot.
29    pub group: Option<String>,
30    /// The recipe book's tab (`building`, `equipment`, `misc`, …).
31    pub category: Option<String>,
32    /// Mods it needs — Yog or Java mod ids (`pehkui`): without every one of
33    /// them loaded it isn't there (an optional integration).
34    #[data(default)]
35    pub requires: Vec<String>,
36}
37
38fn one() -> u32 {
39    1
40}
41
42/// One ingredient: an item or a tag, or a list of them (any of them does).
43pub type Ingredient = yog_data::OneOrList<ItemIdOrTag>;
44
45/// `ingredient` as the generated code and the runtime write it: ids, `#tag`s.
46pub fn ids(ingredient: &Ingredient) -> Vec<String> {
47    ingredient
48        .0
49        .iter()
50        .map(|i| match i {
51            OneOrTag::One(id) => id.id.clone(),
52            OneOrTag::Tag(t) => format!("#{}", t.id),
53        })
54        .collect()
55}
56
57/// A crafting grid: `pattern` rows of `key` characters (space: empty).
58#[derive(Schema)]
59pub struct Shaped {
60    /// 1–3 rows of the same 1–3 characters.
61    pub pattern: Vec<String>,
62    /// What each character stands for.
63    pub key: Ordered<Ingredient>,
64}
65
66/// Crafting ingredients in any arrangement.
67#[derive(Schema)]
68pub struct Shapeless {
69    /// 1–9 ingredients.
70    pub ingredients: Vec<Ingredient>,
71}
72
73/// A furnace, blast furnace, smoker or campfire.
74#[derive(Schema)]
75pub struct Cooking {
76    pub ingredient: Ingredient,
77    /// Experience it gives.
78    #[data(default = "experience")]
79    pub experience: f64,
80    /// Ticks it takes (the station's default if left out).
81    pub cook_time: Option<u32>,
82}
83
84fn experience() -> f64 {
85    yog_registry::COOK_EXPERIENCE
86}
87
88#[derive(Schema)]
89pub struct Stonecutting {
90    pub ingredient: Ingredient,
91}
92
93#[derive(Schema)]
94pub struct Smithing {
95    pub template: Ingredient,
96    pub base: Ingredient,
97    pub addition: Ingredient,
98}
99
100/// A brewing-stand recipe. `result` is the potion id in the common fields.
101#[derive(Schema)]
102pub struct Brewing {
103    /// The potion in the bottle before brewing.
104    #[data(with = "RefTo<yog_data::kinds::Item>")]
105    pub from: String,
106    /// The item added to the brewing stand.
107    #[data(with = "RefTo<yog_data::kinds::Item>")]
108    pub ingredient: String,
109}
110
111/// A recipe of the mod's own kind: named slots and values, for its
112/// machines and books.
113#[derive(Schema)]
114pub struct Custom {
115    /// Slot name → its ingredients.
116    pub slots: Ordered<Vec<Ingredient>>,
117    /// Anything the machine reads: `time`, `energy`, …
118    #[data(default)]
119    pub values: Ordered<Value>,
120}
121
122pub enum Body {
123    Shaped(Shaped),
124    Shapeless(Shapeless),
125    /// `smelting`, `blasting`, `smoking` or `campfire_cooking`.
126    Cooking(String, Cooking),
127    Stonecutting(Stonecutting),
128    Smithing(Smithing),
129    Brewing(Brewing),
130    /// The kind (`mymod:infusion`).
131    Custom(String, Custom),
132}
133
134impl Body {
135    /// Its kind of recipe: the game's id (`shaped`) or the mod's (`mymod:infusion`).
136    pub fn kind(&self) -> &str {
137        match self {
138            Body::Shaped(_) => ids::SHAPED,
139            Body::Shapeless(_) => ids::SHAPELESS,
140            Body::Cooking(t, _) | Body::Custom(t, _) => t,
141            Body::Stonecutting(_) => ids::STONECUTTING,
142            Body::Smithing(_) => ids::SMITHING,
143            Body::Brewing(_) => ids::BREWING,
144        }
145    }
146}
147
148/// The field naming a recipe's kind.
149pub const TYPE_FIELD: &str = "type";
150
151/// Most ingredients a shapeless recipe takes: a crafting grid's places.
152pub const MAX_SHAPELESS: usize =
153    yog_registry::recipe_kinds::GRID_SIDE * yog_registry::recipe_kinds::GRID_SIDE;
154/// The experience a cooking recipe may give (the game's codec's bound).
155pub const EXPERIENCE: std::ops::RangeInclusive<f64> = 0.0..=1000.0;
156/// The ticks a cooking recipe may take (the game's codec's bound).
157pub const COOK_TIME: std::ops::RangeInclusive<u32> = 1..=32767;
158
159/// A whole `.yrecipe`.
160pub struct RecipeFile {
161    pub common: Common,
162    pub body: Body,
163}
164
165impl Schema for RecipeFile {
166    fn shape() -> Shape {
167        // `type` and the common fields; the kinds' own come with `type`.
168        let mut fields = vec![Field {
169            name: TYPE_FIELD,
170            doc: "The game's recipe type, or the mod's own kind (`mymod:infusion`).",
171            shape: String::shape,
172            required: true,
173            range: None,
174        }];
175        if let Shape::Struct(c) = Common::shape() {
176            fields.extend(c.fields);
177        }
178
179        Shape::Struct(StructShape {
180            name: "Recipe",
181            doc: "A recipe.",
182            fields,
183        })
184    }
185
186    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
187        let map = cx.map(v)?;
188        let ty = match map.get(TYPE_FIELD) {
189            Some(ValueRef::Str(t)) => t.to_string(),
190            Some(other) => {
191                cx.at(Seg::Key(TYPE_FIELD.into()), |cx| {
192                    cx.expected("a recipe type", other)
193                });
194                return None;
195            }
196            None => {
197                cx.problem(format!(
198                    "missing `type` ({}, or the mod's own `mymod:kind`)",
199                    TYPES.join(", ")
200                ));
201                return None;
202            }
203        };
204        let also = &[TYPE_FIELD];
205        let (common, body) =
206            match ty.as_str() {
207                ids::SHAPED => {
208                    read_both::<Common, Shaped>(v, cx, also).map(|(c, b)| (c, Body::Shaped(b)))?
209                }
210                ids::SHAPELESS => read_both::<Common, Shapeless>(v, cx, also)
211                    .map(|(c, b)| (c, Body::Shapeless(b)))?,
212                ids::SMELTING | ids::BLASTING | ids::SMOKING | ids::CAMPFIRE_COOKING => {
213                    read_both::<Common, Cooking>(v, cx, also)
214                        .map(|(c, b)| (c, Body::Cooking(ty.clone(), b)))?
215                }
216                ids::STONECUTTING => read_both::<Common, Stonecutting>(v, cx, also)
217                    .map(|(c, b)| (c, Body::Stonecutting(b)))?,
218                ids::SMITHING => read_both::<Common, Smithing>(v, cx, also)
219                    .map(|(c, b)| (c, Body::Smithing(b)))?,
220                ids::BREWING => {
221                    read_both::<Common, Brewing>(v, cx, also).map(|(c, b)| (c, Body::Brewing(b)))?
222                }
223                own if own.contains(':') => read_both::<Common, Custom>(v, cx, also)
224                    .map(|(c, b)| (c, Body::Custom(ty.clone(), b)))?,
225                other => {
226                    cx.at(Seg::Key(TYPE_FIELD.into()), |cx| {
227                        cx.unknown_name(other, TYPES)
228                    });
229                    return None;
230                }
231            };
232        Some(RecipeFile { common, body })
233    }
234
235    fn write(&self) -> Value {
236        let ty = self.body.kind().to_string();
237        let body = match &self.body {
238            Body::Shaped(b) => b.write(),
239            Body::Shapeless(b) => b.write(),
240            Body::Cooking(_, b) => b.write(),
241            Body::Stonecutting(b) => b.write(),
242            Body::Smithing(b) => b.write(),
243            Body::Brewing(b) => b.write(),
244            Body::Custom(_, b) => b.write(),
245        };
246        let mut m = yog_data::Map::new();
247        m.insert(TYPE_FIELD, Value::from(ty));
248        for part in [self.common.write(), body] {
249            if let Value::Map(fields) = part {
250                for (k, v) in fields {
251                    m.insert(k, v);
252                }
253            }
254        }
255        Value::Map(m)
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn item_reference_round_trips() {
265        let item = yog_data::from_text::<ItemId>(r#""@item:minecraft:stick""#)
266            .unwrap()
267            .value;
268        assert_eq!(item.id, "minecraft:stick");
269        let encoded = yog_data::text::to_string(&item.write());
270        assert_eq!(encoded, r#""@item:minecraft:stick""#);
271        assert_eq!(yog_data::from_text::<ItemId>(&encoded).unwrap().value, item);
272    }
273}