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        // A manifest that doesn't read was reported when the build prepared.
226        let manifest =
227            crate::manifest::YogToml::read(&self.shared.root.join(yog_ids::file::MANIFEST)).ok();
228        let externals = manifest
229            .as_ref()
230            .map(|t| t.externals.clone())
231            .unwrap_or_default();
232        let datapack = manifest.as_ref().and_then(|t| t.datapack.clone());
233        let mut known = refs::Known::new(self.shared.ns, &externals, datapack.as_deref());
234        for e in &self.entries {
235            known.add(
236                if e.kind == Format::Block {
237                    refs::RefKind::Block
238                } else {
239                    refs::RefKind::Item
240                },
241                e.id.clone(),
242            );
243        }
244        for id in self.potions.keys() {
245            known.add(refs::RefKind::Item, id.clone());
246        }
247        for e in &self.entities {
248            known.add(refs::RefKind::Entity, e.id.clone());
249            for (wanted, kind) in [(&e.model, Format::Model), (&e.animations, Format::Anim)] {
250                if let Some(id) = wanted
251                    && self.mod_ns.is_some_and(|ns| yog_ids::split(id).0 == ns)
252                    && !self
253                        .bodies
254                        .iter()
255                        .any(|b| b.model == (kind == Format::Model) && &b.id == id)
256                {
257                    self.sink.errors.push(problem(format!(
258                        "{}: {} `{id}`: no such file in {CONTENT_DIR}/ (`.{}`)",
259                        e.file,
260                        kind.noun(),
261                        kind.ext()
262                    )));
263                }
264            }
265        }
266        known
267    }
268
269    /// The `.ydef`s checked against what the files name (`known`).
270    fn defs(&mut self, known: &refs::Known) -> Made {
271        let mut made = Made::default();
272        for f in std::mem::take(&mut self.pending.defs) {
273            let shown = f.shown.clone();
274            let mut cx = self.sink.cx(&self.shared, &shown);
275            compile_def(&mut cx, f, known, &mut made, &mut self.out);
276        }
277        made
278    }
279
280    /// The `.yrecipe`s, against the mod's kinds of recipe and how they're
281    /// shown (`.yui` `recipe_layout` components, returned); their brewing
282    /// recipes onto the potions they make.
283    fn recipes(
284        &mut self,
285        known: &mut refs::Known,
286        made: &Made,
287    ) -> Vec<yog_formats::book::layout::RecipeLayout> {
288        let layouts = recipe_layouts(
289            self.shared.root,
290            self.shared.ns,
291            &made.recipe_kinds,
292            known,
293            &mut self.sink.errors,
294        );
295        let mut brews: Vec<(String, Brew)> = Vec::new();
296        let mut against = Against {
297            mod_ns: self.mod_ns,
298            known,
299            kinds: &made.recipe_kinds,
300            brews: &mut brews,
301        };
302        for (shown, text, path) in std::mem::take(&mut self.pending.recipes) {
303            let mut cx = self.sink.cx(&self.shared, &shown);
304            let Some(r) = compile_recipe(&mut cx, &text, &path, &mut against) else {
305                continue;
306            };
307            self.recipe_ids
308                .claim(&mut self.sink.errors, &shown, yog_ids::kind::RECIPE, &r.id);
309            self.recipes.push(r);
310        }
311        for r in &self.recipes {
312            known.add(refs::RefKind::Recipe, r.id.clone());
313        }
314        for (to, brew) in brews {
315            self.brew(to, brew);
316        }
317        layouts
318    }
319
320    /// A brewing recipe making potion `to`: the mod's potion or a Yog
321    /// default potion alias, from one.
322    fn brew(&mut self, to: String, brew: Brew) {
323        let defined = |p: &BTreeMap<String, PotionDef>, id: &str| {
324            p.contains_key(id) || refs::is_vanilla_potion(id)
325        };
326        if !defined(&self.potions, &brew.from) {
327            return self.sink.errors.push(problem(format!("brewing recipe input `{}`: no potion `.yitem` or Yog default potion alias defines it", brew.from)));
328        }
329        if !defined(&self.potions, &to) {
330            return self.sink.errors.push(problem(format!("brewing recipe result `{to}`: no potion `.yitem` or Yog default potion alias defines it")));
331        }
332        self.potions
333            .entry(to.clone())
334            .or_insert_with(|| PotionDef {
335                id: to,
336                name: None,
337                effects: Vec::new(),
338                brewed: Vec::new(),
339            })
340            .brewed
341            .push(brew);
342    }
343
344    /// What blocks and items name (drops, levels), known now.
345    fn check_deferred(&mut self, defs: &Defs, known: &refs::Known) {
346        for Deferred {
347            file,
348            line,
349            path,
350            wanted,
351        } in std::mem::take(&mut self.sink.deferred)
352        {
353            if let Err(e) = check_wanted(&wanted, defs, known) {
354                self.sink
355                    .errors
356                    .push(problem(format!("{path}: {e}")).in_file(file).at_line(line));
357            }
358        }
359    }
360
361    /// The tags that make blocks need their tool level.
362    fn tool_levels(&mut self, defs: &Defs) {
363        for e in &self.entries {
364            let Some((level, line)) = &e.block.tool_level else {
365                continue;
366            };
367            let at = |d: Diagnostic| d.in_file(&e.file).at_line(*line);
368            let field = yog_data::field!(yog_formats::content::BlockProps, tool_level);
369            match tool_level_tags(&e.id, level, defs, &mut self.out) {
370                Ok(None) => {}
371                Ok(Some(m)) => self
372                    .sink
373                    .warnings
374                    .push(at(caveat(format!("block.{field}: {m}")))),
375                Err(m) => self
376                    .sink
377                    .errors
378                    .push(at(problem(format!("block.{field}: {m}")))),
379            }
380        }
381    }
382
383    /// The `.ybook`s, once every block, item and recipe is known.
384    fn books(&mut self, known: &refs::Known, layouts: &[yog_formats::book::layout::RecipeLayout]) {
385        let recipe_kinds: HashMap<String, String> = self
386            .recipes
387            .iter()
388            .map(|r| (r.id.clone(), r.ty.clone()))
389            .collect();
390        // The mod's own kinds of book page (`.yui` components with `book_page`).
391        let page_types =
392            crate::book_page::find(self.shared.root, self.shared.ns, &mut self.sink.errors);
393        let against = BookAgainst {
394            known,
395            layouts,
396            recipe_kinds: &recipe_kinds,
397            page_types: &page_types,
398        };
399        for (shown, text) in std::mem::take(&mut self.pending.books) {
400            let mut cx = self.sink.cx(&self.shared, &shown);
401            let Some(b) = compile_book(&mut cx, &text, &against) else {
402                continue;
403            };
404            self.book_ids
405                .claim(&mut self.sink.errors, &shown, yog_ids::kind::BOOK, &b.id);
406            self.books.push(b);
407        }
408    }
409
410    /// The inventory screens blocks of this mod open must be its screens
411    /// (another mod's ids can't be checked here).
412    fn inventories(&mut self) {
413        let Some(ns) = self.mod_ns else { return };
414        let screens = inventory_screens(&self.shared.root.join(yog_ids::project::SRC));
415        for (file, line, id) in &self.sink.inventories {
416            if yog_ids::split(id).0 == ns && !screens.contains(id) {
417                self.sink.errors.push(
418                    problem(format!(
419                        "block.inventory `{id}`: no .yui <Screen id=\"{id}\"> with <Slot>s in src/"
420                    ))
421                    .in_file(file)
422                    .at_line(*line),
423                );
424            }
425        }
426    }
427
428    /// Everything made, unless something was wrong: then every problem.
429    fn finish(
430        mut self,
431        defs: &mut Defs,
432        mut made: Made,
433        known: &refs::Known,
434    ) -> Result<Compiled, String> {
435        // Armor's worn look where 1.21.4+ looks for it.
436        for m in &defs.armor_materials {
437            looks::equipment(
438                self.shared.root,
439                &mut self.out,
440                &m.texture,
441                &mut self.sink.warnings,
442            );
443        }
444        self.out.warnings = std::mem::take(&mut self.sink.warnings);
445        if !self.sink.errors.is_empty() {
446            crate::diagnostics::report_all(&self.sink.errors);
447            return Err(format!(
448                "invalid blocks/items in {CONTENT_DIR}/:{}{}",
449                crate::diagnostics::LIST_SEP,
450                crate::diagnostics::listed(&self.sink.errors)
451            ));
452        }
453        let potions = std::mem::take(&mut self.potions);
454        made.definitions
455            .extend(potions.into_iter().map(|(id, potion)| {
456                (
457                    yog_ids::kind::POTION,
458                    id,
459                    yog_data::binary::encode(&potion.write()),
460                )
461            }));
462        self.count();
463        self.data_files(&made);
464        self.out.registrations =
465            registrations(&self.entries, &self.entities, &made.dimensions, defs);
466        self.made_into(defs, made);
467        let content = Content {
468            entries: &self.entries,
469            recipes: &self.recipes,
470            books: &self.books,
471            bodies: &self.bodies,
472            entities: &self.entities,
473            fonts: &self.fonts,
474            defs,
475            textures: self.shared.own_textures,
476        };
477        if content.registers_anything()
478            && !mentions_content_macro(&self.shared.root.join(yog_ids::project::SRC))
479        {
480            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")));
481        }
482        self.out.source = Some(rust_source(&content));
483        version_overlays(&mut self.out, &known.datapack);
484        Ok(self.out)
485    }
486
487    /// How many of each the build says it made.
488    fn count(&mut self) {
489        self.out.blocks = self
490            .entries
491            .iter()
492            .filter(|e| e.kind == Format::Block)
493            .count();
494        self.out.items = self.entries.len() - self.out.blocks;
495        self.out.recipes = self.recipes.len();
496        self.out.books = self.books.len();
497    }
498
499    /// The data the code includes: books, bodies, fonts, entities, the mod's
500    /// own kinds of recipe, dimensions, loot modifiers, definitions.
501    fn data_files(&mut self, made: &Made) {
502        let files = &mut self.out.data_files;
503        files.extend(self.books.iter().map(|b| (b.path(), b.data.clone())));
504        files.extend(self.bodies.iter().map(|b| (b.path(), b.data.clone())));
505        files.extend(self.fonts.iter().map(|f| (f.path(), f.data.clone())));
506        files.extend(self.entities.iter().map(|e| (e.path(), e.data.clone())));
507        files.extend(self.recipes.iter().filter_map(Recipe::data_file));
508        files.extend(
509            made.dimensions
510                .iter()
511                .map(|(id, data)| (dimension_path(id), data.clone())),
512        );
513        files.extend(
514            made.loot
515                .iter()
516                .map(|(id, data)| (loot_path(id), data.clone())),
517        );
518        files.extend(
519            made.definitions
520                .iter()
521                .map(|(kind, id, data)| (definition_path(kind, id), data.clone())),
522        );
523    }
524
525    /// What the `.ydef`s made, into what the code is generated from; ores'
526    /// files and a tool level above the game's tag of what its tools can't mine.
527    fn made_into(&mut self, defs: &mut Defs, made: Made) {
528        for (files, feature) in made.ores {
529            self.out.outputs.extend(files);
530            defs.biome_features.push(feature);
531        }
532        for m in &defs.tool_materials {
533            if yog_ids::split(&m.incorrect).0 != MINECRAFT {
534                let file = datapack::tag_file(yog_ids::kind::BLOCK, &m.incorrect);
535                self.out
536                    .tags
537                    .entry(datapack::overlay(
538                        &yog_registry::ToolLadder::game().incorrect_when,
539                        &file,
540                    ))
541                    .or_default();
542            }
543        }
544        defs.definitions = made
545            .definitions
546            .into_iter()
547            .map(|(kind, id, _)| (kind, id))
548            .collect();
549        defs.loot_modifiers = made.loot.into_iter().map(|(id, _)| id).collect();
550        defs.dimensions = made.dimensions.into_iter().map(|(id, _)| id).collect();
551        defs.advancements = made.advancements;
552        defs.tables = made.tables;
553    }
554}
555
556/// Whether what a file names exists: a level, ladder or material of the
557/// game's or the `.ydef`s'; a tag of one of its kinds; content.
558fn check_wanted(wanted: &Wanted, defs: &Defs, known: &refs::Known) -> Result<(), String> {
559    use yog_formats::def::{ArmorMaterial, SoundGroup, ToolMaterial};
560    use yog_ids::kind;
561    let found = |there: bool, what: &str, id: &str| {
562        if there {
563            Ok(())
564        } else {
565            Err(format!("no {what} {id} — the game's or a .ydef's"))
566        }
567    };
568    match wanted {
569        Wanted::Tag { kinds, id } => {
570            let mut found = kinds.iter().map(|k| known.check_tag(k, id));
571            let first = found.next().unwrap_or(Ok(()));
572            if first.is_ok() || found.any(|f| f.is_ok()) {
573                Ok(())
574            } else {
575                first
576            }
577        }
578        Wanted::Ref(r) => match r.kind.as_str() {
579            kind::LEVEL => found(defs.level(&r.id).is_some(), "level", &r.id),
580            kind::LADDER => found(defs.order.contains_key(&r.id), "ladder", &r.id),
581            kind::TOOL_MATERIAL => found(
582                defs.tool_materials.iter().any(|m| m.id == r.id)
583                    || ToolMaterial::built_in(&r.id).is_some(),
584                "tool material",
585                &r.id,
586            ),
587            kind::ARMOR_MATERIAL => found(
588                defs.armor_materials.iter().any(|m| m.id == r.id)
589                    || ArmorMaterial::built_in(&r.id).is_some(),
590                "armor material",
591                &r.id,
592            ),
593            kind::SOUND_GROUP => found(
594                defs.sound_groups.iter().any(|g| g.id == r.id)
595                    || SoundGroup::built_in(&r.id).is_some(),
596                "sound group",
597                &r.id,
598            ),
599            other => refs::RefKind::from_word(other).map_or(Ok(()), |k| known.check(k, &r.id)),
600        },
601    }
602}