#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, 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(yog_data::Schema, 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>,
pub tool: Option<ToolItem>,
pub armor: Option<ArmorPiece>,
}
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,
tool: None,
armor: None,
}
}
pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
self
}
pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
self
}
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";
fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
id.split_once(':').unwrap_or((default_ns, id))
}
fn json_str(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cooking {
Smelting,
Blasting,
Smoking,
Campfire,
}
impl Cooking {
pub fn word(self) -> &'static str {
match self {
Cooking::Smelting => "smelting",
Cooking::Blasting => "blasting",
Cooking::Smoking => "smoking",
Cooking::Campfire => "campfire_cooking",
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum RecipeKind {
Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
Shapeless { ingredients: Vec<Vec<String>> },
Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
Stonecutting { ingredient: Vec<String> },
Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Recipe {
pub id: String,
pub kind: RecipeKind,
pub result: String,
pub count: u32,
pub group: Option<String>,
pub category: Option<String>,
}
impl Recipe {
fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
}
pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
}
pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
}
pub fn cooking(
id: impl Into<String>,
method: Cooking,
ingredient: impl Into<String>,
result: impl Into<String>,
count: u32,
) -> Self {
let cook_time = match method {
Cooking::Smelting => 200,
Cooking::Blasting | Cooking::Smoking => 100,
Cooking::Campfire => 600,
};
let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
Self::of(id, kind, result, count)
}
pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
}
pub fn smithing(
id: impl Into<String>,
template: impl Into<String>,
base: impl Into<String>,
addition: impl Into<String>,
result: impl Into<String>,
) -> Self {
let kind = RecipeKind::Smithing {
template: vec![template.into()],
base: vec![base.into()],
addition: vec![addition.into()],
};
Self::of(id, kind, result, 1)
}
pub fn row(mut self, pattern: impl Into<String>) -> Self {
if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
p.push(pattern.into());
}
self
}
pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
self.key_any(symbol, [item.into()])
}
pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
if let RecipeKind::Shaped { key, .. } = &mut self.kind {
key.push((symbol, items.into_iter().map(Into::into).collect()));
}
self
}
pub fn ingredient(self, item: impl Into<String>) -> Self {
self.ingredient_any([item.into()])
}
pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
ingredients.push(items.into_iter().map(Into::into).collect());
}
self
}
pub fn experience(mut self, xp: f32) -> Self {
if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
*experience = xp;
}
self
}
pub fn cook_time(mut self, ticks: u32) -> Self {
if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
*cook_time = ticks;
}
self
}
pub fn group(mut self, group: impl Into<String>) -> Self {
self.group = Some(group.into());
self
}
pub fn category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn to_data(&self) -> yog_data::Value {
use yog_data::{Map, Value};
let ing = |items: &[String]| match items {
[one] => Value::from(one.as_str()),
many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
};
let mut m = Map::with_capacity(8);
let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
match &self.kind {
RecipeKind::Shaped { pattern, key } => {
ty(&mut m, "shaped");
m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
let mut keys = Map::with_capacity(key.len());
for (c, items) in key {
keys.insert(c.to_string(), ing(items));
}
m.insert("key", Value::Map(keys));
}
RecipeKind::Shapeless { ingredients } => {
ty(&mut m, "shapeless");
m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
}
RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
ty(&mut m, method.word());
m.insert("ingredient", ing(ingredient));
m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
m.insert("cook_time", Value::Int(*cook_time as i64));
}
RecipeKind::Stonecutting { ingredient } => {
ty(&mut m, "stonecutting");
m.insert("ingredient", ing(ingredient));
}
RecipeKind::Smithing { template, base, addition } => {
ty(&mut m, "smithing");
m.insert("template", ing(template));
m.insert("base", ing(base));
m.insert("addition", ing(addition));
}
}
m.insert("result", Value::from(self.result.as_str()));
if self.count != 1 {
m.insert("count", Value::Int(self.count as i64));
}
if let Some(g) = &self.group {
m.insert("group", Value::from(g.as_str()));
}
if let Some(c) = &self.category {
m.insert("category", Value::from(c.as_str()));
}
Value::Map(m)
}
pub fn ns_name(&self) -> (&str, &str) {
split_id(&self.id, VANILLA_NS)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, 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 mut entries: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":{}}}", json_str(i))).collect();
if let Some(book) = &self.book {
let tag = format!("{{yog_book:\"{}\"}}", book.replace('\\', "\\\\").replace('"', "\\\""));
entries.push(format!(
"{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":{}}}]}}",
json_str(&tag)
));
}
if let Some(cmd) = &self.command {
entries.push(format!("{{\"type\":\"minecraft:command\",\"command\":{}}}", json_str(cmd)));
}
format!("{{\"type\":\"yog:startup_grant\",\"entries\":[{}],\"id\":{}}}", entries.join(","), json_str(&self.id))
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct ToolItem {
pub kind: String,
pub material: String,
pub attack_damage: f32,
pub attack_speed: f32,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct ArmorPiece {
pub slot: String,
pub material: String,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct ToolMaterial {
pub id: String,
pub durability: u32,
pub speed: f32,
pub attack_damage: f32,
pub enchantability: u32,
pub level: String,
pub repair: String,
#[data(default)]
pub level_index: u32,
#[data(default)]
pub incorrect: String,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
pub struct ArmorDefense {
pub head: u32,
pub chest: u32,
pub legs: u32,
pub feet: u32,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct ArmorMaterial {
pub id: String,
pub durability: u32,
pub defense: ArmorDefense,
pub enchantability: u32,
pub equip_sound: String,
#[data(default)]
pub toughness: f32,
#[data(default)]
pub knockback_resistance: f32,
pub repair: String,
pub texture: String,
}
static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
.unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
.value
});
static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
.unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
.value
});
fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
list.iter().find(|x| id(x).strip_prefix("minecraft:") == Some(name))
}
impl ToolMaterial {
pub fn built_ins() -> &'static [ToolMaterial] {
&BUILT_IN_TOOL_MATERIALS
}
pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
built_in(Self::built_ins(), |m| &m.id, name)
}
pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
let mut m = Self::built_in(like)?.clone();
m.id = id.into();
Some(m)
}
}
impl ArmorMaterial {
pub fn built_ins() -> &'static [ArmorMaterial] {
&BUILT_IN_ARMOR_MATERIALS
}
pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
built_in(Self::built_ins(), |m| &m.id, name)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct BiomeFeature {
pub feature: String,
pub biomes: String,
pub step: String,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct SoundGroup {
pub id: String,
#[data(rename = "break")]
pub break_sound: String,
pub step: String,
pub place: String,
pub hit: String,
pub fall: String,
#[data(default = "one")]
#[data(range = "0..=10")]
pub volume: f32,
#[data(default = "one")]
#[data(range = "0..=10")]
pub pitch: f32,
}
fn one() -> f32 {
1.0
}
static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
.unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
.value
});
impl SoundGroup {
pub fn new(id: impl Into<String>) -> Self {
let id = id.into();
let ev = |s: &str| format!("{id}.{s}");
Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
}
pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
[self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
self
}
pub fn volume(mut self, volume: f32) -> Self {
self.volume = volume;
self
}
pub fn pitch(mut self, pitch: f32) -> Self {
self.pitch = pitch;
self
}
pub fn built_ins() -> &'static [SoundGroup] {
&BUILT_IN_SOUND_GROUPS
}
pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
Self::built_ins().iter().find(|g| g.id.strip_prefix("minecraft:") == Some(name))
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, 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,
pub facing: 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,
facing: 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 facing(mut self) -> Self {
self.facing = true;
self
}
pub fn inventory(mut self, id: impl Into<String>) -> Self {
self.inventory_id = Some(id.into());
self
}
}
#[cfg(test)]
mod material_tests {
use super::*;
#[test]
fn the_games_materials_are_built_in() {
assert_eq!(ToolMaterial::built_ins().len(), 6);
let iron = ToolMaterial::built_in("iron").unwrap();
assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
}
}
#[cfg(test)]
mod sound_tests {
use super::*;
#[test]
fn the_games_sound_groups_are_built_in() {
assert!(SoundGroup::built_ins().len() > 100);
let stone = SoundGroup::built_in("stone").unwrap();
assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
assert!(SoundGroup::built_in("mymod:crystal").is_none());
}
#[test]
fn a_mods_group_crosses_as_yog_data() {
use yog_data::Schema;
let g = SoundGroup::new("mymod:crystal").pitch(1.2);
assert_eq!(g.fall, "mymod:crystal.fall");
let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
assert_eq!(back, g);
assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
}
}
#[cfg(test)]
impl Recipe {
fn to_json(&self) -> String {
yog_data::json::to_json(&self.to_data()).to_string()
}
}
#[cfg(test)]
mod recipe_tests {
use super::*;
#[test]
fn writes_the_yog_format() {
let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
assert_eq!(
js(&shaped.to_json()),
js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
);
assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
assert_eq!(js(&smith.to_json())["template"], "m:t");
}
}
#[cfg(test)]
mod json_tests {
use super::*;
#[test]
fn json_survives_any_string() {
let odd = "m:a\"b\\c\nd\te\u{1}";
let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
for r in [
Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
Recipe::stonecutting(odd, odd, odd, 2),
Recipe::smithing(odd, odd, odd, odd, odd),
] {
let v = parse(r.to_json());
assert_eq!(v["result"], odd, "the string comes back as it was");
}
let g = StartupGrant::new(odd).item(odd).book("mymod:\"guide\"").command("say \"hi\" \\o/");
let v = parse(g.to_json());
assert_eq!(v["id"], odd);
assert_eq!(v["entries"][2]["command"], "say \"hi\" \\o/");
}
#[test]
fn recipes_and_grants_name_their_parts() {
let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
assert_eq!(v["type"], "campfire_cooking");
assert_eq!(v["cook_time"], 600, "the method's default time");
assert_eq!(r.ns_name(), ("mymod", "a"));
assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
let g = StartupGrant::new("mymod:starter");
assert!(g.to_json().contains(r#""entries":[]"#));
}
}