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