1#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19pub mod effects;
20pub mod recipe_kinds;
21pub mod loot;
22
23pub const REGISTRATIONS_FILE: &str = "registrations.txt";
29
30pub fn registration_changes(registered: &str, new: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
33 fn read(text: &str) -> std::collections::BTreeMap<String, String> {
34 text.lines()
35 .filter_map(|l| {
36 let (key, hash) = l.trim().rsplit_once(' ')?;
37 Some((key.to_string(), hash.to_string()))
38 })
39 .collect()
40 }
41 let (old, new) = (read(registered), read(new));
42 let added = new.keys().filter(|k| !old.contains_key(*k)).cloned().collect();
43 let removed = old.keys().filter(|k| !new.contains_key(*k)).cloned().collect();
44 let changed = new.iter().filter(|(k, h)| old.get(*k).is_some_and(|o| o != *h)).map(|(k, _)| k.clone()).collect();
45 (added, removed, changed)
46}
47pub use effects::{AttributeDef, Brew, EffectCategory, EffectDef, EffectModifier, ModifierOperation, PotionDef, PotionEffect};
48pub use loot::{Loot, LootAdd, LootModifier, LootScale, LootStack};
49
50#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54#[derive(yog_data::Schema, Debug, Clone)]
55pub struct FoodDef {
56 pub nutrition: u32,
58 pub saturation: f32,
60 pub can_always_eat: bool,
62}
63
64impl FoodDef {
65 pub fn new(nutrition: u32, saturation: f32) -> Self {
66 Self {
67 nutrition,
68 saturation,
69 can_always_eat: false,
70 }
71 }
72
73 pub fn can_always_eat(mut self) -> Self {
74 self.can_always_eat = true;
75 self
76 }
77}
78
79#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
81#[derive(yog_data::Schema, Debug, Clone)]
82pub struct ItemDef {
83 pub id: String,
84 pub max_stack: u8,
85 pub name: Option<String>,
86 pub tooltip: Option<String>,
87 pub max_damage: u32,
89 pub fire_resistant: bool,
91 pub fuel_ticks: u32,
93 pub food: Option<FoodDef>,
95 pub tool: Option<ToolItem>,
97 pub armor: Option<ArmorPiece>,
99 pub wearable: Option<String>,
101 pub hangs: Option<Hangs>,
103}
104
105#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
107#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
108pub struct Hangs {
109 pub entity: String,
111 pub size: Option<[u32; 2]>,
113}
114
115impl ItemDef {
116 pub fn new(id: impl Into<String>) -> Self {
117 Self {
118 id: id.into(),
119 max_stack: 64,
120 name: None,
121 tooltip: None,
122 max_damage: 0,
123 fire_resistant: false,
124 fuel_ticks: 0,
125 food: None,
126 tool: None,
127 armor: None,
128 wearable: None,
129 hangs: None,
130 }
131 }
132
133 pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
137 self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
138 self
139 }
140
141 pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
144 self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
145 self
146 }
147
148 pub fn wearable(mut self, slot: impl Into<String>) -> Self {
150 self.wearable = Some(slot.into());
151 self
152 }
153
154 pub fn hangs(mut self, entity: impl Into<String>, size: Option<[u32; 2]>) -> Self {
158 self.hangs = Some(Hangs { entity: entity.into(), size });
159 self
160 }
161
162 pub fn max_stack(mut self, n: u8) -> Self {
164 self.max_stack = n;
165 self
166 }
167
168 pub fn name(mut self, name: impl Into<String>) -> Self {
170 self.name = Some(name.into());
171 self
172 }
173
174 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
176 self.tooltip = Some(tooltip.into());
177 self
178 }
179
180 pub fn max_damage(mut self, durability: u32) -> Self {
182 self.max_damage = durability;
183 self
184 }
185
186 pub fn fire_resistant(mut self) -> Self {
188 self.fire_resistant = true;
189 self
190 }
191
192 pub fn fuel(mut self, ticks: u32) -> Self {
194 self.fuel_ticks = ticks;
195 self
196 }
197
198 pub fn food(mut self, food: FoodDef) -> Self {
200 self.food = Some(food);
201 self
202 }
203}
204
205const VANILLA_NS: &str = "minecraft";
209
210fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
212 id.split_once(':').unwrap_or((default_ns, id))
213}
214
215
216fn json_str(s: &str) -> String {
218 let mut out = String::with_capacity(s.len() + 2);
219 out.push('"');
220 for c in s.chars() {
221 match c {
222 '"' => out.push_str("\\\""),
223 '\\' => out.push_str("\\\\"),
224 '\n' => out.push_str("\\n"),
225 '\r' => out.push_str("\\r"),
226 '\t' => out.push_str("\\t"),
227 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
228 c => out.push(c),
229 }
230 }
231 out.push('"');
232 out
233}
234
235
236#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum Cooking {
240 Smelting,
241 Blasting,
242 Smoking,
243 Campfire,
244}
245
246impl Cooking {
247 pub const ALL: [Cooking; 4] = [Cooking::Smelting, Cooking::Blasting, Cooking::Smoking, Cooking::Campfire];
249
250 pub fn word(self) -> &'static str {
252 recipe_kinds::ids::COOKING[self as usize]
253 }
254
255 pub fn of_kind(id: &str) -> Option<Cooking> {
257 Cooking::ALL.into_iter().find(|c| c.word() == id)
258 }
259}
260
261#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
263#[derive(Debug, Clone, PartialEq)]
264pub enum RecipeKind {
265 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
267 Shapeless { ingredients: Vec<Vec<String>> },
269 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
270 Stonecutting { ingredient: Vec<String> },
271 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
273}
274
275#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
289#[derive(Debug, Clone, PartialEq)]
290pub struct Recipe {
291 pub id: String,
292 pub kind: RecipeKind,
293 pub result: String,
294 pub count: u32,
295 pub group: Option<String>,
297 pub category: Option<String>,
299}
300
301impl Recipe {
302 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
303 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
304 }
305
306 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
307 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
308 }
309
310 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
311 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
312 }
313
314 pub fn cooking(
317 id: impl Into<String>,
318 method: Cooking,
319 ingredient: impl Into<String>,
320 result: impl Into<String>,
321 count: u32,
322 ) -> Self {
323 let cook_time = match method {
324 Cooking::Smelting => 200,
325 Cooking::Blasting | Cooking::Smoking => 100,
326 Cooking::Campfire => 600,
327 };
328 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
329 Self::of(id, kind, result, count)
330 }
331
332 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
333 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
334 }
335
336 pub fn smithing(
337 id: impl Into<String>,
338 template: impl Into<String>,
339 base: impl Into<String>,
340 addition: impl Into<String>,
341 result: impl Into<String>,
342 ) -> Self {
343 let kind = RecipeKind::Smithing {
344 template: vec![template.into()],
345 base: vec![base.into()],
346 addition: vec![addition.into()],
347 };
348 Self::of(id, kind, result, 1)
349 }
350
351 pub fn row(mut self, pattern: impl Into<String>) -> Self {
353 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
354 p.push(pattern.into());
355 }
356 self
357 }
358
359 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
361 self.key_any(symbol, [item.into()])
362 }
363
364 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
366 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
367 key.push((symbol, items.into_iter().map(Into::into).collect()));
368 }
369 self
370 }
371
372 pub fn ingredient(self, item: impl Into<String>) -> Self {
374 self.ingredient_any([item.into()])
375 }
376
377 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
379 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
380 ingredients.push(items.into_iter().map(Into::into).collect());
381 }
382 self
383 }
384
385 pub fn experience(mut self, xp: f32) -> Self {
387 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
388 *experience = xp;
389 }
390 self
391 }
392
393 pub fn cook_time(mut self, ticks: u32) -> Self {
395 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
396 *cook_time = ticks;
397 }
398 self
399 }
400
401 pub fn group(mut self, group: impl Into<String>) -> Self {
402 self.group = Some(group.into());
403 self
404 }
405
406 pub fn category(mut self, category: impl Into<String>) -> Self {
407 self.category = Some(category.into());
408 self
409 }
410
411 pub fn to_data(&self) -> yog_data::Value {
414 use yog_data::{Map, Value};
415 let ing = |items: &[String]| match items {
416 [one] => Value::from(one.as_str()),
417 many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
418 };
419 let mut m = Map::with_capacity(8);
420 let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
421 match &self.kind {
422 RecipeKind::Shaped { pattern, key } => {
423 ty(&mut m, "shaped");
424 m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
425 let mut keys = Map::with_capacity(key.len());
426 for (c, items) in key {
427 keys.insert(c.to_string(), ing(items));
428 }
429 m.insert("key", Value::Map(keys));
430 }
431 RecipeKind::Shapeless { ingredients } => {
432 ty(&mut m, "shapeless");
433 m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
434 }
435 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
436 ty(&mut m, method.word());
437 m.insert("ingredient", ing(ingredient));
438 m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
440 m.insert("cook_time", Value::Int(*cook_time as i64));
441 }
442 RecipeKind::Stonecutting { ingredient } => {
443 ty(&mut m, "stonecutting");
444 m.insert("ingredient", ing(ingredient));
445 }
446 RecipeKind::Smithing { template, base, addition } => {
447 ty(&mut m, "smithing");
448 m.insert("template", ing(template));
449 m.insert("base", ing(base));
450 m.insert("addition", ing(addition));
451 }
452 }
453 m.insert("result", Value::from(self.result.as_str()));
454 if self.count != 1 {
455 m.insert("count", Value::Int(self.count as i64));
456 }
457 if let Some(g) = &self.group {
458 m.insert("group", Value::from(g.as_str()));
459 }
460 if let Some(c) = &self.category {
461 m.insert("category", Value::from(c.as_str()));
462 }
463 Value::Map(m)
464 }
465
466 pub fn ns_name(&self) -> (&str, &str) {
468 split_id(&self.id, VANILLA_NS)
469 }
470}
471
472#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
485#[derive(yog_data::Schema, Debug, Clone)]
486pub struct StartupGrant {
487 pub id: String,
488 pub items: Vec<String>,
489 pub book: Option<String>,
490 pub command: Option<String>,
491}
492
493impl StartupGrant {
494 pub fn new(id: impl Into<String>) -> Self {
495 Self {
496 id: id.into(),
497 items: Vec::new(),
498 book: None,
499 command: None,
500 }
501 }
502
503 pub fn item(mut self, item_id: impl Into<String>) -> Self {
504 self.items.push(item_id.into());
505 self
506 }
507
508 pub fn book(mut self, book: impl Into<String>) -> Self {
509 self.book = Some(book.into());
510 self
511 }
512
513 pub fn command(mut self, cmd: impl Into<String>) -> Self {
514 self.command = Some(cmd.into());
515 self
516 }
517
518 pub fn to_json(&self) -> String {
519 let mut entries: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":{}}}", json_str(i))).collect();
520 if let Some(book) = &self.book {
521 let tag = format!("{{yog_book:\"{}\"}}", book.replace('\\', "\\\\").replace('"', "\\\""));
523 entries.push(format!(
524 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":{}}}]}}",
525 json_str(&tag)
526 ));
527 }
528 if let Some(cmd) = &self.command {
529 entries.push(format!("{{\"type\":\"minecraft:command\",\"command\":{}}}", json_str(cmd)));
530 }
531 format!("{{\"type\":\"yog:startup_grant\",\"entries\":[{}],\"id\":{}}}", entries.join(","), json_str(&self.id))
532 }
533}
534
535
536
537#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
541#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
542pub struct ToolItem {
543 pub kind: String,
546 pub material: String,
548 pub attack_damage: f32,
550 pub attack_speed: f32,
552}
553
554#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
556#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
557pub struct ArmorPiece {
558 pub slot: String,
560 pub material: String,
562}
563
564#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
569#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
570pub struct ToolMaterial {
571 pub id: String,
572 pub durability: u32,
574 pub speed: f32,
576 pub attack_damage: f32,
578 pub enchantability: u32,
579 pub level: String,
581 pub repair: String,
583 #[data(default)]
585 pub level_index: u32,
586 #[data(default)]
589 pub incorrect: String,
590}
591
592#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
594#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
595pub struct ArmorDefense {
596 pub head: u32,
597 pub chest: u32,
598 pub legs: u32,
599 pub feet: u32,
600}
601
602#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
606#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
607pub struct ArmorMaterial {
608 pub id: String,
609 pub durability: u32,
612 pub defense: ArmorDefense,
613 pub enchantability: u32,
614 pub equip_sound: String,
616 #[data(default)]
617 pub toughness: f32,
618 #[data(default)]
619 pub knockback_resistance: f32,
620 pub repair: String,
622 pub texture: String,
625}
626
627static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
628 yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
629 .unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
630 .value
631});
632
633static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
634 yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
635 .unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
636 .value
637});
638
639fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
641 let name = name.strip_prefix("minecraft:").unwrap_or(name);
642 list.iter().find(|x| id(x).strip_prefix("minecraft:") == Some(name))
643}
644
645impl ToolMaterial {
646 pub fn built_ins() -> &'static [ToolMaterial] {
648 &BUILT_IN_TOOL_MATERIALS
649 }
650
651 pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
653 built_in(Self::built_ins(), |m| &m.id, name)
654 }
655
656 pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
659 let mut m = Self::built_in(like)?.clone();
660 m.id = id.into();
661 Some(m)
662 }
663}
664
665impl ArmorMaterial {
666 pub fn built_ins() -> &'static [ArmorMaterial] {
668 &BUILT_IN_ARMOR_MATERIALS
669 }
670
671 pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
673 built_in(Self::built_ins(), |m| &m.id, name)
674 }
675}
676
677#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
684#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
685pub struct BiomeFeature {
686 pub feature: String,
688 pub biomes: String,
690 pub step: String,
692}
693
694#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
702#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
703pub struct SoundGroup {
704 pub id: String,
705 #[data(rename = "break")]
707 pub break_sound: String,
708 pub step: String,
709 pub place: String,
710 pub hit: String,
711 pub fall: String,
712 #[data(default = "one")]
713 #[data(range = "0..=10")]
714 pub volume: f32,
715 #[data(default = "one")]
716 #[data(range = "0..=10")]
717 pub pitch: f32,
718}
719
720fn one() -> f32 {
721 1.0
722}
723
724static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
725 yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
726 .unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
727 .value
728});
729
730impl SoundGroup {
731 pub fn new(id: impl Into<String>) -> Self {
734 let id = id.into();
735 let ev = |s: &str| format!("{id}.{s}");
736 Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
737 }
738
739 pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
741 [self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
742 self
743 }
744
745 pub fn volume(mut self, volume: f32) -> Self {
746 self.volume = volume;
747 self
748 }
749
750 pub fn pitch(mut self, pitch: f32) -> Self {
751 self.pitch = pitch;
752 self
753 }
754
755 pub fn built_ins() -> &'static [SoundGroup] {
758 &BUILT_IN_SOUND_GROUPS
759 }
760
761 pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
763 let name = name.strip_prefix("minecraft:").unwrap_or(name);
764 Self::built_ins().iter().find(|g| g.id.strip_prefix("minecraft:") == Some(name))
765 }
766}
767
768#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
772#[derive(yog_data::Schema, Debug, Clone)]
773pub struct BlockDef {
774 pub id: String,
775 pub hardness: f32,
776 pub resistance: f32,
777 pub name: Option<String>,
778 pub shape: Option<[f32; 6]>,
781 pub light_level: u8,
783 pub sound: Option<String>,
787 pub requires_tool: bool,
789 pub no_collision: bool,
791 pub slipperiness: f32,
793 pub connects: bool,
799 pub connect_groups: Vec<String>,
805 pub inventory_id: Option<String>,
808 pub redstone_source: bool,
811 pub facing: String,
814 pub placement: String,
816 pub attach: bool,
819 pub waterlogged: bool,
821 pub properties: Vec<BlockProperty>,
823 pub powered: Option<String>,
825 pub comparator: Option<String>,
827 pub accepts_items: bool,
829 #[data(default)]
831 pub no_occlusion: bool,
832 #[data(default)]
837 pub kind: String,
838 #[data(default)]
840 pub base: Option<String>,
841 #[data(default)]
844 pub set: Option<String>,
845 #[data(default = "thirty")]
847 pub press_ticks: u32,
848 #[data(default)]
851 pub mobs_only: bool,
852}
853
854fn thirty() -> u32 {
855 30
856}
857
858#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
861#[derive(yog_data::Schema, Debug, Clone, PartialEq, Eq)]
862pub struct BlockProperty {
863 pub name: String,
864 pub values: Vec<String>,
865}
866
867impl BlockDef {
868 pub fn new(id: impl Into<String>) -> Self {
869 Self {
870 id: id.into(),
871 hardness: 1.5,
872 resistance: 6.0,
873 name: None,
874 shape: None,
875 light_level: 0,
876 sound: None,
877 requires_tool: false,
878 no_collision: false,
879 slipperiness: 0.0,
880 connects: false,
881 connect_groups: Vec::new(),
882 inventory_id: None,
883 redstone_source: false,
884 facing: String::new(),
885 placement: String::new(),
886 attach: false,
887 waterlogged: false,
888 properties: Vec::new(),
889 powered: None,
890 comparator: None,
891 accepts_items: false,
892 no_occlusion: false,
893 kind: String::new(),
894 base: None,
895 set: None,
896 press_ticks: thirty(),
897 mobs_only: false,
898 }
899 }
900
901 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
903 self.hardness = hardness;
904 self.resistance = resistance;
905 self
906 }
907
908 pub fn name(mut self, name: impl Into<String>) -> Self {
910 self.name = Some(name.into());
911 self
912 }
913
914 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
916 self.shape = Some([x1, y1, z1, x2, y2, z2]);
917 self
918 }
919
920 pub fn light_level(mut self, level: u8) -> Self {
922 self.light_level = level.min(15);
923 self
924 }
925
926 pub fn sound(mut self, group: impl Into<String>) -> Self {
929 self.sound = Some(group.into());
930 self
931 }
932
933 pub fn requires_tool(mut self) -> Self {
935 self.requires_tool = true;
936 self
937 }
938
939 pub fn no_collision(mut self) -> Self {
941 self.no_collision = true;
942 self
943 }
944
945 pub fn slipperiness(mut self, value: f32) -> Self {
947 self.slipperiness = value;
948 self
949 }
950
951 pub fn connects_to_neighbors(mut self) -> Self {
957 self.connects = true;
958 self
959 }
960
961 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
968 self.connect_groups = groups
969 .iter()
970 .map(std::string::ToString::to_string)
971 .collect();
972 self
973 }
974
975 pub fn redstone_source(mut self) -> Self {
981 self.redstone_source = true;
982 self
983 }
984
985 pub fn facing(mut self, ways: &str, placement: &str) -> Self {
988 self.facing = ways.to_string();
989 self.placement = placement.to_string();
990 self
991 }
992
993 pub fn attach(mut self) -> Self {
995 self.attach = true;
996 self
997 }
998
999 pub fn waterlogged(mut self) -> Self {
1001 self.waterlogged = true;
1002 self
1003 }
1004
1005 pub fn property(mut self, name: &str, values: &[&str]) -> Self {
1007 self.properties.push(BlockProperty { name: name.into(), values: values.iter().map(|v| v.to_string()).collect() });
1008 self
1009 }
1010
1011 pub fn powered(mut self, name: &str) -> Self {
1013 self.powered = Some(name.into());
1014 self
1015 }
1016
1017 pub fn comparator(mut self, name: &str) -> Self {
1019 self.comparator = Some(name.into());
1020 self
1021 }
1022
1023 pub fn accepts_items(mut self) -> Self {
1025 self.accepts_items = true;
1026 self
1027 }
1028
1029 pub fn inventory(mut self, id: impl Into<String>) -> Self {
1030 self.inventory_id = Some(id.into());
1031 self
1032 }
1033
1034 pub fn no_occlusion(mut self) -> Self {
1036 self.no_occlusion = true;
1037 self
1038 }
1039
1040 pub fn kind(mut self, kind: &str) -> Self {
1042 self.kind = kind.into();
1043 self
1044 }
1045
1046 pub fn base(mut self, id: impl Into<String>) -> Self {
1048 self.base = Some(id.into());
1049 self
1050 }
1051
1052 pub fn set(mut self, name: &str) -> Self {
1054 self.set = Some(name.into());
1055 self
1056 }
1057
1058 pub fn press_ticks(mut self, ticks: u32) -> Self {
1060 self.press_ticks = ticks;
1061 self
1062 }
1063
1064 pub fn mobs_only(mut self) -> Self {
1066 self.mobs_only = true;
1067 self
1068 }
1069}
1070
1071#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq, Eq, Default)]
1075pub enum EntityKind {
1076 #[default]
1079 Object,
1080 Living,
1082 Mob,
1084 Hanging,
1089}
1090
1091#[derive(yog_data::Schema, Debug, Clone)]
1095pub struct EntityDef {
1096 pub id: String,
1097 pub name: Option<String>,
1099 #[data(default)]
1100 pub kind: EntityKind,
1101 #[data(default = "misc")]
1104 pub category: String,
1105 pub width: f32,
1107 pub height: f32,
1108 pub eye_height: Option<f32>,
1110 #[data(default)]
1112 pub fire_immune: bool,
1113 #[data(default)]
1115 pub no_gravity: bool,
1116 pub pushable: Option<bool>,
1118 #[data(default)]
1120 pub solid: bool,
1121 #[data(default = "eight")]
1123 pub tracking_range: u32,
1124 #[data(default = "three")]
1126 pub update_interval: u32,
1127 #[data(default)]
1131 pub attributes: yog_data::Ordered<f64>,
1132 pub model: Option<String>,
1134 pub animations: Option<String>,
1136 #[data(default)]
1139 pub clips: yog_data::Ordered<String>,
1140 pub spawn_egg: Option<[u32; 2]>,
1142 #[data(default)]
1146 pub goals: Vec<String>,
1147 #[data(default)]
1150 pub ticks: bool,
1151 pub place_sound: Option<String>,
1154 pub break_sound: Option<String>,
1155}
1156
1157fn eight() -> u32 {
1158 8
1159}
1160
1161fn three() -> u32 {
1162 3
1163}
1164
1165fn misc() -> String {
1166 "misc".into()
1167}
1168
1169impl EntityDef {
1170 pub fn new(id: impl Into<String>, kind: EntityKind, width: f32, height: f32) -> Self {
1171 Self {
1172 id: id.into(),
1173 name: None,
1174 kind,
1175 category: misc(),
1176 width,
1177 height,
1178 eye_height: None,
1179 fire_immune: false,
1180 no_gravity: false,
1181 pushable: None,
1182 solid: false,
1183 tracking_range: eight(),
1184 update_interval: three(),
1185 attributes: yog_data::Ordered::default(),
1186 model: None,
1187 animations: None,
1188 clips: yog_data::Ordered::default(),
1189 spawn_egg: None,
1190 goals: Vec::new(),
1191 ticks: false,
1192 place_sound: None,
1193 break_sound: None,
1194 }
1195 }
1196}
1197
1198#[cfg(test)]
1199mod registration_tests {
1200 #[test]
1201 fn a_hot_reload_sees_what_registration_changed() {
1202 let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
1203 let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
1204 let (added, removed, changed) = super::registration_changes(old, new);
1205 assert_eq!(added, ["item mymod:d"]);
1206 assert_eq!(removed, ["item mymod:c"]);
1207 assert_eq!(changed, ["block mymod:b"]);
1208 assert_eq!(super::registration_changes(old, old), (vec![], vec![], vec![]));
1209 }
1210}
1211
1212#[cfg(test)]
1213mod material_tests {
1214 use super::*;
1215
1216 #[test]
1217 fn the_games_materials_are_built_in() {
1218 assert_eq!(ToolMaterial::built_ins().len(), 6);
1219 let iron = ToolMaterial::built_in("iron").unwrap();
1220 assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
1221 assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
1222 assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
1223 let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
1224 assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
1225 }
1226}
1227
1228#[cfg(test)]
1229mod sound_tests {
1230 use super::*;
1231
1232 #[test]
1233 fn the_games_sound_groups_are_built_in() {
1234 assert!(SoundGroup::built_ins().len() > 100);
1235 let stone = SoundGroup::built_in("stone").unwrap();
1236 assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
1237 assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
1238 assert!(SoundGroup::built_in("mymod:crystal").is_none());
1239 }
1240
1241 #[test]
1242 fn a_mods_group_crosses_as_yog_data() {
1243 use yog_data::Schema;
1244 let g = SoundGroup::new("mymod:crystal").pitch(1.2);
1245 assert_eq!(g.fall, "mymod:crystal.fall");
1246 let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
1247 assert_eq!(back, g);
1248 assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
1249 }
1250}
1251
1252#[cfg(test)]
1253impl Recipe {
1254 fn to_json(&self) -> String {
1255 yog_data::json::to_json(&self.to_data()).to_string()
1256 }
1257}
1258
1259#[cfg(test)]
1260mod recipe_tests {
1261 use super::*;
1262
1263 #[test]
1264 fn writes_the_yog_format() {
1265 let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
1266 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
1267 assert_eq!(
1268 js(&shaped.to_json()),
1269 js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
1270 );
1271 assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
1272 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
1273 assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
1274 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
1275 assert_eq!(js(&smith.to_json())["template"], "m:t");
1276 }
1277}
1278
1279#[cfg(test)]
1280mod json_tests {
1281 use super::*;
1282
1283 #[test]
1285 fn json_survives_any_string() {
1286 let odd = "m:a\"b\\c\nd\te\u{1}";
1287 let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
1288 for r in [
1289 Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
1290 Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
1291 Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
1292 Recipe::stonecutting(odd, odd, odd, 2),
1293 Recipe::smithing(odd, odd, odd, odd, odd),
1294 ] {
1295 let v = parse(r.to_json());
1296 assert_eq!(v["result"], odd, "the string comes back as it was");
1297 }
1298 let g = StartupGrant::new(odd).item(odd).book("mymod:\"guide\"").command("say \"hi\" \\o/");
1299 let v = parse(g.to_json());
1300 assert_eq!(v["id"], odd);
1301 assert_eq!(v["entries"][2]["command"], "say \"hi\" \\o/");
1302 }
1303
1304 #[test]
1305 fn recipes_and_grants_name_their_parts() {
1306 let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
1307 let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
1308 assert_eq!(v["type"], "campfire_cooking");
1309 assert_eq!(v["cook_time"], 600, "the method's default time");
1310 assert_eq!(r.ns_name(), ("mymod", "a"));
1311 assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
1312 let g = StartupGrant::new("mymod:starter");
1313 assert!(g.to_json().contains(r#""entries":[]"#));
1314 }
1315}