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