Skip to main content

yog_cli/content/
source.rs

1//! The Rust the build generates for a mod's content (`yog-content.g.rs`).
2
3use yog_ids::Format;
4use std::collections::HashMap;
5use std::path::Path;
6use crate::content::BlockProps;
7use crate::content::BodyFile;
8use crate::content::BookFile;
9use crate::content::Compiled;
10use crate::content::Defs;
11use crate::content::EntityFile;
12use crate::content::Entry;
13use crate::content::FontFile;
14use crate::content::GENERATED_FILE;
15use crate::content::GENERATED_HEADER;
16use crate::content::GENERATED_PARTS_DIR;
17use crate::content::Recipe;
18use crate::content::Source;
19use crate::content::block_item::item_props;
20use crate::content::defs::definition_path;
21use crate::content::defs::dimension_path;
22use crate::content::defs::loot_path;
23use crate::content::outputs::write_tags;
24use crate::content::table;
25use std::fmt::Write;
26
27
28/// [`yog_registry::REGISTRATIONS_FILE`]: what the mod registers at the
29/// game's start, each with a hash of what can't change while it runs — a
30/// block's properties and states, not its model, name or tags (the game
31/// reloads those).
32pub(super) fn registrations(entries: &[Entry], entities: &[EntityFile], dimensions: &[(String, Vec<u8>)], defs: &Defs) -> String {
33    fn hash(text: &str) -> String {
34        // FNV-1a: the same on every build of the CLI.
35        let h = text.bytes().fold(0xcbf2_9ce4_8422_2325u64, |h, b| (h ^ b as u64).wrapping_mul(0x0100_0000_01b3));
36        format!("{h:016x}")
37    }
38    let mut lines = Vec::new();
39    for e in entries {
40        let what = match e.kind {
41            Format::Block => {
42                // Its tool tags and level are data (tags), reloaded with the rest.
43                let b = BlockProps { tools: Vec::new(), tool_level: None, ..e.block.clone() };
44                format!("{b:?} {:?} {:?}", e.item, e.requires)
45            }
46            _ => format!("{:?} {:?}", e.item, e.requires),
47        };
48        lines.push(format!("{} {} {}", e.kind.noun(), e.id, hash(&what)));
49    }
50    for e in entities {
51        lines.push(format!("entity {} {}", e.id, hash(&format!("{:?}", e.data))));
52    }
53    for (id, data) in dimensions {
54        lines.push(format!("dimension {id} {}", hash(&format!("{data:?}"))));
55    }
56    for g in &defs.sound_groups {
57        lines.push(format!("sound_group {} {}", g.id, hash(&format!("{g:?}"))));
58    }
59    for m in &defs.tool_materials {
60        lines.push(format!("tool_material {} {}", m.id, hash(&format!("{m:?}"))));
61    }
62    for m in &defs.armor_materials {
63        lines.push(format!("armor_material {} {}", m.id, hash(&format!("{m:?}"))));
64    }
65    lines.sort();
66    lines.join("\n") + "\n"
67}
68
69/// Whether any `src/**/*.rs` has `content!(` — the include of the generated code.
70pub(super) fn mentions_content_macro(dir: &Path) -> bool {
71    let Ok(entries) = std::fs::read_dir(dir) else {
72        return false;
73    };
74    entries.flatten().any(|e| {
75        let p = e.path();
76        if p.is_dir() {
77            mentions_content_macro(&p)
78        } else {
79            p.extension().is_some_and(|x| x == "rs")
80                && std::fs::read_to_string(&p).is_ok_and(|t| t.contains("content!("))
81        }
82    })
83}
84
85/// `ruby_ore`, `ores/ruby` → `ores_ruby`; a Rust keyword gets `r#`.
86pub(super) fn rust_ident(path: &str) -> String {
87    let mut s: String = path
88        .chars()
89        .map(|c| {
90            if c.is_ascii_alphanumeric() {
91                c.to_ascii_lowercase()
92            } else {
93                '_'
94            }
95        })
96        .collect();
97    if s.starts_with(|c: char| c.is_ascii_digit()) {
98        s.insert(0, '_');
99    }
100    s
101}
102
103pub(super) fn float(n: f64) -> String {
104    let s = format!("{n:?}");
105    if s.contains('.') || s.contains('e') {
106        s
107    } else {
108        format!("{s}.0")
109    }
110}
111
112/// `stmt`, only when the mods `requires` names are all loaded (an optional integration).
113pub(super) fn when_loaded(requires: &[String], stmt: &str) -> String {
114    if requires.is_empty() {
115        return stmt.to_string();
116    }
117    let cond: Vec<String> = requires.iter().map(|m| format!("::yog_api::mod_loaded({m:?})")).collect();
118    format!("if {} {{ {stmt} }}", cond.join(" && "))
119}
120
121/// Moves what `code` has so far into part `name`.
122pub(super) fn flush(parts: &mut Vec<(String, String)>, code: &mut String, name: &str) {
123    if !code.trim().is_empty() {
124        parts.push((name.to_string(), format!("{GENERATED_HEADER}{}", std::mem::take(code))));
125    }
126    code.clear();
127}
128
129/// `pub struct <name>` with a `pub const <PATH>: &str = "<id>"` per id —
130/// what a mod's code names its content by. Nothing for no ids.
131fn id_consts<'a>(code: &mut String, doc: &str, name: &str, ids: impl Iterator<Item = &'a str>) {
132    let ids: Vec<&str> = ids.collect();
133    if ids.is_empty() {
134        return;
135    }
136    let _ = writeln!(code, "\n/// {doc}\n#[allow(dead_code)]\npub struct {name};\n\n#[allow(dead_code, clippy::all)]\nimpl {name} {{");
137    let mut used: HashMap<String, usize> = HashMap::new();
138    for id in ids {
139        let mut ident = rust_ident(yog_ids::split(id).1).to_ascii_uppercase();
140        let n = used.entry(ident.clone()).or_insert(0);
141        *n += 1;
142        if *n > 1 {
143            let _ = write!(ident, "_{n}");
144        }
145        let _ = writeln!(code, "    pub const {ident}: &'static str = {id:?};");
146    }
147    code.push_str("}\n");
148}
149
150/// What the generated code is made of: every kind of content the build read.
151pub(super) struct Content<'a> {
152    pub entries: &'a [Entry],
153    pub recipes: &'a [Recipe],
154    pub books: &'a [BookFile],
155    pub bodies: &'a [BodyFile],
156    pub entities: &'a [EntityFile],
157    pub fonts: &'a [FontFile],
158    pub defs: &'a Defs,
159    pub textures: &'a std::collections::BTreeSet<String>,
160}
161
162pub(super) fn rust_source(c: &Content) -> Source {
163    let Content { entries, recipes, books, bodies, entities, fonts, defs, textures } = *c;
164    let sound_groups = &defs.sound_groups;
165    let mut files: Vec<(String, String)> = Vec::new();
166    let mut code = String::new();
167    id_consts(&mut code, "The entity types of `content/` (`.yentity`), by id.", "Entities", entities.iter().map(|e| e.id.as_str()));
168    flush(&mut files, &mut code, "entities");
169    id_consts(&mut code, "The advancements of `content/` (`.ydef` kind `advancement`), by id\n/// (`yog_api::advancements::grant`/`done`).", "Advancements", defs.advancements.iter().map(String::as_str));
170    flush(&mut files, &mut code, "advancements");
171    for (model, set, what) in [(true, "Models", "models (`.ymodel`)"), (false, "Anims", "animations (`.yanim`)")] {
172        let doc = format!("The {what} of `content/`, by id (`yog_api::body::model`/`clip`).");
173        id_consts(&mut code, &doc, set, bodies.iter().filter(|b| b.model == model).map(|b| b.id.as_str()));
174        flush(&mut files, &mut code, &set.to_ascii_lowercase());
175    }
176    id_consts(&mut code, "The textures of `resources/` (`.ytexture`), by id — what the mod's code draws.", "Textures", textures.iter().map(String::as_str));
177    flush(&mut files, &mut code, "textures");
178    for (kind, set, def) in [
179        (Format::Block, "Blocks", "BlockDef"),
180        (Format::Item, "Items", "ItemDef"),
181    ] {
182        let mine: Vec<&Entry> = entries.iter().filter(|e| e.kind == kind).collect();
183        let _ = writeln!(
184            code,
185            "\n/// The {} of `content/`: the id and a [`::yog_api::{def}`] of each.\n#[allow(dead_code)]\npub struct {set};\n\n#[allow(dead_code, clippy::all)]\nimpl {set} {{",
186            if kind == Format::Block {
187                "blocks"
188            } else {
189                "items"
190            }
191        );
192        let mut used: HashMap<String, usize> = HashMap::new();
193        for e in mine {
194            let path = e.id.split_once(':').map_or(e.id.as_str(), |(_, p)| p);
195            let mut ident = rust_ident(path);
196            let n = used.entry(ident.clone()).or_insert(0);
197            *n += 1;
198            if *n > 1 {
199                let _ = write!(ident, "_{n}");
200            }
201            let upper = ident.to_ascii_uppercase();
202            let ident = if is_keyword(&ident) {
203                format!("r#{ident}")
204            } else {
205                ident
206            };
207            let _ = writeln!(
208                code,
209                "    /// `{}` — `{}`.\n    pub const {upper}: &'static str = {:?};",
210                e.id, e.file, e.id
211            );
212            let mut b = format!("::yog_api::{def}::new(Self::{upper})");
213            match kind {
214                Format::Block => {
215                    let p = &e.block;
216                    if p.hardness.is_some() || p.resistance.is_some() {
217                        let _ = write!(
218                            b,
219                            "\n            .strength({}, {})",
220                            float(p.hardness.unwrap_or(1.5)),
221                            float(p.resistance.unwrap_or(6.0))
222                        );
223                    }
224                    if let Some(s) = &p.sound {
225                        let _ = write!(b, "\n            .sound({s:?})");
226                    }
227                    if p.light > 0 {
228                        let _ = write!(b, "\n            .light_level({})", p.light);
229                    }
230                    if p.requires_tool {
231                        b.push_str("\n            .requires_tool()");
232                    }
233                    if p.no_collision {
234                        b.push_str("\n            .no_collision()");
235                    }
236                    if let Some(s) = p.slipperiness {
237                        let _ = write!(b, "\n            .slipperiness({})", float(s));
238                    }
239                    if let Some(s) = p.shape {
240                        let args: Vec<String> = s.iter().map(|n| float(*n)).collect();
241                        let _ = write!(b, "\n            .shape({})", args.join(", "));
242                    }
243                    if p.connects {
244                        b.push_str("\n            .connects_to_neighbors()");
245                    }
246                    if !p.connect_groups.is_empty() {
247                        let groups: Vec<String> =
248                            p.connect_groups.iter().map(|g| format!("{g:?}")).collect();
249                        let _ =
250                            write!(b, "\n            .connect_groups(&[{}])", groups.join(", "));
251                    }
252                    if let Some(inv) = &p.inventory {
253                        let _ = write!(b, "\n            .inventory({inv:?})");
254                    }
255                    if p.redstone {
256                        b.push_str("\n            .redstone_source()");
257                    }
258                    if !p.facing.is_empty() {
259                        let _ = write!(b, "\n            .facing({:?}, {:?})", p.facing, p.placement);
260                    }
261                    if p.attach {
262                        b.push_str("\n            .attach()");
263                    }
264                    if p.waterlogged {
265                        b.push_str("\n            .waterlogged()");
266                    }
267                    for (name, values) in &p.properties {
268                        let vs: Vec<String> = values.iter().map(|v| format!("{v:?}")).collect();
269                        let _ = write!(b, "\n            .property({name:?}, &[{}])", vs.join(", "));
270                    }
271                    if let Some(name) = &p.powered {
272                        let _ = write!(b, "\n            .powered({name:?})");
273                    }
274                    if let Some(name) = &p.comparator {
275                        let _ = write!(b, "\n            .comparator({name:?})");
276                    }
277                    if p.accepts_items {
278                        b.push_str("\n            .accepts_items()");
279                    }
280                    if p.no_occlusion {
281                        b.push_str("\n            .no_occlusion()");
282                    }
283                    if !p.kind.is_empty() {
284                        let _ = write!(b, "\n            .kind({:?})", p.kind);
285                    }
286                    if let Some(base) = &p.base {
287                        let _ = write!(b, "\n            .base({base:?})");
288                    }
289                    if let Some(set) = &p.set {
290                        let _ = write!(b, "\n            .set({set:?})");
291                    }
292                    if let Some(t) = p.press_ticks {
293                        let _ = write!(b, "\n            .press_ticks({t})");
294                    }
295                    if p.mobs_only {
296                        b.push_str("\n            .mobs_only()");
297                    }
298                }
299                Format::Item => b.push_str(&item_props(&e.item)),
300                _ => unreachable!("only blocks and items get here"),
301            }
302            let _ = writeln!(
303                code,
304                "    /// The definition from `{}` (name and tooltip come from the lang files).\n    pub fn {ident}() -> ::yog_api::{def} {{\n        {b}\n    }}",
305                e.file
306            );
307        }
308        code.push_str("}\n");
309        flush(&mut files, &mut code, &set.to_ascii_lowercase());
310    }
311
312    code.push_str("\n/// Registers every block and item of `content/` whose file doesn't say\n/// `\"register\": \"manual\"` — `yog_api::content!()` calls it after\n/// `Mod::register`.\n#[allow(unused_variables)]\npub fn register(registry: &mut ::yog_api::Registry) {\n");
313    let mut used: HashMap<(Format, String), usize> = HashMap::new();
314    for e in entries {
315        let path = e.id.split_once(':').map_or(e.id.as_str(), |(_, p)| p);
316        let mut ident = rust_ident(path);
317        let n = used.entry((e.kind, ident.clone())).or_insert(0);
318        *n += 1;
319        if *n > 1 {
320            let _ = write!(ident, "_{n}");
321        }
322        let ident = if is_keyword(&ident) {
323            format!("r#{ident}")
324        } else {
325            ident
326        };
327        if !e.auto {
328            continue;
329        }
330        let _ = match e.kind {
331            Format::Block => {
332                // Its item form, when the file says how it stacks, burns or survives fire.
333                let item = item_props(&e.item);
334                let stmt = if item.is_empty() {
335                    format!("registry.register_block(Blocks::{ident}());")
336                } else {
337                    format!("{{ registry.register_block(Blocks::{ident}()); registry.register_item(::yog_api::ItemDef::new({:?}){}); }}", e.id, item.replace("\n            ", ""))
338                };
339                writeln!(code, "    {}", when_loaded(&e.requires, &stmt))
340            }
341            Format::Item => writeln!(code, "    {}", when_loaded(&e.requires, &format!("registry.register_item(Items::{ident}());"))),
342            _ => unreachable!("only blocks and items get here"),
343        };
344    }
345    for g in sound_groups {
346        let _ = writeln!(
347            code,
348            "    registry.register_sound_group(::yog_api::SoundGroup::new({:?}).sounds({:?}, {:?}, {:?}, {:?}, {:?}).volume({}).pitch({}));",
349            g.id,
350            g.break_sound,
351            g.step,
352            g.place,
353            g.hit,
354            g.fall,
355            float(g.volume as f64),
356            float(g.pitch as f64)
357        );
358    }
359    for id in &defs.dimensions {
360        let _ = writeln!(
361            code,
362            "    registry.register_dimension_data({id:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{}\")));",
363            dimension_path(id)
364        );
365    }
366    for (feature, biomes, step) in &defs.biome_features {
367        let _ = writeln!(code, "    registry.add_biome_feature({feature:?}, {biomes:?}, {step:?});");
368    }
369    for m in &defs.tool_materials {
370        let _ = writeln!(
371            code,
372            "    registry.register_tool_material(::yog_api::ToolMaterial {{ id: {:?}.into(), durability: {}, speed: {:?}, attack_damage: {:?}, enchantability: {}, level: {:?}.into(), repair: {:?}.into(), level_index: {}, incorrect: {:?}.into() }});",
373            m.id, m.durability, m.speed, m.attack_damage, m.enchantability, m.level, m.repair, m.level_index, m.incorrect
374        );
375    }
376    for m in &defs.armor_materials {
377        let d = m.defense;
378        let _ = writeln!(
379            code,
380            "    registry.register_armor_material(::yog_api::ArmorMaterial {{ id: {:?}.into(), durability: {}, defense: ::yog_api::ArmorDefense {{ head: {}, chest: {}, legs: {}, feet: {} }}, enchantability: {}, equip_sound: {:?}.into(), toughness: {:?}, knockback_resistance: {:?}, repair: {:?}.into(), texture: {:?}.into() }});",
381            m.id, m.durability, d.head, d.chest, d.legs, d.feet, m.enchantability, m.equip_sound, m.toughness, m.knockback_resistance, m.repair, m.texture
382        );
383    }
384    for r in recipes {
385        let _ = writeln!(code, "    {}", when_loaded(&r.requires, &r.registration()));
386    }
387    for b in books {
388        let _ = writeln!(
389            code,
390            "    registry.register_book_data({:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{}\")));",
391            b.id,
392            b.path()
393        );
394    }
395    for (kind, id) in &defs.definitions {
396        let _ = writeln!(
397            code,
398            "    registry.register_definition_data({kind:?}, {id:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{}\")));",
399            definition_path(kind, id)
400        );
401    }
402    for id in &defs.loot_modifiers {
403        let _ = writeln!(
404            code,
405            "    registry.add_loot_modifier_data({id:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{}\")));",
406            loot_path(id)
407        );
408    }
409    for e in entities {
410        let _ = writeln!(
411            code,
412            "    registry.register_entity_data({:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{}\")));",
413            e.id,
414            e.path()
415        );
416    }
417    for b in bodies {
418        let _ = writeln!(
419            code,
420            "    registry.register_body_{}({:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{}\")));",
421            if b.model { "model" } else { "anims" },
422            b.id,
423            b.path()
424        );
425    }
426    for font in fonts {
427        let _ = writeln!(
428            code,
429            "    registry.register_font_data({:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{}\")));",
430            font.id,
431            font.path()
432        );
433    }
434    code.push_str("}\n");
435    if entries.iter().any(|e| e.auto) || !recipes.is_empty() || !books.is_empty() || !bodies.is_empty() || !entities.is_empty() || !fonts.is_empty() || !sound_groups.is_empty() || !defs.tool_materials.is_empty() || !defs.armor_materials.is_empty() || !defs.biome_features.is_empty() || !defs.dimensions.is_empty() {
436        code.push_str(
437            "\n// Runs when the mod's library loads: `export_mod!` calls `register` after\n// `Mod::register`.\n#[::yog_api::ctor::ctor(unsafe, crate_path = ::yog_api::ctor)]\nfn __yog_content_auto_register() {\n    ::yog_api::__yog_content_registrars()\n        .lock()\n        .unwrap_or_else(|e| e.into_inner())\n        .push(register);\n}\n",
438        );
439    }
440    flush(&mut files, &mut code, "register");
441    for t in &defs.tables {
442        code.push_str(&table::rust(t));
443    }
444    flush(&mut files, &mut code, "tables");
445    let mut root = String::from(GENERATED_HEADER);
446    for (name, _) in &files {
447        let _ = writeln!(root, "include!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{GENERATED_PARTS_DIR}/{name}.g.rs\"));");
448    }
449    Source { root, parts: files }
450}
451
452pub(super) fn is_keyword(s: &str) -> bool {
453    const KW: &[&str] = &[
454        "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
455        "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
456        "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use",
457        "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final",
458        "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try", "gen",
459    ];
460    KW.contains(&s)
461}
462
463/// Writes `compiled.source` to [`GENERATED_FILE`] (only when it changed);
464/// deletes it when the mod has no content files.
465pub fn write_source(root: &Path, compiled: &Compiled) -> Result<(), String> {
466    write_tags(root, compiled)?;
467    let out = root.join(GENERATED_FILE);
468    let dir = out.parent().unwrap_or(root);
469    let parts_dir = dir.join(GENERATED_PARTS_DIR);
470    let Some(source) = &compiled.source else {
471        let _ = std::fs::remove_file(&out);
472        let _ = std::fs::remove_dir_all(&parts_dir);
473        return Ok(());
474    };
475    std::fs::create_dir_all(&parts_dir).map_err(|e| format!("{}: {e}", parts_dir.display()))?;
476    let wanted: Vec<std::path::PathBuf> = source.parts.iter().map(|(name, _)| parts_dir.join(format!("{name}.g.rs"))).collect();
477    for ((_, code), file) in source.parts.iter().zip(&wanted) {
478        crate::write_if_changed(file, code)?;
479    }
480    // Parts the content no longer has.
481    for entry in std::fs::read_dir(&parts_dir).map_err(|e| format!("{}: {e}", parts_dir.display()))?.flatten() {
482        if !wanted.contains(&entry.path()) {
483            let _ = std::fs::remove_file(entry.path());
484        }
485    }
486    crate::write_if_changed(&out, &source.root)?;
487    for (path, bytes) in &compiled.data_files {
488        let file = dir.join(path);
489        if std::fs::read(&file).ok().as_deref() != Some(bytes.as_slice()) {
490            if let Some(parent) = file.parent() {
491                std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
492            }
493            std::fs::write(&file, bytes).map_err(|e| format!("{}: {e}", file.display()))?;
494        }
495    }
496    Ok(())
497}