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