#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct FoodDef {
pub nutrition: u32,
pub saturation: f32,
pub can_always_eat: bool,
}
impl FoodDef {
pub fn new(nutrition: u32, saturation: f32) -> Self {
Self {
nutrition,
saturation,
can_always_eat: false,
}
}
pub fn can_always_eat(mut self) -> Self {
self.can_always_eat = true;
self
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ItemDef {
pub id: String,
pub max_stack: u8,
pub name: Option<String>,
pub tooltip: Option<String>,
pub max_damage: u32,
pub fire_resistant: bool,
pub fuel_ticks: u32,
pub food: Option<FoodDef>,
}
impl ItemDef {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
max_stack: 64,
name: None,
tooltip: None,
max_damage: 0,
fire_resistant: false,
fuel_ticks: 0,
food: None,
}
}
pub fn max_stack(mut self, n: u8) -> Self {
self.max_stack = n;
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn max_damage(mut self, durability: u32) -> Self {
self.max_damage = durability;
self
}
pub fn fire_resistant(mut self) -> Self {
self.fire_resistant = true;
self
}
pub fn fuel(mut self, ticks: u32) -> Self {
self.fuel_ticks = ticks;
self
}
pub fn food(mut self, food: FoodDef) -> Self {
self.food = Some(food);
self
}
}
const VANILLA_NS: &str = "minecraft";
const YOG_NS: &str = "yog";
fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
id.split_once(':').unwrap_or((default_ns, id))
}
fn ingredient_json(id: &str) -> String {
match id.strip_prefix('#') {
Some(tag) => format!("{{\"tag\":\"{tag}\"}}"),
None => format!("{{\"item\":\"{id}\"}}"),
}
}
fn item_list_json(items: &[String]) -> String {
items
.iter()
.map(|i| ingredient_json(i))
.collect::<Vec<_>>()
.join(",")
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ShapedRecipe {
pub id: String,
pub output: String,
pub count: u32,
rows: Vec<String>,
keys: Vec<(char, String)>,
}
impl ShapedRecipe {
pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
Self {
id: id.into(),
output: output.into(),
count,
rows: Vec::new(),
keys: Vec::new(),
}
}
pub fn row(mut self, pattern: impl Into<String>) -> Self {
self.rows.push(pattern.into());
self
}
pub fn key(mut self, symbol: char, item_id: impl Into<String>) -> Self {
self.keys.push((symbol, item_id.into()));
self
}
pub fn to_json(&self) -> String {
let pattern: String = self
.rows
.iter()
.map(|r| format!("\"{r}\""))
.collect::<Vec<_>>()
.join(",");
let keys: String = self
.keys
.iter()
.map(|(ch, item)| format!("\"{ch}\":{}", ingredient_json(item)))
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"type\":\"minecraft:crafting_shaped\",\"pattern\":[{}],\"key\":{{{}}},\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
pattern, keys, self.output, self.count
)
}
pub fn ns_name(&self) -> (&str, &str) {
split_id(&self.id, VANILLA_NS)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ShapelessRecipe {
pub id: String,
pub output: String,
pub count: u32,
pub ingredients: Vec<String>,
}
impl ShapelessRecipe {
pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
Self {
id: id.into(),
output: output.into(),
count,
ingredients: Vec::new(),
}
}
pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
self.ingredients.push(item_id.into());
self
}
pub fn to_json(&self) -> String {
let ingr = item_list_json(&self.ingredients);
format!(
"{{\"type\":\"minecraft:crafting_shapeless\",\"ingredients\":[{}],\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
ingr, self.output, self.count
)
}
pub fn ns_name(&self) -> (&str, &str) {
split_id(&self.id, VANILLA_NS)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct FurnaceRecipe {
pub id: String,
pub input: String,
pub output: String,
pub count: u32,
pub experience: f32,
pub cook_time: u32,
}
impl FurnaceRecipe {
pub fn new(
id: impl Into<String>,
input: impl Into<String>,
output: impl Into<String>,
count: u32,
) -> Self {
Self {
id: id.into(),
input: input.into(),
output: output.into(),
count,
experience: 0.1,
cook_time: 200,
}
}
pub fn experience(mut self, xp: f32) -> Self {
self.experience = xp;
self
}
pub fn cook_time(mut self, ticks: u32) -> Self {
self.cook_time = ticks;
self
}
pub fn to_json(&self) -> String {
format!(
"{{\"type\":\"minecraft:smelting\",\"ingredient\":{},\"result\":\"{}\",\"experience\":{},\"cookingtime\":{}}}",
ingredient_json(&self.input), self.output, self.experience, self.cook_time
)
}
pub fn ns_name(&self) -> (&str, &str) {
split_id(&self.id, VANILLA_NS)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct BookRecipe {
pub id: String,
pub book: String,
pub ingredients: Vec<String>,
}
impl BookRecipe {
pub fn new(id: impl Into<String>, book: impl Into<String>) -> Self {
Self {
id: id.into(),
book: book.into(),
ingredients: Vec::new(),
}
}
pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
self.ingredients.push(item_id.into());
self
}
pub fn to_json(&self) -> String {
let ingr = item_list_json(&self.ingredients);
format!(
"{{\"type\":\"yog:crafting_book\",\"ingredients\":[{}],\"book\":\"{}\"}}",
ingr, self.book
)
}
pub fn ns_name(&self) -> (&str, &str) {
split_id(&self.id, YOG_NS)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ItemModifier {
pub id: String,
pub function: String,
pub parameters: std::collections::HashMap<String, String>,
}
impl ItemModifier {
pub fn new(id: impl Into<String>, function: impl Into<String>) -> Self {
Self {
id: id.into(),
function: function.into(),
parameters: std::collections::HashMap::new(),
}
}
pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.parameters.insert(key.into(), value.into());
self
}
pub fn to_json(&self) -> String {
let params: String = self
.parameters
.iter()
.map(|(k, v)| format!("\"{k}\":{v}"))
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"function\":\"{}\",{}}}",
self.function,
if params.is_empty() {
String::new()
} else {
params
}
)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct StartupGrant {
pub id: String,
pub items: Vec<String>,
pub book: Option<String>,
pub command: Option<String>,
}
impl StartupGrant {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
items: Vec::new(),
book: None,
command: None,
}
}
pub fn item(mut self, item_id: impl Into<String>) -> Self {
self.items.push(item_id.into());
self
}
pub fn book(mut self, book: impl Into<String>) -> Self {
self.book = Some(book.into());
self
}
pub fn command(mut self, cmd: impl Into<String>) -> Self {
self.command = Some(cmd.into());
self
}
pub fn to_json(&self) -> String {
let items: Vec<String> = self
.items
.iter()
.map(|i| format!("{{\"item\":\"{i}\"}}"))
.collect();
let mut entries = String::new();
if !items.is_empty() {
entries.push_str(&format!("[{}]", items.join(",")));
}
if let Some(book) = &self.book {
if !entries.is_empty() {
entries.push(',');
}
let book_entry = format!(
"{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":\"{{yog_book:\\\"{book}\\\"}}\"}}]}}"
);
entries.push_str(&book_entry);
}
if let Some(cmd) = &self.command {
if !entries.is_empty() {
entries.push(',');
}
entries.push_str(&format!(
"{{\"type\":\"minecraft:command\",\"command\":\"{}\"}}",
cmd.replace('"', "\\\"")
));
}
format!(
"{{\"type\":\"yog:startup_grant\",\"entries\":{},\"id\":\"{}\"}}",
if entries.is_empty() {
"[]".to_string()
} else {
entries
},
self.id
)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct AdvancementReward {
pub id: String,
pub item: String,
pub nbt: Option<String>,
pub book: Option<String>,
}
impl AdvancementReward {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
item: "minecraft:written_book".into(),
nbt: None,
book: None,
}
}
pub fn item(mut self, item: impl Into<String>) -> Self {
self.item = item.into();
self
}
pub fn nbt(mut self, nbt: impl Into<String>) -> Self {
self.nbt = Some(nbt.into());
self
}
pub fn book(mut self, book: impl Into<String>) -> Self {
let book_str: String = book.into();
self.book = Some(book_str.clone());
self.nbt = Some(format!(
"{{yog_book:\"{book_str}\",title:\"Yog Book\",author:\"Yog\"}}"
));
self
}
pub fn to_json(&self) -> String {
let nbt_part = self
.nbt
.as_ref()
.map(|n| format!("{{\"function\":\"set_nbt\",\"tag\":{n}}}"));
let entries = if let Some(nbt_part) = nbt_part {
format!(
"[{{\"type\":\"item\",\"name\":\"{}\",\"functions\":[{}]}}]",
self.item, nbt_part
)
} else {
format!("[{{\"type\":\"item\",\"name\":\"{}\"}}]", self.item)
};
format!(
"{{\"type\":\"advancement_reward\",\"pools\":[{{\"rolls\":1,\"entries\":{entries}}}]}}"
)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct BlockDef {
pub id: String,
pub hardness: f32,
pub resistance: f32,
pub name: Option<String>,
pub shape: Option<[f32; 6]>,
pub light_level: u8,
pub sound: Option<String>,
pub requires_tool: bool,
pub no_collision: bool,
pub slipperiness: f32,
pub connects: bool,
pub connect_groups: Vec<String>,
pub inventory_id: Option<String>,
pub redstone_source: bool,
}
impl BlockDef {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
hardness: 1.5,
resistance: 6.0,
name: None,
shape: None,
light_level: 0,
sound: None,
requires_tool: false,
no_collision: false,
slipperiness: 0.0,
connects: false,
connect_groups: Vec::new(),
inventory_id: None,
redstone_source: false,
}
}
pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
self.hardness = hardness;
self.resistance = resistance;
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
self.shape = Some([x1, y1, z1, x2, y2, z2]);
self
}
pub fn light_level(mut self, level: u8) -> Self {
self.light_level = level.min(15);
self
}
pub fn sound(mut self, group: impl Into<String>) -> Self {
self.sound = Some(group.into());
self
}
pub fn requires_tool(mut self) -> Self {
self.requires_tool = true;
self
}
pub fn no_collision(mut self) -> Self {
self.no_collision = true;
self
}
pub fn slipperiness(mut self, value: f32) -> Self {
self.slipperiness = value;
self
}
pub fn connects_to_neighbors(mut self) -> Self {
self.connects = true;
self
}
pub fn connect_groups(mut self, groups: &[&str]) -> Self {
self.connect_groups = groups
.iter()
.map(std::string::ToString::to_string)
.collect();
self
}
pub fn redstone_source(mut self) -> Self {
self.redstone_source = true;
self
}
pub fn inventory(mut self, id: impl Into<String>) -> Self {
self.inventory_id = Some(id.into());
self
}
}
#[cfg(test)]
mod recipe_tests {
use super::*;
#[test]
fn tags_are_tag_ingredients() {
let shaped = ShapedRecipe::new("m:a", "m:b", 1).row("D").key('D', "#c:dusts").to_json();
assert!(shaped.contains(r#""D":{"tag":"c:dusts"}"#), "{shaped}");
let shapeless = ShapelessRecipe::new("m:a", "m:b", 1).ingredient("minecraft:paper").ingredient("#minecraft:logs").to_json();
assert!(shapeless.contains(r#"[{"item":"minecraft:paper"},{"tag":"minecraft:logs"}]"#), "{shapeless}");
let smelt = FurnaceRecipe::new("m:a", "#c:ores", "m:b", 1).to_json();
assert!(smelt.contains(r#""ingredient":{"tag":"c:ores"}"#), "{smelt}");
}
}