1use 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
28pub(super) fn registrations(entries: &[Entry], entities: &[EntityFile], dimensions: &[(String, Vec<u8>)], defs: &Defs) -> String {
33 fn hash(text: &str) -> String {
34 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 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
69pub(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
85pub(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
112pub(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
121pub(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
129fn 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
150pub(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 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
463pub 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 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}