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 item_list_json(items: &[String]) -> String {
145 items
146 .iter()
147 .map(|i| ingredient_json(i))
148 .collect::<Vec<_>>()
149 .join(",")
150}
151
152#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
161#[derive(Debug, Clone)]
162pub struct ShapedRecipe {
163 pub id: String,
164 pub output: String,
165 pub count: u32,
166 rows: Vec<String>,
167 keys: Vec<(char, String)>,
168}
169
170impl ShapedRecipe {
171 pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
172 Self {
173 id: id.into(),
174 output: output.into(),
175 count,
176 rows: Vec::new(),
177 keys: Vec::new(),
178 }
179 }
180
181 pub fn row(mut self, pattern: impl Into<String>) -> Self {
182 self.rows.push(pattern.into());
183 self
184 }
185
186 pub fn key(mut self, symbol: char, item_id: impl Into<String>) -> Self {
188 self.keys.push((symbol, item_id.into()));
189 self
190 }
191
192 pub fn to_json(&self) -> String {
194 let pattern: String = self
195 .rows
196 .iter()
197 .map(|r| format!("\"{r}\""))
198 .collect::<Vec<_>>()
199 .join(",");
200 let keys: String = self
201 .keys
202 .iter()
203 .map(|(ch, item)| format!("\"{ch}\":{}", ingredient_json(item)))
204 .collect::<Vec<_>>()
205 .join(",");
206 format!(
207 "{{\"type\":\"minecraft:crafting_shaped\",\"pattern\":[{}],\"key\":{{{}}},\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
208 pattern, keys, self.output, self.count
209 )
210 }
211
212 pub fn ns_name(&self) -> (&str, &str) {
214 split_id(&self.id, VANILLA_NS)
215 }
216}
217
218#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
220#[derive(Debug, Clone)]
221pub struct ShapelessRecipe {
222 pub id: String,
223 pub output: String,
224 pub count: u32,
225 pub ingredients: Vec<String>,
226}
227
228impl ShapelessRecipe {
229 pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
230 Self {
231 id: id.into(),
232 output: output.into(),
233 count,
234 ingredients: Vec::new(),
235 }
236 }
237
238 pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
240 self.ingredients.push(item_id.into());
241 self
242 }
243
244 pub fn to_json(&self) -> String {
245 let ingr = item_list_json(&self.ingredients);
246 format!(
247 "{{\"type\":\"minecraft:crafting_shapeless\",\"ingredients\":[{}],\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
248 ingr, self.output, self.count
249 )
250 }
251
252 pub fn ns_name(&self) -> (&str, &str) {
253 split_id(&self.id, VANILLA_NS)
254 }
255}
256
257#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
259#[derive(Debug, Clone)]
260pub struct FurnaceRecipe {
261 pub id: String,
262 pub input: String,
263 pub output: String,
264 pub count: u32,
265 pub experience: f32,
266 pub cook_time: u32,
267}
268
269impl FurnaceRecipe {
270 pub fn new(
271 id: impl Into<String>,
272 input: impl Into<String>,
273 output: impl Into<String>,
274 count: u32,
275 ) -> Self {
276 Self {
277 id: id.into(),
278 input: input.into(),
279 output: output.into(),
280 count,
281 experience: 0.1,
282 cook_time: 200,
283 }
284 }
285
286 pub fn experience(mut self, xp: f32) -> Self {
287 self.experience = xp;
288 self
289 }
290
291 pub fn cook_time(mut self, ticks: u32) -> Self {
293 self.cook_time = ticks;
294 self
295 }
296
297 pub fn to_json(&self) -> String {
298 format!(
299 "{{\"type\":\"minecraft:smelting\",\"ingredient\":{},\"result\":\"{}\",\"experience\":{},\"cookingtime\":{}}}",
300 ingredient_json(&self.input), self.output, self.experience, self.cook_time
301 )
302 }
303
304 pub fn ns_name(&self) -> (&str, &str) {
305 split_id(&self.id, VANILLA_NS)
306 }
307}
308
309#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
314#[derive(Debug, Clone)]
315pub struct BookRecipe {
316 pub id: String,
317 pub book: String,
318 pub ingredients: Vec<String>,
319}
320
321impl BookRecipe {
322 pub fn new(id: impl Into<String>, book: impl Into<String>) -> Self {
323 Self {
324 id: id.into(),
325 book: book.into(),
326 ingredients: Vec::new(),
327 }
328 }
329
330 pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
331 self.ingredients.push(item_id.into());
332 self
333 }
334
335 pub fn to_json(&self) -> String {
336 let ingr = item_list_json(&self.ingredients);
337 format!(
338 "{{\"type\":\"yog:crafting_book\",\"ingredients\":[{}],\"book\":\"{}\"}}",
339 ingr, self.book
340 )
341 }
342
343 pub fn ns_name(&self) -> (&str, &str) {
344 split_id(&self.id, YOG_NS)
345 }
346}
347
348#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
352#[derive(Debug, Clone)]
353pub struct ItemModifier {
354 pub id: String,
355 pub function: String,
357 pub parameters: std::collections::HashMap<String, String>,
359}
360
361impl ItemModifier {
362 pub fn new(id: impl Into<String>, function: impl Into<String>) -> Self {
363 Self {
364 id: id.into(),
365 function: function.into(),
366 parameters: std::collections::HashMap::new(),
367 }
368 }
369
370 pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
371 self.parameters.insert(key.into(), value.into());
372 self
373 }
374
375 pub fn to_json(&self) -> String {
376 let params: String = self
377 .parameters
378 .iter()
379 .map(|(k, v)| format!("\"{k}\":{v}"))
380 .collect::<Vec<_>>()
381 .join(",");
382 format!(
383 "{{\"function\":\"{}\",{}}}",
384 self.function,
385 if params.is_empty() {
386 String::new()
387 } else {
388 params
389 }
390 )
391 }
392}
393
394#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
399#[derive(Debug, Clone)]
400pub struct StartupGrant {
401 pub id: String,
402 pub items: Vec<String>,
403 pub book: Option<String>,
404 pub command: Option<String>,
405}
406
407impl StartupGrant {
408 pub fn new(id: impl Into<String>) -> Self {
409 Self {
410 id: id.into(),
411 items: Vec::new(),
412 book: None,
413 command: None,
414 }
415 }
416
417 pub fn item(mut self, item_id: impl Into<String>) -> Self {
418 self.items.push(item_id.into());
419 self
420 }
421
422 pub fn book(mut self, book: impl Into<String>) -> Self {
423 self.book = Some(book.into());
424 self
425 }
426
427 pub fn command(mut self, cmd: impl Into<String>) -> Self {
428 self.command = Some(cmd.into());
429 self
430 }
431
432 pub fn to_json(&self) -> String {
433 let items: Vec<String> = self
434 .items
435 .iter()
436 .map(|i| format!("{{\"item\":\"{i}\"}}"))
437 .collect();
438 let mut entries = String::new();
439 if !items.is_empty() {
440 entries.push_str(&format!("[{}]", items.join(",")));
441 }
442 if let Some(book) = &self.book {
443 if !entries.is_empty() {
444 entries.push(',');
445 }
446 let book_entry = format!(
447 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":\"{{yog_book:\\\"{book}\\\"}}\"}}]}}"
448 );
449 entries.push_str(&book_entry);
450 }
451 if let Some(cmd) = &self.command {
452 if !entries.is_empty() {
453 entries.push(',');
454 }
455 entries.push_str(&format!(
456 "{{\"type\":\"minecraft:command\",\"command\":\"{}\"}}",
457 cmd.replace('"', "\\\"")
458 ));
459 }
460 format!(
461 "{{\"type\":\"yog:startup_grant\",\"entries\":{},\"id\":\"{}\"}}",
462 if entries.is_empty() {
463 "[]".to_string()
464 } else {
465 entries
466 },
467 self.id
468 )
469 }
470}
471
472#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
474#[derive(Debug, Clone)]
475pub struct AdvancementReward {
476 pub id: String,
477 pub item: String,
479 pub nbt: Option<String>,
481 pub book: Option<String>,
483}
484
485impl AdvancementReward {
486 pub fn new(id: impl Into<String>) -> Self {
487 Self {
488 id: id.into(),
489 item: "minecraft:written_book".into(),
490 nbt: None,
491 book: None,
492 }
493 }
494
495 pub fn item(mut self, item: impl Into<String>) -> Self {
496 self.item = item.into();
497 self
498 }
499
500 pub fn nbt(mut self, nbt: impl Into<String>) -> Self {
501 self.nbt = Some(nbt.into());
502 self
503 }
504
505 pub fn book(mut self, book: impl Into<String>) -> Self {
506 let book_str: String = book.into();
507 self.book = Some(book_str.clone());
508 self.nbt = Some(format!(
509 "{{yog_book:\"{book_str}\",title:\"Yog Book\",author:\"Yog\"}}"
510 ));
511 self
512 }
513
514 pub fn to_json(&self) -> String {
515 let nbt_part = self
516 .nbt
517 .as_ref()
518 .map(|n| format!("{{\"function\":\"set_nbt\",\"tag\":{n}}}"));
519 let entries = if let Some(nbt_part) = nbt_part {
520 format!(
521 "[{{\"type\":\"item\",\"name\":\"{}\",\"functions\":[{}]}}]",
522 self.item, nbt_part
523 )
524 } else {
525 format!("[{{\"type\":\"item\",\"name\":\"{}\"}}]", self.item)
526 };
527 format!(
528 "{{\"type\":\"advancement_reward\",\"pools\":[{{\"rolls\":1,\"entries\":{entries}}}]}}"
529 )
530 }
531}
532
533#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
537#[derive(Debug, Clone)]
538pub struct BlockDef {
539 pub id: String,
540 pub hardness: f32,
541 pub resistance: f32,
542 pub name: Option<String>,
543 pub shape: Option<[f32; 6]>,
546 pub light_level: u8,
548 pub sound: Option<String>,
552 pub requires_tool: bool,
554 pub no_collision: bool,
556 pub slipperiness: f32,
558 pub connects: bool,
564 pub connect_groups: Vec<String>,
570 pub inventory_id: Option<String>,
573 pub redstone_source: bool,
576}
577
578impl BlockDef {
579 pub fn new(id: impl Into<String>) -> Self {
580 Self {
581 id: id.into(),
582 hardness: 1.5,
583 resistance: 6.0,
584 name: None,
585 shape: None,
586 light_level: 0,
587 sound: None,
588 requires_tool: false,
589 no_collision: false,
590 slipperiness: 0.0,
591 connects: false,
592 connect_groups: Vec::new(),
593 inventory_id: None,
594 redstone_source: false,
595 }
596 }
597
598 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
600 self.hardness = hardness;
601 self.resistance = resistance;
602 self
603 }
604
605 pub fn name(mut self, name: impl Into<String>) -> Self {
607 self.name = Some(name.into());
608 self
609 }
610
611 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
613 self.shape = Some([x1, y1, z1, x2, y2, z2]);
614 self
615 }
616
617 pub fn light_level(mut self, level: u8) -> Self {
619 self.light_level = level.min(15);
620 self
621 }
622
623 pub fn sound(mut self, group: impl Into<String>) -> Self {
626 self.sound = Some(group.into());
627 self
628 }
629
630 pub fn requires_tool(mut self) -> Self {
632 self.requires_tool = true;
633 self
634 }
635
636 pub fn no_collision(mut self) -> Self {
638 self.no_collision = true;
639 self
640 }
641
642 pub fn slipperiness(mut self, value: f32) -> Self {
644 self.slipperiness = value;
645 self
646 }
647
648 pub fn connects_to_neighbors(mut self) -> Self {
654 self.connects = true;
655 self
656 }
657
658 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
665 self.connect_groups = groups
666 .iter()
667 .map(std::string::ToString::to_string)
668 .collect();
669 self
670 }
671
672 pub fn redstone_source(mut self) -> Self {
678 self.redstone_source = true;
679 self
680 }
681
682 pub fn inventory(mut self, id: impl Into<String>) -> Self {
683 self.inventory_id = Some(id.into());
684 self
685 }
686}
687
688#[cfg(test)]
689mod recipe_tests {
690 use super::*;
691
692 #[test]
693 fn tags_are_tag_ingredients() {
694 let shaped = ShapedRecipe::new("m:a", "m:b", 1).row("D").key('D', "#c:dusts").to_json();
695 assert!(shaped.contains(r#""D":{"tag":"c:dusts"}"#), "{shaped}");
696 let shapeless = ShapelessRecipe::new("m:a", "m:b", 1).ingredient("minecraft:paper").ingredient("#minecraft:logs").to_json();
697 assert!(shapeless.contains(r#"[{"item":"minecraft:paper"},{"tag":"minecraft:logs"}]"#), "{shapeless}");
698 let smelt = FurnaceRecipe::new("m:a", "#c:ores", "m:b", 1).to_json();
699 assert!(smelt.contains(r#""ingredient":{"tag":"c:ores"}"#), "{smelt}");
700 }
701}