Skip to main content

yog_cli/content/
compile.rs

1//! Compiling a mod's content files: each one read and checked, then what
2//! they name checked against everything they define, then the code and the
3//! data made of them.
4
5use std::collections::{BTreeMap, HashMap};
6use std::path::{Path, PathBuf};
7use yog_data::Schema;
8use yog_ids::Format;
9use yog_ids::ns::MINECRAFT;
10use yog_registry::{Brew, PotionDef};
11
12use crate::content::block_item::{compile_file, inventory_screens};
13use crate::content::body::{compile_body, compile_entity, compile_font};
14use crate::content::book::{BookAgainst, compile_book, recipe_layouts};
15use crate::content::ctx::{Deferred, Shared, Sink, Wanted};
16use crate::content::ctx::{caveat, problem};
17use crate::content::defs::data::{definition_path, loot_path};
18use crate::content::defs::ladder::{read_defs, tool_level_tags};
19use crate::content::defs::worldgen::dimension_path;
20use crate::content::defs::{DefFile, Defs, Made, compile_def};
21use crate::content::entry::Entry;
22use crate::content::files::{BodyFile, BookFile, EntityFile, FontFile};
23use crate::content::outputs::version_overlays;
24use crate::content::recipe::registration::Recipe;
25use crate::content::recipe::{Against, compile_recipe};
26use crate::content::source::{Content, mentions_content_macro, registrations, rust_source};
27use crate::content::{
28    CONTENT_DIR, Compiled, content_format, datapack, find, looks, refs, textures, valid_namespace,
29};
30use crate::diagnostics::Diagnostic;
31use crate::yres;
32
33/// Ids defined so far, each by the file defining it: a second one is a mistake.
34#[derive(Default)]
35struct Ids(HashMap<String, String>);
36
37impl Ids {
38    /// `id` (a `what`) defined by `file`.
39    fn claim(&mut self, errors: &mut Vec<Diagnostic>, file: &str, what: &str, id: &str) {
40        if let Some(other) = self.0.insert(id.to_string(), file.to_string()) {
41            errors.push(problem(format!("{what} {id} is also defined by {other}")).in_file(file));
42        }
43    }
44}
45
46/// Files read and checked later, once what they name is known: (file, text).
47#[derive(Default)]
48struct Pending {
49    books: Vec<(String, String)>,
50    recipes: Vec<(String, String, PathBuf)>,
51    defs: Vec<DefFile>,
52}
53
54/// Everything checked so far.
55struct Build<'a> {
56    shared: Shared<'a>,
57    /// The mod's namespace, when its id is one.
58    mod_ns: Option<&'a str>,
59    sink: Sink,
60    out: Compiled,
61    /// The models the files write, by path: one file each.
62    generated: BTreeMap<String, String>,
63    entries: Vec<Entry>,
64    potions: BTreeMap<String, PotionDef>,
65    entities: Vec<EntityFile>,
66    bodies: Vec<BodyFile>,
67    fonts: Vec<FontFile>,
68    recipes: Vec<Recipe>,
69    books: Vec<BookFile>,
70    /// Blocks', items' and entities' ids, then each other kind's.
71    ids: Ids,
72    model_ids: Ids,
73    anim_ids: Ids,
74    font_ids: Ids,
75    recipe_ids: Ids,
76    book_ids: Ids,
77    pending: Pending,
78}
79
80/// Compiles the mod's content files (see the module docs). Any error fails
81/// the whole thing, listing every problem.
82pub fn compile(root: &Path, mod_id: &str, refs: &yres::Refs) -> Result<Compiled, String> {
83    let files = find(root);
84    if files.is_empty() {
85        return Ok(Compiled::default());
86    }
87    let mod_ns = valid_namespace(mod_id).then_some(mod_id);
88    let mut sink = Sink::default();
89    let own = textures::compile(root, mod_id, refs, &mut sink.errors);
90    let shared = Shared {
91        root,
92        refs,
93        ns: mod_ns.unwrap_or(MINECRAFT),
94        own_textures: &own.ids,
95    };
96    let mut b = Build::new(shared, mod_ns, sink);
97    for path in &files {
98        b.read(path);
99    }
100    let mut known = b.known();
101    let mut defs = read_defs(&b.pending.defs, b.shared.ns, &mut b.sink.errors);
102    let made = b.defs(&known);
103    let layouts = b.recipes(&mut known, &made);
104    b.check_deferred(&defs, &known);
105    b.tool_levels(&defs);
106    b.books(&known, &layouts);
107    b.inventories();
108    b.out.outputs.extend(own.outputs);
109    b.out.packed = own.packed;
110    b.finish(&mut defs, made, &known)
111}
112
113impl<'a> Build<'a> {
114    fn new(shared: Shared<'a>, mod_ns: Option<&'a str>, sink: Sink) -> Build<'a> {
115        Build {
116            shared,
117            mod_ns,
118            sink,
119            out: Compiled::default(),
120            generated: BTreeMap::new(),
121            entries: Vec::new(),
122            potions: BTreeMap::new(),
123            entities: Vec::new(),
124            bodies: Vec::new(),
125            fonts: Vec::new(),
126            recipes: Vec::new(),
127            books: Vec::new(),
128            ids: Ids::default(),
129            model_ids: Ids::default(),
130            anim_ids: Ids::default(),
131            font_ids: Ids::default(),
132            recipe_ids: Ids::default(),
133            book_ids: Ids::default(),
134            pending: Pending::default(),
135        }
136    }
137
138    /// Reads the content file at `path`: checked now, or kept for when
139    /// what it names is known.
140    fn read(&mut self, path: &Path) {
141        let shown = crate::shown_path(self.shared.root, path);
142        let Some(kind) = content_format(path) else {
143            return;
144        };
145        let text = match crate::files::read_source(path) {
146            Ok(t) => t,
147            Err(e) => {
148                return self
149                    .sink
150                    .errors
151                    .push(problem(format!("{e}")).in_file(&shown));
152            }
153        };
154        let errors = &mut self.sink.errors;
155        match kind {
156            Format::Model | Format::Anim => {
157                let Some(b) = compile_body(&self.shared, &shown, &text, kind, path, errors) else {
158                    return;
159                };
160                let ids = if b.model {
161                    &mut self.model_ids
162                } else {
163                    &mut self.anim_ids
164                };
165                ids.claim(errors, &shown, kind.noun(), &b.id);
166                self.bodies.push(b);
167            }
168            Format::Font => {
169                let Some(font) = compile_font(&self.shared, &shown, &text, path, errors) else {
170                    return;
171                };
172                self.font_ids
173                    .claim(errors, &shown, yog_ids::kind::FONT, &font.id);
174                self.fonts.push(font);
175            }
176            // Checked once every block, item and recipe is known.
177            Format::Book => self.pending.books.push((shown, text)),
178            // Checked once every block and item is known.
179            Format::Recipe => self.pending.recipes.push((shown, text, path.to_path_buf())),
180            Format::Def => {
181                let mut cx = self.sink.cx(&self.shared, &shown);
182                if let Some(f) = DefFile::read(&mut cx, text) {
183                    self.pending.defs.push(f);
184                }
185            }
186            Format::Entity => {
187                let mut cx = self.sink.cx(&self.shared, &shown);
188                let Some(e) = compile_entity(&mut cx, &text, path, self.mod_ns, &mut self.out)
189                else {
190                    return;
191                };
192                self.ids
193                    .claim(&mut self.sink.errors, &shown, yog_ids::word::ID, &e.id);
194                self.entities.push(e);
195            }
196            _ => {
197                let mut cx = self.sink.cx(&self.shared, &shown);
198                let Some(entry) = compile_file(
199                    &mut cx,
200                    &text,
201                    kind,
202                    path,
203                    self.mod_ns,
204                    &mut self.out,
205                    &mut self.generated,
206                ) else {
207                    return;
208                };
209                self.ids
210                    .claim(&mut self.sink.errors, &shown, yog_ids::word::ID, &entry.id);
211                match entry.potion.clone() {
212                    Some(p) => {
213                        self.potions.insert(entry.id, p);
214                    }
215                    None => self.entries.push(entry),
216                }
217            }
218        }
219    }
220
221    /// What the files may name: the mod's content, vanilla's and other mods'
222    /// declared ids. A model or clips of this mod an entity names must be
223    /// among its `.ymodel`/`.yanim`.
224    fn known(&mut self) -> refs::Known {
225        let manifest =
226            crate::manifest::YogToml::read(&self.shared.root.join(yog_ids::file::MANIFEST)).ok();
227        let externals = manifest
228            .as_ref()
229            .map(|t| t.externals.clone())
230            .unwrap_or_default();
231        let datapack = manifest.as_ref().and_then(|t| t.datapack.clone());
232        let mut known = refs::Known::new(self.shared.ns, &externals, datapack.as_deref());
233        for e in &self.entries {
234            known.add(
235                if e.kind == Format::Block {
236                    refs::RefKind::Block
237                } else {
238                    refs::RefKind::Item
239                },
240                e.id.clone(),
241            );
242        }
243        for id in self.potions.keys() {
244            known.add(refs::RefKind::Item, id.clone());
245        }
246        for e in &self.entities {
247            known.add(refs::RefKind::Entity, e.id.clone());
248            for (wanted, kind) in [(&e.model, Format::Model), (&e.animations, Format::Anim)] {
249                if let Some(id) = wanted
250                    && self.mod_ns.is_some_and(|ns| yog_ids::split(id).0 == ns)
251                    && !self
252                        .bodies
253                        .iter()
254                        .any(|b| b.model == (kind == Format::Model) && &b.id == id)
255                {
256                    self.sink.errors.push(problem(format!(
257                        "{}: {} `{id}`: no such file in {CONTENT_DIR}/ (`.{}`)",
258                        e.file,
259                        kind.noun(),
260                        kind.ext()
261                    )));
262                }
263            }
264        }
265        known
266    }
267
268    /// The `.ydef`s checked against what the files name (`known`).
269    fn defs(&mut self, known: &refs::Known) -> Made {
270        let mut made = Made::default();
271        for f in std::mem::take(&mut self.pending.defs) {
272            let shown = f.shown.clone();
273            let mut cx = self.sink.cx(&self.shared, &shown);
274            compile_def(&mut cx, f, known, &mut made, &mut self.out);
275        }
276        made
277    }
278
279    /// The `.yrecipe`s, against the mod's kinds of recipe and how they're
280    /// shown (`.yui` `recipe_layout` components, returned); their brewing
281    /// recipes onto the potions they make.
282    fn recipes(
283        &mut self,
284        known: &mut refs::Known,
285        made: &Made,
286    ) -> Vec<yog_formats::book::layout::RecipeLayout> {
287        let layouts = recipe_layouts(
288            self.shared.root,
289            self.shared.ns,
290            &made.recipe_kinds,
291            known,
292            &mut self.sink.errors,
293        );
294        let mut brews: Vec<(String, Brew)> = Vec::new();
295        let mut against = Against {
296            mod_ns: self.mod_ns,
297            known,
298            kinds: &made.recipe_kinds,
299            brews: &mut brews,
300        };
301        for (shown, text, path) in std::mem::take(&mut self.pending.recipes) {
302            let mut cx = self.sink.cx(&self.shared, &shown);
303            let Some(r) = compile_recipe(&mut cx, &text, &path, &mut against) else {
304                continue;
305            };
306            self.recipe_ids
307                .claim(&mut self.sink.errors, &shown, yog_ids::kind::RECIPE, &r.id);
308            self.recipes.push(r);
309        }
310        for r in &self.recipes {
311            known.add(refs::RefKind::Recipe, r.id.clone());
312        }
313        for (to, brew) in brews {
314            self.brew(to, brew);
315        }
316        layouts
317    }
318
319    /// A brewing recipe making potion `to`: the mod's potion or a Yog
320    /// default potion alias, from one.
321    fn brew(&mut self, to: String, brew: Brew) {
322        let defined = |p: &BTreeMap<String, PotionDef>, id: &str| {
323            p.contains_key(id) || refs::is_vanilla_potion(id)
324        };
325        if !defined(&self.potions, &brew.from) {
326            return self.sink.errors.push(problem(format!("brewing recipe input `{}`: no potion `.yitem` or Yog default potion alias defines it", brew.from)));
327        }
328        if !defined(&self.potions, &to) {
329            return self.sink.errors.push(problem(format!("brewing recipe result `{to}`: no potion `.yitem` or Yog default potion alias defines it")));
330        }
331        self.potions
332            .entry(to.clone())
333            .or_insert_with(|| PotionDef {
334                id: to,
335                name: None,
336                effects: Vec::new(),
337                brewed: Vec::new(),
338            })
339            .brewed
340            .push(brew);
341    }
342
343    /// What blocks and items name (drops, levels), known now.
344    fn check_deferred(&mut self, defs: &Defs, known: &refs::Known) {
345        for Deferred {
346            file,
347            line,
348            path,
349            wanted,
350        } in std::mem::take(&mut self.sink.deferred)
351        {
352            if let Err(e) = check_wanted(&wanted, defs, known) {
353                self.sink
354                    .errors
355                    .push(problem(format!("{path}: {e}")).in_file(file).at_line(line));
356            }
357        }
358    }
359
360    /// The tags that make blocks need their tool level.
361    fn tool_levels(&mut self, defs: &Defs) {
362        for e in &self.entries {
363            let Some((level, line)) = &e.block.tool_level else {
364                continue;
365            };
366            let at = |d: Diagnostic| d.in_file(&e.file).at_line(*line);
367            let field = yog_data::field!(yog_formats::content::BlockProps, tool_level);
368            match tool_level_tags(&e.id, level, defs, &mut self.out) {
369                Ok(None) => {}
370                Ok(Some(m)) => self
371                    .sink
372                    .warnings
373                    .push(at(caveat(format!("block.{field}: {m}")))),
374                Err(m) => self
375                    .sink
376                    .errors
377                    .push(at(problem(format!("block.{field}: {m}")))),
378            }
379        }
380    }
381
382    /// The `.ybook`s, once every block, item and recipe is known.
383    fn books(&mut self, known: &refs::Known, layouts: &[yog_formats::book::layout::RecipeLayout]) {
384        let recipe_kinds: HashMap<String, String> = self
385            .recipes
386            .iter()
387            .map(|r| (r.id.clone(), r.ty.clone()))
388            .collect();
389        // The mod's own kinds of book page (`.yui` components with `book_page`).
390        let page_types =
391            crate::book_page::find(self.shared.root, self.shared.ns, &mut self.sink.errors);
392        let against = BookAgainst {
393            known,
394            layouts,
395            recipe_kinds: &recipe_kinds,
396            page_types: &page_types,
397        };
398        for (shown, text) in std::mem::take(&mut self.pending.books) {
399            let mut cx = self.sink.cx(&self.shared, &shown);
400            let Some(b) = compile_book(&mut cx, &text, &against) else {
401                continue;
402            };
403            self.book_ids
404                .claim(&mut self.sink.errors, &shown, yog_ids::kind::BOOK, &b.id);
405            self.books.push(b);
406        }
407    }
408
409    /// The inventory screens blocks of this mod open must be its screens
410    /// (another mod's ids can't be checked here).
411    fn inventories(&mut self) {
412        let Some(ns) = self.mod_ns else { return };
413        let screens = inventory_screens(&self.shared.root.join(yog_ids::project::SRC));
414        for (file, line, id) in &self.sink.inventories {
415            if yog_ids::split(id).0 == ns && !screens.contains(id) {
416                self.sink.errors.push(
417                    problem(format!(
418                        "block.inventory `{id}`: no .yui <Screen id=\"{id}\"> with <Slot>s in src/"
419                    ))
420                    .in_file(file)
421                    .at_line(*line),
422                );
423            }
424        }
425    }
426
427    /// Everything made, unless something was wrong: then every problem.
428    fn finish(
429        mut self,
430        defs: &mut Defs,
431        mut made: Made,
432        known: &refs::Known,
433    ) -> Result<Compiled, String> {
434        // Armor's worn look where 1.21.4+ looks for it.
435        for m in &defs.armor_materials {
436            looks::equipment(
437                self.shared.root,
438                &mut self.out,
439                &m.texture,
440                &mut self.sink.warnings,
441            );
442        }
443        self.out.warnings = std::mem::take(&mut self.sink.warnings);
444        if !self.sink.errors.is_empty() {
445            crate::diagnostics::report_all(&self.sink.errors);
446            return Err(format!(
447                "invalid blocks/items in {CONTENT_DIR}/:{}{}",
448                crate::diagnostics::LIST_SEP,
449                crate::diagnostics::listed(&self.sink.errors)
450            ));
451        }
452        let potions = std::mem::take(&mut self.potions);
453        made.definitions
454            .extend(potions.into_iter().map(|(id, potion)| {
455                (
456                    yog_ids::kind::POTION,
457                    id,
458                    yog_data::binary::encode(&potion.write()),
459                )
460            }));
461        self.count();
462        self.data_files(&made);
463        self.out.registrations =
464            registrations(&self.entries, &self.entities, &made.dimensions, defs);
465        self.made_into(defs, made);
466        let content = Content {
467            entries: &self.entries,
468            recipes: &self.recipes,
469            books: &self.books,
470            bodies: &self.bodies,
471            entities: &self.entities,
472            fonts: &self.fonts,
473            defs,
474            textures: self.shared.own_textures,
475        };
476        if content.registers_anything()
477            && !mentions_content_macro(&self.shared.root.join(yog_ids::project::SRC))
478        {
479            self.out.warnings.push(caveat(format!("{CONTENT_DIR} has blocks/items, but src/ never calls `yog_api::content!();` — they get models but aren't registered")));
480        }
481        self.out.source = Some(rust_source(&content));
482        version_overlays(&mut self.out, &known.datapack);
483        Ok(self.out)
484    }
485
486    /// How many of each the build says it made.
487    fn count(&mut self) {
488        self.out.blocks = self
489            .entries
490            .iter()
491            .filter(|e| e.kind == Format::Block)
492            .count();
493        self.out.items = self.entries.len() - self.out.blocks;
494        self.out.recipes = self.recipes.len();
495        self.out.books = self.books.len();
496    }
497
498    /// The data the code includes: books, bodies, fonts, entities, the mod's
499    /// own kinds of recipe, dimensions, loot modifiers, definitions.
500    fn data_files(&mut self, made: &Made) {
501        let files = &mut self.out.data_files;
502        files.extend(self.books.iter().map(|b| (b.path(), b.data.clone())));
503        files.extend(self.bodies.iter().map(|b| (b.path(), b.data.clone())));
504        files.extend(self.fonts.iter().map(|f| (f.path(), f.data.clone())));
505        files.extend(self.entities.iter().map(|e| (e.path(), e.data.clone())));
506        files.extend(self.recipes.iter().filter_map(Recipe::data_file));
507        files.extend(
508            made.dimensions
509                .iter()
510                .map(|(id, data)| (dimension_path(id), data.clone())),
511        );
512        files.extend(
513            made.loot
514                .iter()
515                .map(|(id, data)| (loot_path(id), data.clone())),
516        );
517        files.extend(
518            made.definitions
519                .iter()
520                .map(|(kind, id, data)| (definition_path(kind, id), data.clone())),
521        );
522    }
523
524    /// What the `.ydef`s made, into what the code is generated from; ores'
525    /// files and a tool level above the game's tag of what its tools can't mine.
526    fn made_into(&mut self, defs: &mut Defs, made: Made) {
527        for (files, feature) in made.ores {
528            self.out.outputs.extend(files);
529            defs.biome_features.push(feature);
530        }
531        for m in &defs.tool_materials {
532            if yog_ids::split(&m.incorrect).0 != MINECRAFT {
533                let file = datapack::tag_file(yog_ids::kind::BLOCK, &m.incorrect);
534                self.out
535                    .tags
536                    .entry(datapack::overlay(
537                        &yog_registry::ToolLadder::game().incorrect_when,
538                        &file,
539                    ))
540                    .or_default();
541            }
542        }
543        defs.definitions = made
544            .definitions
545            .into_iter()
546            .map(|(kind, id, _)| (kind, id))
547            .collect();
548        defs.loot_modifiers = made.loot.into_iter().map(|(id, _)| id).collect();
549        defs.dimensions = made.dimensions.into_iter().map(|(id, _)| id).collect();
550        defs.advancements = made.advancements;
551        defs.tables = made.tables;
552    }
553}
554
555/// Whether what a file names exists: a level, ladder or material of the
556/// game's or the `.ydef`s'; a tag of one of its kinds; content.
557fn check_wanted(wanted: &Wanted, defs: &Defs, known: &refs::Known) -> Result<(), String> {
558    use yog_formats::def::{ArmorMaterial, SoundGroup, ToolMaterial};
559    use yog_ids::kind;
560    let found = |there: bool, what: &str, id: &str| {
561        if there {
562            Ok(())
563        } else {
564            Err(format!("no {what} {id} — the game's or a .ydef's"))
565        }
566    };
567    match wanted {
568        Wanted::Tag { kinds, id } => {
569            let mut found = kinds.iter().map(|k| known.check_tag(k, id));
570            let first = found.next().unwrap_or(Ok(()));
571            if first.is_ok() || found.any(|f| f.is_ok()) {
572                Ok(())
573            } else {
574                first
575            }
576        }
577        Wanted::Ref(r) => match r.kind.as_str() {
578            kind::LEVEL => found(defs.level(&r.id).is_some(), "level", &r.id),
579            kind::LADDER => found(defs.order.contains_key(&r.id), "ladder", &r.id),
580            kind::TOOL_MATERIAL => found(
581                defs.tool_materials.iter().any(|m| m.id == r.id)
582                    || ToolMaterial::built_in(&r.id).is_some(),
583                "tool material",
584                &r.id,
585            ),
586            kind::ARMOR_MATERIAL => found(
587                defs.armor_materials.iter().any(|m| m.id == r.id)
588                    || ArmorMaterial::built_in(&r.id).is_some(),
589                "armor material",
590                &r.id,
591            ),
592            kind::SOUND_GROUP => found(
593                defs.sound_groups.iter().any(|g| g.id == r.id)
594                    || SoundGroup::built_in(&r.id).is_some(),
595                "sound group",
596                &r.id,
597            ),
598            other => refs::RefKind::from_word(other).map_or(Ok(()), |k| known.check(k, &r.id)),
599        },
600    }
601}