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 c => out.push(c),
153 }
154 }
155 out.push('"');
156 out
157}
158
159fn item_list_json(items: &[String]) -> String {
160 items
161 .iter()
162 .map(|i| ingredient_json(i))
163 .collect::<Vec<_>>()
164 .join(",")
165}
166
167#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum Cooking {
171 Smelting,
172 Blasting,
173 Smoking,
174 Campfire,
175}
176
177impl Cooking {
178 pub fn word(self) -> &'static str {
180 match self {
181 Cooking::Smelting => "smelting",
182 Cooking::Blasting => "blasting",
183 Cooking::Smoking => "smoking",
184 Cooking::Campfire => "campfire_cooking",
185 }
186 }
187}
188
189#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
191#[derive(Debug, Clone, PartialEq)]
192pub enum RecipeKind {
193 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
195 Shapeless { ingredients: Vec<Vec<String>> },
197 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
198 Stonecutting { ingredient: Vec<String> },
199 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
201}
202
203#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
217#[derive(Debug, Clone, PartialEq)]
218pub struct Recipe {
219 pub id: String,
220 pub kind: RecipeKind,
221 pub result: String,
222 pub count: u32,
223 pub group: Option<String>,
225 pub category: Option<String>,
227}
228
229impl Recipe {
230 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
231 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
232 }
233
234 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
235 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
236 }
237
238 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
239 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
240 }
241
242 pub fn cooking(
245 id: impl Into<String>,
246 method: Cooking,
247 ingredient: impl Into<String>,
248 result: impl Into<String>,
249 count: u32,
250 ) -> Self {
251 let cook_time = match method {
252 Cooking::Smelting => 200,
253 Cooking::Blasting | Cooking::Smoking => 100,
254 Cooking::Campfire => 600,
255 };
256 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
257 Self::of(id, kind, result, count)
258 }
259
260 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
261 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
262 }
263
264 pub fn smithing(
265 id: impl Into<String>,
266 template: impl Into<String>,
267 base: impl Into<String>,
268 addition: impl Into<String>,
269 result: impl Into<String>,
270 ) -> Self {
271 let kind = RecipeKind::Smithing {
272 template: vec![template.into()],
273 base: vec![base.into()],
274 addition: vec![addition.into()],
275 };
276 Self::of(id, kind, result, 1)
277 }
278
279 pub fn row(mut self, pattern: impl Into<String>) -> Self {
281 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
282 p.push(pattern.into());
283 }
284 self
285 }
286
287 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
289 self.key_any(symbol, [item.into()])
290 }
291
292 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
294 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
295 key.push((symbol, items.into_iter().map(Into::into).collect()));
296 }
297 self
298 }
299
300 pub fn ingredient(self, item: impl Into<String>) -> Self {
302 self.ingredient_any([item.into()])
303 }
304
305 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
307 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
308 ingredients.push(items.into_iter().map(Into::into).collect());
309 }
310 self
311 }
312
313 pub fn experience(mut self, xp: f32) -> Self {
315 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
316 *experience = xp;
317 }
318 self
319 }
320
321 pub fn cook_time(mut self, ticks: u32) -> Self {
323 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
324 *cook_time = ticks;
325 }
326 self
327 }
328
329 pub fn group(mut self, group: impl Into<String>) -> Self {
330 self.group = Some(group.into());
331 self
332 }
333
334 pub fn category(mut self, category: impl Into<String>) -> Self {
335 self.category = Some(category.into());
336 self
337 }
338
339 pub fn to_json(&self) -> String {
341 let ing = |items: &[String]| match items {
342 [one] => json_str(one),
343 many => format!("[{}]", many.iter().map(|i| json_str(i)).collect::<Vec<_>>().join(",")),
344 };
345 let mut fields = Vec::new();
346 match &self.kind {
347 RecipeKind::Shaped { pattern, key } => {
348 fields.push("\"type\":\"shaped\"".to_string());
349 let rows: Vec<String> = pattern.iter().map(|r| json_str(r)).collect();
350 fields.push(format!("\"pattern\":[{}]", rows.join(",")));
351 let keys: Vec<String> = key.iter().map(|(c, items)| format!("{}:{}", json_str(&c.to_string()), ing(items))).collect();
352 fields.push(format!("\"key\":{{{}}}", keys.join(",")));
353 }
354 RecipeKind::Shapeless { ingredients } => {
355 fields.push("\"type\":\"shapeless\"".to_string());
356 let list: Vec<String> = ingredients.iter().map(|i| ing(i)).collect();
357 fields.push(format!("\"ingredients\":[{}]", list.join(",")));
358 }
359 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
360 fields.push(format!("\"type\":\"{}\"", method.word()));
361 fields.push(format!("\"ingredient\":{}", ing(ingredient)));
362 fields.push(format!("\"experience\":{experience}"));
363 fields.push(format!("\"cook_time\":{cook_time}"));
364 }
365 RecipeKind::Stonecutting { ingredient } => {
366 fields.push("\"type\":\"stonecutting\"".to_string());
367 fields.push(format!("\"ingredient\":{}", ing(ingredient)));
368 }
369 RecipeKind::Smithing { template, base, addition } => {
370 fields.push("\"type\":\"smithing\"".to_string());
371 fields.push(format!("\"template\":{}", ing(template)));
372 fields.push(format!("\"base\":{}", ing(base)));
373 fields.push(format!("\"addition\":{}", ing(addition)));
374 }
375 }
376 fields.push(format!("\"result\":{}", json_str(&self.result)));
377 if self.count != 1 {
378 fields.push(format!("\"count\":{}", self.count));
379 }
380 if let Some(g) = &self.group {
381 fields.push(format!("\"group\":{}", json_str(g)));
382 }
383 if let Some(c) = &self.category {
384 fields.push(format!("\"category\":{}", json_str(c)));
385 }
386 format!("{{{}}}", fields.join(","))
387 }
388
389 pub fn ns_name(&self) -> (&str, &str) {
391 split_id(&self.id, VANILLA_NS)
392 }
393}
394
395#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
400#[derive(Debug, Clone)]
401pub struct BookRecipe {
402 pub id: String,
403 pub book: String,
404 pub ingredients: Vec<String>,
405}
406
407impl BookRecipe {
408 pub fn new(id: impl Into<String>, book: impl Into<String>) -> Self {
409 Self {
410 id: id.into(),
411 book: book.into(),
412 ingredients: Vec::new(),
413 }
414 }
415
416 pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
417 self.ingredients.push(item_id.into());
418 self
419 }
420
421 pub fn to_json(&self) -> String {
422 let ingr = item_list_json(&self.ingredients);
423 format!(
424 "{{\"type\":\"yog:crafting_book\",\"ingredients\":[{}],\"book\":\"{}\"}}",
425 ingr, self.book
426 )
427 }
428
429 pub fn ns_name(&self) -> (&str, &str) {
430 split_id(&self.id, YOG_NS)
431 }
432}
433
434#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
438#[derive(Debug, Clone)]
439pub struct ItemModifier {
440 pub id: String,
441 pub function: String,
443 pub parameters: std::collections::HashMap<String, String>,
445}
446
447impl ItemModifier {
448 pub fn new(id: impl Into<String>, function: impl Into<String>) -> Self {
449 Self {
450 id: id.into(),
451 function: function.into(),
452 parameters: std::collections::HashMap::new(),
453 }
454 }
455
456 pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
457 self.parameters.insert(key.into(), value.into());
458 self
459 }
460
461 pub fn to_json(&self) -> String {
462 let params: String = self
463 .parameters
464 .iter()
465 .map(|(k, v)| format!("\"{k}\":{v}"))
466 .collect::<Vec<_>>()
467 .join(",");
468 format!(
469 "{{\"function\":\"{}\",{}}}",
470 self.function,
471 if params.is_empty() {
472 String::new()
473 } else {
474 params
475 }
476 )
477 }
478}
479
480#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
485#[derive(Debug, Clone)]
486pub struct StartupGrant {
487 pub id: String,
488 pub items: Vec<String>,
489 pub book: Option<String>,
490 pub command: Option<String>,
491}
492
493impl StartupGrant {
494 pub fn new(id: impl Into<String>) -> Self {
495 Self {
496 id: id.into(),
497 items: Vec::new(),
498 book: None,
499 command: None,
500 }
501 }
502
503 pub fn item(mut self, item_id: impl Into<String>) -> Self {
504 self.items.push(item_id.into());
505 self
506 }
507
508 pub fn book(mut self, book: impl Into<String>) -> Self {
509 self.book = Some(book.into());
510 self
511 }
512
513 pub fn command(mut self, cmd: impl Into<String>) -> Self {
514 self.command = Some(cmd.into());
515 self
516 }
517
518 pub fn to_json(&self) -> String {
519 let items: Vec<String> = self
520 .items
521 .iter()
522 .map(|i| format!("{{\"item\":\"{i}\"}}"))
523 .collect();
524 let mut entries = String::new();
525 if !items.is_empty() {
526 entries.push_str(&format!("[{}]", items.join(",")));
527 }
528 if let Some(book) = &self.book {
529 if !entries.is_empty() {
530 entries.push(',');
531 }
532 let book_entry = format!(
533 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":\"{{yog_book:\\\"{book}\\\"}}\"}}]}}"
534 );
535 entries.push_str(&book_entry);
536 }
537 if let Some(cmd) = &self.command {
538 if !entries.is_empty() {
539 entries.push(',');
540 }
541 entries.push_str(&format!(
542 "{{\"type\":\"minecraft:command\",\"command\":\"{}\"}}",
543 cmd.replace('"', "\\\"")
544 ));
545 }
546 format!(
547 "{{\"type\":\"yog:startup_grant\",\"entries\":{},\"id\":\"{}\"}}",
548 if entries.is_empty() {
549 "[]".to_string()
550 } else {
551 entries
552 },
553 self.id
554 )
555 }
556}
557
558#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
560#[derive(Debug, Clone)]
561pub struct AdvancementReward {
562 pub id: String,
563 pub item: String,
565 pub nbt: Option<String>,
567 pub book: Option<String>,
569}
570
571impl AdvancementReward {
572 pub fn new(id: impl Into<String>) -> Self {
573 Self {
574 id: id.into(),
575 item: "minecraft:written_book".into(),
576 nbt: None,
577 book: None,
578 }
579 }
580
581 pub fn item(mut self, item: impl Into<String>) -> Self {
582 self.item = item.into();
583 self
584 }
585
586 pub fn nbt(mut self, nbt: impl Into<String>) -> Self {
587 self.nbt = Some(nbt.into());
588 self
589 }
590
591 pub fn book(mut self, book: impl Into<String>) -> Self {
592 let book_str: String = book.into();
593 self.book = Some(book_str.clone());
594 self.nbt = Some(format!(
595 "{{yog_book:\"{book_str}\",title:\"Yog Book\",author:\"Yog\"}}"
596 ));
597 self
598 }
599
600 pub fn to_json(&self) -> String {
601 let nbt_part = self
602 .nbt
603 .as_ref()
604 .map(|n| format!("{{\"function\":\"set_nbt\",\"tag\":{n}}}"));
605 let entries = if let Some(nbt_part) = nbt_part {
606 format!(
607 "[{{\"type\":\"item\",\"name\":\"{}\",\"functions\":[{}]}}]",
608 self.item, nbt_part
609 )
610 } else {
611 format!("[{{\"type\":\"item\",\"name\":\"{}\"}}]", self.item)
612 };
613 format!(
614 "{{\"type\":\"advancement_reward\",\"pools\":[{{\"rolls\":1,\"entries\":{entries}}}]}}"
615 )
616 }
617}
618
619#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
623#[derive(Debug, Clone)]
624pub struct BlockDef {
625 pub id: String,
626 pub hardness: f32,
627 pub resistance: f32,
628 pub name: Option<String>,
629 pub shape: Option<[f32; 6]>,
632 pub light_level: u8,
634 pub sound: Option<String>,
638 pub requires_tool: bool,
640 pub no_collision: bool,
642 pub slipperiness: f32,
644 pub connects: bool,
650 pub connect_groups: Vec<String>,
656 pub inventory_id: Option<String>,
659 pub redstone_source: bool,
662}
663
664impl BlockDef {
665 pub fn new(id: impl Into<String>) -> Self {
666 Self {
667 id: id.into(),
668 hardness: 1.5,
669 resistance: 6.0,
670 name: None,
671 shape: None,
672 light_level: 0,
673 sound: None,
674 requires_tool: false,
675 no_collision: false,
676 slipperiness: 0.0,
677 connects: false,
678 connect_groups: Vec::new(),
679 inventory_id: None,
680 redstone_source: false,
681 }
682 }
683
684 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
686 self.hardness = hardness;
687 self.resistance = resistance;
688 self
689 }
690
691 pub fn name(mut self, name: impl Into<String>) -> Self {
693 self.name = Some(name.into());
694 self
695 }
696
697 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
699 self.shape = Some([x1, y1, z1, x2, y2, z2]);
700 self
701 }
702
703 pub fn light_level(mut self, level: u8) -> Self {
705 self.light_level = level.min(15);
706 self
707 }
708
709 pub fn sound(mut self, group: impl Into<String>) -> Self {
712 self.sound = Some(group.into());
713 self
714 }
715
716 pub fn requires_tool(mut self) -> Self {
718 self.requires_tool = true;
719 self
720 }
721
722 pub fn no_collision(mut self) -> Self {
724 self.no_collision = true;
725 self
726 }
727
728 pub fn slipperiness(mut self, value: f32) -> Self {
730 self.slipperiness = value;
731 self
732 }
733
734 pub fn connects_to_neighbors(mut self) -> Self {
740 self.connects = true;
741 self
742 }
743
744 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
751 self.connect_groups = groups
752 .iter()
753 .map(std::string::ToString::to_string)
754 .collect();
755 self
756 }
757
758 pub fn redstone_source(mut self) -> Self {
764 self.redstone_source = true;
765 self
766 }
767
768 pub fn inventory(mut self, id: impl Into<String>) -> Self {
769 self.inventory_id = Some(id.into());
770 self
771 }
772}
773
774#[cfg(test)]
775mod recipe_tests {
776 use super::*;
777
778 #[test]
779 fn writes_the_yog_format() {
780 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
781 assert_eq!(
782 shaped.to_json(),
783 r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##
784 );
785 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.5);
786 assert_eq!(blast.to_json(), r##"{"type":"blasting","ingredient":"#c:ores","experience":0.5,"cook_time":100,"result":"m:b"}"##);
787 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
788 assert!(smith.to_json().contains(r#""type":"smithing","template":"m:t""#));
789 }
790}