1use std::collections::{BTreeMap, HashMap};
6use std::path::{Path, PathBuf};
7use yog_data::Schema;
8use yog_ids::Format;
9use yog_ids::ns::MINECRAFT;
10use yog_registry::{Brew, PotionDef};
11
12use crate::content::block_item::{compile_file, inventory_screens};
13use crate::content::body::{compile_body, compile_entity, compile_font};
14use crate::content::book::{BookAgainst, compile_book, recipe_layouts};
15use crate::content::ctx::{Deferred, Shared, Sink, Wanted};
16use crate::content::ctx::{caveat, problem};
17use crate::content::defs::data::{definition_path, loot_path};
18use crate::content::defs::ladder::{read_defs, tool_level_tags};
19use crate::content::defs::worldgen::dimension_path;
20use crate::content::defs::{DefFile, Defs, Made, compile_def};
21use crate::content::entry::Entry;
22use crate::content::files::{BodyFile, BookFile, EntityFile, FontFile};
23use crate::content::outputs::version_overlays;
24use crate::content::recipe::registration::Recipe;
25use crate::content::recipe::{Against, compile_recipe};
26use crate::content::source::{Content, mentions_content_macro, registrations, rust_source};
27use crate::content::{
28 CONTENT_DIR, Compiled, content_format, datapack, find, looks, particles, refs, sounds,
29 textures, valid_namespace, videos,
30};
31use crate::diagnostics::Diagnostic;
32use crate::yres;
33
34#[derive(Default)]
36struct Ids(HashMap<String, String>);
37
38impl Ids {
39 fn claim(&mut self, errors: &mut Vec<Diagnostic>, file: &str, what: &str, id: &str) {
41 if let Some(other) = self.0.insert(id.to_string(), file.to_string()) {
42 errors.push(problem(format!("{what} {id} is also defined by {other}")).in_file(file));
43 }
44 }
45}
46
47#[derive(Default)]
49struct Pending {
50 books: Vec<(String, String)>,
51 recipes: Vec<(String, String, PathBuf)>,
52 defs: Vec<DefFile>,
53}
54
55struct Build<'a> {
57 shared: Shared<'a>,
58 mod_ns: Option<&'a str>,
60 sink: Sink,
61 out: Compiled,
62 generated: BTreeMap<String, String>,
64 entries: Vec<Entry>,
65 potions: BTreeMap<String, PotionDef>,
66 entities: Vec<EntityFile>,
67 bodies: Vec<BodyFile>,
68 fonts: Vec<FontFile>,
69 recipes: Vec<Recipe>,
70 books: Vec<BookFile>,
71 ids: Ids,
73 model_ids: Ids,
74 anim_ids: Ids,
75 font_ids: Ids,
76 recipe_ids: Ids,
77 book_ids: Ids,
78 pending: Pending,
79}
80
81pub fn compile(root: &Path, mod_id: &str, refs: &yres::Refs) -> Result<Compiled, String> {
84 let files = find(root);
85 let mut sink = Sink::default();
86 let own = textures::compile(root, mod_id, refs, &mut sink.errors);
87 let mut videos = videos::compile(root, mod_id, &mut sink.errors);
88 let events = std::mem::take(&mut videos.events);
89 let sounds = sounds::compile(root, mod_id, refs, events, &mut sink.errors);
90 let particles = particles::compile(root, mod_id, &own.ids, &mut sink.errors);
91 if files.is_empty() && particles.is_empty() && videos.is_empty() {
92 return media_only(own, sounds, sink);
93 }
94 let mod_ns = valid_namespace(mod_id).then_some(mod_id);
95 let shared = Shared {
96 root,
97 refs,
98 ns: mod_ns.unwrap_or(MINECRAFT),
99 own_textures: &own.ids,
100 };
101 let mut b = Build::new(shared, mod_ns, sink);
102 for path in &files {
103 b.read(path);
104 }
105 let mut known = b.known();
106 let mut defs = read_defs(&b.pending.defs, b.shared.ns, &mut b.sink.errors);
107 let mut made = b.defs(&known);
108 made.definitions.extend(particles.definitions);
109 made.definitions.extend(videos.definitions);
110 let layouts = b.recipes(&mut known, &made);
111 b.check_deferred(&defs, &known);
112 b.tool_levels(&defs);
113 b.books(&known, &layouts);
114 b.inventories();
115 b.out.packed = own.packed_but(&b.out.loose);
116 b.out.outputs.extend(own.outputs);
117 b.out.outputs.extend(sounds.output);
118 b.out.outputs.extend(sounds.rendered);
119 b.out.outputs.extend(particles.outputs);
120 b.out.outputs.extend(videos.outputs);
121 b.finish(&mut defs, made, &known)
122}
123
124fn media_only(
126 own: textures::Textures,
127 sounds: sounds::Sounds,
128 sink: Sink,
129) -> Result<Compiled, String> {
130 if !sink.errors.is_empty() {
131 crate::diagnostics::report_all(&sink.errors);
132 return Err(format!(
133 "invalid textures/sounds in {}/:{}{}",
134 crate::yres::RESOURCES_DIR,
135 crate::diagnostics::LIST_SEP,
136 crate::diagnostics::listed(&sink.errors)
137 ));
138 }
139 let mut out = Compiled::default();
140 out.outputs.extend(own.outputs);
141 out.outputs.extend(sounds.output);
142 out.outputs.extend(sounds.rendered);
143 out.packed = own.packed;
144 Ok(out)
145}
146
147impl<'a> Build<'a> {
148 fn new(shared: Shared<'a>, mod_ns: Option<&'a str>, sink: Sink) -> Build<'a> {
149 Build {
150 shared,
151 mod_ns,
152 sink,
153 out: Compiled::default(),
154 generated: BTreeMap::new(),
155 entries: Vec::new(),
156 potions: BTreeMap::new(),
157 entities: Vec::new(),
158 bodies: Vec::new(),
159 fonts: Vec::new(),
160 recipes: Vec::new(),
161 books: Vec::new(),
162 ids: Ids::default(),
163 model_ids: Ids::default(),
164 anim_ids: Ids::default(),
165 font_ids: Ids::default(),
166 recipe_ids: Ids::default(),
167 book_ids: Ids::default(),
168 pending: Pending::default(),
169 }
170 }
171
172 fn read(&mut self, path: &Path) {
175 let shown = crate::shown_path(self.shared.root, path);
176 let Some(kind) = content_format(path) else {
177 return;
178 };
179 let text = match crate::files::read_source(path) {
180 Ok(t) => t,
181 Err(e) => {
182 return self
183 .sink
184 .errors
185 .push(problem(format!("{e}")).in_file(&shown));
186 }
187 };
188 let errors = &mut self.sink.errors;
189 match kind {
190 Format::Model | Format::Anim => {
191 let Some(b) = compile_body(&self.shared, &shown, &text, kind, path, errors) else {
192 return;
193 };
194 let ids = if b.model {
195 &mut self.model_ids
196 } else {
197 &mut self.anim_ids
198 };
199 ids.claim(errors, &shown, kind.noun(), &b.id);
200 self.bodies.push(b);
201 }
202 Format::Font => {
203 let Some(font) = compile_font(&self.shared, &shown, &text, path, errors) else {
204 return;
205 };
206 self.font_ids
207 .claim(errors, &shown, yog_ids::kind::FONT, &font.id);
208 self.fonts.push(font);
209 }
210 Format::Book => self.pending.books.push((shown, text)),
212 Format::Recipe => self.pending.recipes.push((shown, text, path.to_path_buf())),
214 Format::Def => {
215 let mut cx = self.sink.cx(&self.shared, &shown);
216 if let Some(f) = DefFile::read(&mut cx, text) {
217 self.pending.defs.push(f);
218 }
219 }
220 Format::Entity => {
221 let mut cx = self.sink.cx(&self.shared, &shown);
222 let Some(e) = compile_entity(&mut cx, &text, path, self.mod_ns, &mut self.out)
223 else {
224 return;
225 };
226 self.ids
227 .claim(&mut self.sink.errors, &shown, yog_ids::word::ID, &e.id);
228 self.entities.push(e);
229 }
230 _ => {
231 let mut cx = self.sink.cx(&self.shared, &shown);
232 let Some(entry) = compile_file(
233 &mut cx,
234 &text,
235 kind,
236 path,
237 self.mod_ns,
238 &mut self.out,
239 &mut self.generated,
240 ) else {
241 return;
242 };
243 self.ids
244 .claim(&mut self.sink.errors, &shown, yog_ids::word::ID, &entry.id);
245 match entry.potion.clone() {
246 Some(p) => {
247 self.potions.insert(entry.id, p);
248 }
249 None => self.entries.push(entry),
250 }
251 }
252 }
253 }
254
255 fn known(&mut self) -> refs::Known {
259 let manifest =
261 crate::manifest::YogToml::read(&self.shared.root.join(yog_ids::file::MANIFEST)).ok();
262 let externals = manifest
263 .as_ref()
264 .map(|t| t.externals.clone())
265 .unwrap_or_default();
266 let datapack = manifest.as_ref().and_then(|t| t.datapack.clone());
267 let mut known = refs::Known::new(self.shared.ns, &externals, datapack.as_deref());
268 for e in &self.entries {
269 known.add(
270 if e.kind == Format::Block {
271 refs::RefKind::Block
272 } else {
273 refs::RefKind::Item
274 },
275 e.id.clone(),
276 );
277 }
278 for id in self.potions.keys() {
279 known.add(refs::RefKind::Item, id.clone());
280 }
281 for e in &self.entities {
282 known.add(refs::RefKind::Entity, e.id.clone());
283 for (wanted, kind) in [(&e.model, Format::Model), (&e.animations, Format::Anim)] {
284 if let Some(id) = wanted
285 && self.mod_ns.is_some_and(|ns| yog_ids::split(id).0 == ns)
286 && !self
287 .bodies
288 .iter()
289 .any(|b| b.model == (kind == Format::Model) && &b.id == id)
290 {
291 self.sink.errors.push(problem(format!(
292 "{}: {} `{id}`: no such file in {CONTENT_DIR}/ (`.{}`)",
293 e.file,
294 kind.noun(),
295 kind.ext()
296 )));
297 }
298 }
299 }
300 known
301 }
302
303 fn defs(&mut self, known: &refs::Known) -> Made {
305 let mut made = Made::default();
306 for f in std::mem::take(&mut self.pending.defs) {
307 let shown = f.shown.clone();
308 let mut cx = self.sink.cx(&self.shared, &shown);
309 compile_def(&mut cx, f, known, &mut made, &mut self.out);
310 }
311 made
312 }
313
314 fn recipes(
318 &mut self,
319 known: &mut refs::Known,
320 made: &Made,
321 ) -> Vec<yog_formats::book::layout::RecipeLayout> {
322 let layouts = recipe_layouts(
323 self.shared.root,
324 self.shared.ns,
325 &made.recipe_kinds,
326 known,
327 &mut self.sink.errors,
328 );
329 let mut brews: Vec<(String, Brew)> = Vec::new();
330 let mut against = Against {
331 mod_ns: self.mod_ns,
332 known,
333 kinds: &made.recipe_kinds,
334 brews: &mut brews,
335 };
336 for (shown, text, path) in std::mem::take(&mut self.pending.recipes) {
337 let mut cx = self.sink.cx(&self.shared, &shown);
338 let Some(r) = compile_recipe(&mut cx, &text, &path, &mut against) else {
339 continue;
340 };
341 self.recipe_ids
342 .claim(&mut self.sink.errors, &shown, yog_ids::kind::RECIPE, &r.id);
343 self.recipes.push(r);
344 }
345 for r in &self.recipes {
346 known.add(refs::RefKind::Recipe, r.id.clone());
347 }
348 for (to, brew) in brews {
349 self.brew(to, brew);
350 }
351 layouts
352 }
353
354 fn brew(&mut self, to: String, brew: Brew) {
357 let defined = |p: &BTreeMap<String, PotionDef>, id: &str| {
358 p.contains_key(id) || refs::is_vanilla_potion(id)
359 };
360 if !defined(&self.potions, &brew.from) {
361 return self.sink.errors.push(problem(format!("brewing recipe input `{}`: no potion `.yitem` or Yog default potion alias defines it", brew.from)));
362 }
363 if !defined(&self.potions, &to) {
364 return self.sink.errors.push(problem(format!("brewing recipe result `{to}`: no potion `.yitem` or Yog default potion alias defines it")));
365 }
366 self.potions
367 .entry(to.clone())
368 .or_insert_with(|| PotionDef {
369 id: to,
370 name: None,
371 effects: Vec::new(),
372 brewed: Vec::new(),
373 })
374 .brewed
375 .push(brew);
376 }
377
378 fn check_deferred(&mut self, defs: &Defs, known: &refs::Known) {
380 for Deferred {
381 file,
382 line,
383 path,
384 wanted,
385 } in std::mem::take(&mut self.sink.deferred)
386 {
387 if let Err(e) = check_wanted(&wanted, defs, known) {
388 self.sink
389 .errors
390 .push(problem(format!("{path}: {e}")).in_file(file).at_line(line));
391 }
392 }
393 }
394
395 fn tool_levels(&mut self, defs: &Defs) {
397 for e in &self.entries {
398 let Some((level, line)) = &e.block.tool_level else {
399 continue;
400 };
401 let at = |d: Diagnostic| d.in_file(&e.file).at_line(*line);
402 let field = yog_data::field!(yog_formats::content::BlockProps, tool_level);
403 match tool_level_tags(&e.id, level, defs, &mut self.out) {
404 Ok(None) => {}
405 Ok(Some(m)) => self
406 .sink
407 .warnings
408 .push(at(caveat(format!("block.{field}: {m}")))),
409 Err(m) => self
410 .sink
411 .errors
412 .push(at(problem(format!("block.{field}: {m}")))),
413 }
414 }
415 }
416
417 fn books(&mut self, known: &refs::Known, layouts: &[yog_formats::book::layout::RecipeLayout]) {
419 let recipe_kinds: HashMap<String, String> = self
420 .recipes
421 .iter()
422 .map(|r| (r.id.clone(), r.ty.clone()))
423 .collect();
424 let page_types =
426 crate::book_page::find(self.shared.root, self.shared.ns, &mut self.sink.errors);
427 let against = BookAgainst {
428 known,
429 layouts,
430 recipe_kinds: &recipe_kinds,
431 page_types: &page_types,
432 };
433 for (shown, text) in std::mem::take(&mut self.pending.books) {
434 let mut cx = self.sink.cx(&self.shared, &shown);
435 let Some(b) = compile_book(&mut cx, &text, &against) else {
436 continue;
437 };
438 self.book_ids
439 .claim(&mut self.sink.errors, &shown, yog_ids::kind::BOOK, &b.id);
440 self.books.push(b);
441 }
442 }
443
444 fn inventories(&mut self) {
447 let Some(ns) = self.mod_ns else { return };
448 let screens = inventory_screens(&self.shared.root.join(yog_ids::project::SRC));
449 for (file, line, id) in &self.sink.inventories {
450 if yog_ids::split(id).0 == ns && !screens.contains(id) {
451 self.sink.errors.push(
452 problem(format!(
453 "block.inventory `{id}`: no .yui <Screen id=\"{id}\"> with <Slot>s in src/"
454 ))
455 .in_file(file)
456 .at_line(*line),
457 );
458 }
459 }
460 }
461
462 fn finish(
464 mut self,
465 defs: &mut Defs,
466 mut made: Made,
467 known: &refs::Known,
468 ) -> Result<Compiled, String> {
469 for m in &defs.armor_materials {
471 looks::equipment(
472 self.shared.root,
473 &mut self.out,
474 &m.texture,
475 &mut self.sink.warnings,
476 );
477 }
478 self.out.warnings = std::mem::take(&mut self.sink.warnings);
479 if !self.sink.errors.is_empty() {
480 crate::diagnostics::report_all(&self.sink.errors);
481 return Err(format!(
482 "invalid blocks/items in {CONTENT_DIR}/:{}{}",
483 crate::diagnostics::LIST_SEP,
484 crate::diagnostics::listed(&self.sink.errors)
485 ));
486 }
487 let potions = std::mem::take(&mut self.potions);
488 made.definitions
489 .extend(potions.into_iter().map(|(id, potion)| {
490 (
491 yog_ids::kind::POTION,
492 id,
493 yog_data::binary::encode(&potion.write()),
494 )
495 }));
496 self.count();
497 self.data_files(&made);
498 self.out.registrations =
499 registrations(&self.entries, &self.entities, &made.dimensions, defs);
500 self.made_into(defs, made);
501 let content = Content {
502 entries: &self.entries,
503 recipes: &self.recipes,
504 books: &self.books,
505 bodies: &self.bodies,
506 entities: &self.entities,
507 fonts: &self.fonts,
508 defs,
509 textures: self.shared.own_textures,
510 };
511 if content.registers_anything()
512 && !mentions_content_macro(&self.shared.root.join(yog_ids::project::SRC))
513 {
514 self.out.warnings.push(caveat("the mod has content (blocks, items, particles, videos, …), but src/ never calls `yog_api::content!();` — it isn't registered"));
515 }
516 self.out.source = Some(rust_source(&content));
517 version_overlays(&mut self.out, &known.datapack);
518 Ok(self.out)
519 }
520
521 fn count(&mut self) {
523 self.out.blocks = self
524 .entries
525 .iter()
526 .filter(|e| e.kind == Format::Block)
527 .count();
528 self.out.items = self.entries.len() - self.out.blocks;
529 self.out.recipes = self.recipes.len();
530 self.out.books = self.books.len();
531 }
532
533 fn data_files(&mut self, made: &Made) {
536 let files = &mut self.out.data_files;
537 files.extend(self.books.iter().map(|b| (b.path(), b.data.clone())));
538 files.extend(self.bodies.iter().map(|b| (b.path(), b.data.clone())));
539 files.extend(self.fonts.iter().map(|f| (f.path(), f.data.clone())));
540 files.extend(self.entities.iter().map(|e| (e.path(), e.data.clone())));
541 files.extend(self.recipes.iter().filter_map(Recipe::data_file));
542 files.extend(
543 made.dimensions
544 .iter()
545 .map(|(id, data)| (dimension_path(id), data.clone())),
546 );
547 files.extend(
548 made.loot
549 .iter()
550 .map(|(id, data)| (loot_path(id), data.clone())),
551 );
552 files.extend(
553 made.definitions
554 .iter()
555 .map(|(kind, id, data)| (definition_path(kind, id), data.clone())),
556 );
557 }
558
559 fn made_into(&mut self, defs: &mut Defs, made: Made) {
562 for (files, feature) in made.ores {
563 self.out.outputs.extend(files);
564 defs.biome_features.push(feature);
565 }
566 for m in &defs.tool_materials {
567 if yog_ids::split(&m.incorrect).0 != MINECRAFT {
568 let file = datapack::tag_file(yog_ids::kind::BLOCK, &m.incorrect);
569 self.out
570 .tags
571 .entry(datapack::overlay(
572 &yog_registry::ToolLadder::game().incorrect_when,
573 &file,
574 ))
575 .or_default();
576 }
577 }
578 defs.definitions = made
579 .definitions
580 .into_iter()
581 .map(|(kind, id, _)| (kind, id))
582 .collect();
583 defs.loot_modifiers = made.loot.into_iter().map(|(id, _)| id).collect();
584 defs.dimensions = made.dimensions.into_iter().map(|(id, _)| id).collect();
585 defs.advancements = made.advancements;
586 defs.tables = made.tables;
587 }
588}
589
590fn check_wanted(wanted: &Wanted, defs: &Defs, known: &refs::Known) -> Result<(), String> {
593 use yog_formats::def::{ArmorMaterial, SoundGroup, ToolMaterial};
594 use yog_ids::kind;
595 let found = |there: bool, what: &str, id: &str| {
596 if there {
597 Ok(())
598 } else {
599 Err(format!("no {what} {id} — the game's or a .ydef's"))
600 }
601 };
602 match wanted {
603 Wanted::Tag { kinds, id } => {
604 let mut found = kinds.iter().map(|k| known.check_tag(k, id));
605 let first = found.next().unwrap_or(Ok(()));
606 if first.is_ok() || found.any(|f| f.is_ok()) {
607 Ok(())
608 } else {
609 first
610 }
611 }
612 Wanted::Ref(r) => match r.kind.as_str() {
613 kind::LEVEL => found(defs.level(&r.id).is_some(), "level", &r.id),
614 kind::LADDER => found(defs.order.contains_key(&r.id), "ladder", &r.id),
615 kind::TOOL_MATERIAL => found(
616 defs.tool_materials.iter().any(|m| m.id == r.id)
617 || ToolMaterial::built_in(&r.id).is_some(),
618 "tool material",
619 &r.id,
620 ),
621 kind::ARMOR_MATERIAL => found(
622 defs.armor_materials.iter().any(|m| m.id == r.id)
623 || ArmorMaterial::built_in(&r.id).is_some(),
624 "armor material",
625 &r.id,
626 ),
627 kind::SOUND_GROUP => found(
628 defs.sound_groups.iter().any(|g| g.id == r.id)
629 || SoundGroup::built_in(&r.id).is_some(),
630 "sound group",
631 &r.id,
632 ),
633 other => refs::RefKind::from_word(other).map_or(Ok(()), |k| known.check(k, &r.id)),
634 },
635 }
636}