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