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