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}
69
70impl ItemDef {
71 pub fn new(id: impl Into<String>) -> Self {
72 Self {
73 id: id.into(),
74 max_stack: 64,
75 name: None,
76 tooltip: None,
77 max_damage: 0,
78 fire_resistant: false,
79 fuel_ticks: 0,
80 food: None,
81 tool: None,
82 armor: None,
83 }
84 }
85
86 pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
90 self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
91 self
92 }
93
94 pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
97 self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
98 self
99 }
100
101 pub fn max_stack(mut self, n: u8) -> Self {
103 self.max_stack = n;
104 self
105 }
106
107 pub fn name(mut self, name: impl Into<String>) -> Self {
109 self.name = Some(name.into());
110 self
111 }
112
113 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
115 self.tooltip = Some(tooltip.into());
116 self
117 }
118
119 pub fn max_damage(mut self, durability: u32) -> Self {
121 self.max_damage = durability;
122 self
123 }
124
125 pub fn fire_resistant(mut self) -> Self {
127 self.fire_resistant = true;
128 self
129 }
130
131 pub fn fuel(mut self, ticks: u32) -> Self {
133 self.fuel_ticks = ticks;
134 self
135 }
136
137 pub fn food(mut self, food: FoodDef) -> Self {
139 self.food = Some(food);
140 self
141 }
142}
143
144const VANILLA_NS: &str = "minecraft";
148
149fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
151 id.split_once(':').unwrap_or((default_ns, id))
152}
153
154
155fn json_str(s: &str) -> String {
157 let mut out = String::with_capacity(s.len() + 2);
158 out.push('"');
159 for c in s.chars() {
160 match c {
161 '"' => out.push_str("\\\""),
162 '\\' => out.push_str("\\\\"),
163 '\n' => out.push_str("\\n"),
164 '\r' => out.push_str("\\r"),
165 '\t' => out.push_str("\\t"),
166 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
167 c => out.push(c),
168 }
169 }
170 out.push('"');
171 out
172}
173
174
175#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum Cooking {
179 Smelting,
180 Blasting,
181 Smoking,
182 Campfire,
183}
184
185impl Cooking {
186 pub fn word(self) -> &'static str {
188 match self {
189 Cooking::Smelting => "smelting",
190 Cooking::Blasting => "blasting",
191 Cooking::Smoking => "smoking",
192 Cooking::Campfire => "campfire_cooking",
193 }
194 }
195}
196
197#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
199#[derive(Debug, Clone, PartialEq)]
200pub enum RecipeKind {
201 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
203 Shapeless { ingredients: Vec<Vec<String>> },
205 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
206 Stonecutting { ingredient: Vec<String> },
207 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
209}
210
211#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
225#[derive(Debug, Clone, PartialEq)]
226pub struct Recipe {
227 pub id: String,
228 pub kind: RecipeKind,
229 pub result: String,
230 pub count: u32,
231 pub group: Option<String>,
233 pub category: Option<String>,
235}
236
237impl Recipe {
238 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
239 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
240 }
241
242 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
243 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
244 }
245
246 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
247 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
248 }
249
250 pub fn cooking(
253 id: impl Into<String>,
254 method: Cooking,
255 ingredient: impl Into<String>,
256 result: impl Into<String>,
257 count: u32,
258 ) -> Self {
259 let cook_time = match method {
260 Cooking::Smelting => 200,
261 Cooking::Blasting | Cooking::Smoking => 100,
262 Cooking::Campfire => 600,
263 };
264 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
265 Self::of(id, kind, result, count)
266 }
267
268 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
269 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
270 }
271
272 pub fn smithing(
273 id: impl Into<String>,
274 template: impl Into<String>,
275 base: impl Into<String>,
276 addition: impl Into<String>,
277 result: impl Into<String>,
278 ) -> Self {
279 let kind = RecipeKind::Smithing {
280 template: vec![template.into()],
281 base: vec![base.into()],
282 addition: vec![addition.into()],
283 };
284 Self::of(id, kind, result, 1)
285 }
286
287 pub fn row(mut self, pattern: impl Into<String>) -> Self {
289 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
290 p.push(pattern.into());
291 }
292 self
293 }
294
295 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
297 self.key_any(symbol, [item.into()])
298 }
299
300 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
302 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
303 key.push((symbol, items.into_iter().map(Into::into).collect()));
304 }
305 self
306 }
307
308 pub fn ingredient(self, item: impl Into<String>) -> Self {
310 self.ingredient_any([item.into()])
311 }
312
313 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
315 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
316 ingredients.push(items.into_iter().map(Into::into).collect());
317 }
318 self
319 }
320
321 pub fn experience(mut self, xp: f32) -> Self {
323 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
324 *experience = xp;
325 }
326 self
327 }
328
329 pub fn cook_time(mut self, ticks: u32) -> Self {
331 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
332 *cook_time = ticks;
333 }
334 self
335 }
336
337 pub fn group(mut self, group: impl Into<String>) -> Self {
338 self.group = Some(group.into());
339 self
340 }
341
342 pub fn category(mut self, category: impl Into<String>) -> Self {
343 self.category = Some(category.into());
344 self
345 }
346
347 pub fn to_data(&self) -> yog_data::Value {
350 use yog_data::{Map, Value};
351 let ing = |items: &[String]| match items {
352 [one] => Value::from(one.as_str()),
353 many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
354 };
355 let mut m = Map::with_capacity(8);
356 let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
357 match &self.kind {
358 RecipeKind::Shaped { pattern, key } => {
359 ty(&mut m, "shaped");
360 m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
361 let mut keys = Map::with_capacity(key.len());
362 for (c, items) in key {
363 keys.insert(c.to_string(), ing(items));
364 }
365 m.insert("key", Value::Map(keys));
366 }
367 RecipeKind::Shapeless { ingredients } => {
368 ty(&mut m, "shapeless");
369 m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
370 }
371 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
372 ty(&mut m, method.word());
373 m.insert("ingredient", ing(ingredient));
374 m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
376 m.insert("cook_time", Value::Int(*cook_time as i64));
377 }
378 RecipeKind::Stonecutting { ingredient } => {
379 ty(&mut m, "stonecutting");
380 m.insert("ingredient", ing(ingredient));
381 }
382 RecipeKind::Smithing { template, base, addition } => {
383 ty(&mut m, "smithing");
384 m.insert("template", ing(template));
385 m.insert("base", ing(base));
386 m.insert("addition", ing(addition));
387 }
388 }
389 m.insert("result", Value::from(self.result.as_str()));
390 if self.count != 1 {
391 m.insert("count", Value::Int(self.count as i64));
392 }
393 if let Some(g) = &self.group {
394 m.insert("group", Value::from(g.as_str()));
395 }
396 if let Some(c) = &self.category {
397 m.insert("category", Value::from(c.as_str()));
398 }
399 Value::Map(m)
400 }
401
402 pub fn ns_name(&self) -> (&str, &str) {
404 split_id(&self.id, VANILLA_NS)
405 }
406}
407
408#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
421#[derive(yog_data::Schema, Debug, Clone)]
422pub struct StartupGrant {
423 pub id: String,
424 pub items: Vec<String>,
425 pub book: Option<String>,
426 pub command: Option<String>,
427}
428
429impl StartupGrant {
430 pub fn new(id: impl Into<String>) -> Self {
431 Self {
432 id: id.into(),
433 items: Vec::new(),
434 book: None,
435 command: None,
436 }
437 }
438
439 pub fn item(mut self, item_id: impl Into<String>) -> Self {
440 self.items.push(item_id.into());
441 self
442 }
443
444 pub fn book(mut self, book: impl Into<String>) -> Self {
445 self.book = Some(book.into());
446 self
447 }
448
449 pub fn command(mut self, cmd: impl Into<String>) -> Self {
450 self.command = Some(cmd.into());
451 self
452 }
453
454 pub fn to_json(&self) -> String {
455 let mut entries: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":{}}}", json_str(i))).collect();
456 if let Some(book) = &self.book {
457 let tag = format!("{{yog_book:\"{}\"}}", book.replace('\\', "\\\\").replace('"', "\\\""));
459 entries.push(format!(
460 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":{}}}]}}",
461 json_str(&tag)
462 ));
463 }
464 if let Some(cmd) = &self.command {
465 entries.push(format!("{{\"type\":\"minecraft:command\",\"command\":{}}}", json_str(cmd)));
466 }
467 format!("{{\"type\":\"yog:startup_grant\",\"entries\":[{}],\"id\":{}}}", entries.join(","), json_str(&self.id))
468 }
469}
470
471
472
473#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
477#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
478pub struct ToolItem {
479 pub kind: String,
482 pub material: String,
484 pub attack_damage: f32,
486 pub attack_speed: f32,
488}
489
490#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
492#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
493pub struct ArmorPiece {
494 pub slot: String,
496 pub material: String,
498}
499
500#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
505#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
506pub struct ToolMaterial {
507 pub id: String,
508 pub durability: u32,
510 pub speed: f32,
512 pub attack_damage: f32,
514 pub enchantability: u32,
515 pub level: String,
517 pub repair: String,
519 #[data(default)]
521 pub level_index: u32,
522 #[data(default)]
525 pub incorrect: String,
526}
527
528#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
530#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
531pub struct ArmorDefense {
532 pub head: u32,
533 pub chest: u32,
534 pub legs: u32,
535 pub feet: u32,
536}
537
538#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
542#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
543pub struct ArmorMaterial {
544 pub id: String,
545 pub durability: u32,
548 pub defense: ArmorDefense,
549 pub enchantability: u32,
550 pub equip_sound: String,
552 #[data(default)]
553 pub toughness: f32,
554 #[data(default)]
555 pub knockback_resistance: f32,
556 pub repair: String,
558 pub texture: String,
561}
562
563static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
564 yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
565 .unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
566 .value
567});
568
569static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
570 yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
571 .unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
572 .value
573});
574
575fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
577 let name = name.strip_prefix("minecraft:").unwrap_or(name);
578 list.iter().find(|x| id(x).strip_prefix("minecraft:") == Some(name))
579}
580
581impl ToolMaterial {
582 pub fn built_ins() -> &'static [ToolMaterial] {
584 &BUILT_IN_TOOL_MATERIALS
585 }
586
587 pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
589 built_in(Self::built_ins(), |m| &m.id, name)
590 }
591
592 pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
595 let mut m = Self::built_in(like)?.clone();
596 m.id = id.into();
597 Some(m)
598 }
599}
600
601impl ArmorMaterial {
602 pub fn built_ins() -> &'static [ArmorMaterial] {
604 &BUILT_IN_ARMOR_MATERIALS
605 }
606
607 pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
609 built_in(Self::built_ins(), |m| &m.id, name)
610 }
611}
612
613#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
620#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
621pub struct BiomeFeature {
622 pub feature: String,
624 pub biomes: String,
626 pub step: String,
628}
629
630#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
638#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
639pub struct SoundGroup {
640 pub id: String,
641 #[data(rename = "break")]
643 pub break_sound: String,
644 pub step: String,
645 pub place: String,
646 pub hit: String,
647 pub fall: String,
648 #[data(default = "one")]
649 #[data(range = "0..=10")]
650 pub volume: f32,
651 #[data(default = "one")]
652 #[data(range = "0..=10")]
653 pub pitch: f32,
654}
655
656fn one() -> f32 {
657 1.0
658}
659
660static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
661 yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
662 .unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
663 .value
664});
665
666impl SoundGroup {
667 pub fn new(id: impl Into<String>) -> Self {
670 let id = id.into();
671 let ev = |s: &str| format!("{id}.{s}");
672 Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
673 }
674
675 pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
677 [self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
678 self
679 }
680
681 pub fn volume(mut self, volume: f32) -> Self {
682 self.volume = volume;
683 self
684 }
685
686 pub fn pitch(mut self, pitch: f32) -> Self {
687 self.pitch = pitch;
688 self
689 }
690
691 pub fn built_ins() -> &'static [SoundGroup] {
694 &BUILT_IN_SOUND_GROUPS
695 }
696
697 pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
699 let name = name.strip_prefix("minecraft:").unwrap_or(name);
700 Self::built_ins().iter().find(|g| g.id.strip_prefix("minecraft:") == Some(name))
701 }
702}
703
704#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
708#[derive(yog_data::Schema, Debug, Clone)]
709pub struct BlockDef {
710 pub id: String,
711 pub hardness: f32,
712 pub resistance: f32,
713 pub name: Option<String>,
714 pub shape: Option<[f32; 6]>,
717 pub light_level: u8,
719 pub sound: Option<String>,
723 pub requires_tool: bool,
725 pub no_collision: bool,
727 pub slipperiness: f32,
729 pub connects: bool,
735 pub connect_groups: Vec<String>,
741 pub inventory_id: Option<String>,
744 pub redstone_source: bool,
747}
748
749impl BlockDef {
750 pub fn new(id: impl Into<String>) -> Self {
751 Self {
752 id: id.into(),
753 hardness: 1.5,
754 resistance: 6.0,
755 name: None,
756 shape: None,
757 light_level: 0,
758 sound: None,
759 requires_tool: false,
760 no_collision: false,
761 slipperiness: 0.0,
762 connects: false,
763 connect_groups: Vec::new(),
764 inventory_id: None,
765 redstone_source: false,
766 }
767 }
768
769 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
771 self.hardness = hardness;
772 self.resistance = resistance;
773 self
774 }
775
776 pub fn name(mut self, name: impl Into<String>) -> Self {
778 self.name = Some(name.into());
779 self
780 }
781
782 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
784 self.shape = Some([x1, y1, z1, x2, y2, z2]);
785 self
786 }
787
788 pub fn light_level(mut self, level: u8) -> Self {
790 self.light_level = level.min(15);
791 self
792 }
793
794 pub fn sound(mut self, group: impl Into<String>) -> Self {
797 self.sound = Some(group.into());
798 self
799 }
800
801 pub fn requires_tool(mut self) -> Self {
803 self.requires_tool = true;
804 self
805 }
806
807 pub fn no_collision(mut self) -> Self {
809 self.no_collision = true;
810 self
811 }
812
813 pub fn slipperiness(mut self, value: f32) -> Self {
815 self.slipperiness = value;
816 self
817 }
818
819 pub fn connects_to_neighbors(mut self) -> Self {
825 self.connects = true;
826 self
827 }
828
829 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
836 self.connect_groups = groups
837 .iter()
838 .map(std::string::ToString::to_string)
839 .collect();
840 self
841 }
842
843 pub fn redstone_source(mut self) -> Self {
849 self.redstone_source = true;
850 self
851 }
852
853 pub fn inventory(mut self, id: impl Into<String>) -> Self {
854 self.inventory_id = Some(id.into());
855 self
856 }
857}
858
859#[cfg(test)]
860mod material_tests {
861 use super::*;
862
863 #[test]
864 fn the_games_materials_are_built_in() {
865 assert_eq!(ToolMaterial::built_ins().len(), 6);
866 let iron = ToolMaterial::built_in("iron").unwrap();
867 assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
868 assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
869 assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
870 let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
871 assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
872 }
873}
874
875#[cfg(test)]
876mod sound_tests {
877 use super::*;
878
879 #[test]
880 fn the_games_sound_groups_are_built_in() {
881 assert!(SoundGroup::built_ins().len() > 100);
882 let stone = SoundGroup::built_in("stone").unwrap();
883 assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
884 assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
885 assert!(SoundGroup::built_in("mymod:crystal").is_none());
886 }
887
888 #[test]
889 fn a_mods_group_crosses_as_yog_data() {
890 use yog_data::Schema;
891 let g = SoundGroup::new("mymod:crystal").pitch(1.2);
892 assert_eq!(g.fall, "mymod:crystal.fall");
893 let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
894 assert_eq!(back, g);
895 assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
896 }
897}
898
899#[cfg(test)]
900impl Recipe {
901 fn to_json(&self) -> String {
902 yog_data::json::to_json(&self.to_data()).to_string()
903 }
904}
905
906#[cfg(test)]
907mod recipe_tests {
908 use super::*;
909
910 #[test]
911 fn writes_the_yog_format() {
912 let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
913 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
914 assert_eq!(
915 js(&shaped.to_json()),
916 js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
917 );
918 assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
919 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
920 assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
921 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
922 assert_eq!(js(&smith.to_json())["template"], "m:t");
923 }
924}
925
926#[cfg(test)]
927mod json_tests {
928 use super::*;
929
930 #[test]
932 fn json_survives_any_string() {
933 let odd = "m:a\"b\\c\nd\te\u{1}";
934 let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
935 for r in [
936 Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
937 Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
938 Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
939 Recipe::stonecutting(odd, odd, odd, 2),
940 Recipe::smithing(odd, odd, odd, odd, odd),
941 ] {
942 let v = parse(r.to_json());
943 assert_eq!(v["result"], odd, "the string comes back as it was");
944 }
945 let g = StartupGrant::new(odd).item(odd).book("mymod:\"guide\"").command("say \"hi\" \\o/");
946 let v = parse(g.to_json());
947 assert_eq!(v["id"], odd);
948 assert_eq!(v["entries"][2]["command"], "say \"hi\" \\o/");
949 }
950
951 #[test]
952 fn recipes_and_grants_name_their_parts() {
953 let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
954 let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
955 assert_eq!(v["type"], "campfire_cooking");
956 assert_eq!(v["cook_time"], 600, "the method's default time");
957 assert_eq!(r.ns_name(), ("mymod", "a"));
958 assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
959 let g = StartupGrant::new("mymod:starter");
960 assert!(g.to_json().contains(r#""entries":[]"#));
961 }
962}