use yog_data::schema::{Field, StructShape, read_both};
use yog_data::with::RefTo;
use yog_data::{
ItemId, ItemIdOrTag, OneOrTag, Ordered, Reader, Schema, Seg, Shape, Value, ValueRef,
};
use yog_registry::recipe_kinds::ids;
pub use yog_registry::recipe_kinds::ids::ALL as TYPES;
#[derive(Schema)]
pub struct Common {
pub format: Option<u32>,
pub id: Option<String>,
pub result: ItemId,
#[data(default = "one")]
pub count: u32,
pub group: Option<String>,
pub category: Option<String>,
#[data(default)]
pub requires: Vec<String>,
}
fn one() -> u32 {
1
}
pub type Ingredient = yog_data::OneOrList<ItemIdOrTag>;
pub fn ids(ingredient: &Ingredient) -> Vec<String> {
ingredient
.0
.iter()
.map(|i| match i {
OneOrTag::One(id) => id.id.clone(),
OneOrTag::Tag(t) => format!("#{}", t.id),
})
.collect()
}
#[derive(Schema)]
pub struct Shaped {
pub pattern: Vec<String>,
pub key: Ordered<Ingredient>,
}
#[derive(Schema)]
pub struct Shapeless {
pub ingredients: Vec<Ingredient>,
}
#[derive(Schema)]
pub struct Cooking {
pub ingredient: Ingredient,
#[data(default = "experience")]
pub experience: f64,
pub cook_time: Option<u32>,
}
fn experience() -> f64 {
yog_registry::COOK_EXPERIENCE
}
#[derive(Schema)]
pub struct Stonecutting {
pub ingredient: Ingredient,
}
#[derive(Schema)]
pub struct Smithing {
pub template: Ingredient,
pub base: Ingredient,
pub addition: Ingredient,
}
#[derive(Schema)]
pub struct Brewing {
#[data(with = "RefTo<yog_data::kinds::Item>")]
pub from: String,
#[data(with = "RefTo<yog_data::kinds::Item>")]
pub ingredient: String,
}
#[derive(Schema)]
pub struct Custom {
pub slots: Ordered<Vec<Ingredient>>,
#[data(default)]
pub values: Ordered<Value>,
}
pub enum Body {
Shaped(Shaped),
Shapeless(Shapeless),
Cooking(String, Cooking),
Stonecutting(Stonecutting),
Smithing(Smithing),
Brewing(Brewing),
Custom(String, Custom),
}
impl Body {
pub fn kind(&self) -> &str {
match self {
Body::Shaped(_) => ids::SHAPED,
Body::Shapeless(_) => ids::SHAPELESS,
Body::Cooking(t, _) | Body::Custom(t, _) => t,
Body::Stonecutting(_) => ids::STONECUTTING,
Body::Smithing(_) => ids::SMITHING,
Body::Brewing(_) => ids::BREWING,
}
}
}
pub const TYPE_FIELD: &str = "type";
pub const MAX_SHAPELESS: usize =
yog_registry::recipe_kinds::GRID_SIDE * yog_registry::recipe_kinds::GRID_SIDE;
pub const EXPERIENCE: std::ops::RangeInclusive<f64> = 0.0..=1000.0;
pub const COOK_TIME: std::ops::RangeInclusive<u32> = 1..=32767;
pub struct RecipeFile {
pub common: Common,
pub body: Body,
}
impl Schema for RecipeFile {
fn shape() -> Shape {
let mut fields = vec![Field {
name: TYPE_FIELD,
doc: "The game's recipe type, or the mod's own kind (`mymod:infusion`).",
shape: String::shape,
required: true,
range: None,
}];
if let Shape::Struct(c) = Common::shape() {
fields.extend(c.fields);
}
Shape::Struct(StructShape {
name: "Recipe",
doc: "A recipe.",
fields,
})
}
fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
let map = cx.map(v)?;
let ty = match map.get(TYPE_FIELD) {
Some(ValueRef::Str(t)) => t.to_string(),
Some(other) => {
cx.at(Seg::Key(TYPE_FIELD.into()), |cx| {
cx.expected("a recipe type", other)
});
return None;
}
None => {
cx.problem(format!(
"missing `type` ({}, or the mod's own `mymod:kind`)",
TYPES.join(", ")
));
return None;
}
};
let also = &[TYPE_FIELD];
let (common, body) =
match ty.as_str() {
ids::SHAPED => {
read_both::<Common, Shaped>(v, cx, also).map(|(c, b)| (c, Body::Shaped(b)))?
}
ids::SHAPELESS => read_both::<Common, Shapeless>(v, cx, also)
.map(|(c, b)| (c, Body::Shapeless(b)))?,
ids::SMELTING | ids::BLASTING | ids::SMOKING | ids::CAMPFIRE_COOKING => {
read_both::<Common, Cooking>(v, cx, also)
.map(|(c, b)| (c, Body::Cooking(ty.clone(), b)))?
}
ids::STONECUTTING => read_both::<Common, Stonecutting>(v, cx, also)
.map(|(c, b)| (c, Body::Stonecutting(b)))?,
ids::SMITHING => read_both::<Common, Smithing>(v, cx, also)
.map(|(c, b)| (c, Body::Smithing(b)))?,
ids::BREWING => {
read_both::<Common, Brewing>(v, cx, also).map(|(c, b)| (c, Body::Brewing(b)))?
}
own if own.contains(':') => read_both::<Common, Custom>(v, cx, also)
.map(|(c, b)| (c, Body::Custom(ty.clone(), b)))?,
other => {
cx.at(Seg::Key(TYPE_FIELD.into()), |cx| {
cx.unknown_name(other, TYPES)
});
return None;
}
};
Some(RecipeFile { common, body })
}
fn write(&self) -> Value {
let ty = self.body.kind().to_string();
let body = match &self.body {
Body::Shaped(b) => b.write(),
Body::Shapeless(b) => b.write(),
Body::Cooking(_, b) => b.write(),
Body::Stonecutting(b) => b.write(),
Body::Smithing(b) => b.write(),
Body::Brewing(b) => b.write(),
Body::Custom(_, b) => b.write(),
};
let mut m = yog_data::Map::new();
m.insert(TYPE_FIELD, Value::from(ty));
for part in [self.common.write(), body] {
if let Value::Map(fields) = part {
for (k, v) in fields {
m.insert(k, v);
}
}
}
Value::Map(m)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn item_reference_round_trips() {
let item = yog_data::from_text::<ItemId>(r#""@item:minecraft:stick""#)
.unwrap()
.value;
assert_eq!(item.id, "minecraft:stick");
let encoded = yog_data::text::to_string(&item.write());
assert_eq!(encoded, r#""@item:minecraft:stick""#);
assert_eq!(yog_data::from_text::<ItemId>(&encoded).unwrap().value, item);
}
}