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