yog_cli/content/pipeline/source/
mod.rs1mod blocks;
4mod register;
5
6pub(in crate::content) use register::included;
7
8use yog_ids::code_set;
9
10use crate::content::formats::defs::Defs;
11use crate::content::formats::recipe::registration::Recipe;
12use crate::content::pipeline::entry::{BlockProps, Entry};
13use crate::content::pipeline::files::{BodyFile, BookFile, EntityFile, FontFile};
14use crate::content::pipeline::outputs::write_tags;
15use crate::content::{Compiled, GENERATED_FILE, formats::table};
16use std::collections::HashMap;
17use std::fmt::Write;
18use std::path::Path;
19use yog_codegen::{Header, Regenerate};
20use yog_codegen_rust::Rust;
21use yog_ids::Format;
22
23pub(in crate::content) fn registrations(
28 entries: &[Entry],
29 entities: &[EntityFile],
30 dimensions: &[(String, Vec<u8>)],
31 defs: &Defs,
32) -> String {
33 fn hash(text: &str) -> String {
34 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
36 const PRIME: u64 = 0x0100_0000_01b3;
37 let h = text
38 .bytes()
39 .fold(OFFSET, |h, b| (h ^ u64::from(b)).wrapping_mul(PRIME));
40 format!("{h:016x}")
41 }
42 let mut lines = Vec::new();
43 for e in entries {
44 let what = match e.kind {
45 Format::Block => {
46 let b = BlockProps {
48 tools: Vec::new(),
49 tool_level: None,
50 ..e.block.clone()
51 };
52 format!("{b:?} {:?} {:?}", e.item, e.requires)
53 }
54 _ => format!("{:?} {:?}", e.item, e.requires),
55 };
56 lines.push(format!("{} {} {}", e.kind.noun(), e.id, hash(&what)));
57 }
58 for e in entities {
59 lines.push(format!(
60 "{} {} {}",
61 yog_ids::kind::ENTITY,
62 e.id,
63 hash(&format!("{:?}", e.data))
64 ));
65 }
66 for (id, data) in dimensions {
67 lines.push(format!(
68 "{} {id} {}",
69 yog_ids::kind::DIMENSION,
70 hash(&format!("{data:?}"))
71 ));
72 }
73 for g in &defs.sound_groups {
74 lines.push(format!(
75 "{} {} {}",
76 yog_ids::kind::SOUND_GROUP,
77 g.id,
78 hash(&format!("{g:?}"))
79 ));
80 }
81 for m in &defs.tool_materials {
82 lines.push(format!(
83 "{} {} {}",
84 yog_ids::kind::TOOL_MATERIAL,
85 m.id,
86 hash(&format!("{m:?}"))
87 ));
88 }
89 for m in &defs.armor_materials {
90 lines.push(format!(
91 "{} {} {}",
92 yog_ids::kind::ARMOR_MATERIAL,
93 m.id,
94 hash(&format!("{m:?}"))
95 ));
96 }
97 lines.sort();
98 lines.join("\n") + "\n"
99}
100
101pub(in crate::content) fn mentions_content_macro(dir: &Path) -> bool {
103 let Ok(entries) = std::fs::read_dir(dir) else {
104 return false;
105 };
106 entries.flatten().any(|e| {
107 let p = e.path();
108 if p.is_dir() {
109 mentions_content_macro(&p)
110 } else {
111 p.extension().is_some_and(|x| x == "rs")
112 && std::fs::read_to_string(&p).is_ok_and(|t| t.contains("content!("))
113 }
114 })
115}
116
117pub(in crate::content) fn rust_ident(path: &str) -> String {
119 let mut s: String = path
120 .chars()
121 .map(|c| {
122 if c.is_ascii_alphanumeric() {
123 c.to_ascii_lowercase()
124 } else {
125 '_'
126 }
127 })
128 .collect();
129 if s.starts_with(|c: char| c.is_ascii_digit()) {
130 s.insert(0, '_');
131 }
132 s
133}
134
135pub(in crate::content) fn float(n: f64) -> String {
136 let s = format!("{n:?}");
137 if s.contains('.') || s.contains('e') {
138 s
139 } else {
140 format!("{s}.0")
141 }
142}
143
144fn unique_ident(used: &mut HashMap<String, usize>, name: String) -> String {
146 let n = used.entry(name.clone()).or_insert(0);
147 *n += 1;
148 if *n > 1 { format!("{name}_{n}") } else { name }
149}
150
151fn id_consts<'a>(doc: &str, name: &str, ids: impl Iterator<Item = &'a str>) -> String {
154 let ids: Vec<&str> = ids.collect();
155 if ids.is_empty() {
156 return String::new();
157 }
158 let mut code = format!(
159 "\n/// {doc}\n#[allow(dead_code)]\npub struct {name};\n\n#[allow(dead_code, clippy::all)]\nimpl {name} {{\n"
160 );
161 let mut used: HashMap<String, usize> = HashMap::new();
162 for id in ids {
163 let ident = unique_ident(
164 &mut used,
165 rust_ident(yog_ids::split(id).1).to_ascii_uppercase(),
166 );
167 let _ = writeln!(code, " pub const {ident}: &'static str = {id:?};");
168 }
169 code.push_str("}\n");
170 code
171}
172
173pub(in crate::content) struct Content<'a> {
175 pub(crate) entries: &'a [Entry],
176 pub(crate) recipes: &'a [Recipe],
177 pub(crate) books: &'a [BookFile],
178 pub(crate) bodies: &'a [BodyFile],
179 pub(crate) entities: &'a [EntityFile],
180 pub(crate) fonts: &'a [FontFile],
181 pub(crate) defs: &'a Defs,
182 pub(crate) textures: &'a std::collections::BTreeSet<String>,
183}
184
185impl Content<'_> {
186 pub(in crate::content) fn registers_anything(&self) -> bool {
188 let d = self.defs;
189 self.entries.iter().any(|e| e.auto)
190 || !self.recipes.is_empty()
191 || !self.books.is_empty()
192 || !self.bodies.is_empty()
193 || !self.entities.is_empty()
194 || !self.fonts.is_empty()
195 || !d.sound_groups.is_empty()
196 || !d.tool_materials.is_empty()
197 || !d.armor_materials.is_empty()
198 || !d.biome_features.is_empty()
199 || !d.dimensions.is_empty()
200 || !d.loot_modifiers.is_empty()
201 || !d.definitions.is_empty()
202 }
203}
204
205pub(in crate::content) fn rust_source(c: &Content) -> Source {
207 let mut parts: Vec<(String, String)> = Vec::new();
208 let mut part = |name: &str, code: String| {
209 if !code.trim().is_empty() {
210 parts.push((
211 name.to_string(),
212 format!("{}{code}", GENERATED_HEADER.comment::<Rust>()),
213 ));
214 }
215 };
216 let doc = "The entity types of `content/` (`.yentity`), by id.";
217 part(
218 "entities",
219 id_consts(
220 doc,
221 code_set::ENTITIES,
222 c.entities.iter().map(|e| e.id.as_str()),
223 ),
224 );
225 let doc = "The advancements of `content/` (`.ydef` kind `advancement`), by id\n/// (`yog_api::advancements::grant`/`done`).";
226 part(
227 "advancements",
228 id_consts(
229 doc,
230 code_set::ADVANCEMENTS,
231 c.defs.advancements.iter().map(String::as_str),
232 ),
233 );
234 for (model, set, what) in [
235 (true, code_set::MODELS, "models (`.ymodel`)"),
236 (false, code_set::ANIMS, "animations (`.yanim`)"),
237 ] {
238 let doc = format!("The {what} of `content/`, by id (`yog_api::body::model`/`clip`).");
239 part(
240 &set.to_ascii_lowercase(),
241 id_consts(
242 &doc,
243 set,
244 c.bodies
245 .iter()
246 .filter(|b| b.model == model)
247 .map(|b| b.id.as_str()),
248 ),
249 );
250 }
251 let doc = "The textures of `resources/` (`.ytexture`), by id — what the mod's code draws.";
252 part(
253 "textures",
254 id_consts(
255 doc,
256 code_set::TEXTURES,
257 c.textures.iter().map(String::as_str),
258 ),
259 );
260 let idents = blocks::entry_idents(c.entries);
261 for (name, code) in blocks::entries_code(c.entries, &idents) {
262 part(&name, code);
263 }
264 part("register", register::register_code(c, &idents));
265 part("tables", c.defs.tables.iter().map(table::rust).collect());
266 let mut root = GENERATED_HEADER.comment::<Rust>();
267 for (name, _) in &parts {
268 let _ = writeln!(
269 root,
270 "include!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/generated/{GENERATED_PARTS_DIR}/{name}.g.rs\"));"
271 );
272 }
273 Source { root, parts }
274}
275
276pub fn write_source(root: &Path, compiled: &Compiled) -> Result<(), String> {
279 write_tags(root, compiled)?;
280 let out = root.join(GENERATED_FILE);
281 let dir = out.parent().unwrap_or(root);
282 let parts_dir = dir.join(GENERATED_PARTS_DIR);
283 let Some(source) = &compiled.source else {
284 crate::files::remove(&out);
285 crate::files::remove(&parts_dir);
286 return Ok(());
287 };
288 std::fs::create_dir_all(&parts_dir).map_err(|e| format!("{}: {e}", parts_dir.display()))?;
289 let wanted: Vec<std::path::PathBuf> = source
290 .parts
291 .iter()
292 .map(|(name, _)| parts_dir.join(format!("{name}.g.rs")))
293 .collect();
294 for ((_, code), file) in source.parts.iter().zip(&wanted) {
295 crate::write_if_changed(file, code)?;
296 }
297 for entry in std::fs::read_dir(&parts_dir)
299 .map_err(|e| format!("{}: {e}", parts_dir.display()))?
300 .flatten()
301 {
302 if !wanted.contains(&entry.path()) {
303 crate::files::remove(&entry.path());
304 }
305 }
306 crate::write_if_changed(&out, &source.root)?;
307 for (path, bytes) in &compiled.data_files {
308 let file = dir.join(path);
309 if std::fs::read(&file).ok().as_deref() != Some(bytes.as_slice()) {
310 if let Some(parent) = file.parent() {
311 std::fs::create_dir_all(parent)
312 .map_err(|e| format!("{}: {e}", parent.display()))?;
313 }
314 std::fs::write(&file, bytes).map_err(|e| format!("{}: {e}", file.display()))?;
315 }
316 }
317 Ok(())
318}
319
320const GENERATED_HEADER: Header = Header {
322 tool: "`yog build`",
323 source: "content/**",
324 regenerate: Regenerate::EveryBuild,
325};
326
327pub const GENERATED_PARTS_DIR: &str = "content";
330
331#[derive(Debug, Clone, Default)]
334pub struct Source {
335 pub root: String,
337 pub parts: Vec<(String, String)>,
339}
340
341impl Source {
342 #[cfg(test)]
344 pub(crate) fn part(&self, name: &str) -> &str {
345 self.parts
346 .iter()
347 .find(|(n, _)| n == name)
348 .map_or("", |(_, c)| c.as_str())
349 }
350
351 #[cfg(test)]
353 pub(crate) fn all(&self) -> String {
354 self.parts.iter().map(|(_, c)| c.as_str()).collect()
355 }
356}