Skip to main content

yog_registry/
lib.rs

1//! Content registration — declare custom items and blocks from Rust.
2//!
3//! Definitions are collected at registration time and handed to the host, which
4//! registers real `Item`/`Block` objects before the game's registries freeze,
5//! puts them in per-namespace creative tabs, and applies their properties.
6//! (Textures, models and recipes are assets/data shipped in the `.yog` and
7//! served to the game.)
8//!
9//! ```
10//! # use yog_registry::{ItemDef, BlockDef, FoodDef};
11//! ItemDef::new("mymod:ruby").name("Ruby").tooltip("A shiny gem.").max_stack(16);
12//! ItemDef::new("mymod:pie").food(FoodDef::new(4, 0.3));
13//! BlockDef::new("mymod:lamp").light_level(15).sound("stone");
14//! ```
15
16#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19pub mod effects;
20pub mod recipe_kinds;
21pub mod loot;
22pub mod atlases;
23pub mod item_looks;
24pub mod tool_levels;
25
26/// What a `.yog` registers when the game starts — content the game can't
27/// add or change while it runs (blocks, items, entity types, sound groups,
28/// materials, dimensions): a line each, `<kind> <id> <hash>`, the hash of
29/// what can't change. A hot reload compares the new `.yog`'s with what's
30/// registered ([`registration_changes`]).
31pub const REGISTRATIONS_FILE: &str = "registrations.txt";
32
33/// How `new` (a [`REGISTRATIONS_FILE`]) differs from `registered`: what it
34/// adds, removes and changes — `<kind> <id>` each.
35pub fn registration_changes(registered: &str, new: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
36    fn read(text: &str) -> std::collections::BTreeMap<String, String> {
37        text.lines()
38            .filter_map(|l| {
39                let (key, hash) = l.trim().rsplit_once(' ')?;
40                Some((key.to_string(), hash.to_string()))
41            })
42            .collect()
43    }
44    let (old, new) = (read(registered), read(new));
45    let added = new.keys().filter(|k| !old.contains_key(*k)).cloned().collect();
46    let removed = old.keys().filter(|k| !new.contains_key(*k)).cloned().collect();
47    let changed = new.iter().filter(|(k, h)| old.get(*k).is_some_and(|o| o != *h)).map(|(k, _)| k.clone()).collect();
48    (added, removed, changed)
49}
50pub use effects::{AttributeDef, Brew, EffectCategory, EffectDef, EffectModifier, ModifierOperation, PotionDef, PotionEffect};
51pub use loot::{Loot, LootAdd, LootModifier, LootScale, LootStack};
52pub use atlases::Atlas;
53pub use item_looks::{ItemLooks, ItemTint, LookModels};
54pub use tool_levels::{LevelTag, ToolLadder, ToolLevel};
55
56// ── Items ────────────────────────────────────────────────────────────────────
57
58/// Nutritional properties for a food item.
59#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60#[derive(yog_data::Schema, Debug, Clone)]
61pub struct FoodDef {
62    /// Hunger points restored (1 unit = half a drumstick).
63    pub nutrition: u32,
64    /// Saturation modifier applied after eating.
65    pub saturation: f32,
66    /// If `true`, edible even when the hunger bar is full.
67    pub can_always_eat: bool,
68}
69
70impl FoodDef {
71    pub fn new(nutrition: u32, saturation: f32) -> Self {
72        Self {
73            nutrition,
74            saturation,
75            can_always_eat: false,
76        }
77    }
78
79    pub fn can_always_eat(mut self) -> Self {
80        self.can_always_eat = true;
81        self
82    }
83}
84
85/// A custom item to register, identified by `namespace:path`.
86#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
87#[derive(yog_data::Schema, Debug, Clone)]
88pub struct ItemDef {
89    pub id: String,
90    pub max_stack: u8,
91    pub name: Option<String>,
92    pub tooltip: Option<String>,
93    /// Durability. `0` = non-damageable. If set, `max_stack` is forced to 1.
94    pub max_damage: u32,
95    /// Immune to fire and lava damage (like netherite items).
96    pub fire_resistant: bool,
97    /// Furnace fuel burn time in ticks (`0` = not fuel; 200 = one coal equivalent).
98    pub fuel_ticks: u32,
99    /// Nutritional properties; `None` = not food.
100    pub food: Option<FoodDef>,
101    /// A tool or a weapon of a material.
102    pub tool: Option<ToolItem>,
103    /// A piece of armor of a material.
104    pub armor: Option<ArmorPiece>,
105    /// Worn in this slot without being armor: `head`, `chest`, `legs`, `feet`.
106    pub wearable: Option<String>,
107    /// Used on a block's side, it hangs there as an entity of a hanging type.
108    pub hangs: Option<Hangs>,
109}
110
111/// What an item hangs on a wall ([`ItemDef::hangs`]).
112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
114pub struct Hangs {
115    /// A hanging entity type ([`EntityKind::Hanging`]).
116    pub entity: String,
117    /// Whole blocks of wall, wide and high; `None`: the type's size.
118    pub size: Option<[u32; 2]>,
119}
120
121impl ItemDef {
122    pub fn new(id: impl Into<String>) -> Self {
123        Self {
124            id: id.into(),
125            max_stack: 64,
126            name: None,
127            tooltip: None,
128            max_damage: 0,
129            fire_resistant: false,
130            fuel_ticks: 0,
131            food: None,
132            tool: None,
133            armor: None,
134            wearable: None,
135            hangs: None,
136        }
137    }
138
139    /// A tool (`pickaxe`, `axe`, `shovel`, `hoe`, a mod's mineable tag
140    /// `mymod:mineable/hammer`) or a `sword`, of `material` (a
141    /// [`ToolMaterial`]: the game's `iron`… or a mod's).
142    pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
143        self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
144        self
145    }
146
147    /// A piece of armor for `slot` (`head`, `chest`, `legs`, `feet`) of
148    /// `material` (an [`ArmorMaterial`]: the game's `diamond`… or a mod's).
149    pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
150        self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
151        self
152    }
153
154    /// Worn in `slot` (`head`, `chest`, `legs`, `feet`) without being armor.
155    pub fn wearable(mut self, slot: impl Into<String>) -> Self {
156        self.wearable = Some(slot.into());
157        self
158    }
159
160    /// Used on a block's side, it hangs an `entity` (a hanging type) there,
161    /// `size` blocks wide and high (`None`: the type's own); the entity
162    /// keeps one of this item and drops it when it breaks.
163    pub fn hangs(mut self, entity: impl Into<String>, size: Option<[u32; 2]>) -> Self {
164        self.hangs = Some(Hangs { entity: entity.into(), size });
165        self
166    }
167
168    /// Maximum stack size (default 64). Ignored when `max_damage` is set.
169    pub fn max_stack(mut self, n: u8) -> Self {
170        self.max_stack = n;
171        self
172    }
173
174    /// Display name shown in-game.
175    pub fn name(mut self, name: impl Into<String>) -> Self {
176        self.name = Some(name.into());
177        self
178    }
179
180    /// A tooltip line shown on hover.
181    pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
182        self.tooltip = Some(tooltip.into());
183        self
184    }
185
186    /// Make this a damageable item (tool/weapon/armour). Forces stack size to 1.
187    pub fn max_damage(mut self, durability: u32) -> Self {
188        self.max_damage = durability;
189        self
190    }
191
192    /// Make this item fire-resistant (won't burn in fire or lava).
193    pub fn fire_resistant(mut self) -> Self {
194        self.fire_resistant = true;
195        self
196    }
197
198    /// Register as furnace fuel burning for `ticks` (200 ticks = 1 item smelted).
199    pub fn fuel(mut self, ticks: u32) -> Self {
200        self.fuel_ticks = ticks;
201        self
202    }
203
204    /// Make this item edible with the given nutritional properties.
205    pub fn food(mut self, food: FoodDef) -> Self {
206        self.food = Some(food);
207        self
208    }
209}
210
211// ── Recipes ──────────────────────────────────────────────────────────────────
212
213
214
215/// How a cooking recipe cooks.
216#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum Cooking {
219    Smelting,
220    Blasting,
221    Smoking,
222    Campfire,
223}
224
225impl Cooking {
226    /// Every one, in [`recipe_kinds::ids::COOKING`]'s order.
227    pub const ALL: [Cooking; 4] = [Cooking::Smelting, Cooking::Blasting, Cooking::Smoking, Cooking::Campfire];
228
229    /// Its kind of recipe (`smelting`).
230    pub fn word(self) -> &'static str {
231        recipe_kinds::ids::COOKING[self as usize]
232    }
233
234    /// The cooking of kind `id`, if `id` is one.
235    pub fn of_kind(id: &str) -> Option<Cooking> {
236        Cooking::ALL.into_iter().find(|c| c.word() == id)
237    }
238}
239
240/// What a recipe is made of besides its result.
241#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
242#[derive(Debug, Clone, PartialEq)]
243pub enum RecipeKind {
244    /// A crafting grid: rows of key characters (space = empty).
245    Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
246    /// Crafting, in any arrangement.
247    Shapeless { ingredients: Vec<Vec<String>> },
248    Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
249    Stonecutting { ingredient: Vec<String> },
250    /// A smithing table: `base` upgraded with `addition`, `template` used up.
251    Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
252}
253
254/// A recipe, in Yog's own format — what a `.yrecipe` says. The host writes
255/// the recipe JSON of its Minecraft version from it.
256///
257/// An ingredient is an item id (`minecraft:stick`), an item tag (`#c:dusts`)
258/// or — the `_any` builders — any of several.
259///
260/// ```
261/// # use yog_registry::{Cooking, Recipe};
262/// Recipe::shaped("yog:ruby_sword", "yog:ruby_sword", 1)
263///     .row(" R ").row(" R ").row(" S ")
264///     .key('R', "yog:ruby").key('S', "minecraft:stick");
265/// Recipe::cooking("yog:ruby", Cooking::Blasting, "#c:ruby_ores", "yog:ruby", 1).experience(0.7);
266/// ```
267#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
268#[derive(Debug, Clone, PartialEq)]
269pub struct Recipe {
270    pub id: String,
271    pub kind: RecipeKind,
272    pub result: String,
273    pub count: u32,
274    /// Recipes of a group share a slot in the recipe book.
275    pub group: Option<String>,
276    /// The recipe book's tab (`building`, `equipment`, `misc`, …).
277    pub category: Option<String>,
278}
279
280impl Recipe {
281    fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
282        Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
283    }
284
285    pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
286        Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
287    }
288
289    pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
290        Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
291    }
292
293    /// Cooking `ingredient` (100 % of a vanilla furnace: 0.1 experience,
294    /// 200 ticks — see [`Self::experience`], [`Self::cook_time`]).
295    pub fn cooking(
296        id: impl Into<String>,
297        method: Cooking,
298        ingredient: impl Into<String>,
299        result: impl Into<String>,
300        count: u32,
301    ) -> Self {
302        let cook_time = match method {
303            Cooking::Smelting => 200,
304            Cooking::Blasting | Cooking::Smoking => 100,
305            Cooking::Campfire => 600,
306        };
307        let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
308        Self::of(id, kind, result, count)
309    }
310
311    pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
312        Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
313    }
314
315    pub fn smithing(
316        id: impl Into<String>,
317        template: impl Into<String>,
318        base: impl Into<String>,
319        addition: impl Into<String>,
320        result: impl Into<String>,
321    ) -> Self {
322        let kind = RecipeKind::Smithing {
323            template: vec![template.into()],
324            base: vec![base.into()],
325            addition: vec![addition.into()],
326        };
327        Self::of(id, kind, result, 1)
328    }
329
330    /// A row of a shaped recipe's pattern.
331    pub fn row(mut self, pattern: impl Into<String>) -> Self {
332        if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
333            p.push(pattern.into());
334        }
335        self
336    }
337
338    /// What `symbol` stands for in a shaped recipe.
339    pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
340        self.key_any(symbol, [item.into()])
341    }
342
343    /// `symbol` stands for any of `items`.
344    pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
345        if let RecipeKind::Shaped { key, .. } = &mut self.kind {
346            key.push((symbol, items.into_iter().map(Into::into).collect()));
347        }
348        self
349    }
350
351    /// One more ingredient of a shapeless recipe.
352    pub fn ingredient(self, item: impl Into<String>) -> Self {
353        self.ingredient_any([item.into()])
354    }
355
356    /// One more ingredient of a shapeless recipe: any of `items`.
357    pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
358        if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
359            ingredients.push(items.into_iter().map(Into::into).collect());
360        }
361        self
362    }
363
364    /// Experience a cooking recipe gives.
365    pub fn experience(mut self, xp: f32) -> Self {
366        if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
367            *experience = xp;
368        }
369        self
370    }
371
372    /// A cooking recipe's time in ticks.
373    pub fn cook_time(mut self, ticks: u32) -> Self {
374        if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
375            *cook_time = ticks;
376        }
377        self
378    }
379
380    pub fn group(mut self, group: impl Into<String>) -> Self {
381        self.group = Some(group.into());
382        self
383    }
384
385    pub fn category(mut self, category: impl Into<String>) -> Self {
386        self.category = Some(category.into());
387        self
388    }
389
390    /// The recipe in Yog's own format (the `.yrecipe` shape) — what
391    /// `Registry::add_recipe` hands the runtime.
392    pub fn to_data(&self) -> yog_data::Value {
393        use yog_data::{Map, Value};
394        let ing = |items: &[String]| match items {
395            [one] => Value::from(one.as_str()),
396            many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
397        };
398        let mut m = Map::with_capacity(8);
399        let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
400        match &self.kind {
401            RecipeKind::Shaped { pattern, key } => {
402                ty(&mut m, "shaped");
403                m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
404                let mut keys = Map::with_capacity(key.len());
405                for (c, items) in key {
406                    keys.insert(c.to_string(), ing(items));
407                }
408                m.insert("key", Value::Map(keys));
409            }
410            RecipeKind::Shapeless { ingredients } => {
411                ty(&mut m, "shapeless");
412                m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
413            }
414            RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
415                ty(&mut m, method.word());
416                m.insert("ingredient", ing(ingredient));
417                // As written (`0.7`, not f32's 0.699999988…).
418                m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
419                m.insert("cook_time", Value::Int(*cook_time as i64));
420            }
421            RecipeKind::Stonecutting { ingredient } => {
422                ty(&mut m, "stonecutting");
423                m.insert("ingredient", ing(ingredient));
424            }
425            RecipeKind::Smithing { template, base, addition } => {
426                ty(&mut m, "smithing");
427                m.insert("template", ing(template));
428                m.insert("base", ing(base));
429                m.insert("addition", ing(addition));
430            }
431        }
432        m.insert("result", Value::from(self.result.as_str()));
433        if self.count != 1 {
434            m.insert("count", Value::Int(self.count as i64));
435        }
436        if let Some(g) = &self.group {
437            m.insert("group", Value::from(g.as_str()));
438        }
439        if let Some(c) = &self.category {
440            m.insert("category", Value::from(c.as_str()));
441        }
442        Value::Map(m)
443    }
444
445    /// Split `namespace:name` from the recipe id.
446    pub fn ns_name(&self) -> (&str, &str) {
447        yog_ids::split(&self.id)
448    }
449}
450
451// ── BookRecipe (like Patchouli's shapeless_book_recipe) ──────────────────────
452
453
454
455// ── ItemModifier ─────────────────────────────────────────────────────────────
456
457
458
459// ── StartupGrant ─────────────────────────────────────────────────────────────
460
461/// Grant items/books to every player once when they first join.
462/// This is the Yog-side replacement for `grant_patchi_book.json`.
463#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
464#[derive(yog_data::Schema, Debug, Clone)]
465pub struct StartupGrant {
466    pub id: String,
467    pub items: Vec<String>,
468    pub book: Option<String>,
469    pub command: Option<String>,
470}
471
472impl StartupGrant {
473    pub fn new(id: impl Into<String>) -> Self {
474        Self {
475            id: id.into(),
476            items: Vec::new(),
477            book: None,
478            command: None,
479        }
480    }
481
482    pub fn item(mut self, item_id: impl Into<String>) -> Self {
483        self.items.push(item_id.into());
484        self
485    }
486
487    pub fn book(mut self, book: impl Into<String>) -> Self {
488        self.book = Some(book.into());
489        self
490    }
491
492    pub fn command(mut self, cmd: impl Into<String>) -> Self {
493        self.command = Some(cmd.into());
494        self
495    }
496}
497
498
499
500// ── Tools and armor ─────────────────────────────────────────────────────────
501
502/// An item as a tool or weapon.
503#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
504#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
505pub struct ToolItem {
506    /// `pickaxe`, `axe`, `shovel`, `hoe`, `sword`, or the mineable tag of
507    /// a mod's kind of tool (`mymod:mineable/hammer`).
508    pub kind: String,
509    /// Its [`ToolMaterial`]'s id (`minecraft:iron`, `mymod:mythril`).
510    pub material: String,
511    /// Attack damage on top of the material's (an iron pickaxe: 1).
512    pub attack_damage: f32,
513    /// Attack speed change (a pickaxe: -2.8, a sword: -2.4).
514    pub attack_speed: f32,
515}
516
517/// An item as a piece of armor.
518#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
519#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
520pub struct ArmorPiece {
521    /// `head`, `chest`, `legs` or `feet`.
522    pub slot: String,
523    /// Its [`ArmorMaterial`]'s id.
524    pub material: String,
525}
526
527/// What tools are made of. The game's are built in
528/// ([`ToolMaterial::built_ins`]); a mod declares its own in a `.ydef`
529/// (`kind: "tool_material"`) or registers one
530/// (`Registry::register_tool_material`).
531#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
532#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
533pub struct ToolMaterial {
534    pub id: String,
535    /// Uses before it breaks.
536    pub durability: u32,
537    /// How fast it mines (wood 2 … netherite 9, gold 12).
538    pub speed: f32,
539    /// Attack damage it adds (wood 0 … netherite 4).
540    pub attack_damage: f32,
541    pub enchantability: u32,
542    /// Its level on the tool ladder (`minecraft:iron`, `mymod:mythril`).
543    pub level: String,
544    /// What repairs it: an item id or `#tag`.
545    pub repair: String,
546    /// Minecraft 1.20's level number of `level` (wood 0 … netherite 4).
547    #[data(default)]
548    pub level_index: u32,
549    /// Minecraft 1.21's tag of the blocks a tool of it can't mine for
550    /// drops (`minecraft:incorrect_for_iron_tool`).
551    #[data(default)]
552    pub incorrect: String,
553}
554
555/// Defense points per armor piece.
556#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
557#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
558pub struct ArmorDefense {
559    pub head: u32,
560    pub chest: u32,
561    pub legs: u32,
562    pub feet: u32,
563}
564
565/// What armor is made of. The game's are built in
566/// ([`ArmorMaterial::built_ins`]); a mod declares its own in a `.ydef`
567/// (`kind: "armor_material"`) or registers one.
568#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
569#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
570pub struct ArmorMaterial {
571    pub id: String,
572    /// Multiplies each piece's base durability (helmet 11, chestplate 16,
573    /// leggings 15, boots 13).
574    pub durability: u32,
575    pub defense: ArmorDefense,
576    pub enchantability: u32,
577    /// The sound event of putting it on.
578    pub equip_sound: String,
579    #[data(default)]
580    pub toughness: f32,
581    #[data(default)]
582    pub knockback_resistance: f32,
583    /// What repairs it: an item id or `#tag`.
584    pub repair: String,
585    /// Its look on a player: `<ns>:<name>` is
586    /// `<ns>:textures/models/armor/<name>_layer_1.png` (and `_layer_2`).
587    pub texture: String,
588}
589
590static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
591    yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
592        .unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
593        .value
594});
595
596static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
597    yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
598        .unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
599        .value
600});
601
602/// One of `list` by short name or id (`iron` = `minecraft:iron`).
603fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
604    let name = yog_ids::qualify(name);
605    list.iter().find(|x| id(x) == name)
606}
607
608impl ToolMaterial {
609    /// The game's materials: wood, stone, iron, diamond, gold, netherite.
610    pub fn built_ins() -> &'static [ToolMaterial] {
611        &BUILT_IN_TOOL_MATERIALS
612    }
613
614    /// One of the game's, by short name or id.
615    pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
616        built_in(Self::built_ins(), |m| &m.id, name)
617    }
618
619    /// `id` with `like`'s level (one of the game's: `iron`), durability,
620    /// speed, damage, enchantability and repair — change what differs.
621    pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
622        let mut m = Self::built_in(like)?.clone();
623        m.id = id.into();
624        Some(m)
625    }
626}
627
628impl ArmorMaterial {
629    /// The game's materials: leather, chainmail, iron, gold, diamond, turtle, netherite.
630    pub fn built_ins() -> &'static [ArmorMaterial] {
631        &BUILT_IN_ARMOR_MATERIALS
632    }
633
634    /// One of the game's, by short name or id.
635    pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
636        built_in(Self::built_ins(), |m| &m.id, name)
637    }
638}
639
640// ── World generation ────────────────────────────────────────────────────────
641
642/// A placed feature (`mymod:mythril_ore`, the mod's datapack's) added to
643/// biomes at a generation step — what an ore `.ydef` makes. Forge and
644/// NeoForge read the biome modifiers `yog build` writes; Fabric's host adds
645/// these through its biome API.
646#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
647#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
648pub struct BiomeFeature {
649    /// The placed feature's id.
650    pub feature: String,
651    /// The tag of biomes it goes in (`minecraft:is_overworld`).
652    pub biomes: String,
653    /// The generation step (`underground_ores`, `vegetal_decoration`, …).
654    pub step: String,
655}
656
657// ── Sound groups ─────────────────────────────────────────────────────────────
658
659/// What a block sounds like: the sound events it plays when broken,
660/// stepped on, placed, hit while mining and fallen on. The game's groups
661/// are built in ([`SoundGroup::built_in`]); a mod registers its own
662/// (`Registry::register_sound_group`, or a `.ydef` of kind `sound_group`)
663/// and names it in `BlockDef::sound`.
664#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
665#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
666pub struct SoundGroup {
667    pub id: String,
668    /// Sound event ids (`minecraft:block.stone.break`, `mymod:crystal.break`).
669    #[data(rename = "break")]
670    pub break_sound: String,
671    pub step: String,
672    pub place: String,
673    pub hit: String,
674    pub fall: String,
675    #[data(default = "one")]
676    #[data(range = "0..=10")]
677    pub volume: f32,
678    #[data(default = "one")]
679    #[data(range = "0..=10")]
680    pub pitch: f32,
681}
682
683fn one() -> f32 {
684    1.0
685}
686
687static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
688    yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
689        .unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
690        .value
691});
692
693impl SoundGroup {
694    /// `mymod:crystal`, its sounds `mymod:crystal.break`, `.step`, `.place`,
695    /// `.hit`, `.fall`; volume and pitch 1.
696    pub fn new(id: impl Into<String>) -> Self {
697        let id = id.into();
698        let ev = |s: &str| format!("{id}.{s}");
699        Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
700    }
701
702    /// The five sound events: break, step, place, hit, fall.
703    pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
704        [self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
705        self
706    }
707
708    pub fn volume(mut self, volume: f32) -> Self {
709        self.volume = volume;
710        self
711    }
712
713    pub fn pitch(mut self, pitch: f32) -> Self {
714        self.pitch = pitch;
715        self
716    }
717
718    /// The game's groups (`minecraft:stone`, `minecraft:amethyst`…, from
719    /// Minecraft's `SoundType`), as data.
720    pub fn built_ins() -> &'static [SoundGroup] {
721        &BUILT_IN_SOUND_GROUPS
722    }
723
724    /// One of the game's groups, by short name or id (`stone` = `minecraft:stone`).
725    pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
726        built_in(Self::built_ins(), |g| &g.id, name)
727    }
728}
729
730// ── Blocks ───────────────────────────────────────────────────────────────────
731
732/// A custom block to register; it also gets a matching block-item.
733#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
734#[derive(yog_data::Schema, Debug, Clone)]
735pub struct BlockDef {
736    pub id: String,
737    pub hardness: f32,
738    pub resistance: f32,
739    pub name: Option<String>,
740    /// Optional collision/outline box in pixel units (0–16): `[x1,y1,z1,x2,y2,z2]`.
741    /// `None` = full cube.
742    pub shape: Option<[f32; 6]>,
743    /// Light emitted by this block (0 = none, 15 = max, like a torch).
744    pub light_level: u8,
745    /// Its [`SoundGroup`]: one of the game's by name (`"stone"`,
746    /// `"amethyst"`, `"copper"` — [`SoundGroup::built_in`]) or a mod's
747    /// (`"mymod:crystal"`). `None` = stone (Minecraft default).
748    pub sound: Option<String>,
749    /// If `true`, the correct tool (from the block's tags) is required for drops.
750    pub requires_tool: bool,
751    /// If `true`, entities pass through this block (like flowers or torches).
752    pub no_collision: bool,
753    /// Friction coefficient. `0.0` = default (0.6). Ice = 0.989.
754    pub slipperiness: f32,
755    /// If `true`, this block dynamically grows arms toward neighbors it's
756    /// compatible with (fence/pipe-style): the Java host tracks N/S/E/W/U/D
757    /// boolean blockstate properties, recomputed on placement and on
758    /// neighbor change, and grows the collision/outline shape from the
759    /// `shape` core box (or a default post) out to each connected side.
760    pub connects: bool,
761    /// Connection compatibility tags (configured in code via
762    /// `.connect_groups(&[...])` — see that method's docs for how this
763    /// drives which blocks link up). Independent of `connects`: a block can
764    /// carry tags purely as a connection *target* (e.g. an ALU accepting a
765    /// Digital Cable) without dynamically growing its own shape.
766    pub connect_groups: Vec<String>,
767    /// Id of a `YogInventoryDef` this block is backed by — `None` = plain
768    /// block (default). See `yog_inventory`'s DESIGN.md.
769    pub inventory_id: Option<String>,
770    /// Gives off redstone power — how much, per position, the mod sets with
771    /// `Server::set_redstone_output`.
772    pub redstone_source: bool,
773    /// How it turns when placed (a `facing` property): `""` (it doesn't),
774    /// `"horizontal"` or `"all"`.
775    pub facing: String,
776    /// `"toward"`: its front toward the placer; `"away"`: the way they look.
777    pub placement: String,
778    /// It hangs on a floor, wall or ceiling, as a button (a `face`
779    /// property and a horizontal `facing`).
780    pub attach: bool,
781    /// It can stand in water (a `waterlogged` property).
782    pub waterlogged: bool,
783    /// Properties of its own.
784    pub properties: Vec<BlockProperty>,
785    /// A yes/no property the block keeps on while it gets redstone power.
786    pub powered: Option<String>,
787    /// A yes/no property a comparator reads 15 from while it's on.
788    pub comparator: Option<String>,
789    /// Hoppers and droppers can put items into it (`on_item_insert`).
790    pub accepts_items: bool,
791    /// It doesn't hide the faces behind it or stop light (as glass, a door).
792    #[data(default)]
793    pub no_occlusion: bool,
794    /// One of the game's kinds of block it is (`stairs`, `slab`, `door`,
795    /// `trapdoor`, `button`, `pressure_plate`, `fence`, `fence_gate`): the
796    /// host makes it of the game's class, with that class's states; `""`: a
797    /// block of Yog's own.
798    #[data(default)]
799    pub kind: String,
800    /// The block a kind is made from (stairs act as it).
801    #[data(default)]
802    pub base: Option<String>,
803    /// A kind's set — a registered block set by name (`oak`, `iron`…; a
804    /// fence gate's wood type); `None`: `oak`.
805    #[data(default)]
806    pub set: Option<String>,
807    /// How long a button stays pressed, ticks.
808    #[data(default = "thirty")]
809    pub press_ticks: u32,
810    /// Only living things set off a button or a pressure plate (not items
811    /// or arrows).
812    #[data(default)]
813    pub mobs_only: bool,
814}
815
816fn thirty() -> u32 {
817    30
818}
819
820/// A property of a block's own: its name and values (the first the
821/// default; `false`, `true` is a yes/no).
822#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
823#[derive(yog_data::Schema, Debug, Clone, PartialEq, Eq)]
824pub struct BlockProperty {
825    pub name: String,
826    pub values: Vec<String>,
827}
828
829impl BlockDef {
830    pub fn new(id: impl Into<String>) -> Self {
831        Self {
832            id: id.into(),
833            hardness: 1.5,
834            resistance: 6.0,
835            name: None,
836            shape: None,
837            light_level: 0,
838            sound: None,
839            requires_tool: false,
840            no_collision: false,
841            slipperiness: 0.0,
842            connects: false,
843            connect_groups: Vec::new(),
844            inventory_id: None,
845            redstone_source: false,
846            facing: String::new(),
847            placement: String::new(),
848            attach: false,
849            waterlogged: false,
850            properties: Vec::new(),
851            powered: None,
852            comparator: None,
853            accepts_items: false,
854            no_occlusion: false,
855            kind: String::new(),
856            base: None,
857            set: None,
858            press_ticks: thirty(),
859            mobs_only: false,
860        }
861    }
862
863    /// Mining hardness and blast resistance (defaults 1.5 / 6.0).
864    pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
865        self.hardness = hardness;
866        self.resistance = resistance;
867        self
868    }
869
870    /// Display name shown in-game.
871    pub fn name(mut self, name: impl Into<String>) -> Self {
872        self.name = Some(name.into());
873        self
874    }
875
876    /// Custom hitbox/outline in pixel units (0–16).
877    pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
878        self.shape = Some([x1, y1, z1, x2, y2, z2]);
879        self
880    }
881
882    /// Emitted light level (0–15).
883    pub fn light_level(mut self, level: u8) -> Self {
884        self.light_level = level.min(15);
885        self
886    }
887
888    /// Sound group: one of the game's (`"stone"`, `"amethyst"`…) or a
889    /// mod's [`SoundGroup`] (`"mymod:crystal"`).
890    pub fn sound(mut self, group: impl Into<String>) -> Self {
891        self.sound = Some(group.into());
892        self
893    }
894
895    /// Correct tool required for loot drops (equivalent to `requiresTool()`).
896    pub fn requires_tool(mut self) -> Self {
897        self.requires_tool = true;
898        self
899    }
900
901    /// No physical collision — entities pass through (like flowers).
902    pub fn no_collision(mut self) -> Self {
903        self.no_collision = true;
904        self
905    }
906
907    /// Friction (default 0.6). Set to 0.989 for ice-like slipperiness.
908    pub fn slipperiness(mut self, value: f32) -> Self {
909        self.slipperiness = value;
910        self
911    }
912
913    /// Dynamically grow arms toward compatible neighbors — fence/pipe-style.
914    /// "Compatible" means the neighbor also has a `connect_groups` tag in
915    /// common (set here, or via `connect_groups` alone on a static target
916    /// block). Call `.connect_groups(&[...])` too, or this connects to
917    /// nothing. See the `connects` field doc.
918    pub fn connects_to_neighbors(mut self) -> Self {
919        self.connects = true;
920        self
921    }
922
923    /// Connection compatibility tags — two blocks link up (for
924    /// `connects_to_neighbors` arm growth, and as valid connection targets
925    /// generally) when their tag sets share at least one entry. Example:
926    /// `analog_cable` and `redstone_port` both carry `"analog"`;
927    /// `digital_cable` carries `"digital"`; an ALU block carries both, so it
928    /// accepts either cable while a Redstone Port only accepts the analog one.
929    pub fn connect_groups(mut self, groups: &[&str]) -> Self {
930        self.connect_groups = groups
931            .iter()
932            .map(std::string::ToString::to_string)
933            .collect();
934        self
935    }
936
937    /// Back this block with a real Container/Menu inventory screen, whose
938    /// shape/layout was registered separately via `Registry::register_inventory`
939    /// — see `yog_inventory::InventoryDef`. `id` is that def's id.
940    /// A redstone power source: what each placed block gives off is set
941    /// with `Server::set_redstone_output` (0 until then).
942    pub fn redstone_source(mut self) -> Self {
943        self.redstone_source = true;
944        self
945    }
946
947    /// Turns when placed: `"horizontal"` or `"all"` ways, its front (the
948    /// model's north side) `"toward"` the placer or `"away"` (the way they look).
949    pub fn facing(mut self, ways: &str, placement: &str) -> Self {
950        self.facing = ways.to_string();
951        self.placement = placement.to_string();
952        self
953    }
954
955    /// Hangs on a floor, wall or ceiling, as a button.
956    pub fn attach(mut self) -> Self {
957        self.attach = true;
958        self
959    }
960
961    /// Can stand in water.
962    pub fn waterlogged(mut self) -> Self {
963        self.waterlogged = true;
964        self
965    }
966
967    /// A property of its own: `values`, the first the default.
968    pub fn property(mut self, name: &str, values: &[&str]) -> Self {
969        self.properties.push(BlockProperty { name: name.into(), values: values.iter().map(|v| v.to_string()).collect() });
970        self
971    }
972
973    /// Keeps yes/no property `name` on while it gets redstone power.
974    pub fn powered(mut self, name: &str) -> Self {
975        self.powered = Some(name.into());
976        self
977    }
978
979    /// A comparator reads 15 from it while yes/no property `name` is on.
980    pub fn comparator(mut self, name: &str) -> Self {
981        self.comparator = Some(name.into());
982        self
983    }
984
985    /// Hoppers and droppers can put items into it (`on_item_insert`).
986    pub fn accepts_items(mut self) -> Self {
987        self.accepts_items = true;
988        self
989    }
990
991    pub fn inventory(mut self, id: impl Into<String>) -> Self {
992        self.inventory_id = Some(id.into());
993        self
994    }
995
996    /// Doesn't hide the faces behind it or stop light (as glass).
997    pub fn no_occlusion(mut self) -> Self {
998        self.no_occlusion = true;
999        self
1000    }
1001
1002    /// One of the game's kinds of block (`"stairs"`, `"door"`…).
1003    pub fn kind(mut self, kind: &str) -> Self {
1004        self.kind = kind.into();
1005        self
1006    }
1007
1008    /// The block a kind is made from.
1009    pub fn base(mut self, id: impl Into<String>) -> Self {
1010        self.base = Some(id.into());
1011        self
1012    }
1013
1014    /// A kind's block set (`"oak"`, `"dark_oak"`, `"iron"`…).
1015    pub fn set(mut self, name: &str) -> Self {
1016        self.set = Some(name.into());
1017        self
1018    }
1019
1020    /// How long a button stays pressed, ticks (30 by default).
1021    pub fn press_ticks(mut self, ticks: u32) -> Self {
1022        self.press_ticks = ticks;
1023        self
1024    }
1025
1026    /// Only living things set off a button or a pressure plate.
1027    pub fn mobs_only(mut self) -> Self {
1028        self.mobs_only = true;
1029        self
1030    }
1031}
1032
1033// ── Entities ─────────────────────────────────────────────────────────────────
1034
1035/// What kind of entity a type is.
1036#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq, Eq, Default)]
1037pub enum EntityKind {
1038    /// Not alive: a thing in the world (a scroll on a wall, a vehicle, a
1039    /// display) — no health, no AI.
1040    #[default]
1041    Object,
1042    /// Alive: health, attributes, hurt and death, but no AI of its own.
1043    Living,
1044    /// A creature with AI: `goals`, and walking about.
1045    Mob,
1046    /// Hangs on a block's side, as a painting: `width`×`height` whole
1047    /// blocks of wall (the item it's placed from may say another size —
1048    /// [`ItemDef::hangs`]); it stays while the blocks behind hold it, and
1049    /// breaking it, or losing its wall, drops the item it was placed from.
1050    Hanging,
1051}
1052
1053/// An entity type to register (`.yentity`). Its look is a body model
1054/// (`model`, `.ymodel`) with clips by state; what it does beyond `goals` is
1055/// the mod's code (entity events, a tick of its own).
1056#[derive(yog_data::Schema, Debug, Clone)]
1057pub struct EntityDef {
1058    pub id: String,
1059    /// Its name (a text or a `@Set.key` resource); default: the id's lang key.
1060    pub name: Option<String>,
1061    #[data(default)]
1062    pub kind: EntityKind,
1063    /// Its spawn group (`MobCategory`): `misc`, `monster`, `creature`,
1064    /// `ambient`, `water_creature`, `water_ambient`, `underground_water_creature`.
1065    #[data(default = "misc")]
1066    pub category: String,
1067    /// Hitbox, blocks.
1068    pub width: f32,
1069    pub height: f32,
1070    /// Eye height, blocks; default 85% of the height.
1071    pub eye_height: Option<f32>,
1072    /// Lava and fire don't hurt it.
1073    #[data(default)]
1074    pub fire_immune: bool,
1075    /// It doesn't fall.
1076    #[data(default)]
1077    pub no_gravity: bool,
1078    /// Others push it (living things by default).
1079    pub pushable: Option<bool>,
1080    /// Others stand on and bump into it as into a block (a boat).
1081    #[data(default)]
1082    pub solid: bool,
1083    /// How far players see it, chunks.
1084    #[data(default = "eight")]
1085    pub tracking_range: u32,
1086    /// How often its movement is sent, ticks.
1087    #[data(default = "three")]
1088    pub update_interval: u32,
1089    /// Living and mob attributes by id: `max_health`, `movement_speed`,
1090    /// `attack_damage`, `follow_range`, `armor`, `knockback_resistance`… (the
1091    /// game's, `minecraft:` optional, or a mod's).
1092    #[data(default)]
1093    pub attributes: yog_data::Ordered<f64>,
1094    /// Its body model (`.ymodel` id); none: the mod draws it (`on_body`).
1095    pub model: Option<String>,
1096    /// Its clips (`.yanim` id).
1097    pub animations: Option<String>,
1098    /// Clip by state: `idle`, `walk`, `attack`, `hurt`, `death`, or the mod's
1099    /// own states (entity data `yog:state`).
1100    #[data(default)]
1101    pub clips: yog_data::Ordered<String>,
1102    /// Spawn egg colors `[base, spots]` (`0xRRGGBB`); none: no egg.
1103    pub spawn_egg: Option<[u32; 2]>,
1104    /// Mobs' AI goals, in priority order — the game's by name: `float`,
1105    /// `panic`, `melee_attack`, `wander`, `look_at_player`,
1106    /// `look_around`, `target_player`, `avoid_player`.
1107    #[data(default)]
1108    pub goals: Vec<String>,
1109    /// The mod handles each tick of every entity of this type
1110    /// (`on_entity_tick`).
1111    #[data(default)]
1112    pub ticks: bool,
1113    /// A hanging thing's sound events when it's placed and when it breaks;
1114    /// default: a painting's.
1115    pub place_sound: Option<String>,
1116    pub break_sound: Option<String>,
1117}
1118
1119fn eight() -> u32 {
1120    8
1121}
1122
1123fn three() -> u32 {
1124    3
1125}
1126
1127fn misc() -> String {
1128    "misc".into()
1129}
1130
1131impl EntityDef {
1132    pub fn new(id: impl Into<String>, kind: EntityKind, width: f32, height: f32) -> Self {
1133        Self {
1134            id: id.into(),
1135            name: None,
1136            kind,
1137            category: misc(),
1138            width,
1139            height,
1140            eye_height: None,
1141            fire_immune: false,
1142            no_gravity: false,
1143            pushable: None,
1144            solid: false,
1145            tracking_range: eight(),
1146            update_interval: three(),
1147            attributes: yog_data::Ordered::default(),
1148            model: None,
1149            animations: None,
1150            clips: yog_data::Ordered::default(),
1151            spawn_egg: None,
1152            goals: Vec::new(),
1153            ticks: false,
1154            place_sound: None,
1155            break_sound: None,
1156        }
1157    }
1158}
1159
1160#[cfg(test)]
1161mod registration_tests {
1162    #[test]
1163    fn a_hot_reload_sees_what_registration_changed() {
1164        let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
1165        let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
1166        let (added, removed, changed) = super::registration_changes(old, new);
1167        assert_eq!(added, ["item mymod:d"]);
1168        assert_eq!(removed, ["item mymod:c"]);
1169        assert_eq!(changed, ["block mymod:b"]);
1170        assert_eq!(super::registration_changes(old, old), (vec![], vec![], vec![]));
1171    }
1172}
1173
1174#[cfg(test)]
1175mod material_tests {
1176    use super::*;
1177
1178    #[test]
1179    fn the_games_materials_are_built_in() {
1180        assert_eq!(ToolMaterial::built_ins().len(), 6);
1181        let iron = ToolMaterial::built_in("iron").unwrap();
1182        assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
1183        assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
1184        assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
1185        let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
1186        assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
1187    }
1188}
1189
1190#[cfg(test)]
1191mod sound_tests {
1192    use super::*;
1193
1194    #[test]
1195    fn the_games_sound_groups_are_built_in() {
1196        assert!(SoundGroup::built_ins().len() > 100);
1197        let stone = SoundGroup::built_in("stone").unwrap();
1198        assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
1199        assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
1200        assert!(SoundGroup::built_in("mymod:crystal").is_none());
1201    }
1202
1203    #[test]
1204    fn a_mods_group_crosses_as_yog_data() {
1205        use yog_data::Schema;
1206        let g = SoundGroup::new("mymod:crystal").pitch(1.2);
1207        assert_eq!(g.fall, "mymod:crystal.fall");
1208        let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
1209        assert_eq!(back, g);
1210        assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
1211    }
1212}
1213
1214#[cfg(test)]
1215impl Recipe {
1216    fn to_json(&self) -> String {
1217        yog_data::json::to_json(&self.to_data()).to_string()
1218    }
1219}
1220
1221#[cfg(test)]
1222mod recipe_tests {
1223    use super::*;
1224
1225    #[test]
1226    fn writes_the_yog_format() {
1227        let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
1228        let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
1229        assert_eq!(
1230            js(&shaped.to_json()),
1231            js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
1232        );
1233        assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
1234        let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
1235        assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
1236        let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
1237        assert_eq!(js(&smith.to_json())["template"], "m:t");
1238    }
1239}
1240
1241#[cfg(test)]
1242mod json_tests {
1243    use super::*;
1244
1245    /// Every `to_json` makes valid JSON, whatever the strings hold.
1246    #[test]
1247    fn json_survives_any_string() {
1248        let odd = "m:a\"b\\c\nd\te\u{1}";
1249        let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
1250        for r in [
1251            Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
1252            Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
1253            Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
1254            Recipe::stonecutting(odd, odd, odd, 2),
1255            Recipe::smithing(odd, odd, odd, odd, odd),
1256        ] {
1257            let v = parse(r.to_json());
1258            assert_eq!(v["result"], odd, "the string comes back as it was");
1259        }
1260    }
1261
1262    #[test]
1263    fn recipes_and_grants_name_their_parts() {
1264        let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
1265        let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
1266        assert_eq!(v["type"], "campfire_cooking");
1267        assert_eq!(v["cook_time"], 600, "the method's default time");
1268        assert_eq!(r.ns_name(), ("mymod", "a"));
1269        assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
1270    }
1271}