1#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19pub mod effects;
20pub mod recipe_kinds;
21pub mod loot;
22pub mod atlases;
23pub mod item_looks;
24pub mod tool_levels;
25
26pub const REGISTRATIONS_FILE: &str = "registrations.txt";
32
33pub fn registration_changes(registered: &str, new: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
36 fn read(text: &str) -> std::collections::BTreeMap<String, String> {
37 text.lines()
38 .filter_map(|l| {
39 let (key, hash) = l.trim().rsplit_once(' ')?;
40 Some((key.to_string(), hash.to_string()))
41 })
42 .collect()
43 }
44 let (old, new) = (read(registered), read(new));
45 let added = new.keys().filter(|k| !old.contains_key(*k)).cloned().collect();
46 let removed = old.keys().filter(|k| !new.contains_key(*k)).cloned().collect();
47 let changed = new.iter().filter(|(k, h)| old.get(*k).is_some_and(|o| o != *h)).map(|(k, _)| k.clone()).collect();
48 (added, removed, changed)
49}
50pub use effects::{AttributeDef, Brew, EffectCategory, EffectDef, EffectModifier, ModifierOperation, PotionDef, PotionEffect};
51pub use loot::{Loot, LootAdd, LootModifier, LootScale, LootStack};
52pub use atlases::Atlas;
53pub use item_looks::{ItemLooks, ItemTint, LookModels};
54pub use tool_levels::{LevelTag, ToolLadder, ToolLevel};
55
56#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60#[derive(yog_data::Schema, Debug, Clone)]
61pub struct FoodDef {
62 pub nutrition: u32,
64 pub saturation: f32,
66 pub can_always_eat: bool,
68}
69
70impl FoodDef {
71 pub fn new(nutrition: u32, saturation: f32) -> Self {
72 Self {
73 nutrition,
74 saturation,
75 can_always_eat: false,
76 }
77 }
78
79 pub fn can_always_eat(mut self) -> Self {
80 self.can_always_eat = true;
81 self
82 }
83}
84
85#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
87#[derive(yog_data::Schema, Debug, Clone)]
88pub struct ItemDef {
89 pub id: String,
90 pub max_stack: u8,
91 pub name: Option<String>,
92 pub tooltip: Option<String>,
93 pub max_damage: u32,
95 pub fire_resistant: bool,
97 pub fuel_ticks: u32,
99 pub food: Option<FoodDef>,
101 pub tool: Option<ToolItem>,
103 pub armor: Option<ArmorPiece>,
105 pub wearable: Option<String>,
107 pub hangs: Option<Hangs>,
109}
110
111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
114pub struct Hangs {
115 pub entity: String,
117 pub size: Option<[u32; 2]>,
119}
120
121impl ItemDef {
122 pub fn new(id: impl Into<String>) -> Self {
123 Self {
124 id: id.into(),
125 max_stack: 64,
126 name: None,
127 tooltip: None,
128 max_damage: 0,
129 fire_resistant: false,
130 fuel_ticks: 0,
131 food: None,
132 tool: None,
133 armor: None,
134 wearable: None,
135 hangs: None,
136 }
137 }
138
139 pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
143 self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
144 self
145 }
146
147 pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
150 self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
151 self
152 }
153
154 pub fn wearable(mut self, slot: impl Into<String>) -> Self {
156 self.wearable = Some(slot.into());
157 self
158 }
159
160 pub fn hangs(mut self, entity: impl Into<String>, size: Option<[u32; 2]>) -> Self {
164 self.hangs = Some(Hangs { entity: entity.into(), size });
165 self
166 }
167
168 pub fn max_stack(mut self, n: u8) -> Self {
170 self.max_stack = n;
171 self
172 }
173
174 pub fn name(mut self, name: impl Into<String>) -> Self {
176 self.name = Some(name.into());
177 self
178 }
179
180 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
182 self.tooltip = Some(tooltip.into());
183 self
184 }
185
186 pub fn max_damage(mut self, durability: u32) -> Self {
188 self.max_damage = durability;
189 self
190 }
191
192 pub fn fire_resistant(mut self) -> Self {
194 self.fire_resistant = true;
195 self
196 }
197
198 pub fn fuel(mut self, ticks: u32) -> Self {
200 self.fuel_ticks = ticks;
201 self
202 }
203
204 pub fn food(mut self, food: FoodDef) -> Self {
206 self.food = Some(food);
207 self
208 }
209}
210
211#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum Cooking {
219 Smelting,
220 Blasting,
221 Smoking,
222 Campfire,
223}
224
225impl Cooking {
226 pub const ALL: [Cooking; 4] = [Cooking::Smelting, Cooking::Blasting, Cooking::Smoking, Cooking::Campfire];
228
229 pub fn word(self) -> &'static str {
231 recipe_kinds::ids::COOKING[self as usize]
232 }
233
234 pub fn of_kind(id: &str) -> Option<Cooking> {
236 Cooking::ALL.into_iter().find(|c| c.word() == id)
237 }
238}
239
240#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
242#[derive(Debug, Clone, PartialEq)]
243pub enum RecipeKind {
244 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
246 Shapeless { ingredients: Vec<Vec<String>> },
248 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
249 Stonecutting { ingredient: Vec<String> },
250 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
252}
253
254#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
268#[derive(Debug, Clone, PartialEq)]
269pub struct Recipe {
270 pub id: String,
271 pub kind: RecipeKind,
272 pub result: String,
273 pub count: u32,
274 pub group: Option<String>,
276 pub category: Option<String>,
278}
279
280impl Recipe {
281 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
282 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
283 }
284
285 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
286 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
287 }
288
289 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
290 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
291 }
292
293 pub fn cooking(
296 id: impl Into<String>,
297 method: Cooking,
298 ingredient: impl Into<String>,
299 result: impl Into<String>,
300 count: u32,
301 ) -> Self {
302 let cook_time = match method {
303 Cooking::Smelting => 200,
304 Cooking::Blasting | Cooking::Smoking => 100,
305 Cooking::Campfire => 600,
306 };
307 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
308 Self::of(id, kind, result, count)
309 }
310
311 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
312 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
313 }
314
315 pub fn smithing(
316 id: impl Into<String>,
317 template: impl Into<String>,
318 base: impl Into<String>,
319 addition: impl Into<String>,
320 result: impl Into<String>,
321 ) -> Self {
322 let kind = RecipeKind::Smithing {
323 template: vec![template.into()],
324 base: vec![base.into()],
325 addition: vec![addition.into()],
326 };
327 Self::of(id, kind, result, 1)
328 }
329
330 pub fn row(mut self, pattern: impl Into<String>) -> Self {
332 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
333 p.push(pattern.into());
334 }
335 self
336 }
337
338 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
340 self.key_any(symbol, [item.into()])
341 }
342
343 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
345 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
346 key.push((symbol, items.into_iter().map(Into::into).collect()));
347 }
348 self
349 }
350
351 pub fn ingredient(self, item: impl Into<String>) -> Self {
353 self.ingredient_any([item.into()])
354 }
355
356 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
358 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
359 ingredients.push(items.into_iter().map(Into::into).collect());
360 }
361 self
362 }
363
364 pub fn experience(mut self, xp: f32) -> Self {
366 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
367 *experience = xp;
368 }
369 self
370 }
371
372 pub fn cook_time(mut self, ticks: u32) -> Self {
374 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
375 *cook_time = ticks;
376 }
377 self
378 }
379
380 pub fn group(mut self, group: impl Into<String>) -> Self {
381 self.group = Some(group.into());
382 self
383 }
384
385 pub fn category(mut self, category: impl Into<String>) -> Self {
386 self.category = Some(category.into());
387 self
388 }
389
390 pub fn to_data(&self) -> yog_data::Value {
393 use yog_data::{Map, Value};
394 let ing = |items: &[String]| match items {
395 [one] => Value::from(one.as_str()),
396 many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
397 };
398 let mut m = Map::with_capacity(8);
399 let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
400 match &self.kind {
401 RecipeKind::Shaped { pattern, key } => {
402 ty(&mut m, "shaped");
403 m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
404 let mut keys = Map::with_capacity(key.len());
405 for (c, items) in key {
406 keys.insert(c.to_string(), ing(items));
407 }
408 m.insert("key", Value::Map(keys));
409 }
410 RecipeKind::Shapeless { ingredients } => {
411 ty(&mut m, "shapeless");
412 m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
413 }
414 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
415 ty(&mut m, method.word());
416 m.insert("ingredient", ing(ingredient));
417 m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
419 m.insert("cook_time", Value::Int(*cook_time as i64));
420 }
421 RecipeKind::Stonecutting { ingredient } => {
422 ty(&mut m, "stonecutting");
423 m.insert("ingredient", ing(ingredient));
424 }
425 RecipeKind::Smithing { template, base, addition } => {
426 ty(&mut m, "smithing");
427 m.insert("template", ing(template));
428 m.insert("base", ing(base));
429 m.insert("addition", ing(addition));
430 }
431 }
432 m.insert("result", Value::from(self.result.as_str()));
433 if self.count != 1 {
434 m.insert("count", Value::Int(self.count as i64));
435 }
436 if let Some(g) = &self.group {
437 m.insert("group", Value::from(g.as_str()));
438 }
439 if let Some(c) = &self.category {
440 m.insert("category", Value::from(c.as_str()));
441 }
442 Value::Map(m)
443 }
444
445 pub fn ns_name(&self) -> (&str, &str) {
447 yog_ids::split(&self.id)
448 }
449}
450
451#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
464#[derive(yog_data::Schema, Debug, Clone)]
465pub struct StartupGrant {
466 pub id: String,
467 pub items: Vec<String>,
468 pub book: Option<String>,
469 pub command: Option<String>,
470}
471
472impl StartupGrant {
473 pub fn new(id: impl Into<String>) -> Self {
474 Self {
475 id: id.into(),
476 items: Vec::new(),
477 book: None,
478 command: None,
479 }
480 }
481
482 pub fn item(mut self, item_id: impl Into<String>) -> Self {
483 self.items.push(item_id.into());
484 self
485 }
486
487 pub fn book(mut self, book: impl Into<String>) -> Self {
488 self.book = Some(book.into());
489 self
490 }
491
492 pub fn command(mut self, cmd: impl Into<String>) -> Self {
493 self.command = Some(cmd.into());
494 self
495 }
496}
497
498
499
500#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
504#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
505pub struct ToolItem {
506 pub kind: String,
509 pub material: String,
511 pub attack_damage: f32,
513 pub attack_speed: f32,
515}
516
517#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
519#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
520pub struct ArmorPiece {
521 pub slot: String,
523 pub material: String,
525}
526
527#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
532#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
533pub struct ToolMaterial {
534 pub id: String,
535 pub durability: u32,
537 pub speed: f32,
539 pub attack_damage: f32,
541 pub enchantability: u32,
542 pub level: String,
544 pub repair: String,
546 #[data(default)]
548 pub level_index: u32,
549 #[data(default)]
552 pub incorrect: String,
553}
554
555#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
557#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
558pub struct ArmorDefense {
559 pub head: u32,
560 pub chest: u32,
561 pub legs: u32,
562 pub feet: u32,
563}
564
565#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
569#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
570pub struct ArmorMaterial {
571 pub id: String,
572 pub durability: u32,
575 pub defense: ArmorDefense,
576 pub enchantability: u32,
577 pub equip_sound: String,
579 #[data(default)]
580 pub toughness: f32,
581 #[data(default)]
582 pub knockback_resistance: f32,
583 pub repair: String,
585 pub texture: String,
588}
589
590static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
591 yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
592 .unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
593 .value
594});
595
596static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
597 yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
598 .unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
599 .value
600});
601
602fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
604 let name = yog_ids::qualify(name);
605 list.iter().find(|x| id(x) == name)
606}
607
608impl ToolMaterial {
609 pub fn built_ins() -> &'static [ToolMaterial] {
611 &BUILT_IN_TOOL_MATERIALS
612 }
613
614 pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
616 built_in(Self::built_ins(), |m| &m.id, name)
617 }
618
619 pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
622 let mut m = Self::built_in(like)?.clone();
623 m.id = id.into();
624 Some(m)
625 }
626}
627
628impl ArmorMaterial {
629 pub fn built_ins() -> &'static [ArmorMaterial] {
631 &BUILT_IN_ARMOR_MATERIALS
632 }
633
634 pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
636 built_in(Self::built_ins(), |m| &m.id, name)
637 }
638}
639
640#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
647#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
648pub struct BiomeFeature {
649 pub feature: String,
651 pub biomes: String,
653 pub step: String,
655}
656
657#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
665#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
666pub struct SoundGroup {
667 pub id: String,
668 #[data(rename = "break")]
670 pub break_sound: String,
671 pub step: String,
672 pub place: String,
673 pub hit: String,
674 pub fall: String,
675 #[data(default = "one")]
676 #[data(range = "0..=10")]
677 pub volume: f32,
678 #[data(default = "one")]
679 #[data(range = "0..=10")]
680 pub pitch: f32,
681}
682
683fn one() -> f32 {
684 1.0
685}
686
687static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
688 yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
689 .unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
690 .value
691});
692
693impl SoundGroup {
694 pub fn new(id: impl Into<String>) -> Self {
697 let id = id.into();
698 let ev = |s: &str| format!("{id}.{s}");
699 Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
700 }
701
702 pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
704 [self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
705 self
706 }
707
708 pub fn volume(mut self, volume: f32) -> Self {
709 self.volume = volume;
710 self
711 }
712
713 pub fn pitch(mut self, pitch: f32) -> Self {
714 self.pitch = pitch;
715 self
716 }
717
718 pub fn built_ins() -> &'static [SoundGroup] {
721 &BUILT_IN_SOUND_GROUPS
722 }
723
724 pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
726 built_in(Self::built_ins(), |g| &g.id, name)
727 }
728}
729
730#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
734#[derive(yog_data::Schema, Debug, Clone)]
735pub struct BlockDef {
736 pub id: String,
737 pub hardness: f32,
738 pub resistance: f32,
739 pub name: Option<String>,
740 pub shape: Option<[f32; 6]>,
743 pub light_level: u8,
745 pub sound: Option<String>,
749 pub requires_tool: bool,
751 pub no_collision: bool,
753 pub slipperiness: f32,
755 pub connects: bool,
761 pub connect_groups: Vec<String>,
767 pub inventory_id: Option<String>,
770 pub redstone_source: bool,
773 pub facing: String,
776 pub placement: String,
778 pub attach: bool,
781 pub waterlogged: bool,
783 pub properties: Vec<BlockProperty>,
785 pub powered: Option<String>,
787 pub comparator: Option<String>,
789 pub accepts_items: bool,
791 #[data(default)]
793 pub no_occlusion: bool,
794 #[data(default)]
799 pub kind: String,
800 #[data(default)]
802 pub base: Option<String>,
803 #[data(default)]
806 pub set: Option<String>,
807 #[data(default = "thirty")]
809 pub press_ticks: u32,
810 #[data(default)]
813 pub mobs_only: bool,
814}
815
816fn thirty() -> u32 {
817 30
818}
819
820#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
823#[derive(yog_data::Schema, Debug, Clone, PartialEq, Eq)]
824pub struct BlockProperty {
825 pub name: String,
826 pub values: Vec<String>,
827}
828
829impl BlockDef {
830 pub fn new(id: impl Into<String>) -> Self {
831 Self {
832 id: id.into(),
833 hardness: 1.5,
834 resistance: 6.0,
835 name: None,
836 shape: None,
837 light_level: 0,
838 sound: None,
839 requires_tool: false,
840 no_collision: false,
841 slipperiness: 0.0,
842 connects: false,
843 connect_groups: Vec::new(),
844 inventory_id: None,
845 redstone_source: false,
846 facing: String::new(),
847 placement: String::new(),
848 attach: false,
849 waterlogged: false,
850 properties: Vec::new(),
851 powered: None,
852 comparator: None,
853 accepts_items: false,
854 no_occlusion: false,
855 kind: String::new(),
856 base: None,
857 set: None,
858 press_ticks: thirty(),
859 mobs_only: false,
860 }
861 }
862
863 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
865 self.hardness = hardness;
866 self.resistance = resistance;
867 self
868 }
869
870 pub fn name(mut self, name: impl Into<String>) -> Self {
872 self.name = Some(name.into());
873 self
874 }
875
876 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
878 self.shape = Some([x1, y1, z1, x2, y2, z2]);
879 self
880 }
881
882 pub fn light_level(mut self, level: u8) -> Self {
884 self.light_level = level.min(15);
885 self
886 }
887
888 pub fn sound(mut self, group: impl Into<String>) -> Self {
891 self.sound = Some(group.into());
892 self
893 }
894
895 pub fn requires_tool(mut self) -> Self {
897 self.requires_tool = true;
898 self
899 }
900
901 pub fn no_collision(mut self) -> Self {
903 self.no_collision = true;
904 self
905 }
906
907 pub fn slipperiness(mut self, value: f32) -> Self {
909 self.slipperiness = value;
910 self
911 }
912
913 pub fn connects_to_neighbors(mut self) -> Self {
919 self.connects = true;
920 self
921 }
922
923 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
930 self.connect_groups = groups
931 .iter()
932 .map(std::string::ToString::to_string)
933 .collect();
934 self
935 }
936
937 pub fn redstone_source(mut self) -> Self {
943 self.redstone_source = true;
944 self
945 }
946
947 pub fn facing(mut self, ways: &str, placement: &str) -> Self {
950 self.facing = ways.to_string();
951 self.placement = placement.to_string();
952 self
953 }
954
955 pub fn attach(mut self) -> Self {
957 self.attach = true;
958 self
959 }
960
961 pub fn waterlogged(mut self) -> Self {
963 self.waterlogged = true;
964 self
965 }
966
967 pub fn property(mut self, name: &str, values: &[&str]) -> Self {
969 self.properties.push(BlockProperty { name: name.into(), values: values.iter().map(|v| v.to_string()).collect() });
970 self
971 }
972
973 pub fn powered(mut self, name: &str) -> Self {
975 self.powered = Some(name.into());
976 self
977 }
978
979 pub fn comparator(mut self, name: &str) -> Self {
981 self.comparator = Some(name.into());
982 self
983 }
984
985 pub fn accepts_items(mut self) -> Self {
987 self.accepts_items = true;
988 self
989 }
990
991 pub fn inventory(mut self, id: impl Into<String>) -> Self {
992 self.inventory_id = Some(id.into());
993 self
994 }
995
996 pub fn no_occlusion(mut self) -> Self {
998 self.no_occlusion = true;
999 self
1000 }
1001
1002 pub fn kind(mut self, kind: &str) -> Self {
1004 self.kind = kind.into();
1005 self
1006 }
1007
1008 pub fn base(mut self, id: impl Into<String>) -> Self {
1010 self.base = Some(id.into());
1011 self
1012 }
1013
1014 pub fn set(mut self, name: &str) -> Self {
1016 self.set = Some(name.into());
1017 self
1018 }
1019
1020 pub fn press_ticks(mut self, ticks: u32) -> Self {
1022 self.press_ticks = ticks;
1023 self
1024 }
1025
1026 pub fn mobs_only(mut self) -> Self {
1028 self.mobs_only = true;
1029 self
1030 }
1031}
1032
1033#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq, Eq, Default)]
1037pub enum EntityKind {
1038 #[default]
1041 Object,
1042 Living,
1044 Mob,
1046 Hanging,
1051}
1052
1053#[derive(yog_data::Schema, Debug, Clone)]
1057pub struct EntityDef {
1058 pub id: String,
1059 pub name: Option<String>,
1061 #[data(default)]
1062 pub kind: EntityKind,
1063 #[data(default = "misc")]
1066 pub category: String,
1067 pub width: f32,
1069 pub height: f32,
1070 pub eye_height: Option<f32>,
1072 #[data(default)]
1074 pub fire_immune: bool,
1075 #[data(default)]
1077 pub no_gravity: bool,
1078 pub pushable: Option<bool>,
1080 #[data(default)]
1082 pub solid: bool,
1083 #[data(default = "eight")]
1085 pub tracking_range: u32,
1086 #[data(default = "three")]
1088 pub update_interval: u32,
1089 #[data(default)]
1093 pub attributes: yog_data::Ordered<f64>,
1094 pub model: Option<String>,
1096 pub animations: Option<String>,
1098 #[data(default)]
1101 pub clips: yog_data::Ordered<String>,
1102 pub spawn_egg: Option<[u32; 2]>,
1104 #[data(default)]
1108 pub goals: Vec<String>,
1109 #[data(default)]
1112 pub ticks: bool,
1113 pub place_sound: Option<String>,
1116 pub break_sound: Option<String>,
1117}
1118
1119fn eight() -> u32 {
1120 8
1121}
1122
1123fn three() -> u32 {
1124 3
1125}
1126
1127fn misc() -> String {
1128 "misc".into()
1129}
1130
1131impl EntityDef {
1132 pub fn new(id: impl Into<String>, kind: EntityKind, width: f32, height: f32) -> Self {
1133 Self {
1134 id: id.into(),
1135 name: None,
1136 kind,
1137 category: misc(),
1138 width,
1139 height,
1140 eye_height: None,
1141 fire_immune: false,
1142 no_gravity: false,
1143 pushable: None,
1144 solid: false,
1145 tracking_range: eight(),
1146 update_interval: three(),
1147 attributes: yog_data::Ordered::default(),
1148 model: None,
1149 animations: None,
1150 clips: yog_data::Ordered::default(),
1151 spawn_egg: None,
1152 goals: Vec::new(),
1153 ticks: false,
1154 place_sound: None,
1155 break_sound: None,
1156 }
1157 }
1158}
1159
1160#[cfg(test)]
1161mod registration_tests {
1162 #[test]
1163 fn a_hot_reload_sees_what_registration_changed() {
1164 let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
1165 let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
1166 let (added, removed, changed) = super::registration_changes(old, new);
1167 assert_eq!(added, ["item mymod:d"]);
1168 assert_eq!(removed, ["item mymod:c"]);
1169 assert_eq!(changed, ["block mymod:b"]);
1170 assert_eq!(super::registration_changes(old, old), (vec![], vec![], vec![]));
1171 }
1172}
1173
1174#[cfg(test)]
1175mod material_tests {
1176 use super::*;
1177
1178 #[test]
1179 fn the_games_materials_are_built_in() {
1180 assert_eq!(ToolMaterial::built_ins().len(), 6);
1181 let iron = ToolMaterial::built_in("iron").unwrap();
1182 assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
1183 assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
1184 assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
1185 let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
1186 assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
1187 }
1188}
1189
1190#[cfg(test)]
1191mod sound_tests {
1192 use super::*;
1193
1194 #[test]
1195 fn the_games_sound_groups_are_built_in() {
1196 assert!(SoundGroup::built_ins().len() > 100);
1197 let stone = SoundGroup::built_in("stone").unwrap();
1198 assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
1199 assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
1200 assert!(SoundGroup::built_in("mymod:crystal").is_none());
1201 }
1202
1203 #[test]
1204 fn a_mods_group_crosses_as_yog_data() {
1205 use yog_data::Schema;
1206 let g = SoundGroup::new("mymod:crystal").pitch(1.2);
1207 assert_eq!(g.fall, "mymod:crystal.fall");
1208 let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
1209 assert_eq!(back, g);
1210 assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
1211 }
1212}
1213
1214#[cfg(test)]
1215impl Recipe {
1216 fn to_json(&self) -> String {
1217 yog_data::json::to_json(&self.to_data()).to_string()
1218 }
1219}
1220
1221#[cfg(test)]
1222mod recipe_tests {
1223 use super::*;
1224
1225 #[test]
1226 fn writes_the_yog_format() {
1227 let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
1228 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
1229 assert_eq!(
1230 js(&shaped.to_json()),
1231 js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
1232 );
1233 assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
1234 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
1235 assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
1236 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
1237 assert_eq!(js(&smith.to_json())["template"], "m:t");
1238 }
1239}
1240
1241#[cfg(test)]
1242mod json_tests {
1243 use super::*;
1244
1245 #[test]
1247 fn json_survives_any_string() {
1248 let odd = "m:a\"b\\c\nd\te\u{1}";
1249 let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
1250 for r in [
1251 Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
1252 Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
1253 Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
1254 Recipe::stonecutting(odd, odd, odd, 2),
1255 Recipe::smithing(odd, odd, odd, odd, odd),
1256 ] {
1257 let v = parse(r.to_json());
1258 assert_eq!(v["result"], odd, "the string comes back as it was");
1259 }
1260 }
1261
1262 #[test]
1263 fn recipes_and_grants_name_their_parts() {
1264 let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
1265 let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
1266 assert_eq!(v["type"], "campfire_cooking");
1267 assert_eq!(v["cook_time"], 600, "the method's default time");
1268 assert_eq!(r.ns_name(), ("mymod", "a"));
1269 assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
1270 }
1271}