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