Skip to main content

yog_cli/content/pipeline/source/
mod.rs

1//! The Rust the build generates for a mod's content (`yog-content.g.rs`).
2
3mod blocks;
4mod register;
5mod script;
6
7pub(in crate::content) use script::ys_source;
8
9pub(in crate::content) use register::included;
10
11use yog_ids::code_set;
12
13use crate::content::formats::defs::Defs;
14use crate::content::formats::recipe::registration::Recipe;
15use crate::content::pipeline::entry::{BlockProps, Entry};
16use crate::content::pipeline::files::{BodyFile, BookFile, EntityFile, FontFile};
17use crate::content::pipeline::outputs::write_tags;
18use crate::content::{Compiled, GENERATED_FILE, GENERATED_SCRIPT, formats::table};
19use std::collections::HashMap;
20use std::fmt::Write;
21use std::path::Path;
22use yog_codegen::{Header, Regenerate};
23use yog_codegen_rust::Rust;
24use yog_ids::Format;
25
26/// [`yog_registry::REGISTRATIONS_FILE`]: what the mod registers at the
27/// game's start, each with a hash of what can't change while it runs — a
28/// block's properties and states, not its model, name or tags (the game
29/// reloads those).
30pub(in crate::content) fn registrations(
31    entries: &[Entry],
32    entities: &[EntityFile],
33    dimensions: &[(String, Vec<u8>)],
34    defs: &Defs,
35) -> String {
36    fn hash(text: &str) -> String {
37        // FNV-1a: the same on every build of the CLI.
38        const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
39        const PRIME: u64 = 0x0100_0000_01b3;
40        let h = text
41            .bytes()
42            .fold(OFFSET, |h, b| (h ^ u64::from(b)).wrapping_mul(PRIME));
43        format!("{h:016x}")
44    }
45    let mut lines = Vec::new();
46    for e in entries {
47        let what = match e.kind {
48            Format::Block => {
49                // Its tool tags and level are data (tags), reloaded with the rest.
50                let b = BlockProps {
51                    tools: Vec::new(),
52                    tool_level: None,
53                    ..e.block.clone()
54                };
55                format!("{b:?} {:?} {:?}", e.item, e.requires)
56            }
57            _ => format!("{:?} {:?}", e.item, e.requires),
58        };
59        lines.push(format!("{} {} {}", e.kind.noun(), e.id, hash(&what)));
60    }
61    for e in entities {
62        lines.push(format!(
63            "{} {} {}",
64            yog_ids::kind::ENTITY,
65            e.id,
66            hash(&format!("{:?}", e.data))
67        ));
68    }
69    for (id, data) in dimensions {
70        lines.push(format!(
71            "{} {id} {}",
72            yog_ids::kind::DIMENSION,
73            hash(&format!("{data:?}"))
74        ));
75    }
76    for g in &defs.sound_groups {
77        lines.push(format!(
78            "{} {} {}",
79            yog_ids::kind::SOUND_GROUP,
80            g.id,
81            hash(&format!("{g:?}"))
82        ));
83    }
84    for m in &defs.tool_materials {
85        lines.push(format!(
86            "{} {} {}",
87            yog_ids::kind::TOOL_MATERIAL,
88            m.id,
89            hash(&format!("{m:?}"))
90        ));
91    }
92    for m in &defs.armor_materials {
93        lines.push(format!(
94            "{} {} {}",
95            yog_ids::kind::ARMOR_MATERIAL,
96            m.id,
97            hash(&format!("{m:?}"))
98        ));
99    }
100    lines.sort();
101    lines.join("\n") + "\n"
102}
103
104/// Whether any `src/**/*.rs` has `content!(` — the include of the generated code.
105pub(in crate::content) fn mentions_content_macro(dir: &Path) -> bool {
106    let Ok(entries) = std::fs::read_dir(dir) else {
107        return false;
108    };
109    entries.flatten().any(|e| {
110        let p = e.path();
111        if p.is_dir() {
112            mentions_content_macro(&p)
113        } else {
114            p.extension().is_some_and(|x| x == "rs")
115                && std::fs::read_to_string(&p).is_ok_and(|t| t.contains("content!("))
116        }
117    })
118}
119
120/// `ruby_ore`, `ores/ruby` → `ores_ruby`; a Rust keyword gets `r#`.
121pub(in crate::content) fn rust_ident(path: &str) -> String {
122    let mut s: String = path
123        .chars()
124        .map(|c| {
125            if c.is_ascii_alphanumeric() {
126                c.to_ascii_lowercase()
127            } else {
128                '_'
129            }
130        })
131        .collect();
132    if s.starts_with(|c: char| c.is_ascii_digit()) {
133        s.insert(0, '_');
134    }
135    s
136}
137
138pub(in crate::content) fn float(n: f64) -> String {
139    let s = format!("{n:?}");
140    if s.contains('.') || s.contains('e') {
141        s
142    } else {
143        format!("{s}.0")
144    }
145}
146
147/// `name`, unique among those `used` so far: `ruby`, then `ruby_2`.
148fn unique_ident(used: &mut HashMap<String, usize>, name: String) -> String {
149    let n = used.entry(name.clone()).or_insert(0);
150    *n += 1;
151    if *n > 1 { format!("{name}_{n}") } else { name }
152}
153
154/// `pub struct <name>` with a `pub const <PATH>: &str = "<id>"` per id —
155/// what a mod's code names its content by. Nothing for no ids.
156fn id_consts<'a>(doc: &str, name: &str, ids: impl Iterator<Item = &'a str>) -> String {
157    let ids: Vec<&str> = ids.collect();
158    if ids.is_empty() {
159        return String::new();
160    }
161    let mut code = format!(
162        "\n/// {doc}\n#[allow(dead_code)]\npub struct {name};\n\n#[allow(dead_code, clippy::all)]\nimpl {name} {{\n"
163    );
164    let mut used: HashMap<String, usize> = HashMap::new();
165    for id in ids {
166        let ident = unique_ident(
167            &mut used,
168            rust_ident(yog_ids::split(id).1).to_ascii_uppercase(),
169        );
170        let _ = writeln!(code, "    pub const {ident}: &'static str = {id:?};");
171    }
172    code.push_str("}\n");
173    code
174}
175
176/// What the generated code is made of: every kind of content the build read.
177pub(in crate::content) struct Content<'a> {
178    pub(crate) entries: &'a [Entry],
179    pub(crate) recipes: &'a [Recipe],
180    pub(crate) books: &'a [BookFile],
181    pub(crate) bodies: &'a [BodyFile],
182    pub(crate) entities: &'a [EntityFile],
183    pub(crate) fonts: &'a [FontFile],
184    pub(crate) defs: &'a Defs,
185    pub(crate) textures: &'a std::collections::BTreeSet<String>,
186}
187
188impl Content<'_> {
189    /// Whether the content registers anything: its code's `register` must run.
190    pub(in crate::content) fn registers_anything(&self) -> bool {
191        let d = self.defs;
192        self.entries.iter().any(|e| e.auto)
193            || !self.recipes.is_empty()
194            || !self.books.is_empty()
195            || !self.bodies.is_empty()
196            || !self.entities.is_empty()
197            || !self.fonts.is_empty()
198            || !d.sound_groups.is_empty()
199            || !d.tool_materials.is_empty()
200            || !d.armor_materials.is_empty()
201            || !d.biome_features.is_empty()
202            || !d.dimensions.is_empty()
203            || !d.loot_modifiers.is_empty()
204            || !d.definitions.is_empty()
205    }
206}
207
208/// A set of the content's ids the mod's code names its content by: its
209/// name (`Entities`), what it holds, its ids — the same in each language
210/// the code is generated in.
211pub(in crate::content) struct IdSet<'a> {
212    pub(in crate::content) name: &'static str,
213    pub(in crate::content) doc: String,
214    pub(in crate::content) ids: Vec<&'a str>,
215}
216
217/// The sets of ids beside the blocks' and items' (whose Rust has their
218/// definitions too).
219fn id_sets<'a>(c: &'a Content) -> Vec<IdSet<'a>> {
220    let mut sets = vec![
221        IdSet {
222            name: code_set::ENTITIES,
223            doc: "The entity types of `content/` (`.yentity`), by id.".into(),
224            ids: c.entities.iter().map(|e| e.id.as_str()).collect(),
225        },
226        IdSet {
227            name: code_set::ADVANCEMENTS,
228            doc: "The advancements of `content/` (`.ydef` kind `advancement`), by id\n/// (`yog_api::advancements::grant`/`done`).".into(),
229            ids: c.defs.advancements.iter().map(String::as_str).collect(),
230        },
231    ];
232    for (model, name, what) in [
233        (true, code_set::MODELS, "models (`.ymodel`)"),
234        (false, code_set::ANIMS, "animations (`.yanim`)"),
235    ] {
236        sets.push(IdSet {
237            name,
238            doc: format!("The {what} of `content/`, by id (`yog_api::body::model`/`clip`)."),
239            ids: c
240                .bodies
241                .iter()
242                .filter(|b| b.model == model)
243                .map(|b| b.id.as_str())
244                .collect(),
245        });
246    }
247    sets.push(IdSet {
248        name: code_set::TEXTURES,
249        doc: "The textures of `resources/` (`.ytexture`), by id — what the mod's code draws."
250            .into(),
251        ids: c.textures.iter().map(String::as_str).collect(),
252    });
253    sets
254}
255
256/// Every set of the content's ids, the blocks' and items' first: what the
257/// mod's Yog Script names its content by.
258pub(in crate::content) fn all_id_sets<'a>(c: &'a Content) -> Vec<IdSet<'a>> {
259    let mut sets: Vec<IdSet> = [
260        (Format::Block, code_set::BLOCKS, "blocks (`.yblock`)"),
261        (Format::Item, code_set::ITEMS, "items (`.yitem`)"),
262    ]
263    .into_iter()
264    .map(|(format, name, what)| IdSet {
265        name,
266        doc: format!("The {what} of `content/`, by id."),
267        ids: c
268            .entries
269            .iter()
270            .filter(|e| e.kind == format)
271            .map(|e| e.id.as_str())
272            .collect(),
273    })
274    .collect();
275    sets.extend(id_sets(c));
276    sets
277}
278
279/// The content's code, part by part.
280pub(in crate::content) fn rust_source(c: &Content) -> Source {
281    let mut parts: Vec<(String, String)> = Vec::new();
282    let mut part = |name: &str, code: String| {
283        if !code.trim().is_empty() {
284            parts.push((
285                name.to_string(),
286                format!("{}{code}", GENERATED_HEADER.comment::<Rust>()),
287            ));
288        }
289    };
290    for set in id_sets(c) {
291        part(
292            &set.name.to_ascii_lowercase(),
293            id_consts(&set.doc, set.name, set.ids.into_iter()),
294        );
295    }
296    let idents = blocks::entry_idents(c.entries);
297    for (name, code) in blocks::entries_code(c.entries, &idents) {
298        part(&name, code);
299    }
300    part("register", register::register_code(c, &idents));
301    part("tables", c.defs.tables.iter().map(table::rust).collect());
302    let mut root = GENERATED_HEADER.comment::<Rust>();
303    for (name, _) in &parts {
304        let _ = writeln!(
305            root,
306            "include!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{GENERATED_PARTS_DIR}/{name}.g.rs\"));"
307        );
308    }
309    Source { root, parts }
310}
311
312/// Writes `compiled.source` to [`GENERATED_FILE`] (only when it changed);
313/// deletes it when the mod has no content files.
314pub fn write_source(root: &Path, compiled: &Compiled) -> Result<(), String> {
315    write_tags(root, compiled)?;
316    let out = root.join(GENERATED_FILE);
317    let dir = out.parent().unwrap_or(root);
318    let parts_dir = dir.join(GENERATED_PARTS_DIR);
319    let Some(source) = &compiled.source else {
320        crate::files::remove(&out);
321        crate::files::remove(&parts_dir);
322        return write_script(root, None);
323    };
324    std::fs::create_dir_all(&parts_dir).map_err(|e| format!("{}: {e}", parts_dir.display()))?;
325    let wanted: Vec<std::path::PathBuf> = source
326        .parts
327        .iter()
328        .map(|(name, _)| parts_dir.join(format!("{name}.g.rs")))
329        .collect();
330    for ((_, code), file) in source.parts.iter().zip(&wanted) {
331        crate::write_if_changed(file, code)?;
332    }
333    // Parts the content no longer has.
334    for entry in std::fs::read_dir(&parts_dir)
335        .map_err(|e| format!("{}: {e}", parts_dir.display()))?
336        .flatten()
337    {
338        if !wanted.contains(&entry.path()) {
339            crate::files::remove(&entry.path());
340        }
341    }
342    crate::write_if_changed(&out, &source.root)?;
343    write_script(root, compiled.script.as_deref())?;
344    for (path, bytes) in &compiled.data_files {
345        let file = dir.join(path);
346        if std::fs::read(&file).ok().as_deref() != Some(bytes.as_slice()) {
347            if let Some(parent) = file.parent() {
348                std::fs::create_dir_all(parent)
349                    .map_err(|e| format!("{}: {e}", parent.display()))?;
350            }
351            std::fs::write(&file, bytes).map_err(|e| format!("{}: {e}", file.display()))?;
352        }
353    }
354    Ok(())
355}
356
357/// Writes the content's Yog Script ([`GENERATED_SCRIPT`]) when it has
358/// one (only when it changed), deletes it when it hasn't.
359fn write_script(root: &Path, script: Option<&str>) -> Result<(), String> {
360    let file = root.join(GENERATED_SCRIPT);
361    match script {
362        Some(code) => crate::write_if_changed(&file, code),
363        None => {
364            crate::files::remove(&file);
365            Ok(())
366        }
367    }
368}
369
370/// The generated files' header; `yog_api::content!()` includes them.
371const GENERATED_HEADER: Header = Header {
372    tool: "`yog build`",
373    source: "content/**",
374    regenerate: Regenerate::EveryBuild,
375};
376
377/// Where the content's code goes besides the root file, under
378/// `obj/generated/`: one file a part (`items.g.rs`, `register.g.rs`, …).
379pub const GENERATED_PARTS_DIR: &str = "content";
380
381/// The content's code: the root file ([`GENERATED_FILE`]), which only
382/// includes the parts, and the parts — (name, code) each.
383#[derive(Debug, Clone, Default)]
384pub struct Source {
385    /// The root file's code.
386    pub root: String,
387    /// Each part: its file's name and its code.
388    pub parts: Vec<(String, String)>,
389}
390
391impl Source {
392    /// Part `name`'s code (`"items"`).
393    #[cfg(test)]
394    pub(crate) fn part(&self, name: &str) -> &str {
395        self.parts
396            .iter()
397            .find(|(n, _)| n == name)
398            .map_or("", |(_, c)| c.as_str())
399    }
400
401    /// Every part's code, for tests that look for a line wherever it is.
402    #[cfg(test)]
403    pub(crate) fn all(&self) -> String {
404        self.parts.iter().map(|(_, c)| c.as_str()).collect()
405    }
406}