Skip to main content

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