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 =
226 crate::manifest::YogToml::read(&self.shared.root.join(yog_ids::file::MANIFEST)).ok();
227 let externals = manifest
228 .as_ref()
229 .map(|t| t.externals.clone())
230 .unwrap_or_default();
231 let datapack = manifest.as_ref().and_then(|t| t.datapack.clone());
232 let mut known = refs::Known::new(self.shared.ns, &externals, datapack.as_deref());
233 for e in &self.entries {
234 known.add(
235 if e.kind == Format::Block {
236 refs::RefKind::Block
237 } else {
238 refs::RefKind::Item
239 },
240 e.id.clone(),
241 );
242 }
243 for id in self.potions.keys() {
244 known.add(refs::RefKind::Item, id.clone());
245 }
246 for e in &self.entities {
247 known.add(refs::RefKind::Entity, e.id.clone());
248 for (wanted, kind) in [(&e.model, Format::Model), (&e.animations, Format::Anim)] {
249 if let Some(id) = wanted
250 && self.mod_ns.is_some_and(|ns| yog_ids::split(id).0 == ns)
251 && !self
252 .bodies
253 .iter()
254 .any(|b| b.model == (kind == Format::Model) && &b.id == id)
255 {
256 self.sink.errors.push(problem(format!(
257 "{}: {} `{id}`: no such file in {CONTENT_DIR}/ (`.{}`)",
258 e.file,
259 kind.noun(),
260 kind.ext()
261 )));
262 }
263 }
264 }
265 known
266 }
267
268 fn defs(&mut self, known: &refs::Known) -> Made {
270 let mut made = Made::default();
271 for f in std::mem::take(&mut self.pending.defs) {
272 let shown = f.shown.clone();
273 let mut cx = self.sink.cx(&self.shared, &shown);
274 compile_def(&mut cx, f, known, &mut made, &mut self.out);
275 }
276 made
277 }
278
279 fn recipes(
283 &mut self,
284 known: &mut refs::Known,
285 made: &Made,
286 ) -> Vec<yog_formats::book::layout::RecipeLayout> {
287 let layouts = recipe_layouts(
288 self.shared.root,
289 self.shared.ns,
290 &made.recipe_kinds,
291 known,
292 &mut self.sink.errors,
293 );
294 let mut brews: Vec<(String, Brew)> = Vec::new();
295 let mut against = Against {
296 mod_ns: self.mod_ns,
297 known,
298 kinds: &made.recipe_kinds,
299 brews: &mut brews,
300 };
301 for (shown, text, path) in std::mem::take(&mut self.pending.recipes) {
302 let mut cx = self.sink.cx(&self.shared, &shown);
303 let Some(r) = compile_recipe(&mut cx, &text, &path, &mut against) else {
304 continue;
305 };
306 self.recipe_ids
307 .claim(&mut self.sink.errors, &shown, yog_ids::kind::RECIPE, &r.id);
308 self.recipes.push(r);
309 }
310 for r in &self.recipes {
311 known.add(refs::RefKind::Recipe, r.id.clone());
312 }
313 for (to, brew) in brews {
314 self.brew(to, brew);
315 }
316 layouts
317 }
318
319 fn brew(&mut self, to: String, brew: Brew) {
322 let defined = |p: &BTreeMap<String, PotionDef>, id: &str| {
323 p.contains_key(id) || refs::is_vanilla_potion(id)
324 };
325 if !defined(&self.potions, &brew.from) {
326 return self.sink.errors.push(problem(format!("brewing recipe input `{}`: no potion `.yitem` or Yog default potion alias defines it", brew.from)));
327 }
328 if !defined(&self.potions, &to) {
329 return self.sink.errors.push(problem(format!("brewing recipe result `{to}`: no potion `.yitem` or Yog default potion alias defines it")));
330 }
331 self.potions
332 .entry(to.clone())
333 .or_insert_with(|| PotionDef {
334 id: to,
335 name: None,
336 effects: Vec::new(),
337 brewed: Vec::new(),
338 })
339 .brewed
340 .push(brew);
341 }
342
343 fn check_deferred(&mut self, defs: &Defs, known: &refs::Known) {
345 for Deferred {
346 file,
347 line,
348 path,
349 wanted,
350 } in std::mem::take(&mut self.sink.deferred)
351 {
352 if let Err(e) = check_wanted(&wanted, defs, known) {
353 self.sink
354 .errors
355 .push(problem(format!("{path}: {e}")).in_file(file).at_line(line));
356 }
357 }
358 }
359
360 fn tool_levels(&mut self, defs: &Defs) {
362 for e in &self.entries {
363 let Some((level, line)) = &e.block.tool_level else {
364 continue;
365 };
366 let at = |d: Diagnostic| d.in_file(&e.file).at_line(*line);
367 let field = yog_data::field!(yog_formats::content::BlockProps, tool_level);
368 match tool_level_tags(&e.id, level, defs, &mut self.out) {
369 Ok(None) => {}
370 Ok(Some(m)) => self
371 .sink
372 .warnings
373 .push(at(caveat(format!("block.{field}: {m}")))),
374 Err(m) => self
375 .sink
376 .errors
377 .push(at(problem(format!("block.{field}: {m}")))),
378 }
379 }
380 }
381
382 fn books(&mut self, known: &refs::Known, layouts: &[yog_formats::book::layout::RecipeLayout]) {
384 let recipe_kinds: HashMap<String, String> = self
385 .recipes
386 .iter()
387 .map(|r| (r.id.clone(), r.ty.clone()))
388 .collect();
389 let page_types =
391 crate::book_page::find(self.shared.root, self.shared.ns, &mut self.sink.errors);
392 let against = BookAgainst {
393 known,
394 layouts,
395 recipe_kinds: &recipe_kinds,
396 page_types: &page_types,
397 };
398 for (shown, text) in std::mem::take(&mut self.pending.books) {
399 let mut cx = self.sink.cx(&self.shared, &shown);
400 let Some(b) = compile_book(&mut cx, &text, &against) else {
401 continue;
402 };
403 self.book_ids
404 .claim(&mut self.sink.errors, &shown, yog_ids::kind::BOOK, &b.id);
405 self.books.push(b);
406 }
407 }
408
409 fn inventories(&mut self) {
412 let Some(ns) = self.mod_ns else { return };
413 let screens = inventory_screens(&self.shared.root.join(yog_ids::project::SRC));
414 for (file, line, id) in &self.sink.inventories {
415 if yog_ids::split(id).0 == ns && !screens.contains(id) {
416 self.sink.errors.push(
417 problem(format!(
418 "block.inventory `{id}`: no .yui <Screen id=\"{id}\"> with <Slot>s in src/"
419 ))
420 .in_file(file)
421 .at_line(*line),
422 );
423 }
424 }
425 }
426
427 fn finish(
429 mut self,
430 defs: &mut Defs,
431 mut made: Made,
432 known: &refs::Known,
433 ) -> Result<Compiled, String> {
434 for m in &defs.armor_materials {
436 looks::equipment(
437 self.shared.root,
438 &mut self.out,
439 &m.texture,
440 &mut self.sink.warnings,
441 );
442 }
443 self.out.warnings = std::mem::take(&mut self.sink.warnings);
444 if !self.sink.errors.is_empty() {
445 crate::diagnostics::report_all(&self.sink.errors);
446 return Err(format!(
447 "invalid blocks/items in {CONTENT_DIR}/:{}{}",
448 crate::diagnostics::LIST_SEP,
449 crate::diagnostics::listed(&self.sink.errors)
450 ));
451 }
452 let potions = std::mem::take(&mut self.potions);
453 made.definitions
454 .extend(potions.into_iter().map(|(id, potion)| {
455 (
456 yog_ids::kind::POTION,
457 id,
458 yog_data::binary::encode(&potion.write()),
459 )
460 }));
461 self.count();
462 self.data_files(&made);
463 self.out.registrations =
464 registrations(&self.entries, &self.entities, &made.dimensions, defs);
465 self.made_into(defs, made);
466 let content = Content {
467 entries: &self.entries,
468 recipes: &self.recipes,
469 books: &self.books,
470 bodies: &self.bodies,
471 entities: &self.entities,
472 fonts: &self.fonts,
473 defs,
474 textures: self.shared.own_textures,
475 };
476 if content.registers_anything()
477 && !mentions_content_macro(&self.shared.root.join(yog_ids::project::SRC))
478 {
479 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")));
480 }
481 self.out.source = Some(rust_source(&content));
482 version_overlays(&mut self.out, &known.datapack);
483 Ok(self.out)
484 }
485
486 fn count(&mut self) {
488 self.out.blocks = self
489 .entries
490 .iter()
491 .filter(|e| e.kind == Format::Block)
492 .count();
493 self.out.items = self.entries.len() - self.out.blocks;
494 self.out.recipes = self.recipes.len();
495 self.out.books = self.books.len();
496 }
497
498 fn data_files(&mut self, made: &Made) {
501 let files = &mut self.out.data_files;
502 files.extend(self.books.iter().map(|b| (b.path(), b.data.clone())));
503 files.extend(self.bodies.iter().map(|b| (b.path(), b.data.clone())));
504 files.extend(self.fonts.iter().map(|f| (f.path(), f.data.clone())));
505 files.extend(self.entities.iter().map(|e| (e.path(), e.data.clone())));
506 files.extend(self.recipes.iter().filter_map(Recipe::data_file));
507 files.extend(
508 made.dimensions
509 .iter()
510 .map(|(id, data)| (dimension_path(id), data.clone())),
511 );
512 files.extend(
513 made.loot
514 .iter()
515 .map(|(id, data)| (loot_path(id), data.clone())),
516 );
517 files.extend(
518 made.definitions
519 .iter()
520 .map(|(kind, id, data)| (definition_path(kind, id), data.clone())),
521 );
522 }
523
524 fn made_into(&mut self, defs: &mut Defs, made: Made) {
527 for (files, feature) in made.ores {
528 self.out.outputs.extend(files);
529 defs.biome_features.push(feature);
530 }
531 for m in &defs.tool_materials {
532 if yog_ids::split(&m.incorrect).0 != MINECRAFT {
533 let file = datapack::tag_file(yog_ids::kind::BLOCK, &m.incorrect);
534 self.out
535 .tags
536 .entry(datapack::overlay(
537 &yog_registry::ToolLadder::game().incorrect_when,
538 &file,
539 ))
540 .or_default();
541 }
542 }
543 defs.definitions = made
544 .definitions
545 .into_iter()
546 .map(|(kind, id, _)| (kind, id))
547 .collect();
548 defs.loot_modifiers = made.loot.into_iter().map(|(id, _)| id).collect();
549 defs.dimensions = made.dimensions.into_iter().map(|(id, _)| id).collect();
550 defs.advancements = made.advancements;
551 defs.tables = made.tables;
552 }
553}
554
555fn check_wanted(wanted: &Wanted, defs: &Defs, known: &refs::Known) -> Result<(), String> {
558 use yog_formats::def::{ArmorMaterial, SoundGroup, ToolMaterial};
559 use yog_ids::kind;
560 let found = |there: bool, what: &str, id: &str| {
561 if there {
562 Ok(())
563 } else {
564 Err(format!("no {what} {id} — the game's or a .ydef's"))
565 }
566 };
567 match wanted {
568 Wanted::Tag { kinds, id } => {
569 let mut found = kinds.iter().map(|k| known.check_tag(k, id));
570 let first = found.next().unwrap_or(Ok(()));
571 if first.is_ok() || found.any(|f| f.is_ok()) {
572 Ok(())
573 } else {
574 first
575 }
576 }
577 Wanted::Ref(r) => match r.kind.as_str() {
578 kind::LEVEL => found(defs.level(&r.id).is_some(), "level", &r.id),
579 kind::LADDER => found(defs.order.contains_key(&r.id), "ladder", &r.id),
580 kind::TOOL_MATERIAL => found(
581 defs.tool_materials.iter().any(|m| m.id == r.id)
582 || ToolMaterial::built_in(&r.id).is_some(),
583 "tool material",
584 &r.id,
585 ),
586 kind::ARMOR_MATERIAL => found(
587 defs.armor_materials.iter().any(|m| m.id == r.id)
588 || ArmorMaterial::built_in(&r.id).is_some(),
589 "armor material",
590 &r.id,
591 ),
592 kind::SOUND_GROUP => found(
593 defs.sound_groups.iter().any(|g| g.id == r.id)
594 || SoundGroup::built_in(&r.id).is_some(),
595 "sound group",
596 &r.id,
597 ),
598 other => refs::RefKind::from_word(other).map_or(Ok(()), |k| known.check(k, &r.id)),
599 },
600 }
601}