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