1#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23#[derive(yog_data::Schema, Debug, Clone)]
24pub struct FoodDef {
25 pub nutrition: u32,
27 pub saturation: f32,
29 pub can_always_eat: bool,
31}
32
33impl FoodDef {
34 pub fn new(nutrition: u32, saturation: f32) -> Self {
35 Self {
36 nutrition,
37 saturation,
38 can_always_eat: false,
39 }
40 }
41
42 pub fn can_always_eat(mut self) -> Self {
43 self.can_always_eat = true;
44 self
45 }
46}
47
48#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
50#[derive(yog_data::Schema, Debug, Clone)]
51pub struct ItemDef {
52 pub id: String,
53 pub max_stack: u8,
54 pub name: Option<String>,
55 pub tooltip: Option<String>,
56 pub max_damage: u32,
58 pub fire_resistant: bool,
60 pub fuel_ticks: u32,
62 pub food: Option<FoodDef>,
64 pub tool: Option<ToolItem>,
66 pub armor: Option<ArmorPiece>,
68 pub wearable: Option<String>,
70}
71
72impl ItemDef {
73 pub fn new(id: impl Into<String>) -> Self {
74 Self {
75 id: id.into(),
76 max_stack: 64,
77 name: None,
78 tooltip: None,
79 max_damage: 0,
80 fire_resistant: false,
81 fuel_ticks: 0,
82 food: None,
83 tool: None,
84 armor: None,
85 wearable: None,
86 }
87 }
88
89 pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
93 self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
94 self
95 }
96
97 pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
100 self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
101 self
102 }
103
104 pub fn wearable(mut self, slot: impl Into<String>) -> Self {
106 self.wearable = Some(slot.into());
107 self
108 }
109
110 pub fn max_stack(mut self, n: u8) -> Self {
112 self.max_stack = n;
113 self
114 }
115
116 pub fn name(mut self, name: impl Into<String>) -> Self {
118 self.name = Some(name.into());
119 self
120 }
121
122 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
124 self.tooltip = Some(tooltip.into());
125 self
126 }
127
128 pub fn max_damage(mut self, durability: u32) -> Self {
130 self.max_damage = durability;
131 self
132 }
133
134 pub fn fire_resistant(mut self) -> Self {
136 self.fire_resistant = true;
137 self
138 }
139
140 pub fn fuel(mut self, ticks: u32) -> Self {
142 self.fuel_ticks = ticks;
143 self
144 }
145
146 pub fn food(mut self, food: FoodDef) -> Self {
148 self.food = Some(food);
149 self
150 }
151}
152
153const VANILLA_NS: &str = "minecraft";
157
158fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
160 id.split_once(':').unwrap_or((default_ns, id))
161}
162
163
164fn json_str(s: &str) -> String {
166 let mut out = String::with_capacity(s.len() + 2);
167 out.push('"');
168 for c in s.chars() {
169 match c {
170 '"' => out.push_str("\\\""),
171 '\\' => out.push_str("\\\\"),
172 '\n' => out.push_str("\\n"),
173 '\r' => out.push_str("\\r"),
174 '\t' => out.push_str("\\t"),
175 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
176 c => out.push(c),
177 }
178 }
179 out.push('"');
180 out
181}
182
183
184#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum Cooking {
188 Smelting,
189 Blasting,
190 Smoking,
191 Campfire,
192}
193
194impl Cooking {
195 pub fn word(self) -> &'static str {
197 match self {
198 Cooking::Smelting => "smelting",
199 Cooking::Blasting => "blasting",
200 Cooking::Smoking => "smoking",
201 Cooking::Campfire => "campfire_cooking",
202 }
203 }
204}
205
206#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
208#[derive(Debug, Clone, PartialEq)]
209pub enum RecipeKind {
210 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
212 Shapeless { ingredients: Vec<Vec<String>> },
214 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
215 Stonecutting { ingredient: Vec<String> },
216 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
218}
219
220#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
234#[derive(Debug, Clone, PartialEq)]
235pub struct Recipe {
236 pub id: String,
237 pub kind: RecipeKind,
238 pub result: String,
239 pub count: u32,
240 pub group: Option<String>,
242 pub category: Option<String>,
244}
245
246impl Recipe {
247 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
248 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
249 }
250
251 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
252 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
253 }
254
255 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
256 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
257 }
258
259 pub fn cooking(
262 id: impl Into<String>,
263 method: Cooking,
264 ingredient: impl Into<String>,
265 result: impl Into<String>,
266 count: u32,
267 ) -> Self {
268 let cook_time = match method {
269 Cooking::Smelting => 200,
270 Cooking::Blasting | Cooking::Smoking => 100,
271 Cooking::Campfire => 600,
272 };
273 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
274 Self::of(id, kind, result, count)
275 }
276
277 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
278 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
279 }
280
281 pub fn smithing(
282 id: impl Into<String>,
283 template: impl Into<String>,
284 base: impl Into<String>,
285 addition: impl Into<String>,
286 result: impl Into<String>,
287 ) -> Self {
288 let kind = RecipeKind::Smithing {
289 template: vec![template.into()],
290 base: vec![base.into()],
291 addition: vec![addition.into()],
292 };
293 Self::of(id, kind, result, 1)
294 }
295
296 pub fn row(mut self, pattern: impl Into<String>) -> Self {
298 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
299 p.push(pattern.into());
300 }
301 self
302 }
303
304 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
306 self.key_any(symbol, [item.into()])
307 }
308
309 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
311 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
312 key.push((symbol, items.into_iter().map(Into::into).collect()));
313 }
314 self
315 }
316
317 pub fn ingredient(self, item: impl Into<String>) -> Self {
319 self.ingredient_any([item.into()])
320 }
321
322 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
324 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
325 ingredients.push(items.into_iter().map(Into::into).collect());
326 }
327 self
328 }
329
330 pub fn experience(mut self, xp: f32) -> Self {
332 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
333 *experience = xp;
334 }
335 self
336 }
337
338 pub fn cook_time(mut self, ticks: u32) -> Self {
340 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
341 *cook_time = ticks;
342 }
343 self
344 }
345
346 pub fn group(mut self, group: impl Into<String>) -> Self {
347 self.group = Some(group.into());
348 self
349 }
350
351 pub fn category(mut self, category: impl Into<String>) -> Self {
352 self.category = Some(category.into());
353 self
354 }
355
356 pub fn to_data(&self) -> yog_data::Value {
359 use yog_data::{Map, Value};
360 let ing = |items: &[String]| match items {
361 [one] => Value::from(one.as_str()),
362 many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
363 };
364 let mut m = Map::with_capacity(8);
365 let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
366 match &self.kind {
367 RecipeKind::Shaped { pattern, key } => {
368 ty(&mut m, "shaped");
369 m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
370 let mut keys = Map::with_capacity(key.len());
371 for (c, items) in key {
372 keys.insert(c.to_string(), ing(items));
373 }
374 m.insert("key", Value::Map(keys));
375 }
376 RecipeKind::Shapeless { ingredients } => {
377 ty(&mut m, "shapeless");
378 m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
379 }
380 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
381 ty(&mut m, method.word());
382 m.insert("ingredient", ing(ingredient));
383 m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
385 m.insert("cook_time", Value::Int(*cook_time as i64));
386 }
387 RecipeKind::Stonecutting { ingredient } => {
388 ty(&mut m, "stonecutting");
389 m.insert("ingredient", ing(ingredient));
390 }
391 RecipeKind::Smithing { template, base, addition } => {
392 ty(&mut m, "smithing");
393 m.insert("template", ing(template));
394 m.insert("base", ing(base));
395 m.insert("addition", ing(addition));
396 }
397 }
398 m.insert("result", Value::from(self.result.as_str()));
399 if self.count != 1 {
400 m.insert("count", Value::Int(self.count as i64));
401 }
402 if let Some(g) = &self.group {
403 m.insert("group", Value::from(g.as_str()));
404 }
405 if let Some(c) = &self.category {
406 m.insert("category", Value::from(c.as_str()));
407 }
408 Value::Map(m)
409 }
410
411 pub fn ns_name(&self) -> (&str, &str) {
413 split_id(&self.id, VANILLA_NS)
414 }
415}
416
417#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
430#[derive(yog_data::Schema, Debug, Clone)]
431pub struct StartupGrant {
432 pub id: String,
433 pub items: Vec<String>,
434 pub book: Option<String>,
435 pub command: Option<String>,
436}
437
438impl StartupGrant {
439 pub fn new(id: impl Into<String>) -> Self {
440 Self {
441 id: id.into(),
442 items: Vec::new(),
443 book: None,
444 command: None,
445 }
446 }
447
448 pub fn item(mut self, item_id: impl Into<String>) -> Self {
449 self.items.push(item_id.into());
450 self
451 }
452
453 pub fn book(mut self, book: impl Into<String>) -> Self {
454 self.book = Some(book.into());
455 self
456 }
457
458 pub fn command(mut self, cmd: impl Into<String>) -> Self {
459 self.command = Some(cmd.into());
460 self
461 }
462
463 pub fn to_json(&self) -> String {
464 let mut entries: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":{}}}", json_str(i))).collect();
465 if let Some(book) = &self.book {
466 let tag = format!("{{yog_book:\"{}\"}}", book.replace('\\', "\\\\").replace('"', "\\\""));
468 entries.push(format!(
469 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":{}}}]}}",
470 json_str(&tag)
471 ));
472 }
473 if let Some(cmd) = &self.command {
474 entries.push(format!("{{\"type\":\"minecraft:command\",\"command\":{}}}", json_str(cmd)));
475 }
476 format!("{{\"type\":\"yog:startup_grant\",\"entries\":[{}],\"id\":{}}}", entries.join(","), json_str(&self.id))
477 }
478}
479
480
481
482#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
486#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
487pub struct ToolItem {
488 pub kind: String,
491 pub material: String,
493 pub attack_damage: f32,
495 pub attack_speed: f32,
497}
498
499#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
501#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
502pub struct ArmorPiece {
503 pub slot: String,
505 pub material: String,
507}
508
509#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
514#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
515pub struct ToolMaterial {
516 pub id: String,
517 pub durability: u32,
519 pub speed: f32,
521 pub attack_damage: f32,
523 pub enchantability: u32,
524 pub level: String,
526 pub repair: String,
528 #[data(default)]
530 pub level_index: u32,
531 #[data(default)]
534 pub incorrect: String,
535}
536
537#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
539#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
540pub struct ArmorDefense {
541 pub head: u32,
542 pub chest: u32,
543 pub legs: u32,
544 pub feet: u32,
545}
546
547#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
551#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
552pub struct ArmorMaterial {
553 pub id: String,
554 pub durability: u32,
557 pub defense: ArmorDefense,
558 pub enchantability: u32,
559 pub equip_sound: String,
561 #[data(default)]
562 pub toughness: f32,
563 #[data(default)]
564 pub knockback_resistance: f32,
565 pub repair: String,
567 pub texture: String,
570}
571
572static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
573 yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
574 .unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
575 .value
576});
577
578static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
579 yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
580 .unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
581 .value
582});
583
584fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
586 let name = name.strip_prefix("minecraft:").unwrap_or(name);
587 list.iter().find(|x| id(x).strip_prefix("minecraft:") == Some(name))
588}
589
590impl ToolMaterial {
591 pub fn built_ins() -> &'static [ToolMaterial] {
593 &BUILT_IN_TOOL_MATERIALS
594 }
595
596 pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
598 built_in(Self::built_ins(), |m| &m.id, name)
599 }
600
601 pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
604 let mut m = Self::built_in(like)?.clone();
605 m.id = id.into();
606 Some(m)
607 }
608}
609
610impl ArmorMaterial {
611 pub fn built_ins() -> &'static [ArmorMaterial] {
613 &BUILT_IN_ARMOR_MATERIALS
614 }
615
616 pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
618 built_in(Self::built_ins(), |m| &m.id, name)
619 }
620}
621
622#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
629#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
630pub struct BiomeFeature {
631 pub feature: String,
633 pub biomes: String,
635 pub step: String,
637}
638
639#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
647#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
648pub struct SoundGroup {
649 pub id: String,
650 #[data(rename = "break")]
652 pub break_sound: String,
653 pub step: String,
654 pub place: String,
655 pub hit: String,
656 pub fall: String,
657 #[data(default = "one")]
658 #[data(range = "0..=10")]
659 pub volume: f32,
660 #[data(default = "one")]
661 #[data(range = "0..=10")]
662 pub pitch: f32,
663}
664
665fn one() -> f32 {
666 1.0
667}
668
669static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
670 yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
671 .unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
672 .value
673});
674
675impl SoundGroup {
676 pub fn new(id: impl Into<String>) -> Self {
679 let id = id.into();
680 let ev = |s: &str| format!("{id}.{s}");
681 Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
682 }
683
684 pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
686 [self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
687 self
688 }
689
690 pub fn volume(mut self, volume: f32) -> Self {
691 self.volume = volume;
692 self
693 }
694
695 pub fn pitch(mut self, pitch: f32) -> Self {
696 self.pitch = pitch;
697 self
698 }
699
700 pub fn built_ins() -> &'static [SoundGroup] {
703 &BUILT_IN_SOUND_GROUPS
704 }
705
706 pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
708 let name = name.strip_prefix("minecraft:").unwrap_or(name);
709 Self::built_ins().iter().find(|g| g.id.strip_prefix("minecraft:") == Some(name))
710 }
711}
712
713#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
717#[derive(yog_data::Schema, Debug, Clone)]
718pub struct BlockDef {
719 pub id: String,
720 pub hardness: f32,
721 pub resistance: f32,
722 pub name: Option<String>,
723 pub shape: Option<[f32; 6]>,
726 pub light_level: u8,
728 pub sound: Option<String>,
732 pub requires_tool: bool,
734 pub no_collision: bool,
736 pub slipperiness: f32,
738 pub connects: bool,
744 pub connect_groups: Vec<String>,
750 pub inventory_id: Option<String>,
753 pub redstone_source: bool,
756 pub facing: String,
759 pub placement: String,
761 pub attach: bool,
764 pub waterlogged: bool,
766 pub properties: Vec<BlockProperty>,
768 pub powered: Option<String>,
770 pub comparator: Option<String>,
772 pub accepts_items: bool,
774}
775
776#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
779#[derive(yog_data::Schema, Debug, Clone, PartialEq, Eq)]
780pub struct BlockProperty {
781 pub name: String,
782 pub values: Vec<String>,
783}
784
785impl BlockDef {
786 pub fn new(id: impl Into<String>) -> Self {
787 Self {
788 id: id.into(),
789 hardness: 1.5,
790 resistance: 6.0,
791 name: None,
792 shape: None,
793 light_level: 0,
794 sound: None,
795 requires_tool: false,
796 no_collision: false,
797 slipperiness: 0.0,
798 connects: false,
799 connect_groups: Vec::new(),
800 inventory_id: None,
801 redstone_source: false,
802 facing: String::new(),
803 placement: String::new(),
804 attach: false,
805 waterlogged: false,
806 properties: Vec::new(),
807 powered: None,
808 comparator: None,
809 accepts_items: false,
810 }
811 }
812
813 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
815 self.hardness = hardness;
816 self.resistance = resistance;
817 self
818 }
819
820 pub fn name(mut self, name: impl Into<String>) -> Self {
822 self.name = Some(name.into());
823 self
824 }
825
826 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
828 self.shape = Some([x1, y1, z1, x2, y2, z2]);
829 self
830 }
831
832 pub fn light_level(mut self, level: u8) -> Self {
834 self.light_level = level.min(15);
835 self
836 }
837
838 pub fn sound(mut self, group: impl Into<String>) -> Self {
841 self.sound = Some(group.into());
842 self
843 }
844
845 pub fn requires_tool(mut self) -> Self {
847 self.requires_tool = true;
848 self
849 }
850
851 pub fn no_collision(mut self) -> Self {
853 self.no_collision = true;
854 self
855 }
856
857 pub fn slipperiness(mut self, value: f32) -> Self {
859 self.slipperiness = value;
860 self
861 }
862
863 pub fn connects_to_neighbors(mut self) -> Self {
869 self.connects = true;
870 self
871 }
872
873 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
880 self.connect_groups = groups
881 .iter()
882 .map(std::string::ToString::to_string)
883 .collect();
884 self
885 }
886
887 pub fn redstone_source(mut self) -> Self {
893 self.redstone_source = true;
894 self
895 }
896
897 pub fn facing(mut self, ways: &str, placement: &str) -> Self {
900 self.facing = ways.to_string();
901 self.placement = placement.to_string();
902 self
903 }
904
905 pub fn attach(mut self) -> Self {
907 self.attach = true;
908 self
909 }
910
911 pub fn waterlogged(mut self) -> Self {
913 self.waterlogged = true;
914 self
915 }
916
917 pub fn property(mut self, name: &str, values: &[&str]) -> Self {
919 self.properties.push(BlockProperty { name: name.into(), values: values.iter().map(|v| v.to_string()).collect() });
920 self
921 }
922
923 pub fn powered(mut self, name: &str) -> Self {
925 self.powered = Some(name.into());
926 self
927 }
928
929 pub fn comparator(mut self, name: &str) -> Self {
931 self.comparator = Some(name.into());
932 self
933 }
934
935 pub fn accepts_items(mut self) -> Self {
937 self.accepts_items = true;
938 self
939 }
940
941 pub fn inventory(mut self, id: impl Into<String>) -> Self {
942 self.inventory_id = Some(id.into());
943 self
944 }
945}
946
947#[cfg(test)]
948mod material_tests {
949 use super::*;
950
951 #[test]
952 fn the_games_materials_are_built_in() {
953 assert_eq!(ToolMaterial::built_ins().len(), 6);
954 let iron = ToolMaterial::built_in("iron").unwrap();
955 assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
956 assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
957 assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
958 let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
959 assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
960 }
961}
962
963#[cfg(test)]
964mod sound_tests {
965 use super::*;
966
967 #[test]
968 fn the_games_sound_groups_are_built_in() {
969 assert!(SoundGroup::built_ins().len() > 100);
970 let stone = SoundGroup::built_in("stone").unwrap();
971 assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
972 assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
973 assert!(SoundGroup::built_in("mymod:crystal").is_none());
974 }
975
976 #[test]
977 fn a_mods_group_crosses_as_yog_data() {
978 use yog_data::Schema;
979 let g = SoundGroup::new("mymod:crystal").pitch(1.2);
980 assert_eq!(g.fall, "mymod:crystal.fall");
981 let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
982 assert_eq!(back, g);
983 assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
984 }
985}
986
987#[cfg(test)]
988impl Recipe {
989 fn to_json(&self) -> String {
990 yog_data::json::to_json(&self.to_data()).to_string()
991 }
992}
993
994#[cfg(test)]
995mod recipe_tests {
996 use super::*;
997
998 #[test]
999 fn writes_the_yog_format() {
1000 let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
1001 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
1002 assert_eq!(
1003 js(&shaped.to_json()),
1004 js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
1005 );
1006 assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
1007 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
1008 assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
1009 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
1010 assert_eq!(js(&smith.to_json())["template"], "m:t");
1011 }
1012}
1013
1014#[cfg(test)]
1015mod json_tests {
1016 use super::*;
1017
1018 #[test]
1020 fn json_survives_any_string() {
1021 let odd = "m:a\"b\\c\nd\te\u{1}";
1022 let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
1023 for r in [
1024 Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
1025 Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
1026 Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
1027 Recipe::stonecutting(odd, odd, odd, 2),
1028 Recipe::smithing(odd, odd, odd, odd, odd),
1029 ] {
1030 let v = parse(r.to_json());
1031 assert_eq!(v["result"], odd, "the string comes back as it was");
1032 }
1033 let g = StartupGrant::new(odd).item(odd).book("mymod:\"guide\"").command("say \"hi\" \\o/");
1034 let v = parse(g.to_json());
1035 assert_eq!(v["id"], odd);
1036 assert_eq!(v["entries"][2]["command"], "say \"hi\" \\o/");
1037 }
1038
1039 #[test]
1040 fn recipes_and_grants_name_their_parts() {
1041 let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
1042 let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
1043 assert_eq!(v["type"], "campfire_cooking");
1044 assert_eq!(v["cook_time"], 600, "the method's default time");
1045 assert_eq!(r.ns_name(), ("mymod", "a"));
1046 assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
1047 let g = StartupGrant::new("mymod:starter");
1048 assert!(g.to_json().contains(r#""entries":[]"#));
1049 }
1050}