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