1pub mod atlases;
17pub mod effects;
18pub mod item_looks;
19pub mod loot;
20pub mod recipe_kinds;
21pub mod tool_levels;
22
23pub const REGISTRATIONS_FILE: &str = "registrations.txt";
29
30pub 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 #[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#[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}