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
16pub mod atlases;
17pub mod effects;
18pub mod item_looks;
19pub mod loot;
20pub mod recipe_kinds;
21pub mod tool_levels;
22
23/// What a `.yog` registers when the game starts — content the game can't
24/// add or change while it runs (blocks, items, entity types, sound groups,
25/// materials, dimensions): a line each, `<kind> <id> <hash>`, the hash of
26/// what can't change. A hot reload compares the new `.yog`'s with what's
27/// registered ([`registration_changes`]).
28pub const REGISTRATIONS_FILE: &str = "registrations.txt";
29
30/// How `new` (a [`REGISTRATIONS_FILE`]) differs from `registered`: what it
31/// adds, removes and changes — `<kind> <id>` each.
32pub fn registration_changes(
33    registered: &str,
34    new: &str,
35) -> (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
46        .keys()
47        .filter(|k| !old.contains_key(*k))
48        .cloned()
49        .collect();
50    let removed = old
51        .keys()
52        .filter(|k| !new.contains_key(*k))
53        .cloned()
54        .collect();
55    let changed = new
56        .iter()
57        .filter(|(k, h)| old.get(*k).is_some_and(|o| o != *h))
58        .map(|(k, _)| k.clone())
59        .collect();
60    (added, removed, changed)
61}
62pub use atlases::Atlas;
63pub use effects::{
64    AttributeDef, Brew, EffectCategory, EffectDef, EffectModifier, ModifierOperation, PotionDef,
65    PotionEffect,
66};
67pub use item_looks::{ItemLooks, ItemTint, LookModels};
68pub use loot::{Loot, LootAdd, LootModifier, LootScale, LootStack};
69pub use tool_levels::{LevelTag, ToolLadder, ToolLevel};
70
71mod blocks;
72mod entities;
73mod equipment;
74mod items;
75mod recipes;
76mod sounds;
77mod worldgen;
78pub use blocks::{BlockDef, BlockProperty, Facing, Placement};
79pub use entities::{EntityDef, EntityKind};
80pub use equipment::{
81    ArmorDefense, ArmorMaterial, ArmorPiece, ArmorSlot, DIGGERS, GameTool, REFERENCE_KIND,
82    REFERENCE_MATERIAL, TOOL_KINDS, ToolItem, ToolMaterial,
83};
84pub use items::{FoodDef, Hangs, ItemDef};
85pub use recipes::{COOK_EXPERIENCE, Cooking, Recipe, RecipeKind, StartupGrant};
86pub use sounds::SoundGroup;
87pub use worldgen::BiomeFeature;
88
89#[cfg(test)]
90mod registration_tests {
91    #[test]
92    fn a_hot_reload_sees_what_registration_changed() {
93        let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
94        let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
95        let (added, removed, changed) = super::registration_changes(old, new);
96        assert_eq!(added, ["item mymod:d"]);
97        assert_eq!(removed, ["item mymod:c"]);
98        assert_eq!(changed, ["block mymod:b"]);
99        assert_eq!(
100            super::registration_changes(old, old),
101            (vec![], vec![], vec![])
102        );
103    }
104}
105
106#[cfg(test)]
107mod material_tests {
108    use super::*;
109
110    #[test]
111    fn the_games_materials_are_built_in() {
112        assert_eq!(ToolMaterial::built_ins().len(), 6);
113        let iron = ToolMaterial::built_in("iron").unwrap();
114        assert_eq!(
115            (iron.durability, iron.level_index, iron.incorrect.as_str()),
116            (250, 2, "minecraft:incorrect_for_iron_tool")
117        );
118        assert_eq!(
119            ArmorMaterial::built_in("minecraft:netherite")
120                .unwrap()
121                .knockback_resistance,
122            0.1
123        );
124        assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
125        let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
126        assert_eq!(
127            (mythril.id.as_str(), mythril.durability),
128            ("mymod:mythril", 1561)
129        );
130    }
131}
132
133#[cfg(test)]
134mod sound_tests {
135    use super::*;
136
137    #[test]
138    fn the_games_sound_groups_are_built_in() {
139        assert!(SoundGroup::built_ins().len() > 100);
140        let stone = SoundGroup::built_in("stone").unwrap();
141        assert_eq!(
142            (stone.break_sound.as_str(), stone.volume),
143            ("minecraft:block.stone.break", 1.0)
144        );
145        assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
146        assert!(SoundGroup::built_in("mymod:crystal").is_none());
147    }
148
149    #[test]
150    fn a_mods_group_crosses_as_yog_data() {
151        use yog_data::Schema;
152        let g = SoundGroup::new("mymod:crystal").pitch(1.2);
153        assert_eq!(g.fall, "mymod:crystal.fall");
154        let back = yog_data::from_value::<SoundGroup>(&g.write())
155            .unwrap()
156            .value;
157        assert_eq!(back, g);
158        assert_eq!(
159            g.write().get("break").and_then(|v| v.as_str()),
160            Some("mymod:crystal.break")
161        );
162    }
163}
164
165#[cfg(test)]
166impl Recipe {
167    fn to_json(&self) -> String {
168        yog_data::json::to_json(&self.to_data()).to_string()
169    }
170}
171
172#[cfg(test)]
173mod recipe_tests {
174    use super::*;
175
176    #[test]
177    fn writes_the_yog_format() {
178        let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
179        let shaped = Recipe::shaped("m:a", "m:b", 2)
180            .row("D ")
181            .key('D', "#c:dusts")
182            .key_any('E', ["m:x", "m:y"])
183            .group("g");
184        assert_eq!(
185            js(&shaped.to_json()),
186            js(
187                r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##
188            )
189        );
190        assert_eq!(
191            shaped.to_data().as_map().unwrap().keys().next(),
192            Some("type"),
193            "the type first"
194        );
195        let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
196        assert_eq!(
197            js(&blast.to_json()),
198            js(
199                r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##
200            )
201        );
202        let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
203        assert_eq!(js(&smith.to_json())["template"], "m:t");
204    }
205}
206
207#[cfg(test)]
208mod json_tests {
209    use super::*;
210
211    /// Every `to_json` makes valid JSON, whatever the strings hold.
212    #[test]
213    fn json_survives_any_string() {
214        let odd = "m:a\"b\\c\nd\te\u{1}";
215        let parse = |j: String| {
216            serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"))
217        };
218        for r in [
219            Recipe::shaped(odd, odd, 1)
220                .row("A")
221                .key('A', odd)
222                .group(odd)
223                .category(odd),
224            Recipe::shapeless(odd, odd, 3)
225                .ingredient(odd)
226                .ingredient_any([odd, "m:x"]),
227            Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
228            Recipe::stonecutting(odd, odd, odd, 2),
229            Recipe::smithing(odd, odd, odd, odd, odd),
230        ] {
231            let v = parse(r.to_json());
232            assert_eq!(v["result"], odd, "the string comes back as it was");
233        }
234    }
235
236    #[test]
237    fn recipes_and_grants_name_their_parts() {
238        let r = Recipe::cooking(
239            "mymod:a",
240            Cooking::Campfire,
241            "minecraft:beef",
242            "minecraft:cooked_beef",
243            1,
244        );
245        let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
246        assert_eq!(v["type"], "campfire_cooking");
247        assert_eq!(v["cook_time"], 600, "the method's default time");
248        assert_eq!(r.ns_name(), ("mymod", "a"));
249        assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
250    }
251}
252
253/// The tables yog-registry ships read: none falls back to empty in the game.
254#[cfg(test)]
255mod built_in_tests {
256    use super::*;
257
258    #[test]
259    fn every_built_in_table_reads() {
260        let reads = |name: &str, ok: bool| assert!(ok, "{name} won't read");
261        reads(
262            "tool materials",
263            yog_data::from_text::<Vec<ToolMaterial>>(include_str!(
264                "../assets/tool_materials.ydata"
265            ))
266            .is_ok(),
267        );
268        reads(
269            "armor materials",
270            yog_data::from_text::<Vec<ArmorMaterial>>(include_str!(
271                "../assets/armor_materials.ydata"
272            ))
273            .is_ok(),
274        );
275        reads(
276            "sound groups",
277            yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
278                .is_ok(),
279        );
280        reads(
281            "item looks",
282            yog_data::from_text::<item_looks::ItemLooks>(include_str!(
283                "../assets/item_looks.ydata"
284            ))
285            .is_ok(),
286        );
287        reads(
288            "tool levels",
289            yog_data::from_text::<tool_levels::ToolLadder>(include_str!(
290                "../assets/tool_levels.ydata"
291            ))
292            .is_ok(),
293        );
294        reads(
295            "atlases",
296            yog_data::from_text::<Vec<atlases::Atlas>>(include_str!("../assets/atlases.ydata"))
297                .is_ok(),
298        );
299        reads(
300            "recipe kinds",
301            yog_data::from_text::<Vec<recipe_kinds::RecipeKindDef>>(include_str!(
302                "../data/recipe_kinds.ydata"
303            ))
304            .is_ok(),
305        );
306    }
307}