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 pub hangs: Option<Hangs>,
102}
103
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
106#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
107pub struct Hangs {
108 pub entity: String,
110 pub size: Option<[u32; 2]>,
112}
113
114impl ItemDef {
115 pub fn new(id: impl Into<String>) -> Self {
116 Self {
117 id: id.into(),
118 max_stack: 64,
119 name: None,
120 tooltip: None,
121 max_damage: 0,
122 fire_resistant: false,
123 fuel_ticks: 0,
124 food: None,
125 tool: None,
126 armor: None,
127 wearable: None,
128 hangs: None,
129 }
130 }
131
132 pub fn tool(mut self, kind: impl Into<String>, material: impl Into<String>, attack_damage: f32, attack_speed: f32) -> Self {
136 self.tool = Some(ToolItem { kind: kind.into(), material: material.into(), attack_damage, attack_speed });
137 self
138 }
139
140 pub fn armor(mut self, slot: impl Into<String>, material: impl Into<String>) -> Self {
143 self.armor = Some(ArmorPiece { slot: slot.into(), material: material.into() });
144 self
145 }
146
147 pub fn wearable(mut self, slot: impl Into<String>) -> Self {
149 self.wearable = Some(slot.into());
150 self
151 }
152
153 pub fn hangs(mut self, entity: impl Into<String>, size: Option<[u32; 2]>) -> Self {
157 self.hangs = Some(Hangs { entity: entity.into(), size });
158 self
159 }
160
161 pub fn max_stack(mut self, n: u8) -> Self {
163 self.max_stack = n;
164 self
165 }
166
167 pub fn name(mut self, name: impl Into<String>) -> Self {
169 self.name = Some(name.into());
170 self
171 }
172
173 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
175 self.tooltip = Some(tooltip.into());
176 self
177 }
178
179 pub fn max_damage(mut self, durability: u32) -> Self {
181 self.max_damage = durability;
182 self
183 }
184
185 pub fn fire_resistant(mut self) -> Self {
187 self.fire_resistant = true;
188 self
189 }
190
191 pub fn fuel(mut self, ticks: u32) -> Self {
193 self.fuel_ticks = ticks;
194 self
195 }
196
197 pub fn food(mut self, food: FoodDef) -> Self {
199 self.food = Some(food);
200 self
201 }
202}
203
204const VANILLA_NS: &str = "minecraft";
208
209fn split_id<'a>(id: &'a str, default_ns: &'a str) -> (&'a str, &'a str) {
211 id.split_once(':').unwrap_or((default_ns, id))
212}
213
214
215fn json_str(s: &str) -> String {
217 let mut out = String::with_capacity(s.len() + 2);
218 out.push('"');
219 for c in s.chars() {
220 match c {
221 '"' => out.push_str("\\\""),
222 '\\' => out.push_str("\\\\"),
223 '\n' => out.push_str("\\n"),
224 '\r' => out.push_str("\\r"),
225 '\t' => out.push_str("\\t"),
226 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
227 c => out.push(c),
228 }
229 }
230 out.push('"');
231 out
232}
233
234
235#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum Cooking {
239 Smelting,
240 Blasting,
241 Smoking,
242 Campfire,
243}
244
245impl Cooking {
246 pub fn word(self) -> &'static str {
248 match self {
249 Cooking::Smelting => "smelting",
250 Cooking::Blasting => "blasting",
251 Cooking::Smoking => "smoking",
252 Cooking::Campfire => "campfire_cooking",
253 }
254 }
255}
256
257#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
259#[derive(Debug, Clone, PartialEq)]
260pub enum RecipeKind {
261 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
263 Shapeless { ingredients: Vec<Vec<String>> },
265 Cooking { method: Cooking, ingredient: Vec<String>, experience: f32, cook_time: u32 },
266 Stonecutting { ingredient: Vec<String> },
267 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
269}
270
271#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
285#[derive(Debug, Clone, PartialEq)]
286pub struct Recipe {
287 pub id: String,
288 pub kind: RecipeKind,
289 pub result: String,
290 pub count: u32,
291 pub group: Option<String>,
293 pub category: Option<String>,
295}
296
297impl Recipe {
298 fn of(id: impl Into<String>, kind: RecipeKind, result: impl Into<String>, count: u32) -> Self {
299 Recipe { id: id.into(), kind, result: result.into(), count, group: None, category: None }
300 }
301
302 pub fn shaped(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
303 Self::of(id, RecipeKind::Shaped { pattern: Vec::new(), key: Vec::new() }, result, count)
304 }
305
306 pub fn shapeless(id: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
307 Self::of(id, RecipeKind::Shapeless { ingredients: Vec::new() }, result, count)
308 }
309
310 pub fn cooking(
313 id: impl Into<String>,
314 method: Cooking,
315 ingredient: impl Into<String>,
316 result: impl Into<String>,
317 count: u32,
318 ) -> Self {
319 let cook_time = match method {
320 Cooking::Smelting => 200,
321 Cooking::Blasting | Cooking::Smoking => 100,
322 Cooking::Campfire => 600,
323 };
324 let kind = RecipeKind::Cooking { method, ingredient: vec![ingredient.into()], experience: 0.1, cook_time };
325 Self::of(id, kind, result, count)
326 }
327
328 pub fn stonecutting(id: impl Into<String>, ingredient: impl Into<String>, result: impl Into<String>, count: u32) -> Self {
329 Self::of(id, RecipeKind::Stonecutting { ingredient: vec![ingredient.into()] }, result, count)
330 }
331
332 pub fn smithing(
333 id: impl Into<String>,
334 template: impl Into<String>,
335 base: impl Into<String>,
336 addition: impl Into<String>,
337 result: impl Into<String>,
338 ) -> Self {
339 let kind = RecipeKind::Smithing {
340 template: vec![template.into()],
341 base: vec![base.into()],
342 addition: vec![addition.into()],
343 };
344 Self::of(id, kind, result, 1)
345 }
346
347 pub fn row(mut self, pattern: impl Into<String>) -> Self {
349 if let RecipeKind::Shaped { pattern: p, .. } = &mut self.kind {
350 p.push(pattern.into());
351 }
352 self
353 }
354
355 pub fn key(self, symbol: char, item: impl Into<String>) -> Self {
357 self.key_any(symbol, [item.into()])
358 }
359
360 pub fn key_any(mut self, symbol: char, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
362 if let RecipeKind::Shaped { key, .. } = &mut self.kind {
363 key.push((symbol, items.into_iter().map(Into::into).collect()));
364 }
365 self
366 }
367
368 pub fn ingredient(self, item: impl Into<String>) -> Self {
370 self.ingredient_any([item.into()])
371 }
372
373 pub fn ingredient_any(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
375 if let RecipeKind::Shapeless { ingredients } = &mut self.kind {
376 ingredients.push(items.into_iter().map(Into::into).collect());
377 }
378 self
379 }
380
381 pub fn experience(mut self, xp: f32) -> Self {
383 if let RecipeKind::Cooking { experience, .. } = &mut self.kind {
384 *experience = xp;
385 }
386 self
387 }
388
389 pub fn cook_time(mut self, ticks: u32) -> Self {
391 if let RecipeKind::Cooking { cook_time, .. } = &mut self.kind {
392 *cook_time = ticks;
393 }
394 self
395 }
396
397 pub fn group(mut self, group: impl Into<String>) -> Self {
398 self.group = Some(group.into());
399 self
400 }
401
402 pub fn category(mut self, category: impl Into<String>) -> Self {
403 self.category = Some(category.into());
404 self
405 }
406
407 pub fn to_data(&self) -> yog_data::Value {
410 use yog_data::{Map, Value};
411 let ing = |items: &[String]| match items {
412 [one] => Value::from(one.as_str()),
413 many => Value::List(many.iter().map(|i| Value::from(i.as_str())).collect()),
414 };
415 let mut m = Map::with_capacity(8);
416 let ty = |m: &mut Map, t: &str| m.insert("type", Value::from(t));
417 match &self.kind {
418 RecipeKind::Shaped { pattern, key } => {
419 ty(&mut m, "shaped");
420 m.insert("pattern", Value::List(pattern.iter().map(|r| Value::from(r.as_str())).collect()));
421 let mut keys = Map::with_capacity(key.len());
422 for (c, items) in key {
423 keys.insert(c.to_string(), ing(items));
424 }
425 m.insert("key", Value::Map(keys));
426 }
427 RecipeKind::Shapeless { ingredients } => {
428 ty(&mut m, "shapeless");
429 m.insert("ingredients", Value::List(ingredients.iter().map(|i| ing(i)).collect()));
430 }
431 RecipeKind::Cooking { method, ingredient, experience, cook_time } => {
432 ty(&mut m, method.word());
433 m.insert("ingredient", ing(ingredient));
434 m.insert("experience", Value::Float(experience.to_string().parse().unwrap_or(0.0)));
436 m.insert("cook_time", Value::Int(*cook_time as i64));
437 }
438 RecipeKind::Stonecutting { ingredient } => {
439 ty(&mut m, "stonecutting");
440 m.insert("ingredient", ing(ingredient));
441 }
442 RecipeKind::Smithing { template, base, addition } => {
443 ty(&mut m, "smithing");
444 m.insert("template", ing(template));
445 m.insert("base", ing(base));
446 m.insert("addition", ing(addition));
447 }
448 }
449 m.insert("result", Value::from(self.result.as_str()));
450 if self.count != 1 {
451 m.insert("count", Value::Int(self.count as i64));
452 }
453 if let Some(g) = &self.group {
454 m.insert("group", Value::from(g.as_str()));
455 }
456 if let Some(c) = &self.category {
457 m.insert("category", Value::from(c.as_str()));
458 }
459 Value::Map(m)
460 }
461
462 pub fn ns_name(&self) -> (&str, &str) {
464 split_id(&self.id, VANILLA_NS)
465 }
466}
467
468#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
481#[derive(yog_data::Schema, Debug, Clone)]
482pub struct StartupGrant {
483 pub id: String,
484 pub items: Vec<String>,
485 pub book: Option<String>,
486 pub command: Option<String>,
487}
488
489impl StartupGrant {
490 pub fn new(id: impl Into<String>) -> Self {
491 Self {
492 id: id.into(),
493 items: Vec::new(),
494 book: None,
495 command: None,
496 }
497 }
498
499 pub fn item(mut self, item_id: impl Into<String>) -> Self {
500 self.items.push(item_id.into());
501 self
502 }
503
504 pub fn book(mut self, book: impl Into<String>) -> Self {
505 self.book = Some(book.into());
506 self
507 }
508
509 pub fn command(mut self, cmd: impl Into<String>) -> Self {
510 self.command = Some(cmd.into());
511 self
512 }
513
514 pub fn to_json(&self) -> String {
515 let mut entries: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":{}}}", json_str(i))).collect();
516 if let Some(book) = &self.book {
517 let tag = format!("{{yog_book:\"{}\"}}", book.replace('\\', "\\\\").replace('"', "\\\""));
519 entries.push(format!(
520 "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":{}}}]}}",
521 json_str(&tag)
522 ));
523 }
524 if let Some(cmd) = &self.command {
525 entries.push(format!("{{\"type\":\"minecraft:command\",\"command\":{}}}", json_str(cmd)));
526 }
527 format!("{{\"type\":\"yog:startup_grant\",\"entries\":[{}],\"id\":{}}}", entries.join(","), json_str(&self.id))
528 }
529}
530
531
532
533#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
537#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
538pub struct ToolItem {
539 pub kind: String,
542 pub material: String,
544 pub attack_damage: f32,
546 pub attack_speed: f32,
548}
549
550#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
552#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
553pub struct ArmorPiece {
554 pub slot: String,
556 pub material: String,
558}
559
560#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
565#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
566pub struct ToolMaterial {
567 pub id: String,
568 pub durability: u32,
570 pub speed: f32,
572 pub attack_damage: f32,
574 pub enchantability: u32,
575 pub level: String,
577 pub repair: String,
579 #[data(default)]
581 pub level_index: u32,
582 #[data(default)]
585 pub incorrect: String,
586}
587
588#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
590#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq)]
591pub struct ArmorDefense {
592 pub head: u32,
593 pub chest: u32,
594 pub legs: u32,
595 pub feet: u32,
596}
597
598#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
602#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
603pub struct ArmorMaterial {
604 pub id: String,
605 pub durability: u32,
608 pub defense: ArmorDefense,
609 pub enchantability: u32,
610 pub equip_sound: String,
612 #[data(default)]
613 pub toughness: f32,
614 #[data(default)]
615 pub knockback_resistance: f32,
616 pub repair: String,
618 pub texture: String,
621}
622
623static BUILT_IN_TOOL_MATERIALS: std::sync::LazyLock<Vec<ToolMaterial>> = std::sync::LazyLock::new(|| {
624 yog_data::from_text::<Vec<ToolMaterial>>(include_str!("../assets/tool_materials.ydata"))
625 .unwrap_or_else(|e| panic!("yog-registry's built-in tool materials: {e}"))
626 .value
627});
628
629static BUILT_IN_ARMOR_MATERIALS: std::sync::LazyLock<Vec<ArmorMaterial>> = std::sync::LazyLock::new(|| {
630 yog_data::from_text::<Vec<ArmorMaterial>>(include_str!("../assets/armor_materials.ydata"))
631 .unwrap_or_else(|e| panic!("yog-registry's built-in armor materials: {e}"))
632 .value
633});
634
635fn built_in<'a, T>(list: &'a [T], id: impl Fn(&T) -> &str, name: &str) -> Option<&'a T> {
637 let name = name.strip_prefix("minecraft:").unwrap_or(name);
638 list.iter().find(|x| id(x).strip_prefix("minecraft:") == Some(name))
639}
640
641impl ToolMaterial {
642 pub fn built_ins() -> &'static [ToolMaterial] {
644 &BUILT_IN_TOOL_MATERIALS
645 }
646
647 pub fn built_in(name: &str) -> Option<&'static ToolMaterial> {
649 built_in(Self::built_ins(), |m| &m.id, name)
650 }
651
652 pub fn like(id: impl Into<String>, like: &str) -> Option<Self> {
655 let mut m = Self::built_in(like)?.clone();
656 m.id = id.into();
657 Some(m)
658 }
659}
660
661impl ArmorMaterial {
662 pub fn built_ins() -> &'static [ArmorMaterial] {
664 &BUILT_IN_ARMOR_MATERIALS
665 }
666
667 pub fn built_in(name: &str) -> Option<&'static ArmorMaterial> {
669 built_in(Self::built_ins(), |m| &m.id, name)
670 }
671}
672
673#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
680#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
681pub struct BiomeFeature {
682 pub feature: String,
684 pub biomes: String,
686 pub step: String,
688}
689
690#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
698#[derive(yog_data::Schema, Debug, Clone, PartialEq)]
699pub struct SoundGroup {
700 pub id: String,
701 #[data(rename = "break")]
703 pub break_sound: String,
704 pub step: String,
705 pub place: String,
706 pub hit: String,
707 pub fall: String,
708 #[data(default = "one")]
709 #[data(range = "0..=10")]
710 pub volume: f32,
711 #[data(default = "one")]
712 #[data(range = "0..=10")]
713 pub pitch: f32,
714}
715
716fn one() -> f32 {
717 1.0
718}
719
720static BUILT_IN_SOUND_GROUPS: std::sync::LazyLock<Vec<SoundGroup>> = std::sync::LazyLock::new(|| {
721 yog_data::from_text::<Vec<SoundGroup>>(include_str!("../assets/sound_groups.ydata"))
722 .unwrap_or_else(|e| panic!("yog-registry's built-in sound groups: {e}"))
723 .value
724});
725
726impl SoundGroup {
727 pub fn new(id: impl Into<String>) -> Self {
730 let id = id.into();
731 let ev = |s: &str| format!("{id}.{s}");
732 Self { break_sound: ev("break"), step: ev("step"), place: ev("place"), hit: ev("hit"), fall: ev("fall"), volume: 1.0, pitch: 1.0, id }
733 }
734
735 pub fn sounds(mut self, break_sound: &str, step: &str, place: &str, hit: &str, fall: &str) -> Self {
737 [self.break_sound, self.step, self.place, self.hit, self.fall] = [break_sound, step, place, hit, fall].map(String::from);
738 self
739 }
740
741 pub fn volume(mut self, volume: f32) -> Self {
742 self.volume = volume;
743 self
744 }
745
746 pub fn pitch(mut self, pitch: f32) -> Self {
747 self.pitch = pitch;
748 self
749 }
750
751 pub fn built_ins() -> &'static [SoundGroup] {
754 &BUILT_IN_SOUND_GROUPS
755 }
756
757 pub fn built_in(name: &str) -> Option<&'static SoundGroup> {
759 let name = name.strip_prefix("minecraft:").unwrap_or(name);
760 Self::built_ins().iter().find(|g| g.id.strip_prefix("minecraft:") == Some(name))
761 }
762}
763
764#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
768#[derive(yog_data::Schema, Debug, Clone)]
769pub struct BlockDef {
770 pub id: String,
771 pub hardness: f32,
772 pub resistance: f32,
773 pub name: Option<String>,
774 pub shape: Option<[f32; 6]>,
777 pub light_level: u8,
779 pub sound: Option<String>,
783 pub requires_tool: bool,
785 pub no_collision: bool,
787 pub slipperiness: f32,
789 pub connects: bool,
795 pub connect_groups: Vec<String>,
801 pub inventory_id: Option<String>,
804 pub redstone_source: bool,
807 pub facing: String,
810 pub placement: String,
812 pub attach: bool,
815 pub waterlogged: bool,
817 pub properties: Vec<BlockProperty>,
819 pub powered: Option<String>,
821 pub comparator: Option<String>,
823 pub accepts_items: bool,
825 #[data(default)]
827 pub no_occlusion: bool,
828 #[data(default)]
833 pub kind: String,
834 #[data(default)]
836 pub base: Option<String>,
837 #[data(default)]
840 pub set: Option<String>,
841 #[data(default = "thirty")]
843 pub press_ticks: u32,
844 #[data(default)]
847 pub mobs_only: bool,
848}
849
850fn thirty() -> u32 {
851 30
852}
853
854#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
857#[derive(yog_data::Schema, Debug, Clone, PartialEq, Eq)]
858pub struct BlockProperty {
859 pub name: String,
860 pub values: Vec<String>,
861}
862
863impl BlockDef {
864 pub fn new(id: impl Into<String>) -> Self {
865 Self {
866 id: id.into(),
867 hardness: 1.5,
868 resistance: 6.0,
869 name: None,
870 shape: None,
871 light_level: 0,
872 sound: None,
873 requires_tool: false,
874 no_collision: false,
875 slipperiness: 0.0,
876 connects: false,
877 connect_groups: Vec::new(),
878 inventory_id: None,
879 redstone_source: false,
880 facing: String::new(),
881 placement: String::new(),
882 attach: false,
883 waterlogged: false,
884 properties: Vec::new(),
885 powered: None,
886 comparator: None,
887 accepts_items: false,
888 no_occlusion: false,
889 kind: String::new(),
890 base: None,
891 set: None,
892 press_ticks: thirty(),
893 mobs_only: false,
894 }
895 }
896
897 pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
899 self.hardness = hardness;
900 self.resistance = resistance;
901 self
902 }
903
904 pub fn name(mut self, name: impl Into<String>) -> Self {
906 self.name = Some(name.into());
907 self
908 }
909
910 pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
912 self.shape = Some([x1, y1, z1, x2, y2, z2]);
913 self
914 }
915
916 pub fn light_level(mut self, level: u8) -> Self {
918 self.light_level = level.min(15);
919 self
920 }
921
922 pub fn sound(mut self, group: impl Into<String>) -> Self {
925 self.sound = Some(group.into());
926 self
927 }
928
929 pub fn requires_tool(mut self) -> Self {
931 self.requires_tool = true;
932 self
933 }
934
935 pub fn no_collision(mut self) -> Self {
937 self.no_collision = true;
938 self
939 }
940
941 pub fn slipperiness(mut self, value: f32) -> Self {
943 self.slipperiness = value;
944 self
945 }
946
947 pub fn connects_to_neighbors(mut self) -> Self {
953 self.connects = true;
954 self
955 }
956
957 pub fn connect_groups(mut self, groups: &[&str]) -> Self {
964 self.connect_groups = groups
965 .iter()
966 .map(std::string::ToString::to_string)
967 .collect();
968 self
969 }
970
971 pub fn redstone_source(mut self) -> Self {
977 self.redstone_source = true;
978 self
979 }
980
981 pub fn facing(mut self, ways: &str, placement: &str) -> Self {
984 self.facing = ways.to_string();
985 self.placement = placement.to_string();
986 self
987 }
988
989 pub fn attach(mut self) -> Self {
991 self.attach = true;
992 self
993 }
994
995 pub fn waterlogged(mut self) -> Self {
997 self.waterlogged = true;
998 self
999 }
1000
1001 pub fn property(mut self, name: &str, values: &[&str]) -> Self {
1003 self.properties.push(BlockProperty { name: name.into(), values: values.iter().map(|v| v.to_string()).collect() });
1004 self
1005 }
1006
1007 pub fn powered(mut self, name: &str) -> Self {
1009 self.powered = Some(name.into());
1010 self
1011 }
1012
1013 pub fn comparator(mut self, name: &str) -> Self {
1015 self.comparator = Some(name.into());
1016 self
1017 }
1018
1019 pub fn accepts_items(mut self) -> Self {
1021 self.accepts_items = true;
1022 self
1023 }
1024
1025 pub fn inventory(mut self, id: impl Into<String>) -> Self {
1026 self.inventory_id = Some(id.into());
1027 self
1028 }
1029
1030 pub fn no_occlusion(mut self) -> Self {
1032 self.no_occlusion = true;
1033 self
1034 }
1035
1036 pub fn kind(mut self, kind: &str) -> Self {
1038 self.kind = kind.into();
1039 self
1040 }
1041
1042 pub fn base(mut self, id: impl Into<String>) -> Self {
1044 self.base = Some(id.into());
1045 self
1046 }
1047
1048 pub fn set(mut self, name: &str) -> Self {
1050 self.set = Some(name.into());
1051 self
1052 }
1053
1054 pub fn press_ticks(mut self, ticks: u32) -> Self {
1056 self.press_ticks = ticks;
1057 self
1058 }
1059
1060 pub fn mobs_only(mut self) -> Self {
1062 self.mobs_only = true;
1063 self
1064 }
1065}
1066
1067#[derive(yog_data::Schema, Debug, Clone, Copy, PartialEq, Eq, Default)]
1071pub enum EntityKind {
1072 #[default]
1075 Object,
1076 Living,
1078 Mob,
1080 Hanging,
1085}
1086
1087#[derive(yog_data::Schema, Debug, Clone)]
1091pub struct EntityDef {
1092 pub id: String,
1093 pub name: Option<String>,
1095 #[data(default)]
1096 pub kind: EntityKind,
1097 #[data(default = "misc")]
1100 pub category: String,
1101 pub width: f32,
1103 pub height: f32,
1104 pub eye_height: Option<f32>,
1106 #[data(default)]
1108 pub fire_immune: bool,
1109 #[data(default)]
1111 pub no_gravity: bool,
1112 pub pushable: Option<bool>,
1114 #[data(default)]
1116 pub solid: bool,
1117 #[data(default = "eight")]
1119 pub tracking_range: u32,
1120 #[data(default = "three")]
1122 pub update_interval: u32,
1123 #[data(default)]
1127 pub attributes: yog_data::Ordered<f64>,
1128 pub model: Option<String>,
1130 pub animations: Option<String>,
1132 #[data(default)]
1135 pub clips: yog_data::Ordered<String>,
1136 pub spawn_egg: Option<[u32; 2]>,
1138 #[data(default)]
1142 pub goals: Vec<String>,
1143 #[data(default)]
1146 pub ticks: bool,
1147 pub place_sound: Option<String>,
1150 pub break_sound: Option<String>,
1151}
1152
1153fn eight() -> u32 {
1154 8
1155}
1156
1157fn three() -> u32 {
1158 3
1159}
1160
1161fn misc() -> String {
1162 "misc".into()
1163}
1164
1165impl EntityDef {
1166 pub fn new(id: impl Into<String>, kind: EntityKind, width: f32, height: f32) -> Self {
1167 Self {
1168 id: id.into(),
1169 name: None,
1170 kind,
1171 category: misc(),
1172 width,
1173 height,
1174 eye_height: None,
1175 fire_immune: false,
1176 no_gravity: false,
1177 pushable: None,
1178 solid: false,
1179 tracking_range: eight(),
1180 update_interval: three(),
1181 attributes: yog_data::Ordered::default(),
1182 model: None,
1183 animations: None,
1184 clips: yog_data::Ordered::default(),
1185 spawn_egg: None,
1186 goals: Vec::new(),
1187 ticks: false,
1188 place_sound: None,
1189 break_sound: None,
1190 }
1191 }
1192}
1193
1194#[cfg(test)]
1195mod registration_tests {
1196 #[test]
1197 fn a_hot_reload_sees_what_registration_changed() {
1198 let old = "block mymod:a 11\nblock mymod:b 22\nitem mymod:c 33\n";
1199 let new = "block mymod:a 11\nblock mymod:b 99\nitem mymod:d 44\n";
1200 let (added, removed, changed) = super::registration_changes(old, new);
1201 assert_eq!(added, ["item mymod:d"]);
1202 assert_eq!(removed, ["item mymod:c"]);
1203 assert_eq!(changed, ["block mymod:b"]);
1204 assert_eq!(super::registration_changes(old, old), (vec![], vec![], vec![]));
1205 }
1206}
1207
1208#[cfg(test)]
1209mod material_tests {
1210 use super::*;
1211
1212 #[test]
1213 fn the_games_materials_are_built_in() {
1214 assert_eq!(ToolMaterial::built_ins().len(), 6);
1215 let iron = ToolMaterial::built_in("iron").unwrap();
1216 assert_eq!((iron.durability, iron.level_index, iron.incorrect.as_str()), (250, 2, "minecraft:incorrect_for_iron_tool"));
1217 assert_eq!(ArmorMaterial::built_in("minecraft:netherite").unwrap().knockback_resistance, 0.1);
1218 assert_eq!(ArmorMaterial::built_in("diamond").unwrap().defense.chest, 8);
1219 let mythril = ToolMaterial::like("mymod:mythril", "diamond").unwrap();
1220 assert_eq!((mythril.id.as_str(), mythril.durability), ("mymod:mythril", 1561));
1221 }
1222}
1223
1224#[cfg(test)]
1225mod sound_tests {
1226 use super::*;
1227
1228 #[test]
1229 fn the_games_sound_groups_are_built_in() {
1230 assert!(SoundGroup::built_ins().len() > 100);
1231 let stone = SoundGroup::built_in("stone").unwrap();
1232 assert_eq!((stone.break_sound.as_str(), stone.volume), ("minecraft:block.stone.break", 1.0));
1233 assert_eq!(SoundGroup::built_in("minecraft:metal").unwrap().pitch, 1.5);
1234 assert!(SoundGroup::built_in("mymod:crystal").is_none());
1235 }
1236
1237 #[test]
1238 fn a_mods_group_crosses_as_yog_data() {
1239 use yog_data::Schema;
1240 let g = SoundGroup::new("mymod:crystal").pitch(1.2);
1241 assert_eq!(g.fall, "mymod:crystal.fall");
1242 let back = yog_data::from_value::<SoundGroup>(&g.write()).unwrap().value;
1243 assert_eq!(back, g);
1244 assert_eq!(g.write().get("break").and_then(|v| v.as_str()), Some("mymod:crystal.break"));
1245 }
1246}
1247
1248#[cfg(test)]
1249impl Recipe {
1250 fn to_json(&self) -> String {
1251 yog_data::json::to_json(&self.to_data()).to_string()
1252 }
1253}
1254
1255#[cfg(test)]
1256mod recipe_tests {
1257 use super::*;
1258
1259 #[test]
1260 fn writes_the_yog_format() {
1261 let js = |s: &str| serde_json::from_str::<serde_json::Value>(s).unwrap();
1262 let shaped = Recipe::shaped("m:a", "m:b", 2).row("D ").key('D', "#c:dusts").key_any('E', ["m:x", "m:y"]).group("g");
1263 assert_eq!(
1264 js(&shaped.to_json()),
1265 js(r##"{"type":"shaped","pattern":["D "],"key":{"D":"#c:dusts","E":["m:x","m:y"]},"result":"m:b","count":2,"group":"g"}"##)
1266 );
1267 assert_eq!(shaped.to_data().as_map().unwrap().keys().next(), Some("type"), "the type first");
1268 let blast = Recipe::cooking("m:a", Cooking::Blasting, "#c:ores", "m:b", 1).experience(0.7);
1269 assert_eq!(js(&blast.to_json()), js(r##"{"type":"blasting","ingredient":"#c:ores","experience":0.7,"cook_time":100,"result":"m:b"}"##));
1270 let smith = Recipe::smithing("m:a", "m:t", "m:base", "m:add", "m:out");
1271 assert_eq!(js(&smith.to_json())["template"], "m:t");
1272 }
1273}
1274
1275#[cfg(test)]
1276mod json_tests {
1277 use super::*;
1278
1279 #[test]
1281 fn json_survives_any_string() {
1282 let odd = "m:a\"b\\c\nd\te\u{1}";
1283 let parse = |j: String| serde_json::from_str::<serde_json::Value>(&j).unwrap_or_else(|e| panic!("{e}: {j}"));
1284 for r in [
1285 Recipe::shaped(odd, odd, 1).row("A").key('A', odd).group(odd).category(odd),
1286 Recipe::shapeless(odd, odd, 3).ingredient(odd).ingredient_any([odd, "m:x"]),
1287 Recipe::cooking(odd, Cooking::Smoking, odd, odd, 1).cook_time(5),
1288 Recipe::stonecutting(odd, odd, odd, 2),
1289 Recipe::smithing(odd, odd, odd, odd, odd),
1290 ] {
1291 let v = parse(r.to_json());
1292 assert_eq!(v["result"], odd, "the string comes back as it was");
1293 }
1294 let g = StartupGrant::new(odd).item(odd).book("mymod:\"guide\"").command("say \"hi\" \\o/");
1295 let v = parse(g.to_json());
1296 assert_eq!(v["id"], odd);
1297 assert_eq!(v["entries"][2]["command"], "say \"hi\" \\o/");
1298 }
1299
1300 #[test]
1301 fn recipes_and_grants_name_their_parts() {
1302 let r = Recipe::cooking("mymod:a", Cooking::Campfire, "minecraft:beef", "minecraft:cooked_beef", 1);
1303 let v: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap();
1304 assert_eq!(v["type"], "campfire_cooking");
1305 assert_eq!(v["cook_time"], 600, "the method's default time");
1306 assert_eq!(r.ns_name(), ("mymod", "a"));
1307 assert_eq!(Recipe::shaped("plain", "x", 1).ns_name().1, "plain");
1308 let g = StartupGrant::new("mymod:starter");
1309 assert!(g.to_json().contains(r#""entries":[]"#));
1310 }
1311}