1mod block_item;
23mod body;
24mod book;
25mod datapack;
26mod defs;
27mod import;
28pub mod json;
29mod looks;
30pub(crate) mod material;
31mod model;
32mod outputs;
33mod recipe;
34mod refs;
35mod source;
36mod table;
37mod textures;
38
39pub use import::import_command;
40pub use source::write_source;
41pub use outputs::merge_outputs;
42
43use yog_ids::Format;
44use crate::yres;
45use yog_data::Path as At;
46use json::num_value;
47use serde_json::{Map, Value, json};
48use std::collections::{BTreeMap, BTreeSet, HashMap};
49use std::path::{Path, PathBuf};
50use yog_data::Schema;
51use crate::content::block_item::compile_file;
52use crate::content::block_item::inventory_screens;
53use crate::content::body::{BodyCx, compile_body};
54use crate::content::body::compile_entity;
55use crate::content::body::compile_font;
56use crate::content::book::compile_book;
57use crate::content::book::compile_layout;
58use crate::content::book::recipe_layouts;
59use crate::content::defs::definition_path;
60use crate::content::defs::dimension_path;
61use crate::content::defs::loot_path;
62use crate::content::defs::read_defs;
63use crate::content::defs::tool_level_tags;
64use crate::content::outputs::version_overlays;
65use crate::content::recipe::compile_recipe;
66use crate::content::source::float;
67use crate::content::source::mentions_content_macro;
68use crate::content::source::registrations;
69use crate::content::source::{Content, rust_source};
70
71pub const CONTENT_DIR: &str = yog_ids::project::CONTENT;
73pub const GENERATED_FILE: &str = ".yog-build/generated/yog-content.g.rs";
75
76pub type Output = (String, Vec<u8>);
78
79const CONTENT: [Format; 9] = [
81 Format::Block,
82 Format::Item,
83 Format::Recipe,
84 Format::Book,
85 Format::Def,
86 Format::Model,
87 Format::Anim,
88 Format::Entity,
89 Format::Font,
90];
91
92fn content_format(path: &Path) -> Option<Format> {
94 Format::of_path(path).filter(|f| CONTENT.contains(f))
95}
96
97#[derive(Debug, Default)]
99pub struct Compiled {
100 pub outputs: Vec<Output>,
102 lang: BTreeMap<(String, String), BTreeMap<String, String>>,
105 lang_aliases: Vec<(String, String)>,
107 tags: BTreeMap<String, BTreeMap<String, bool>>,
110 pub source: Option<Source>,
112 pub data_files: Vec<(String, Vec<u8>)>,
115 pub warnings: Vec<String>,
116 pub registrations: String,
119 pub packed: BTreeSet<String>,
121 pub blocks: usize,
122 pub items: usize,
123 pub recipes: usize,
124 pub books: usize,
125}
126
127pub const DIRECTIONS: [&str; 6] = ["down", "up", "north", "south", "west", "east"];
130pub const DISPLAY_SLOTS: [&str; 8] = [
131 "thirdperson_righthand",
132 "thirdperson_lefthand",
133 "firstperson_righthand",
134 "firstperson_lefthand",
135 "gui",
136 "head",
137 "ground",
138 "fixed",
139];
140pub const RENDER_LAYERS: [&str; 4] = ["solid", "cutout", "cutout_mipped", "translucent"];
141
142pub struct Template {
144 pub name: &'static str,
145 pub parent: &'static str,
147 pub slots: &'static [&'static str],
149 pub sprite: bool,
151 pub has_particle: bool,
153}
154
155pub const TEMPLATES: &[Template] = &[
156 Template {
157 name: "cube_all",
158 parent: "minecraft:block/cube_all",
159 slots: &["all"],
160 sprite: false,
161 has_particle: true,
162 },
163 Template {
164 name: "cube_column",
165 parent: "minecraft:block/cube_column",
166 slots: &["end", "side"],
167 sprite: false,
168 has_particle: true,
169 },
170 Template {
171 name: "cube_bottom_top",
172 parent: "minecraft:block/cube_bottom_top",
173 slots: &["top", "bottom", "side"],
174 sprite: false,
175 has_particle: true,
176 },
177 Template {
178 name: "cube",
179 parent: "minecraft:block/cube",
180 slots: &["down", "up", "north", "south", "west", "east"],
181 sprite: false,
182 has_particle: false,
183 },
184 Template {
185 name: "orientable",
186 parent: "minecraft:block/orientable",
187 slots: &["front", "side", "top"],
188 sprite: false,
189 has_particle: true,
190 },
191 Template {
192 name: "cross",
193 parent: "minecraft:block/cross",
194 slots: &["cross"],
195 sprite: false,
196 has_particle: true,
197 },
198 Template {
199 name: "generated",
200 parent: "minecraft:item/generated",
201 slots: &["layer0"],
202 sprite: true,
203 has_particle: true,
204 },
205 Template {
206 name: "handheld",
207 parent: "minecraft:item/handheld",
208 slots: &["layer0"],
209 sprite: true,
210 has_particle: true,
211 },
212];
213
214pub fn template(name: &str) -> Option<&'static Template> {
215 TEMPLATES.iter().find(|t| t.name == name)
216}
217
218pub fn valid_namespace(s: &str) -> bool {
220 !s.is_empty()
221 && s.bytes().all(|b| {
222 b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'.' | b'-')
223 })
224}
225
226pub fn valid_path(s: &str) -> bool {
228 !s.is_empty()
229 && !s.starts_with('/')
230 && !s.ends_with('/')
231 && s.bytes().all(|b| {
232 b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'.' | b'-' | b'/')
233 })
234}
235
236pub fn valid_id(s: &str) -> bool {
238 match s.split_once(':') {
239 Some((ns, path)) => valid_namespace(ns) && valid_path(path),
240 None => valid_path(s),
241 }
242}
243
244pub fn find(root: &Path) -> Vec<PathBuf> {
248 fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
249 let Ok(entries) = std::fs::read_dir(dir) else {
250 return;
251 };
252 for entry in entries.flatten() {
253 let path = entry.path();
254 if path.is_dir() {
255 walk(&path, out);
256 } else if content_format(&path).is_some() {
257 out.push(path);
258 }
259 }
260 }
261 let mut out = Vec::new();
262 walk(&root.join(CONTENT_DIR), &mut out);
263 out.sort();
264 out
265}
266
267fn display(root: &Path, p: &Path) -> String {
268 p.strip_prefix(root)
269 .unwrap_or(p)
270 .to_string_lossy()
271 .replace('\\', "/")
272}
273
274#[derive(Debug, Clone, Default)]
277struct BlockProps {
278 hardness: Option<f64>,
279 resistance: Option<f64>,
280 sound: Option<String>,
281 light: u8,
282 tools: Vec<String>,
284 tool_level: Option<(String, usize)>,
287 requires_tool: bool,
288 no_collision: bool,
289 slipperiness: Option<f64>,
290 shape: Option<[f64; 6]>,
292 connect_groups: Vec<String>,
293 connects: bool,
294 facing: String,
296 placement: String,
298 attach: bool,
299 waterlogged: bool,
300 properties: Vec<(String, Vec<String>)>,
302 powered: Option<String>,
304 comparator: Option<String>,
305 accepts_items: bool,
307 inventory: Option<String>,
309 redstone: bool,
311 no_occlusion: bool,
313 kind: String,
315 base: Option<String>,
317 set: Option<String>,
318 press_ticks: Option<u32>,
319 mobs_only: bool,
320}
321
322#[derive(Debug, Clone, Default)]
323struct ItemProps {
324 stack_size: Option<u8>,
325 durability: u32,
326 fire_resistant: bool,
327 fuel_ticks: u32,
328 food: Option<(u32, f64, bool)>,
330 tool: Option<(String, String, f32, f32)>,
332 armor: Option<(String, String)>,
334 wearable: Option<String>,
336 hangs: Option<(String, Option<[u32; 2]>)>,
338}
339
340#[derive(Debug, Clone)]
342struct Entry {
343 kind: Format,
344 id: String,
345 file: String,
346 auto: bool,
347 requires: Vec<String>,
349 block: BlockProps,
350 item: ItemProps,
351 potion: Option<yog_registry::PotionDef>,
352}
353
354struct Ctx<'a> {
356 root: &'a Path,
357 refs: &'a yres::Refs,
358 file: String,
359 ns: String,
360 errors: &'a mut Vec<String>,
361 warnings: &'a mut Vec<String>,
362 own_textures: &'a BTreeSet<String>,
364 inventories: &'a mut Vec<(String, usize, String)>,
367 deferred: &'a mut Vec<(String, usize, String, yog_data::Ref)>,
370}
371
372impl Ctx<'_> {
373 fn err(&mut self, line: usize, msg: impl std::fmt::Display) {
374 self.errors.push(format!("{}:{line}: {msg}", self.file));
375 }
376
377 fn warn(&mut self, line: usize, msg: impl std::fmt::Display) {
378 self.warnings.push(format!("{}:{line}: {msg}", self.file));
379 }
380
381 fn err_at(&mut self, loc: Loc, path: &At, msg: impl std::fmt::Display) {
383 let line = loc(path);
384 self.err(line, format!("{path}: {msg}"));
385 }
386
387 fn warn_at(&mut self, loc: Loc, path: &At, msg: impl std::fmt::Display) {
388 let line = loc(path);
389 self.warn(line, format!("{path}: {msg}"));
390 }
391
392 fn schema_errors(&mut self, errors: yog_data::Errors) {
394 for e in errors.0 {
395 let l = e.at.map_or(1, |a| a.line as usize);
396 if e.path.is_root() {
397 self.err(l, e.message)
398 } else {
399 self.err(l, format!("{}: {}", e.path, e.message))
400 }
401 }
402 }
403
404 fn texture(&mut self, loc: Loc, path: &At, t: &yog_formats::content::Texture) -> Option<String> {
407 use yog_formats::content::{SLOT_MARK, Texture};
408 match t {
409 Texture::Slot(slot) => Some(format!("{SLOT_MARK}{slot}")),
410 Texture::Ref(id) => {
411 let (ns, tex) = yog_ids::split(id);
412 if ns == self.ns && !self.own_textures.contains(id) {
413 self.err_at(loc, path, format!("`{}{id}`: no {}/{tex}.{}", yog_ids::reference::TEXTURE, yres::RESOURCES_DIR, Format::Texture.ext()));
414 return None;
415 }
416 Some(id.clone())
417 }
418 }
419 }
420}
421
422type Loc<'a> = &'a dyn Fn(&At) -> usize;
424
425struct Model {
427 json: Value,
428 bounds: Option<[f64; 6]>,
430}
431
432fn pretty(v: &Value) -> Vec<u8> {
433 let mut s = serde_json::to_string_pretty(v).unwrap_or_default();
434 s.push('\n');
435 s.into_bytes()
436}
437
438#[derive(Debug, Clone)]
440enum RecipeKind {
441 Shaped { pattern: Vec<String>, key: Vec<(char, Vec<String>)> },
444 Shapeless(Vec<Vec<String>>),
446 Cooking { method: yog_registry::Cooking, input: Vec<String>, experience: f64, cook_time: u32 },
447 Stonecutting(Vec<String>),
448 Smithing { template: Vec<String>, base: Vec<String>, addition: Vec<String> },
449 Custom { kind: String, slots: Vec<(String, Vec<Vec<String>>)>, values: serde_json::Map<String, Value> },
453}
454
455struct BookFile {
457 id: String,
458 data: Vec<u8>,
461}
462
463struct EntityFile {
465 id: String,
466 file: String,
467 model: Option<String>,
469 animations: Option<String>,
470 data: Vec<u8>,
472}
473
474impl EntityFile {
475 fn path(&self) -> String {
476 format!("entities/{}.ydata", self.id.replace([':', '/'], "__"))
477 }
478}
479
480struct BodyFile {
482 model: bool,
483 id: String,
485 data: Vec<u8>,
487}
488
489struct FontFile {
491 id: String,
493 data: Vec<u8>,
495}
496
497impl FontFile {
498 fn path(&self) -> String {
499 format!("fonts/{}.yfont", self.id.replace([':', '/'], "__"))
500 }
501}
502
503impl BodyFile {
504 fn path(&self) -> String {
506 let dir = if self.model { "models" } else { "anims" };
507 format!("body/{dir}/{}.ydata", self.id.replace([':', '/'], "__"))
508 }
509}
510
511impl BookFile {
512 fn path(&self) -> String {
514 format!("books/{}.ydata", self.id.replace(':', "__"))
515 }
516}
517
518type Lang = BTreeMap<(String, String), BTreeMap<String, String>>;
520
521const TAG_REF: &str = "tag:";
523
524type OreOut = (Vec<Output>, (String, String, String));
526
527#[derive(Debug, Clone)]
529struct Recipe {
530 id: String,
531 ty: String,
533 kind: RecipeKind,
534 result: String,
535 count: u32,
536 group: Option<String>,
537 category: Option<String>,
538 requires: Vec<String>,
540}
541
542impl Recipe {
543 fn registration(&self) -> String {
545 let list = |items: &[String]| {
546 let quoted: Vec<String> = items.iter().map(|i| format!("{i:?}.into()")).collect();
547 format!("vec![{}]", quoted.join(", "))
548 };
549 let kind = match &self.kind {
550 RecipeKind::Shaped { pattern, key } => {
551 let rows: Vec<String> = pattern.iter().map(|r| format!("{r:?}.into()")).collect();
552 let keys: Vec<String> = key.iter().map(|(c, items)| format!("({c:?}, {})", list(items))).collect();
553 format!("Shaped {{ pattern: vec![{}], key: vec![{}] }}", rows.join(", "), keys.join(", "))
554 }
555 RecipeKind::Shapeless(ingredients) => {
556 let all: Vec<String> = ingredients.iter().map(|i| list(i)).collect();
557 format!("Shapeless {{ ingredients: vec![{}] }}", all.join(", "))
558 }
559 RecipeKind::Cooking { method, input, experience, cook_time } => format!(
560 "Cooking {{ method: ::yog_api::Cooking::{method:?}, ingredient: {}, experience: {}_f32, cook_time: {cook_time} }}",
561 list(input),
562 float(*experience)
563 ),
564 RecipeKind::Stonecutting(input) => format!("Stonecutting {{ ingredient: {} }}", list(input)),
565 RecipeKind::Custom { kind, slots, values } => {
566 let mut o = Map::new();
568 o.insert("type".into(), kind.clone().into());
569 o.insert("slots".into(), Value::Object(slots.iter().map(|(k, v)| (k.clone(), json!(v))).collect()));
570 if !values.is_empty() {
571 o.insert("values".into(), Value::Object(values.clone()));
572 }
573 o.insert("result".into(), self.result.clone().into());
574 if self.count != 1 {
575 o.insert("count".into(), self.count.into());
576 }
577 if let Some(g) = &self.group {
578 o.insert("group".into(), g.clone().into());
579 }
580 return format!("registry.add_recipe_json({:?}, {:?});", self.id, Value::Object(o).to_string());
581 }
582 RecipeKind::Smithing { template, base, addition } => format!(
583 "Smithing {{ template: {}, base: {}, addition: {} }}",
584 list(template),
585 list(base),
586 list(addition)
587 ),
588 };
589 let opt = |o: &Option<String>| o.as_ref().map_or("None".to_string(), |v| format!("Some({v:?}.into())"));
590 format!(
591 "registry.add_recipe(::yog_api::Recipe {{ id: {:?}.into(), kind: ::yog_api::RecipeKind::{kind}, result: {:?}.into(), count: {}, group: {}, category: {} }});",
592 self.id,
593 self.result,
594 self.count,
595 opt(&self.group),
596 opt(&self.category)
597 )
598 }
599}
600
601struct Defs {
604 order: BTreeMap<String, Vec<String>>,
605 sound_groups: Vec<yog_formats::def::SoundGroup>,
606 tool_materials: Vec<yog_formats::def::ToolMaterial>,
607 armor_materials: Vec<yog_formats::def::ArmorMaterial>,
608 biome_features: Vec<(String, String, String)>,
610 dimensions: Vec<String>,
612 tables: Vec<table::Table>,
614 loot_modifiers: Vec<String>,
616 definitions: Vec<(&'static str, String)>,
618 advancements: Vec<String>,
620}
621
622impl Defs {
623 fn level(&self, id: &str) -> Option<(&str, usize)> {
625 self.order.iter().find_map(|(l, levels)| levels.iter().position(|x| x == id).map(|i| (l.as_str(), i)))
626 }
627}
628
629pub fn compile(root: &Path, mod_id: &str, refs: &yres::Refs) -> Result<Compiled, String> {
632 let mut out = Compiled::default();
633 let files = find(root);
634 if files.is_empty() {
635 return Ok(out);
636 }
637 let mod_ns = valid_namespace(mod_id).then_some(mod_id);
638 let mut errors: Vec<String> = Vec::new();
639 let mut warnings: Vec<String> = Vec::new();
640 let own_textures = textures::compile(root, mod_id, refs, &mut errors);
641 let mut generated: BTreeMap<String, String> = BTreeMap::new();
642 let mut entries: Vec<Entry> = Vec::new();
643 let mut ids: HashMap<String, String> = HashMap::new();
644 let mut recipes: Vec<Recipe> = Vec::new();
645 let mut recipe_ids: HashMap<String, String> = HashMap::new();
646 let mut books: Vec<BookFile> = Vec::new();
647 let mut book_ids: HashMap<String, String> = HashMap::new();
648 let mut bodies: Vec<BodyFile> = Vec::new();
649 let mut fonts: Vec<FontFile> = Vec::new();
650 let mut font_ids: HashMap<String, String> = HashMap::new();
651 let mut entity_files: Vec<EntityFile> = Vec::new();
652 let mut body_ids: HashMap<(bool, String), String> = HashMap::new();
653 let mut inventories: Vec<(String, usize, String)> = Vec::new();
654 let mut pending_books: Vec<(String, String)> = Vec::new();
655 let mut pending_recipes: Vec<(String, String, PathBuf)> = Vec::new();
656 let mut pending_layouts: Vec<(String, String)> = Vec::new();
657 let mut pending_defs: Vec<(String, String)> = Vec::new();
658 let mut deferred: Vec<(String, usize, String, yog_data::Ref)> = Vec::new();
659 for path in &files {
660 let shown = display(root, path);
661 let Some(kind) = content_format(path) else { continue };
662 let text = match std::fs::read_to_string(path) {
663 Ok(t) => t,
664 Err(e) => {
665 errors.push(format!("{shown}: {e}"));
666 continue;
667 }
668 };
669 if matches!(kind, Format::Model | Format::Anim) {
670 if let Some(b) = compile_body(&BodyCx { root, ns: mod_ns.unwrap_or(yog_ids::ns::MINECRAFT), own_textures: &own_textures.ids }, &shown, &text, kind, path, &mut errors) {
671 if let Some(other) = body_ids.insert((kind == Format::Model, b.id.clone()), shown.clone()) {
672 errors.push(format!("{shown}: {} {} is also defined by {other}", kind.noun(), b.id));
673 }
674 bodies.push(b);
675 }
676 continue;
677 }
678 if kind == Format::Font {
679 if let Some(font) = compile_font(&shown, &text, root, path, mod_ns.unwrap_or("minecraft"), &mut errors) {
680 if let Some(other) = font_ids.insert(font.id.clone(), shown.clone()) {
681 errors.push(format!("{shown}: font {} is also defined by {other}", font.id));
682 }
683 fonts.push(font);
684 }
685 continue;
686 }
687 if kind == Format::Book {
688 pending_books.push((shown.clone(), text));
690 continue;
691 }
692 if kind == Format::Recipe {
693 pending_recipes.push((shown.clone(), text, path.clone()));
695 continue;
696 }
697 if kind == Format::Def {
698 let of = yog_data::text::parse_value(&text).ok().and_then(|v| v.get("kind").and_then(|k| k.as_str()).map(str::to_string));
700 if matches!(of.as_deref(), Some(yog_ids::kind::RECIPE_KIND | yog_ids::kind::ORE | yog_ids::kind::DIMENSION | yog_ids::kind::TABLE | yog_ids::kind::TAG | yog_ids::kind::DAMAGE_TYPE | yog_ids::kind::TREE | yog_ids::kind::LOOT_MODIFIER | yog_ids::kind::LOOT_TABLE | yog_ids::kind::ATTRIBUTE | yog_ids::kind::EFFECT | yog_ids::kind::ADVANCEMENT)) {
701 pending_layouts.push((shown.clone(), text));
702 } else {
703 pending_defs.push((shown.clone(), text));
704 }
705 continue;
706 }
707 let mut cx = Ctx {
708 root,
709 refs,
710 file: shown.clone(),
711 ns: mod_ns.unwrap_or("minecraft").to_string(),
712 errors: &mut errors,
713 warnings: &mut warnings,
714 own_textures: &own_textures.ids,
715 inventories: &mut inventories,
716 deferred: &mut deferred,
717 };
718 if kind == Format::Entity {
719 if let Some(e) = compile_entity(&mut cx, &text, path, mod_ns, &mut out) {
720 if let Some(other) = ids.insert(e.id.clone(), shown.clone()) {
721 errors.push(format!("{shown}: id {} is also defined by {other}", e.id));
722 }
723 entity_files.push(e);
724 }
725 continue;
726 }
727 if let Some(entry) = compile_file(&mut cx, &text, kind, path, mod_ns, &mut out, &mut generated)
728 {
729 if let Some(other) = ids.insert(entry.id.clone(), shown.clone()) {
730 errors.push(format!(
731 "{shown}: id {} is also defined by {other}",
732 entry.id
733 ));
734 }
735 entries.push(entry);
736 }
737 }
738 let mut potion_defs: BTreeMap<String, yog_registry::PotionDef> = entries
739 .iter()
740 .filter_map(|e| e.potion.clone().map(|p| (e.id.clone(), p)))
741 .collect();
742 entries.retain(|e| e.potion.is_none());
743 let manifest = crate::manifest::YogToml::read(&root.join(yog_ids::file::MANIFEST)).ok();
745 let externals = manifest.as_ref().map(|t| t.externals.clone()).unwrap_or_default();
746 let datapack = manifest.as_ref().and_then(|t| t.datapack.clone());
747 let mut known = refs::Known::new(mod_ns.unwrap_or("minecraft"), &externals, datapack.as_deref());
748 for e in &entries {
749 known.add(if e.kind == Format::Block { refs::RefKind::Block } else { refs::RefKind::Item }, e.id.clone());
750 }
751 for id in potion_defs.keys() {
752 known.add(refs::RefKind::Item, id.clone());
753 }
754 for e in &entity_files {
755 known.add(refs::RefKind::Entity, e.id.clone());
756 for (wanted, model, what) in [(&e.model, true, "model"), (&e.animations, false, "animations")] {
758 if let Some(id) = wanted
759 && mod_ns.is_some_and(|ns| id.starts_with(&format!("{ns}:")))
760 && !bodies.iter().any(|b| b.model == model && &b.id == id)
761 {
762 errors.push(format!("{}: {what} `{id}`: no such file in content/ (`.{}`)", e.file, if model { Format::Model } else { Format::Anim }.ext()));
763 }
764 }
765 }
766 let mut recipe_defs: HashMap<String, yog_formats::def::RecipeKindDef> = HashMap::new();
769 let mut ores: Vec<OreOut> = Vec::new();
770 let mut dimensions: Vec<(String, Vec<u8>)> = Vec::new();
771 let mut tables: Vec<table::Table> = Vec::new();
772 let mut loot: Vec<(String, Vec<u8>)> = Vec::new();
774 let mut definitions: Vec<(&'static str, String, Vec<u8>)> = Vec::new();
776 let mut brews: Vec<(String, yog_registry::Brew)> = Vec::new();
777 let mut advancements: Vec<String> = Vec::new();
778 for (shown, text) in &pending_layouts {
779 let mut cx = Ctx {
780 root,
781 refs,
782 file: shown.clone(),
783 ns: mod_ns.unwrap_or("minecraft").to_string(),
784 errors: &mut errors,
785 warnings: &mut warnings,
786 own_textures: &own_textures.ids,
787 inventories: &mut inventories,
788 deferred: &mut deferred,
789 };
790 let Some(k) = compile_layout(&mut cx, text, mod_ns, &known, &mut ores, &mut dimensions, &mut tables, &mut loot, &mut definitions, &mut advancements, &mut out) else { continue };
791 if recipe_defs.contains_key(&k.id) {
792 errors.push(format!("{shown}: recipe kind {} is declared twice", k.id));
793 }
794 recipe_defs.insert(k.id.clone(), k);
795 }
796 let layouts = recipe_layouts(root, mod_ns.unwrap_or("minecraft"), &recipe_defs, &known, &mut errors);
798 for (shown, text, path) in &pending_recipes {
799 let mut cx = Ctx {
800 root,
801 refs,
802 file: shown.clone(),
803 ns: mod_ns.unwrap_or("minecraft").to_string(),
804 errors: &mut errors,
805 warnings: &mut warnings,
806 own_textures: &own_textures.ids,
807 inventories: &mut inventories,
808 deferred: &mut deferred,
809 };
810 if let Some(r) = compile_recipe(&mut cx, text, path, mod_ns, &known, &recipe_defs, &mut brews) {
811 if let Some(other) = recipe_ids.insert(r.id.clone(), shown.clone()) {
812 errors.push(format!("{shown}: recipe {} is also defined by {other}", r.id));
813 }
814 recipes.push(r);
815 }
816 }
817 for r in &recipes {
818 known.add(refs::RefKind::Recipe, r.id.clone());
819 }
820 for (to, brew) in brews {
821 if !potion_defs.contains_key(&brew.from) && !refs::is_vanilla_potion(&brew.from) {
822 errors.push(format!("brewing recipe input `{}`: no potion `.yitem` or Yog default potion alias defines it", brew.from));
823 continue;
824 }
825 if let Some(potion) = potion_defs.get_mut(&to) {
826 potion.brewed.push(brew);
827 } else if refs::is_vanilla_potion(&to) {
828 potion_defs
829 .entry(to.clone())
830 .or_insert_with(|| yog_registry::PotionDef { id: to, name: None, effects: Vec::new(), brewed: Vec::new() })
831 .brewed
832 .push(brew);
833 } else {
834 errors.push(format!("brewing recipe result `{to}`: no potion `.yitem` or Yog default potion alias defines it"));
835 }
836 }
837 let mut defs = read_defs(&pending_defs, mod_ns, &mut errors);
839 definitions.extend(potion_defs.into_iter().map(|(id, potion)| (yog_ids::kind::POTION, id, yog_data::binary::encode(&potion.write()))));
840 for (files, feature) in ores {
841 out.outputs.extend(files);
842 defs.biome_features.push(feature);
843 }
844 for (file, line, path, r) in &deferred {
846 let found = match r.kind.as_str() {
847 yog_ids::kind::LEVEL => defs.level(&r.id).map(|_| ()).ok_or_else(|| format!("no level {} — the game's tool levels or a .ydef's", r.id)),
848 yog_ids::kind::LADDER => defs.order.contains_key(&r.id).then_some(()).ok_or_else(|| format!("no ladder {}", r.id)),
849 yog_ids::kind::TOOL_MATERIAL => (defs.tool_materials.iter().any(|m| m.id == r.id) || yog_formats::def::ToolMaterial::built_in(&r.id).is_some())
850 .then_some(())
851 .ok_or_else(|| format!("no tool material {} — the game's or a .ydef's", r.id)),
852 yog_ids::kind::ARMOR_MATERIAL => (defs.armor_materials.iter().any(|m| m.id == r.id) || yog_formats::def::ArmorMaterial::built_in(&r.id).is_some())
853 .then_some(())
854 .ok_or_else(|| format!("no armor material {} — the game's or a .ydef's", r.id)),
855 yog_ids::kind::SOUND_GROUP => (defs.sound_groups.iter().any(|g| g.id == r.id) || yog_formats::def::SoundGroup::built_in(&r.id).is_some())
856 .then_some(())
857 .ok_or_else(|| format!("no sound group {} — the game's or a .ydef's", r.id)),
858 kind if kind.starts_with(TAG_REF) => {
859 let mut found = kind[TAG_REF.len()..].split('|').map(|k| known.check_tag(k, &r.id));
861 let first = found.next().unwrap_or(Ok(()));
862 if first.is_ok() || found.any(|f| f.is_ok()) { Ok(()) } else { first }
863 }
864 kind => match refs::RefKind::from_word(kind) {
865 Some(k) => known.check(k, &r.id),
866 None => Ok(()),
867 },
868 };
869 if let Err(e) = found {
870 errors.push(format!("{file}:{line}: {path}: {e}"));
871 }
872 }
873 for e in &entries {
874 if let Some((level, line)) = &e.block.tool_level {
875 tool_level_tags(&e.id, level, &defs, &mut out, &mut |m| errors.push(format!("{}:{line}: block.tool_level: {m}", e.file)), &mut |m| warnings.push(format!("{}:{line}: block.tool_level: {m}", e.file)));
876 }
877 }
878 let recipe_kinds: HashMap<String, String> = recipes.iter().map(|r| (r.id.clone(), r.ty.clone())).collect();
879 let page_types = crate::book_page::find(root, mod_ns.unwrap_or("minecraft"), &mut errors);
881 for (shown, text) in &pending_books {
882 let mut cx = Ctx {
883 root,
884 refs,
885 file: shown.clone(),
886 ns: mod_ns.unwrap_or("minecraft").to_string(),
887 errors: &mut errors,
888 warnings: &mut warnings,
889 own_textures: &own_textures.ids,
890 inventories: &mut inventories,
891 deferred: &mut deferred,
892 };
893 if let Some(b) = compile_book(&mut cx, text, mod_ns, &known, &layouts, &recipe_kinds, &page_types) {
894 if let Some(other) = book_ids.insert(b.id.clone(), shown.clone()) {
895 errors.push(format!("{shown}: book {} is also defined by {other}", b.id));
896 }
897 books.push(b);
898 }
899 }
900 if let Some(ns) = mod_ns {
903 let screens = inventory_screens(&root.join("src"));
904 for (file, line, id) in &inventories {
905 if id.split_once(':').is_some_and(|(n, _)| n == ns) && !screens.contains(id) {
906 errors.push(format!(
907 "{file}:{line}: block.inventory `{id}`: no .yui <Screen id=\"{id}\"> with <Slot>s in src/"
908 ));
909 }
910 }
911 }
912 out.outputs.extend(own_textures.outputs);
913 out.packed = own_textures.packed;
914 for m in &defs.armor_materials {
916 looks::equipment(root, &mut out, &m.texture, &mut warnings);
917 }
918 out.warnings = warnings;
919 if !errors.is_empty() {
920 return Err(format!(
921 "invalid blocks/items in content/:\n {}",
922 errors.join("\n ")
923 ));
924 }
925 out.blocks = entries.iter().filter(|e| e.kind == Format::Block).count();
926 out.items = entries.len() - out.blocks;
927 out.recipes = recipes.len();
928 out.books = books.len();
929 if (entries.iter().any(|e| e.auto) || !recipes.is_empty() || !books.is_empty() || !bodies.is_empty() || !entity_files.is_empty() || !fonts.is_empty() || !defs.sound_groups.is_empty() || !defs.tool_materials.is_empty() || !defs.armor_materials.is_empty() || !defs.biome_features.is_empty() || !dimensions.is_empty() || !loot.is_empty() || !definitions.is_empty())
930 && !mentions_content_macro(&root.join("src"))
931 {
932 out.warnings.push(format!(
933 "{} has blocks/items, but src/ never calls `yog_api::content!();` — they get models but aren't registered",
934 CONTENT_DIR
935 ));
936 }
937 out.data_files = books.iter().map(|b| (b.path(), b.data.clone())).collect();
938 out.data_files.extend(bodies.iter().map(|b| (b.path(), b.data.clone())));
939 out.data_files.extend(fonts.iter().map(|f| (f.path(), f.data.clone())));
940 out.data_files.extend(entity_files.iter().map(|e| (e.path(), e.data.clone())));
941 out.data_files.extend(dimensions.iter().map(|(id, data)| (dimension_path(id), data.clone())));
942 out.data_files.extend(loot.iter().map(|(id, data)| (loot_path(id), data.clone())));
943 out.data_files.extend(definitions.iter().map(|(kind, id, data)| (definition_path(kind, id), data.clone())));
944 defs.definitions = definitions.iter().map(|(kind, id, _)| (*kind, id.clone())).collect();
945 defs.loot_modifiers = loot.into_iter().map(|(id, _)| id).collect();
946 defs.advancements = advancements;
947 for m in &defs.tool_materials {
949 if yog_ids::split(&m.incorrect).0 != yog_ids::ns::MINECRAFT {
950 out.tags.entry(datapack::overlay(&yog_registry::ToolLadder::game().incorrect_when, &datapack::tag_file(yog_ids::kind::BLOCK, &m.incorrect))).or_default();
951 }
952 }
953 defs.dimensions = dimensions.iter().map(|(id, _)| id.clone()).collect();
954 defs.tables = tables;
955 out.source = Some(rust_source(&Content {
956 entries: &entries,
957 recipes: &recipes,
958 books: &books,
959 bodies: &bodies,
960 entities: &entity_files,
961 fonts: &fonts,
962 defs: &defs,
963 textures: &own_textures.ids,
964 }));
965 version_overlays(&mut out, &known.datapack);
966 out.registrations = registrations(&entries, &entity_files, &dimensions, &defs);
967 Ok(out)
968}
969
970const GENERATED_HEADER: &str = "// <auto-generated>\n// Generated by `yog build` from content/** — don't edit, it's rewritten\n// on every build. Included by `yog_api::content!()`.\n// </auto-generated>\n";
972
973pub const GENERATED_PARTS_DIR: &str = "content";
976
977#[derive(Debug, Clone, Default)]
980pub struct Source {
981 pub root: String,
982 pub parts: Vec<(String, String)>,
983}
984
985impl Source {
986 #[cfg(test)]
988 pub fn part(&self, name: &str) -> &str {
989 self.parts.iter().find(|(n, _)| n == name).map_or("", |(_, c)| c.as_str())
990 }
991
992 #[cfg(test)]
994 pub fn all(&self) -> String {
995 self.parts.iter().map(|(_, c)| c.as_str()).collect()
996 }
997}
998
999pub const TAGS_FILE: &str = ".yog-build/generated/tags.json";
1002
1003#[derive(Default)]
1006pub struct OutputSet {
1007 items: Vec<Option<Output>>,
1008 at: HashMap<String, usize>,
1009}
1010
1011impl OutputSet {
1012 pub fn get(&self, path: &str) -> Option<&[u8]> {
1013 let i = *self.at.get(path)?;
1014 self.items[i].as_ref().map(|(_, b)| b.as_slice())
1015 }
1016
1017 pub fn contains(&self, path: &str) -> bool {
1018 self.at.contains_key(path)
1019 }
1020
1021 pub fn paths(&self) -> impl Iterator<Item = &String> {
1022 self.at.keys()
1023 }
1024
1025 pub fn put(&mut self, path: String, bytes: Vec<u8>) {
1027 if let Some(i) = self.at.insert(path.clone(), self.items.len()) {
1028 self.items[i] = None;
1029 }
1030 self.items.push(Some((path, bytes)));
1031 }
1032
1033 pub fn into_vec(self) -> Vec<Output> {
1034 self.items.into_iter().flatten().collect()
1035 }
1036}
1037
1038impl From<Vec<Output>> for OutputSet {
1039 fn from(outputs: Vec<Output>) -> Self {
1040 let mut set = OutputSet::default();
1041 for (path, bytes) in outputs {
1042 set.put(path, bytes);
1043 }
1044 set
1045 }
1046}
1047
1048#[cfg(test)]
1049mod tests;