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