1#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19pub mod effects;
20pub mod loot;
21
22pub const REGISTRATIONS_FILE: &str = "registrations.txt";
28
29pub fn registration_changes(registered: &str, new: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
32 fn read(text: &str) -> std::collections::BTreeMap<String, String> {
33 text.lines()
34 .filter_map(|l| {
35 let (key, hash) = l.trim().rsplit_once(' ')?;
36 Some((key.to_string(), hash.to_string()))
37 })
38 .collect()
39 }
40 let (old, new) = (read(registered), read(new));
41 let added = new.keys().filter(|k| !old.contains_key(*k)).cloned().collect();
42 let removed = old.keys().filter(|k| !new.contains_key(*k)).cloned().collect();
43 let changed = new.iter().filter(|(k, h)| old.get(*k).is_some_and(|o| o != *h)).map(|(k, _)| k.clone()).collect();
44 (added, removed, changed)
45}
46pub use effects::{AttributeDef, Brew, EffectCategory, EffectDef, EffectModifier, ModifierOperation, PotionDef, PotionEffect};
47pub use loot::{Loot, LootAdd, LootModifier, LootScale, LootStack};
48
49#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
53#[derive(yog_data::Schema, Debug, Clone)]
54pub struct FoodDef {
55 pub nutrition: u32,
57 pub saturation: f32,
59 pub can_always_eat: bool,
61}
62
63impl FoodDef {
64 pub fn new(nutrition: u32, saturation: f32) -> Self {
65 Self {
66 nutrition,
67 saturation,
68 can_always_eat: false,
69 }
70 }
71
72 pub fn can_always_eat(mut self) -> Self {
73 self.can_always_eat = true;
74 self
75 }
76}
77
78#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
80#[derive(yog_data::Schema, Debug, Clone)]
81pub struct ItemDef {
82 pub id: String,
83 pub max_stack: u8,
84 pub name: Option<String>,
85 pub tooltip: Option<String>,
86 pub max_damage: u32,
88 pub fire_resistant: bool,
90 pub fuel_ticks: u32,
92 pub food: Option<FoodDef>,
94 pub tool: Option<ToolItem>,
96 pub armor: Option<ArmorPiece>,
98 pub wearable: Option<String>,
100}
101
102impl ItemDef {
103 pub fn new(id: impl Into<String>) -> Self {
104 Self {
105 id: id.into(),
106 max_stack: 64,
107 name: None,
108 tooltip: None,
109 max_damage: 0,
110 fire_resistant: false,
111 fuel_ticks: 0,
112 food: None,
113 tool: None,
114 armor: None,
115 wearable: None,
116 }
117 }
118
119 pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
123 self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
124 self
125 }
126
127 pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
130 self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
131 self
132 }
133
134 pub fn wearable(mut self, slot: impl Into<String>) -> Self {
136 self.wearable = Some(slot.into());
137 self
138 }
139
140 pub fn max_stack(mut self, n: u8) -> Self {
142 self.max_stack = n;
143 self
144 }
145
146 pub fn name(mut self, name: impl Into<String>) -> Self {
148 self.name = Some(name.into());
149 self
150 }
151
152 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
154 self.tooltip = Some(tooltip.into());
155 self
156 }
157
158 pub fn max_damage(mut self, durability: u32) -> Self {
160 self.max_damage = durability;
161 self
162 }
163
164 pub fn fire_resistant(mut self) -> Self {
166 self.fire_resistant = true;
167 self
168 }
169
170 pub fn fuel(mut self, ticks: u32) -> Self {
172 self.fuel_ticks = ticks;
173 self
174 }
175
176 pub fn food(mut self, food: FoodDef) -> Self {
178 self.food = Some(food);
179 self
180 }
181}
182
183const VANILLA_NS: &str = "minecraft";
187
188fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
190 id.split_once(':').unwrap_or((default_ns, id))
191}
192
193
194fn json_str(s: &str) -> String {
196 let mut out = String::with_capacity(s.len() + 2);
197 out.push('"');
198 for c in s.chars() {
199 match c {
200 '"' => out.push_str("\\\""),
201 '\\' => out.push_str("\\\\"),
202 '\n' => out.push_str("\\n"),
203 '\r' => out.push_str("\\r"),
204 '\t' => out.push_str("\\t"),
205 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
206 c => out.push(c),
207 }
208 }
209 out.push('"');
210 out
211}
212
213
214#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum Cooking {
218 Smelting,
219 Blasting,
220 Smoking,
221 Campfire,
222}
223
224impl Cooking {
225 pub fn word(self) -> &'static str {
227 match self {
228 Cooking::Smelting => "smelting",
229 Cooking::Blasting => "blasting",
230 Cooking::Smoking => "smoking",
231 Cooking::Campfire => "campfire_cooking",
232 }
233 }
234}
235
236#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
238#[derive(Debug, Clone, PartialEq)]
239pub enum RecipeKind {
240 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
242 Shapeless { ingredients: Vec<Vec<String>> },
244 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
245 Stonecutting { ingredient: Vec<String> },
246 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
248}
249
250#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
264#[derive(Debug, Clone, PartialEq)]
265pub struct Recipe {
266 pub id: String,
267 pub kind: RecipeKind,
268 pub result: String,
269 pub count: u32,
270 pub group: Option<String>,
272 pub category: Option<String>,
274}
275
276impl Recipe {
277 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
278 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
279 }
280
281 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
282 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
283 }
284
285 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
286 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
287 }
288
289 pub fn cooking(
292 id: impl Into<String>,
293 method: Cooking,
294 ingredient: impl Into<String>,
295 result: impl Into<String>,
296 count: u32,
297 ) -> Self {
298 let cook_time = match method {
299 Cooking::Smelting => 200,
300 Cooking::Blasting | Cooking::Smoking => 100,
301 Cooking::Campfire => 600,
302 };
303 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
304 Self::of(id, kind, result, count)
305 }
306
307 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
308 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
309 }
310
311 pub fn smithing(
312 id: impl Into<String>,
313 template: impl Into<String>,
314 base: impl Into<String>,
315 addition: impl Into<String>,
316 result: impl Into<String>,
317 ) -> Self {
318 let kind = RecipeKind::Smithing {
319 template: vec![template.into()],
320 base: vec![base.into()],
321 addition: vec![addition.into()],
322 };
323 Self::of(id, kind, result, 1)
324 }
325
326 pub fn row(mut self, pattern: impl Into<String>) -> Self {
328 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
329 p.push(pattern.into());
330 }
331 self
332 }
333
334 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
336 self.key_any(symbol, [item.into()])
337 }
338
339 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
341 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
342 key.push((symbol, items.into_iter().map(Into::into).collect()));
343 }
344 self
345 }
346
347 pub fn ingredient(self, item: impl Into<String>) -> Self {
349 self.ingredient_any([item.into()])
350 }
351
352 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
354 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
355 ingredients.push(items.into_iter().map(Into::into).collect());
356 }
357 self
358 }
359
360 pub fn experience(mut self, xp: f32) -> Self {
362 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
363 *experience = xp;
364 }
365 self
366 }
367
368 pub fn cook_time(mut self, ticks: u32) -> Self {
370 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
371 *cook_time = ticks;
372 }
373 self
374 }
375
376 pub fn group(mut self, group: impl Into<String>) -> Self {
377 self.group = Some(group.into());
378 self
379 }
380
381 pub fn category(mut self, category: impl Into<String>) -> Self {
382 self.category = Some(category.into());
383 self
384 }
385
386 pub fn to_data(&self) -> yog_data::Value {
389 use yog_data::{Map, Value};
390 let ing = |items: &[String]| match items {
391 [one] => Value::from(one.as_str()),
392 many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
393 };
394 let mut m = Map::with_capacity(8);
395 let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
396 match &self.kind {
397 RecipeKind::Shaped { pattern, key } => {
398 ty(&mut m, "shaped");
399 m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
400 let mut keys = Map::with_capacity(key.len());
401 for (c, items) in key {
402 keys.insert(c.to_string(), ing(items));
403 }
404 m.insert("key", Value::Map(keys));
405 }
406 RecipeKind::Shapeless { ingredients } => {
407 ty(&mut m, "shapeless");
408 m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
409 }
410 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
411 ty(&mut m, method.word());
412 m.insert("ingredient", ing(ingredient));
413 m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
415 m.insert("cook_time", Value::Int(*cook_time as i64));
416 }
417 RecipeKind::Stonecutting { ingredient } => {
418 ty(&mut m, "stonecutting");
419 m.insert("ingredient", ing(ingredient));
420 }
421 RecipeKind::Smithing { template, base, addition } => {
422 ty(&mut m, "smithing");
423 m.insert("template", ing(template));
424 m.insert("base", ing(base));
425 m.insert("addition", ing(addition));
426 }
427 }
428 m.insert("result", Value::from(self.result.as_str()));
429 if self.count != 1 {
430 m.insert("count", Value::Int(self.count as i64));
431 }
432 if let Some(g) = &self.group {
433 m.insert("group", Value::from(g.as_str()));
434 }
435 if let Some(c) = &self.category {
436 m.insert("category", Value::from(c.as_str()));
437 }
438 Value::Map(m)
439 }
440
441 pub fn ns_name(&self) -> (&str, &str) {
443 split_id(&self.id, VANILLA_NS)
444 }
445}
446
447#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
460#[derive(yog_data::Schema, Debug, Clone)]
461pub struct StartupGrant {
462 pub id: String,
463 pub items: Vec<String>,
464 pub book: Option<String>,
465 pub command: Option<String>,
466}
467
468impl StartupGrant {
469 pub fn new(id: impl Into<String>) -> Self {
470 Self {
471 id: id.into(),
472 items: Vec::new(),
473 book: None,
474 command: None,
475 }
476 }
477
478 pub fn item(mut self, item_id: impl Into<String>) -> Self {
479 self.items.push(item_id.into());
480 self
481 }
482
483 pub fn book(mut self, book: impl Into<String>) -> Self {
484 self.book = Some(book.into());
485 self
486 }
487
488 pub fn command(mut self, cmd: impl Into<String>) -> Self {
489 self.command = Some(cmd.into());
490 self
491 }
492
493 pub fn to_json(&self) -> String {
494 let mut entries: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":{}}}", json_str(i))).collect();
495 if let Some(book) = &self.book {
496 let tag = format!("{{yog_book:\"{}\"}}", book.replace('\\', "\\\\").replace('"', "\\\""));
498 entries.push(format!(
499 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":{}}}]}}",
500 json_str(&tag)
501 ));
502 }
503 if let Some(cmd) = &self.command {
504 entries.push(format!("{{\"type\":\"minecraft:command\",\"command\":{}}}", json_str(cmd)));
505 }
506 format!("{{\"type\":\"yog:startup_grant\",\"entries\":[{}],\"id\":{}}}", entries.join(","), json_str(&self.id))
507 }
508}
509
510
511
512#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
516#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
517pub struct ToolItem {
518 pub kind: String,
521 pub material: String,
523 pub attack_damage: f32,
525 pub attack_speed: f32,
527}
528
529#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
531#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
532pub struct ArmorPiece {
533 pub slot: String,
535 pub material: String,
537}
538
539#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
544#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
545pub struct ToolMaterial {
546 pub id: String,
547 pub durability: u32,
549 pub speed: f32,
551 pub attack_damage: f32,
553 pub enchantability: u32,
554 pub level: String,
556 pub repair: String,
558 #[data(default)]
560 pub level_index: u32,
561 #[data(default)]
564 pub incorrect: String,
565}
566
567#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
569#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
570pub struct ArmorDefense {
571 pub head: u32,
572 pub chest: u32,
573 pub legs: u32,
574 pub feet: u32,
575}
576
577#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
581#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
582pub struct ArmorMaterial {
583 pub id: String,
584 pub durability: u32,
587 pub defense: ArmorDefense,
588 pub enchantability: u32,
589 pub equip_sound: String,
591 #[data(default)]
592 pub toughness: f32,
593 #[data(default)]
594 pub knockback_resistance: f32,
595 pub repair: String,
597 pub texture: String,
600}
601
602static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
603 yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
604 .unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
605 .value
606});
607
608static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
609 yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
610 .unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
611 .value
612});
613
614fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
616 let name = name.strip_prefix("minecraft:").unwrap_or(name);
617 list.iter().find(|x| id(x).strip_prefix("minecraft:") == Some(name))
618}
619
620impl ToolMaterial {
621 pub fn built_ins() -> &'static [ToolMaterial] {
623 &BUILT_IN_TOOL_MATERIALS
624 }
625
626 pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
628 built_in(Self::built_ins(), |m| &m.id, name)
629 }
630
631 pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
634 let mut m = Self::built_in(like)?.clone();
635 m.id = id.into();
636 Some(m)
637 }
638}
639
640impl ArmorMaterial {
641 pub fn built_ins() -> &'static [ArmorMaterial] {
643 &BUILT_IN_ARMOR_MATERIALS
644 }
645
646 pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
648 built_in(Self::built_ins(), |m| &m.id, name)
649 }
650}
651
652#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
659#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
660pub struct BiomeFeature {
661 pub feature: String,
663 pub biomes: String,
665 pub step: String,
667}
668
669#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
677#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
678pub struct SoundGroup {
679 pub id: String,
680 #[data(rename = "break")]
682 pub break_sound: String,
683 pub step: String,
684 pub place: String,
685 pub hit: String,
686 pub fall: String,
687 #[data(default = "one")]
688 #[data(range = "0..=10")]
689 pub volume: f32,
690 #[data(default = "one")]
691 #[data(range = "0..=10")]
692 pub pitch: f32,
693}
694
695fn one() -> f32 {
696 1.0
697}
698
699static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
700 yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
701 .unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
702 .value
703});
704
705impl SoundGroup {
706 pub fn new(id: impl Into<String>) -> Self {
709 let id = id.into();
710 let ev = |s: &str| format!("{id}.{s}");
711 Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
712 }
713
714 pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
716 [self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
717 self
718 }
719
720 pub fn volume(mut self, volume: f32) -> Self {
721 self.volume = volume;
722 self
723 }
724
725 pub fn pitch(mut self, pitch: f32) -> Self {
726 self.pitch = pitch;
727 self
728 }
729
730 pub fn built_ins() -> &'static [SoundGroup] {
733 &BUILT_IN_SOUND_GROUPS
734 }
735
736 pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
738 let name = name.strip_prefix("minecraft:").unwrap_or(name);
739 Self::built_ins().iter().find(|g| g.id.strip_prefix("minecraft:") == Some(name))
740 }
741}
742
743#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
747#[derive(yog_data::Schema, Debug, Clone)]
748pub struct BlockDef {
749 pub id: String,
750 pub hardness: f32,
751 pub resistance: f32,
752 pub name: Option<String>,
753 pub shape: Option<[f32; 6]>,
756 pub light_level: u8,
758 pub sound: Option<String>,
762 pub requires_tool: bool,
764 pub no_collision: bool,
766 pub slipperiness: f32,
768 pub connects: bool,
774 pub connect_groups: Vec<String>,
780 pub inventory_id: Option<String>,
783 pub redstone_source: bool,
786 pub facing: String,
789 pub placement: String,
791 pub attach: bool,
794 pub waterlogged: bool,
796 pub properties: Vec<BlockProperty>,
798 pub powered: Option<String>,
800 pub comparator: Option<String>,
802 pub accepts_items: bool,
804}
805
806#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
809#[derive(yog_data::Schema, Debug, Clone, PartialEq, Eq)]
810pub struct BlockProperty {
811 pub name: String,
812 pub values: Vec<String>,
813}
814
815impl BlockDef {
816 pub fn new(id: impl Into<String>) -> Self {
817 Self {
818 id: id.into(),
819 hardness: 1.5,
820 resistance: 6.0,
821 name: None,
822 shape: None,
823 light_level: 0,
824 sound: None,
825 requires_tool: false,
826 no_collision: false,
827 slipperiness: 0.0,
828 connects: false,
829 connect_groups: Vec::new(),
830 inventory_id: None,
831 redstone_source: false,
832 facing: String::new(),
833 placement: String::new(),
834 attach: false,
835 waterlogged: false,
836 properties: Vec::new(),
837 powered: None,
838 comparator: None,
839 accepts_items: false,
840 }
841 }
842
843 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
845 self.hardness = hardness;
846 self.resistance = resistance;
847 self
848 }
849
850 pub fn name(mut self, name: impl Into<String>) -> Self {
852 self.name = Some(name.into());
853 self
854 }
855
856 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
858 self.shape = Some([x1, y1, z1, x2, y2, z2]);
859 self
860 }
861
862 pub fn light_level(mut self, level: u8) -> Self {
864 self.light_level = level.min(15);
865 self
866 }
867
868 pub fn sound(mut self, group: impl Into<String>) -> Self {
871 self.sound = Some(group.into());
872 self
873 }
874
875 pub fn requires_tool(mut self) -> Self {
877 self.requires_tool = true;
878 self
879 }
880
881 pub fn no_collision(mut self) -> Self {
883 self.no_collision = true;
884 self
885 }
886
887 pub fn slipperiness(mut self, value: f32) -> Self {
889 self.slipperiness = value;
890 self
891 }
892
893 pub fn connects_to_neighbors(mut self) -> Self {
899 self.connects = true;
900 self
901 }
902
903 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
910 self.connect_groups = groups
911 .iter()
912 .map(std::string::ToString::to_string)
913 .collect();
914 self
915 }
916
917 pub fn redstone_source(mut self) -> Self {
923 self.redstone_source = true;
924 self
925 }
926
927 pub fn facing(mut self, ways: &str, placement: &str) -> Self {
930 self.facing = ways.to_string();
931 self.placement = placement.to_string();
932 self
933 }
934
935 pub fn attach(mut self) -> Self {
937 self.attach = true;
938 self
939 }
940
941 pub fn waterlogged(mut self) -> Self {
943 self.waterlogged = true;
944 self
945 }
946
947 pub fn property(mut self, name: &str, values: &[&str]) -> Self {
949 self.properties.push(BlockProperty { name: name.into(), values: values.iter().map(|v| v.to_string()).collect() });
950 self
951 }
952
953 pub fn powered(mut self, name: &str) -> Self {
955 self.powered = Some(name.into());
956 self
957 }
958
959 pub fn comparator(mut self, name: &str) -> Self {
961 self.comparator = Some(name.into());
962 self
963 }
964
965 pub fn accepts_items(mut self) -> Self {
967 self.accepts_items = true;
968 self
969 }
970
971 pub fn inventory(mut self, id: impl Into<String>) -> Self {
972 self.inventory_id = Some(id.into());
973 self
974 }
975}
976
977#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq, Eq, Default)]
981pub enum EntityKind {
982 #[default]
985 Object,
986 Living,
988 Mob,
990}
991
992#[derive(yog_data::Schema, Debug, Clone)]
996pub struct EntityDef {
997 pub id: String,
998 pub name: Option<String>,
1000 #[data(default)]
1001 pub kind: EntityKind,
1002 #[data(default = "misc")]
1005 pub category: String,
1006 pub width: f32,
1008 pub height: f32,
1009 pub eye_height: Option<f32>,
1011 #[data(default)]
1013 pub fire_immune: bool,
1014 #[data(default)]
1016 pub no_gravity: bool,
1017 pub pushable: Option<bool>,
1019 #[data(default)]
1021 pub solid: bool,
1022 #[data(default = "eight")]
1024 pub tracking_range: u32,
1025 #[data(default = "three")]
1027 pub update_interval: u32,
1028 #[data(default)]
1032 pub attributes: yog_data::Ordered<f64>,
1033 pub model: Option<String>,
1035 pub animations: Option<String>,
1037 #[data(default)]
1040 pub clips: yog_data::Ordered<String>,
1041 pub spawn_egg: Option<[u32; 2]>,
1043 #[data(default)]
1047 pub goals: Vec<String>,
1048 #[data(default)]
1051 pub ticks: bool,
1052}
1053
1054fn eight() -> u32 {
1055 8
1056}
1057
1058fn three() -> u32 {
1059 3
1060}
1061
1062fn misc() -> String {
1063 "misc".into()
1064}
1065
1066impl EntityDef {
1067 pub fn new(id: impl Into<String>, kind: EntityKind, width: f32, height: f32) -> Self {
1068 Self {
1069 id: id.into(),
1070 name: None,
1071 kind,
1072 category: misc(),
1073 width,
1074 height,
1075 eye_height: None,
1076 fire_immune: false,
1077 no_gravity: false,
1078 pushable: None,
1079 solid: false,
1080 tracking_range: eight(),
1081 update_interval: three(),
1082 attributes: yog_data::Ordered::default(),
1083 model: None,
1084 animations: None,
1085 clips: yog_data::Ordered::default(),
1086 spawn_egg: None,
1087 goals: Vec::new(),
1088 ticks: false,
1089 }
1090 }
1091}
1092
1093#[cfg(test)]
1094mod registration_tests {
1095 #[test]
1096 fn a_hot_reload_sees_what_registration_changed() {
1097 let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
1098 let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
1099 let (added, removed, changed) = super::registration_changes(old, new);
1100 assert_eq!(added, ["item mymod:d"]);
1101 assert_eq!(removed, ["item mymod:c"]);
1102 assert_eq!(changed, ["block mymod:b"]);
1103 assert_eq!(super::registration_changes(old, old), (vec![], vec![], vec![]));
1104 }
1105}
1106
1107#[cfg(test)]
1108mod material_tests {
1109 use super::*;
1110
1111 #[test]
1112 fn the_games_materials_are_built_in() {
1113 assert_eq!(ToolMaterial::built_ins().len(), 6);
1114 let iron = ToolMaterial::built_in("iron").unwrap();
1115 assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
1116 assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
1117 assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
1118 let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
1119 assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
1120 }
1121}
1122
1123#[cfg(test)]
1124mod sound_tests {
1125 use super::*;
1126
1127 #[test]
1128 fn the_games_sound_groups_are_built_in() {
1129 assert!(SoundGroup::built_ins().len() > 100);
1130 let stone = SoundGroup::built_in("stone").unwrap();
1131 assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
1132 assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
1133 assert!(SoundGroup::built_in("mymod:crystal").is_none());
1134 }
1135
1136 #[test]
1137 fn a_mods_group_crosses_as_yog_data() {
1138 use yog_data::Schema;
1139 let g = SoundGroup::new("mymod:crystal").pitch(1.2);
1140 assert_eq!(g.fall, "mymod:crystal.fall");
1141 let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
1142 assert_eq!(back, g);
1143 assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
1144 }
1145}
1146
1147#[cfg(test)]
1148impl Recipe {
1149 fn to_json(&self) -> String {
1150 yog_data::json::to_json(&self.to_data()).to_string()
1151 }
1152}
1153
1154#[cfg(test)]
1155mod recipe_tests {
1156 use super::*;
1157
1158 #[test]
1159 fn writes_the_yog_format() {
1160 let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
1161 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
1162 assert_eq!(
1163 js(&shaped.to_json()),
1164 js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
1165 );
1166 assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
1167 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
1168 assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
1169 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
1170 assert_eq!(js(&smith.to_json())["template"], "m:t");
1171 }
1172}
1173
1174#[cfg(test)]
1175mod json_tests {
1176 use super::*;
1177
1178 #[test]
1180 fn json_survives_any_string() {
1181 let odd = "m:a\"b\\c\nd\te\u{1}";
1182 let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
1183 for r in [
1184 Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
1185 Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
1186 Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
1187 Recipe::stonecutting(odd, odd, odd, 2),
1188 Recipe::smithing(odd, odd, odd, odd, odd),
1189 ] {
1190 let v = parse(r.to_json());
1191 assert_eq!(v["result"], odd, "the string comes back as it was");
1192 }
1193 let g = StartupGrant::new(odd).item(odd).book("mymod:\"guide\"").command("say \"hi\" \\o/");
1194 let v = parse(g.to_json());
1195 assert_eq!(v["id"], odd);
1196 assert_eq!(v["entries"][2]["command"], "say \"hi\" \\o/");
1197 }
1198
1199 #[test]
1200 fn recipes_and_grants_name_their_parts() {
1201 let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
1202 let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
1203 assert_eq!(v["type"], "campfire_cooking");
1204 assert_eq!(v["cook_time"], 600, "the method's default time");
1205 assert_eq!(r.ns_name(), ("mymod", "a"));
1206 assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
1207 let g = StartupGrant::new("mymod:starter");
1208 assert!(g.to_json().contains(r#""entries":[]"#));
1209 }
1210}