1#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23#[derive(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(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}
65
66impl ItemDef {
67 pub fn new(id: impl Into<String>) -> Self {
68 Self {
69 id: id.into(),
70 max_stack: 64,
71 name: None,
72 tooltip: None,
73 max_damage: 0,
74 fire_resistant: false,
75 fuel_ticks: 0,
76 food: None,
77 }
78 }
79
80 pub fn max_stack(mut self, n: u8) -> Self {
82 self.max_stack = n;
83 self
84 }
85
86 pub fn name(mut self, name: impl Into<String>) -> Self {
88 self.name = Some(name.into());
89 self
90 }
91
92 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
94 self.tooltip = Some(tooltip.into());
95 self
96 }
97
98 pub fn max_damage(mut self, durability: u32) -> Self {
100 self.max_damage = durability;
101 self
102 }
103
104 pub fn fire_resistant(mut self) -> Self {
106 self.fire_resistant = true;
107 self
108 }
109
110 pub fn fuel(mut self, ticks: u32) -> Self {
112 self.fuel_ticks = ticks;
113 self
114 }
115
116 pub fn food(mut self, food: FoodDef) -> Self {
118 self.food = Some(food);
119 self
120 }
121}
122
123const VANILLA_NS: &str = "minecraft";
127const YOG_NS: &str = "yog";
129
130fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
132 id.split_once(':').unwrap_or((default_ns, id))
133}
134
135fn ingredient_json(id: &str) -> String {
138 match id.strip_prefix('#') {
139 Some(tag) => format!("{{\"tag\":\"{tag}\"}}"),
140 None => format!("{{\"item\":\"{id}\"}}"),
141 }
142}
143
144fn json_str(s: &str) -> String {
146 let mut out = String::with_capacity(s.len() + 2);
147 out.push('"');
148 for c in s.chars() {
149 match c {
150 '"' => out.push_str("\\\""),
151 '\\' => out.push_str("\\\\"),
152 '\n' => out.push_str("\\n"),
153 '\r' => out.push_str("\\r"),
154 '\t' => out.push_str("\\t"),
155 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
156 c => out.push(c),
157 }
158 }
159 out.push('"');
160 out
161}
162
163fn item_list_json(items: &[String]) -> String {
164 items
165 .iter()
166 .map(|i| ingredient_json(i))
167 .collect::<Vec<_>>()
168 .join(",")
169}
170
171#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum Cooking {
175 Smelting,
176 Blasting,
177 Smoking,
178 Campfire,
179}
180
181impl Cooking {
182 pub fn word(self) -> &'static str {
184 match self {
185 Cooking::Smelting => "smelting",
186 Cooking::Blasting => "blasting",
187 Cooking::Smoking => "smoking",
188 Cooking::Campfire => "campfire_cooking",
189 }
190 }
191}
192
193#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
195#[derive(Debug, Clone, PartialEq)]
196pub enum RecipeKind {
197 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
199 Shapeless { ingredients: Vec<Vec<String>> },
201 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
202 Stonecutting { ingredient: Vec<String> },
203 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
205}
206
207#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
221#[derive(Debug, Clone, PartialEq)]
222pub struct Recipe {
223 pub id: String,
224 pub kind: RecipeKind,
225 pub result: String,
226 pub count: u32,
227 pub group: Option<String>,
229 pub category: Option<String>,
231}
232
233impl Recipe {
234 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
235 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
236 }
237
238 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
239 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
240 }
241
242 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
243 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
244 }
245
246 pub fn cooking(
249 id: impl Into<String>,
250 method: Cooking,
251 ingredient: impl Into<String>,
252 result: impl Into<String>,
253 count: u32,
254 ) -> Self {
255 let cook_time = match method {
256 Cooking::Smelting => 200,
257 Cooking::Blasting | Cooking::Smoking => 100,
258 Cooking::Campfire => 600,
259 };
260 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
261 Self::of(id, kind, result, count)
262 }
263
264 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
265 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
266 }
267
268 pub fn smithing(
269 id: impl Into<String>,
270 template: impl Into<String>,
271 base: impl Into<String>,
272 addition: impl Into<String>,
273 result: impl Into<String>,
274 ) -> Self {
275 let kind = RecipeKind::Smithing {
276 template: vec![template.into()],
277 base: vec![base.into()],
278 addition: vec![addition.into()],
279 };
280 Self::of(id, kind, result, 1)
281 }
282
283 pub fn row(mut self, pattern: impl Into<String>) -> Self {
285 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
286 p.push(pattern.into());
287 }
288 self
289 }
290
291 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
293 self.key_any(symbol, [item.into()])
294 }
295
296 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
298 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
299 key.push((symbol, items.into_iter().map(Into::into).collect()));
300 }
301 self
302 }
303
304 pub fn ingredient(self, item: impl Into<String>) -> Self {
306 self.ingredient_any([item.into()])
307 }
308
309 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
311 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
312 ingredients.push(items.into_iter().map(Into::into).collect());
313 }
314 self
315 }
316
317 pub fn experience(mut self, xp: f32) -> Self {
319 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
320 *experience = xp;
321 }
322 self
323 }
324
325 pub fn cook_time(mut self, ticks: u32) -> Self {
327 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
328 *cook_time = ticks;
329 }
330 self
331 }
332
333 pub fn group(mut self, group: impl Into<String>) -> Self {
334 self.group = Some(group.into());
335 self
336 }
337
338 pub fn category(mut self, category: impl Into<String>) -> Self {
339 self.category = Some(category.into());
340 self
341 }
342
343 pub fn to_json(&self) -> String {
345 let ing = |items: &[String]| match items {
346 [one] => json_str(one),
347 many => format!("[{}]", many.iter().map(|i| json_str(i)).collect::<Vec<_>>().join(",")),
348 };
349 let mut fields = Vec::new();
350 match &self.kind {
351 RecipeKind::Shaped { pattern, key } => {
352 fields.push("\"type\":\"shaped\"".to_string());
353 let rows: Vec<String> = pattern.iter().map(|r| json_str(r)).collect();
354 fields.push(format!("\"pattern\":[{}]", rows.join(",")));
355 let keys: Vec<String> = key.iter().map(|(c, items)| format!("{}:{}", json_str(&c.to_string()), ing(items))).collect();
356 fields.push(format!("\"key\":{{{}}}", keys.join(",")));
357 }
358 RecipeKind::Shapeless { ingredients } => {
359 fields.push("\"type\":\"shapeless\"".to_string());
360 let list: Vec<String> = ingredients.iter().map(|i| ing(i)).collect();
361 fields.push(format!("\"ingredients\":[{}]", list.join(",")));
362 }
363 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
364 fields.push(format!("\"type\":\"{}\"", method.word()));
365 fields.push(format!("\"ingredient\":{}", ing(ingredient)));
366 fields.push(format!("\"experience\":{experience}"));
367 fields.push(format!("\"cook_time\":{cook_time}"));
368 }
369 RecipeKind::Stonecutting { ingredient } => {
370 fields.push("\"type\":\"stonecutting\"".to_string());
371 fields.push(format!("\"ingredient\":{}", ing(ingredient)));
372 }
373 RecipeKind::Smithing { template, base, addition } => {
374 fields.push("\"type\":\"smithing\"".to_string());
375 fields.push(format!("\"template\":{}", ing(template)));
376 fields.push(format!("\"base\":{}", ing(base)));
377 fields.push(format!("\"addition\":{}", ing(addition)));
378 }
379 }
380 fields.push(format!("\"result\":{}", json_str(&self.result)));
381 if self.count != 1 {
382 fields.push(format!("\"count\":{}", self.count));
383 }
384 if let Some(g) = &self.group {
385 fields.push(format!("\"group\":{}", json_str(g)));
386 }
387 if let Some(c) = &self.category {
388 fields.push(format!("\"category\":{}", json_str(c)));
389 }
390 format!("{{{}}}", fields.join(","))
391 }
392
393 pub fn ns_name(&self) -> (&str, &str) {
395 split_id(&self.id, VANILLA_NS)
396 }
397}
398
399#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
404#[derive(Debug, Clone)]
405pub struct BookRecipe {
406 pub id: String,
407 pub book: String,
408 pub ingredients: Vec<String>,
409}
410
411impl BookRecipe {
412 pub fn new(id: impl Into<String>, book: impl Into<String>) -> Self {
413 Self {
414 id: id.into(),
415 book: book.into(),
416 ingredients: Vec::new(),
417 }
418 }
419
420 pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
421 self.ingredients.push(item_id.into());
422 self
423 }
424
425 pub fn to_json(&self) -> String {
426 let ingr = item_list_json(&self.ingredients);
427 format!(
428 "{{\"type\":\"yog:crafting_book\",\"ingredients\":[{}],\"book\":\"{}\"}}",
429 ingr, self.book
430 )
431 }
432
433 pub fn ns_name(&self) -> (&str, &str) {
434 split_id(&self.id, YOG_NS)
435 }
436}
437
438#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
442#[derive(Debug, Clone)]
443pub struct ItemModifier {
444 pub id: String,
445 pub function: String,
447 pub parameters: std::collections::HashMap<String, String>,
449}
450
451impl ItemModifier {
452 pub fn new(id: impl Into<String>, function: impl Into<String>) -> Self {
453 Self {
454 id: id.into(),
455 function: function.into(),
456 parameters: std::collections::HashMap::new(),
457 }
458 }
459
460 pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
461 self.parameters.insert(key.into(), value.into());
462 self
463 }
464
465 pub fn to_json(&self) -> String {
466 let params: String = self
467 .parameters
468 .iter()
469 .map(|(k, v)| format!("\"{k}\":{v}"))
470 .collect::<Vec<_>>()
471 .join(",");
472 format!(
473 "{{\"function\":\"{}\",{}}}",
474 self.function,
475 if params.is_empty() {
476 String::new()
477 } else {
478 params
479 }
480 )
481 }
482}
483
484#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
489#[derive(Debug, Clone)]
490pub struct StartupGrant {
491 pub id: String,
492 pub items: Vec<String>,
493 pub book: Option<String>,
494 pub command: Option<String>,
495}
496
497impl StartupGrant {
498 pub fn new(id: impl Into<String>) -> Self {
499 Self {
500 id: id.into(),
501 items: Vec::new(),
502 book: None,
503 command: None,
504 }
505 }
506
507 pub fn item(mut self, item_id: impl Into<String>) -> Self {
508 self.items.push(item_id.into());
509 self
510 }
511
512 pub fn book(mut self, book: impl Into<String>) -> Self {
513 self.book = Some(book.into());
514 self
515 }
516
517 pub fn command(mut self, cmd: impl Into<String>) -> Self {
518 self.command = Some(cmd.into());
519 self
520 }
521
522 pub fn to_json(&self) -> String {
523 let mut entries: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":{}}}", json_str(i))).collect();
524 if let Some(book) = &self.book {
525 let tag = format!("{{yog_book:\"{}\"}}", book.replace('\\', "\\\\").replace('"', "\\\""));
527 entries.push(format!(
528 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":{}}}]}}",
529 json_str(&tag)
530 ));
531 }
532 if let Some(cmd) = &self.command {
533 entries.push(format!("{{\"type\":\"minecraft:command\",\"command\":{}}}", json_str(cmd)));
534 }
535 format!("{{\"type\":\"yog:startup_grant\",\"entries\":[{}],\"id\":{}}}", entries.join(","), json_str(&self.id))
536 }
537}
538
539#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
541#[derive(Debug, Clone)]
542pub struct AdvancementReward {
543 pub id: String,
544 pub item: String,
546 pub nbt: Option<String>,
548 pub book: Option<String>,
550}
551
552impl AdvancementReward {
553 pub fn new(id: impl Into<String>) -> Self {
554 Self {
555 id: id.into(),
556 item: "minecraft:written_book".into(),
557 nbt: None,
558 book: None,
559 }
560 }
561
562 pub fn item(mut self, item: impl Into<String>) -> Self {
563 self.item = item.into();
564 self
565 }
566
567 pub fn nbt(mut self, nbt: impl Into<String>) -> Self {
568 self.nbt = Some(nbt.into());
569 self
570 }
571
572 pub fn book(mut self, book: impl Into<String>) -> Self {
573 let book_str: String = book.into();
574 self.book = Some(book_str.clone());
575 self.nbt = Some(format!(
576 "{{yog_book:\"{book_str}\",title:\"Yog Book\",author:\"Yog\"}}"
577 ));
578 self
579 }
580
581 pub fn to_json(&self) -> String {
582 let nbt_part = self
583 .nbt
584 .as_ref()
585 .map(|n| format!("{{\"function\":\"set_nbt\",\"tag\":{n}}}"));
586 let entries = if let Some(nbt_part) = nbt_part {
587 format!(
588 "[{{\"type\":\"item\",\"name\":\"{}\",\"functions\":[{}]}}]",
589 self.item, nbt_part
590 )
591 } else {
592 format!("[{{\"type\":\"item\",\"name\":\"{}\"}}]", self.item)
593 };
594 format!(
595 "{{\"type\":\"advancement_reward\",\"pools\":[{{\"rolls\":1,\"entries\":{entries}}}]}}"
596 )
597 }
598}
599
600#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
604#[derive(Debug, Clone)]
605pub struct BlockDef {
606 pub id: String,
607 pub hardness: f32,
608 pub resistance: f32,
609 pub name: Option<String>,
610 pub shape: Option<[f32; 6]>,
613 pub light_level: u8,
615 pub sound: Option<String>,
619 pub requires_tool: bool,
621 pub no_collision: bool,
623 pub slipperiness: f32,
625 pub connects: bool,
631 pub connect_groups: Vec<String>,
637 pub inventory_id: Option<String>,
640 pub redstone_source: bool,
643}
644
645impl BlockDef {
646 pub fn new(id: impl Into<String>) -> Self {
647 Self {
648 id: id.into(),
649 hardness: 1.5,
650 resistance: 6.0,
651 name: None,
652 shape: None,
653 light_level: 0,
654 sound: None,
655 requires_tool: false,
656 no_collision: false,
657 slipperiness: 0.0,
658 connects: false,
659 connect_groups: Vec::new(),
660 inventory_id: None,
661 redstone_source: false,
662 }
663 }
664
665 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
667 self.hardness = hardness;
668 self.resistance = resistance;
669 self
670 }
671
672 pub fn name(mut self, name: impl Into<String>) -> Self {
674 self.name = Some(name.into());
675 self
676 }
677
678 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
680 self.shape = Some([x1, y1, z1, x2, y2, z2]);
681 self
682 }
683
684 pub fn light_level(mut self, level: u8) -> Self {
686 self.light_level = level.min(15);
687 self
688 }
689
690 pub fn sound(mut self, group: impl Into<String>) -> Self {
693 self.sound = Some(group.into());
694 self
695 }
696
697 pub fn requires_tool(mut self) -> Self {
699 self.requires_tool = true;
700 self
701 }
702
703 pub fn no_collision(mut self) -> Self {
705 self.no_collision = true;
706 self
707 }
708
709 pub fn slipperiness(mut self, value: f32) -> Self {
711 self.slipperiness = value;
712 self
713 }
714
715 pub fn connects_to_neighbors(mut self) -> Self {
721 self.connects = true;
722 self
723 }
724
725 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
732 self.connect_groups = groups
733 .iter()
734 .map(std::string::ToString::to_string)
735 .collect();
736 self
737 }
738
739 pub fn redstone_source(mut self) -> Self {
745 self.redstone_source = true;
746 self
747 }
748
749 pub fn inventory(mut self, id: impl Into<String>) -> Self {
750 self.inventory_id = Some(id.into());
751 self
752 }
753}
754
755#[cfg(test)]
756mod recipe_tests {
757 use super::*;
758
759 #[test]
760 fn writes_the_yog_format() {
761 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
762 assert_eq!(
763 shaped.to_json(),
764 r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##
765 );
766 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.5);
767 assert_eq!(blast.to_json(), r##"{"type":"blasting","ingredient":"#c:ores","experience":0.5,"cook_time":100,"result":"m:b"}"##);
768 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
769 assert!(smith.to_json().contains(r#""type":"smithing","template":"m:t""#));
770 }
771}
772
773#[cfg(test)]
774mod json_tests {
775 use super::*;
776
777 #[test]
779 fn json_survives_any_string() {
780 let odd = "m:a\"b\\c\nd\te\u{1}";
781 let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
782 for r in [
783 Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
784 Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
785 Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
786 Recipe::stonecutting(odd, odd, odd, 2),
787 Recipe::smithing(odd, odd, odd, odd, odd),
788 ] {
789 let v = parse(r.to_json());
790 assert_eq!(v["result"], odd, "the string comes back as it was");
791 }
792 let g = StartupGrant::new(odd).item(odd).book("mymod:\"guide\"").command("say \"hi\" \\o/");
793 let v = parse(g.to_json());
794 assert_eq!(v["id"], odd);
795 assert_eq!(v["entries"][2]["command"], "say \"hi\" \\o/");
796 }
797
798 #[test]
799 fn recipes_and_grants_name_their_parts() {
800 let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
801 let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
802 assert_eq!(v["type"], "campfire_cooking");
803 assert_eq!(v["cook_time"], 600, "the method's default time");
804 assert_eq!(r.ns_name(), ("mymod", "a"));
805 assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
806 let g = StartupGrant::new("mymod:starter");
807 assert!(g.to_json().contains(r#""entries":[]"#));
808 }
809}