#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod effects;
pub mod recipe_kinds;
pub mod loot;
pub const REGISTRATIONS_FILE: &str = "registrations.txt";
pub fn registration_changes(registered: &str, new: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
fn read(text: &str) -> std::collections::BTreeMap<String, String> {
text.lines()
.filter_map(|l| {
let (key, hash) = l.trim().rsplit_once(' ')?;
Some((key.to_string(), hash.to_string()))
})
.collect()
}
let (old, new) = (read(registered), read(new));
let added = new.keys().filter(|k| !old.contains_key(*k)).cloned().collect();
let removed = old.keys().filter(|k| !new.contains_key(*k)).cloned().collect();
let changed = new.iter().filter(|(k, h)| old.get(*k).is_some_and(|o| o != *h)).map(|(k, _)| k.clone()).collect();
(added, removed, changed)
}
pub use effects::{AttributeDef, Brew, EffectCategory, EffectDef, EffectModifier, ModifierOperation, PotionDef, PotionEffect};
pub use loot::{Loot, LootAdd, LootModifier, LootScale, LootStack};
#[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>,
pub wearable: Option<String>,
pub hangs: Option<Hangs>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
pub struct Hangs {
pub entity: String,
pub size: Option<[u32; 2]>,
}
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,
wearable: None,
hangs: 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 wearable(mut self, slot: impl Into<String>) -> Self {
self.wearable = Some(slot.into());
self
}
pub fn hangs(mut self, entity: impl Into<String>, size: Option<[u32; 2]>) -> Self {
self.hangs = Some(Hangs { entity: entity.into(), size });
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 const ALL: [Cooking; 4] = [Cooking::Smelting, Cooking::Blasting, Cooking::Smoking, Cooking::Campfire];
pub fn word(self) -> &'static str {
recipe_kinds::ids::COOKING[self as usize]
}
pub fn of_kind(id: &str) -> Option<Cooking> {
Cooking::ALL.into_iter().find(|c| c.word() == id)
}
}
#[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: String,
pub placement: String,
pub attach: bool,
pub waterlogged: bool,
pub properties: Vec<BlockProperty>,
pub powered: Option<String>,
pub comparator: Option<String>,
pub accepts_items: bool,
#[data(default)]
pub no_occlusion: bool,
#[data(default)]
pub kind: String,
#[data(default)]
pub base: Option<String>,
#[data(default)]
pub set: Option<String>,
#[data(default = "thirty")]
pub press_ticks: u32,
#[data(default)]
pub mobs_only: bool,
}
fn thirty() -> u32 {
30
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(yog_data::Schema, Debug, Clone, PartialEq, Eq)]
pub struct BlockProperty {
pub name: String,
pub values: Vec<String>,
}
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: String::new(),
placement: String::new(),
attach: false,
waterlogged: false,
properties: Vec::new(),
powered: None,
comparator: None,
accepts_items: false,
no_occlusion: false,
kind: String::new(),
base: None,
set: None,
press_ticks: thirty(),
mobs_only: 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, ways: &str, placement: &str) -> Self {
self.facing = ways.to_string();
self.placement = placement.to_string();
self
}
pub fn attach(mut self) -> Self {
self.attach = true;
self
}
pub fn waterlogged(mut self) -> Self {
self.waterlogged = true;
self
}
pub fn property(mut self, name: &str, values: &[&str]) -> Self {
self.properties.push(BlockProperty { name: name.into(), values: values.iter().map(|v| v.to_string()).collect() });
self
}
pub fn powered(mut self, name: &str) -> Self {
self.powered = Some(name.into());
self
}
pub fn comparator(mut self, name: &str) -> Self {
self.comparator = Some(name.into());
self
}
pub fn accepts_items(mut self) -> Self {
self.accepts_items = true;
self
}
pub fn inventory(mut self, id: impl Into<String>) -> Self {
self.inventory_id = Some(id.into());
self
}
pub fn no_occlusion(mut self) -> Self {
self.no_occlusion = true;
self
}
pub fn kind(mut self, kind: &str) -> Self {
self.kind = kind.into();
self
}
pub fn base(mut self, id: impl Into<String>) -> Self {
self.base = Some(id.into());
self
}
pub fn set(mut self, name: &str) -> Self {
self.set = Some(name.into());
self
}
pub fn press_ticks(mut self, ticks: u32) -> Self {
self.press_ticks = ticks;
self
}
pub fn mobs_only(mut self) -> Self {
self.mobs_only = true;
self
}
}
#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EntityKind {
#[default]
Object,
Living,
Mob,
Hanging,
}
#[derive(yog_data::Schema, Debug, Clone)]
pub struct EntityDef {
pub id: String,
pub name: Option<String>,
#[data(default)]
pub kind: EntityKind,
#[data(default = "misc")]
pub category: String,
pub width: f32,
pub height: f32,
pub eye_height: Option<f32>,
#[data(default)]
pub fire_immune: bool,
#[data(default)]
pub no_gravity: bool,
pub pushable: Option<bool>,
#[data(default)]
pub solid: bool,
#[data(default = "eight")]
pub tracking_range: u32,
#[data(default = "three")]
pub update_interval: u32,
#[data(default)]
pub attributes: yog_data::Ordered<f64>,
pub model: Option<String>,
pub animations: Option<String>,
#[data(default)]
pub clips: yog_data::Ordered<String>,
pub spawn_egg: Option<[u32; 2]>,
#[data(default)]
pub goals: Vec<String>,
#[data(default)]
pub ticks: bool,
pub place_sound: Option<String>,
pub break_sound: Option<String>,
}
fn eight() -> u32 {
8
}
fn three() -> u32 {
3
}
fn misc() -> String {
"misc".into()
}
impl EntityDef {
pub fn new(id: impl Into<String>, kind: EntityKind, width: f32, height: f32) -> Self {
Self {
id: id.into(),
name: None,
kind,
category: misc(),
width,
height,
eye_height: None,
fire_immune: false,
no_gravity: false,
pushable: None,
solid: false,
tracking_range: eight(),
update_interval: three(),
attributes: yog_data::Ordered::default(),
model: None,
animations: None,
clips: yog_data::Ordered::default(),
spawn_egg: None,
goals: Vec::new(),
ticks: false,
place_sound: None,
break_sound: None,
}
}
}
#[cfg(test)]
mod registration_tests {
#[test]
fn a_hot_reload_sees_what_registration_changed() {
let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
let (added, removed, changed) = super::registration_changes(old, new);
assert_eq!(added, ["item mymod:d"]);
assert_eq!(removed, ["item mymod:c"]);
assert_eq!(changed, ["block mymod:b"]);
assert_eq!(super::registration_changes(old, old), (vec![], vec![], vec![]));
}
}
#[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":[]"#));
}
}