1use 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;
13pub use yog_registry::recipe_kinds::ids::ALL as TYPES;
15
16#[derive(Schema)]
18pub struct Common {
19 pub format: Option<u32>,
21 pub id: Option<String>,
23 pub result: ItemId,
25 #[data(default = "one")]
27 pub count: u32,
28 pub group: Option<String>,
30 pub category: Option<String>,
32 #[data(default)]
35 pub requires: Vec<String>,
36}
37
38fn one() -> u32 {
39 1
40}
41
42pub type Ingredient = yog_data::OneOrList<ItemIdOrTag>;
44
45pub 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#[derive(Schema)]
59pub struct Shaped {
60 pub pattern: Vec<String>,
62 pub key: Ordered<Ingredient>,
64}
65
66#[derive(Schema)]
68pub struct Shapeless {
69 pub ingredients: Vec<Ingredient>,
71}
72
73#[derive(Schema)]
75pub struct Cooking {
76 pub ingredient: Ingredient,
77 #[data(default = "experience")]
79 pub experience: f64,
80 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#[derive(Schema)]
102pub struct Brewing {
103 #[data(with = "RefTo<yog_data::kinds::Item>")]
105 pub from: String,
106 #[data(with = "RefTo<yog_data::kinds::Item>")]
108 pub ingredient: String,
109}
110
111#[derive(Schema)]
114pub struct Custom {
115 pub slots: Ordered<Vec<Ingredient>>,
117 #[data(default)]
119 pub values: Ordered<Value>,
120}
121
122pub enum Body {
123 Shaped(Shaped),
124 Shapeless(Shapeless),
125 Cooking(String, Cooking),
127 Stonecutting(Stonecutting),
128 Smithing(Smithing),
129 Brewing(Brewing),
130 Custom(String, Custom),
132}
133
134impl Body {
135 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
148pub const TYPE_FIELD: &str = "type";
150
151pub const MAX_SHAPELESS: usize =
153 yog_registry::recipe_kinds::GRID_SIDE * yog_registry::recipe_kinds::GRID_SIDE;
154pub const EXPERIENCE: std::ops::RangeInclusive<f64> = 0.0..=1000.0;
156pub const COOK_TIME: std::ops::RangeInclusive<u32> = 1..=32767;
158
159pub struct RecipeFile {
161 pub common: Common,
162 pub body: Body,
163}
164
165impl Schema for RecipeFile {
166 fn shape() -> Shape {
167 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}