yog_cli/content/pipeline/source/
mod.rs1mod 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
26pub(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 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 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
104pub(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
120pub(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
147fn 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
154fn 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
176pub(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 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
208pub(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
217fn 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
256pub(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
279pub(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
312pub 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 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
357fn 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
370const GENERATED_HEADER: Header = Header {
372 tool: "`yog build`",
373 source: "content/**",
374 regenerate: Regenerate::EveryBuild,
375};
376
377pub const GENERATED_PARTS_DIR: &str = "content";
380
381#[derive(Debug, Clone, Default)]
384pub struct Source {
385 pub root: String,
387 pub parts: Vec<(String, String)>,
389}
390
391impl Source {
392 #[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 #[cfg(test)]
403 pub(crate) fn all(&self) -> String {
404 self.parts.iter().map(|(_, c)| c.as_str()).collect()
405 }
406}