1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3use std::rc::Rc;
4
5use toml_edit::DocumentMut;
6
7use crate::code;
8use crate::edit::first_error;
9use crate::migrate::{add_ids, entry_kinds};
10use crate::paths::{Found, Listing, PathError, rooted, slashed};
11use crate::read::{self, Context, Doc, Finding, Parsed};
12use crate::rules::{self, Issues};
13use crate::scene::{Entry, Scene};
14use crate::spans::{KeyPath, Part};
15use crate::types::{
16 Camera, Content, Emitter, File, FileKind, Finish, Haze, Light, Material, Mesh, Mover, Object,
17 Physics, Plates, Proxy, Rig, Sky, Sound, Sun, Text, Tiles, Trace,
18};
19use crate::{Diagnostic, EditError, FORMAT, Id, Patch, PatchGroup, Severity};
20
21type Layers = BTreeMap<String, Vec<String>>;
22
23pub const MAX_DEPTH: usize = 64;
24
25#[derive(Clone, Debug)]
26pub struct Project {
27 root: PathBuf,
28 marked: bool,
29 texts: BTreeMap<PathBuf, Result<String, String>>,
30}
31
32fn key(parts: &[&str]) -> KeyPath {
33 parts.iter().map(|part| part.to_string()).collect()
34}
35
36fn under(path: &KeyPath, rest: &[String]) -> KeyPath {
37 let mut out = path.clone();
38 out.extend(rest.iter().cloned());
39 out
40}
41
42const SKIPPED: [&str; 2] = ["target", "tmp"];
43
44fn skipped(root: &Path) -> BTreeSet<PathBuf> {
45 let listed = std::fs::read_to_string(root.join("project.toml"))
46 .ok()
47 .and_then(|text| toml::from_str::<toml::Table>(&text).ok())
48 .and_then(|table| table.get("project")?.get("ignore")?.as_array().cloned())
49 .unwrap_or_default();
50 SKIPPED
51 .iter()
52 .map(|name| name.to_string())
53 .chain(
54 listed
55 .iter()
56 .filter_map(toml::Value::as_str)
57 .filter_map(|raw| rooted(raw).ok()),
58 )
59 .map(PathBuf::from)
60 .collect()
61}
62
63fn discover(root: &Path, folder: &Path, skip: &BTreeSet<PathBuf>, out: &mut Vec<PathBuf>) {
64 let Ok(entries) = std::fs::read_dir(root.join(folder)) else {
65 return;
66 };
67 let mut entries: Vec<_> = entries.filter_map(|entry| entry.ok()).collect();
68 entries.sort_by_key(|entry| entry.file_name());
69 for entry in entries {
70 let name = entry.file_name();
71 let Some(name) = name.to_str() else { continue };
72 if name.starts_with('.') {
73 continue;
74 }
75 let Ok(kind) = entry.file_type() else {
76 continue;
77 };
78 let relative = folder.join(name);
79 if skip.contains(&relative) {
80 continue;
81 }
82 if kind.is_dir() {
83 if root.join(&relative).join("project.toml").is_file() {
84 continue;
85 }
86 discover(root, &relative, skip, out);
87 } else if kind.is_file() && FileKind::of(&relative).is_some() {
88 out.push(relative);
89 }
90 }
91}
92
93impl Project {
94 pub fn root_of(file: &Path) -> PathBuf {
95 let start = file.parent().unwrap_or_else(|| Path::new(""));
96 let mut at = Some(start);
97 while let Some(folder) = at {
98 if folder.join("project.toml").is_file() {
99 return folder.to_path_buf();
100 }
101 at = folder.parent();
102 }
103 start.to_path_buf()
104 }
105
106 #[allow(clippy::result_large_err)]
107 pub fn open(root: &Path) -> Result<Project, Diagnostic> {
108 let root = std::path::absolute(root).unwrap_or_else(|_| root.to_path_buf());
109 if !root.is_dir() {
110 return Err(Diagnostic {
111 file: PathBuf::new(),
112 line: 1,
113 column: 1,
114 end_line: 1,
115 end_column: 1,
116 severity: Severity::Error,
117 code: code::UNREADABLE,
118 key: String::new(),
119 message: format!("{} is not a folder", root.display()),
120 related: Vec::new(),
121 });
122 }
123 let mut files = Vec::new();
124 discover(&root, Path::new(""), &skipped(&root), &mut files);
125 let texts = files
126 .into_iter()
127 .map(|file| {
128 let text = std::fs::read(root.join(&file))
129 .map_err(|error| format!("cannot read {}: {error}", file.display()))
130 .and_then(|bytes| {
131 String::from_utf8(bytes).map_err(|_| "the file is not UTF-8".to_string())
132 });
133 (file, text)
134 })
135 .collect();
136 Ok(Project {
137 marked: root.join("project.toml").is_file(),
138 root,
139 texts,
140 })
141 }
142
143 pub fn root(&self) -> &Path {
144 &self.root
145 }
146
147 pub fn has_project_file(&self) -> bool {
148 self.marked
149 }
150
151 pub fn files(&self) -> impl Iterator<Item = &Path> {
152 self.texts.keys().map(PathBuf::as_path)
153 }
154
155 pub fn relative(&self, file: &Path) -> PathBuf {
156 let absolute = if file.is_absolute() {
157 file.to_path_buf()
158 } else {
159 return PathBuf::from(slashed(file));
160 };
161 absolute
162 .strip_prefix(&self.root)
163 .map(Path::to_path_buf)
164 .unwrap_or(absolute)
165 }
166
167 pub fn check(&self) -> Vec<Diagnostic> {
168 Checker::new(self, BTreeMap::new()).all()
169 }
170
171 pub fn scene(&self, file: &Path) -> Result<Scene, Vec<Diagnostic>> {
172 self.scene_with(file, BTreeMap::new())
173 }
174
175 pub fn scene_with(
176 &self,
177 file: &Path,
178 overlay: BTreeMap<PathBuf, String>,
179 ) -> Result<Scene, Vec<Diagnostic>> {
180 self.resolve(file, overlay).0
181 }
182
183 pub fn check_with(&self, overlay: BTreeMap<PathBuf, String>) -> Vec<Diagnostic> {
184 Checker::new(self, self.overlaid(overlay)).all()
185 }
186
187 fn overlaid(&self, overlay: BTreeMap<PathBuf, String>) -> BTreeMap<PathBuf, String> {
188 overlay
189 .into_iter()
190 .map(|(file, text)| (self.relative(&file), text))
191 .collect()
192 }
193
194 #[allow(clippy::type_complexity)]
195 fn resolve(
196 &self,
197 file: &Path,
198 overlay: BTreeMap<PathBuf, String>,
199 ) -> (Result<Scene, Vec<Diagnostic>>, BTreeMap<PathBuf, FileKind>) {
200 let file = self.relative(file);
201 let kind = FileKind::of(&file).unwrap_or(FileKind::Scene);
202 let mut checker = Checker::new(self, self.overlaid(overlay));
203 let scene = checker.context(&file, kind);
204 checker.unique_ids();
205 let diagnostics = checker.report();
206 let errors: Vec<Diagnostic> = diagnostics
207 .iter()
208 .filter(|d| d.is_error())
209 .cloned()
210 .collect();
211 let scene = match scene {
212 Some(mut scene) if errors.is_empty() => {
213 scene.warnings = diagnostics;
214 Ok(scene)
215 }
216 _ => Err(if errors.is_empty() {
217 diagnostics
218 } else {
219 errors
220 }),
221 };
222 (scene, checker.kinds)
223 }
224
225 pub fn fix(&self, file: &Path) -> Result<PatchGroup, EditError> {
226 let label = "fix ids";
227 let refuse = |file: &Path, message: &str| EditError {
228 file: self.root.join(file),
229 key: label.to_string(),
230 line: None,
231 code: code::BAD_VALUE,
232 message: message.to_string(),
233 diagnostics: Vec::new(),
234 };
235 let (scene, kinds) = self.resolve(file, BTreeMap::new());
236 let scene = scene.map_err(|found| first_error(&found, &self.root, label))?;
237 let mut taken = self.ids(&BTreeMap::new());
238 let mut patches = Vec::new();
239 for path in &scene.files {
240 let Some(&kind) = kinds.get(path) else {
241 continue;
242 };
243 let (lists, maps) = entry_kinds(kind);
244 if lists.is_empty() && maps.is_empty() {
245 continue;
246 }
247 let text = match self.texts.get(path) {
248 Some(Ok(text)) => text.clone(),
249 _ => std::fs::read_to_string(self.root.join(path))
250 .map_err(|error| refuse(path, &format!("cannot read: {error}")))?,
251 };
252 let mut doc: DocumentMut = text
253 .parse()
254 .map_err(|error: toml_edit::TomlError| refuse(path, error.message().trim()))?;
255 if doc.get("format").and_then(toml_edit::Item::as_integer) != Some(FORMAT.into()) {
256 return Err(refuse(
257 path,
258 "is not format 1; migrate it, which writes its ids",
259 ));
260 }
261 let exact = doc.to_string() == text;
262 if add_ids(&mut doc, &slashed(path), kind, &mut taken).is_empty() {
263 continue;
264 }
265 if !exact {
266 return Err(refuse(
267 path,
268 "cannot be written back byte for byte, so it is not edited",
269 ));
270 }
271 patches.push(Patch {
272 label: label.to_string(),
273 file: self.root.join(path),
274 before: text,
275 after: doc.to_string(),
276 });
277 }
278 let group = PatchGroup {
279 label: label.to_string(),
280 patches,
281 };
282 if !group.is_empty() {
283 self.scene_with(file, group.after())
284 .map_err(|found| first_error(&found, &self.root, label))?;
285 }
286 Ok(group)
287 }
288
289 pub(crate) fn kind_of(
290 &self,
291 file: &Path,
292 overlay: &BTreeMap<PathBuf, String>,
293 ) -> Option<FileKind> {
294 let file = self.relative(file);
295 if let Some(kind) = FileKind::of(&file) {
296 return Some(kind);
297 }
298 let mut checker = Checker::new(self, overlay.clone());
299 checker.all();
300 checker.kinds.get(&file).copied()
301 }
302
303 pub(crate) fn update(&mut self, file: &Path, text: &str) {
304 self.texts.insert(self.relative(file), Ok(text.to_string()));
305 }
306
307 pub(crate) fn ids(&self, overlay: &BTreeMap<PathBuf, String>) -> BTreeSet<Id> {
308 let mut checker = Checker::new(self, overlay.clone());
309 checker.all();
310 let mut ids = BTreeSet::new();
311 for parsed in checker.parsed.values() {
312 let Some(doc) = &parsed.doc else { continue };
313 ids.extend(doc.mesh.iter().filter_map(|(_, v)| v.id));
314 ids.extend(doc.object.iter().filter_map(|(_, v)| v.id));
315 ids.extend(doc.light.iter().filter_map(|(_, v)| v.id));
316 ids.extend(doc.emitter.iter().filter_map(|(_, v)| v.id));
317 ids.extend(doc.mover.iter().filter_map(|(_, v)| v.id));
318 ids.extend(doc.content.iter().filter_map(|(_, v)| v.id));
319 ids.extend(doc.text.iter().filter_map(|(_, v)| v.id));
320 ids.extend(doc.sound.iter().filter_map(|(_, v)| v.id));
321 ids.extend(doc.rig.iter().filter_map(|(_, v)| v.id));
322 ids.extend(doc.tiles.iter().filter_map(|(_, v)| v.id));
323 ids.extend(doc.library.iter().filter_map(|(_, v)| v.id));
324 ids.extend(doc.proxy.iter().filter_map(|(_, v)| v.id));
325 }
326 ids
327 }
328
329 pub fn read(&self, file: &Path) -> Result<File, Vec<Diagnostic>> {
330 let relative = self.relative(file);
331 let overlay = BTreeMap::new();
332 let kind = self.kind_of(&relative, &overlay).unwrap_or(FileKind::Scene);
333 let mut checker = Checker::new(self, overlay);
334 checker.load(&relative, kind);
335 let diagnostics: Vec<Diagnostic> = checker
336 .report()
337 .into_iter()
338 .filter(|diagnostic| diagnostic.file == relative)
339 .collect();
340 let typed = checker
341 .parsed
342 .get(&relative)
343 .and_then(|parsed| parsed.file());
344 match typed {
345 Some(file) if !diagnostics.iter().any(Diagnostic::is_error) => Ok(file),
346 _ => Err(diagnostics),
347 }
348 }
349
350 pub fn migrate(&self, file: &Path) -> Result<String, Vec<Diagnostic>> {
351 let relative = self.relative(file);
352 let overlay = BTreeMap::new();
353 let kind = self.kind_of(&relative, &overlay).unwrap_or(FileKind::Scene);
354 let text = std::fs::read_to_string(self.root.join(&relative)).map_err(|error| {
355 vec![Diagnostic {
356 file: relative.clone(),
357 line: 1,
358 column: 1,
359 end_line: 1,
360 end_column: 1,
361 severity: Severity::Error,
362 code: code::UNREADABLE,
363 key: String::new(),
364 message: format!("cannot read {}: {error}", relative.display()),
365 related: Vec::new(),
366 }]
367 })?;
368 migrate(&text, &relative, &self.root, kind)
369 }
370}
371
372pub fn migrate(
373 text: &str,
374 file: &Path,
375 root: &Path,
376 kind: FileKind,
377) -> Result<String, Vec<Diagnostic>> {
378 let format = toml::de::DeTable::parse(text).ok().and_then(|table| {
379 table.get_ref().get("format").and_then(|value| {
380 value
381 .get_ref()
382 .as_integer()
383 .map(|integer| integer.as_str().to_string())
384 })
385 });
386 if format.as_deref().is_some_and(|format| format != "0") {
387 return Ok(text.to_string());
388 }
389 let relative = PathBuf::from(slashed(file));
390 match crate::migrate::migrate(text, &relative, root, kind, true) {
391 Ok(migration) => Ok(migration.text),
392 Err(problems) => {
393 let findings: Vec<Finding> = problems
394 .into_iter()
395 .map(|problem| {
396 Finding::new(
397 &relative,
398 problem.path,
399 Part::Value,
400 problem.code,
401 problem.message,
402 )
403 })
404 .collect();
405 let mut parsed = BTreeMap::new();
406 parsed.insert(relative.clone(), Box::new(read::plain(text, kind)));
407 Err(read::report(&findings, &parsed))
408 }
409 }
410}
411
412pub fn check(source: &str, file: &Path, project: &Project) -> Vec<Diagnostic> {
413 let file = project.relative(file);
414 let mut overlay = BTreeMap::new();
415 overlay.insert(file.clone(), source.to_string());
416 project
417 .check_with(overlay)
418 .into_iter()
419 .filter(|diagnostic| diagnostic.file == file)
420 .collect()
421}
422
423fn mover_of<'a>(movers: &'a [Entry<Mover>], key: &str) -> Option<&'a Entry<Mover>> {
424 movers
425 .iter()
426 .find(|mover| mover.value.objects.iter().any(|object| object == key))
427}
428
429fn dynamics(objects: &[Entry<Object>], movers: &[Entry<Mover>]) -> Vec<bool> {
430 let index: BTreeMap<&str, usize> = objects
431 .iter()
432 .enumerate()
433 .map(|(at, object)| (object.key.as_str(), at))
434 .collect();
435 let moved: BTreeSet<&str> = movers
436 .iter()
437 .flat_map(|mover| mover.value.objects.iter().map(String::as_str))
438 .collect();
439 let dynamic = |start: usize| {
440 let mut at = start;
441 for _ in 0..=objects.len() {
442 let object = &objects[at];
443 if let Some(dynamic) = object.value.dynamic {
444 return dynamic;
445 }
446 if object.value.face_camera
447 || object.value.character.is_some()
448 || object.value.animation.is_some()
449 || moved.contains(object.key.as_str())
450 {
451 return true;
452 }
453 match object.value.parent.as_deref().and_then(|p| index.get(p)) {
454 Some(parent) => at = *parent,
455 None => return false,
456 }
457 }
458 false
459 };
460 (0..objects.len()).map(dynamic).collect()
461}
462
463fn why_dynamic(placed: &[Out<Object>], movers: &[Entry<Mover>], at: usize) -> String {
464 let object = &placed[at].entry;
465 if object.value.dynamic == Some(true) {
466 "it sets dynamic = true".to_string()
467 } else if let Some(mover) = mover_of(movers, &object.key) {
468 format!("mover {} moves it", mover.key)
469 } else if object.value.character.is_some() {
470 "it is a character".to_string()
471 } else if object.value.animation.is_some() {
472 "it is animated".to_string()
473 } else if object.value.face_camera {
474 "it faces the camera".to_string()
475 } else {
476 match &object.value.parent {
477 Some(parent) => format!("its parent {parent} is dynamic"),
478 None => "it is dynamic".to_string(),
479 }
480 }
481}
482
483#[derive(Clone, Debug)]
484struct Item<T> {
485 file: PathBuf,
486 path: KeyPath,
487 name: String,
488 id: Option<Id>,
489 value: T,
490}
491
492#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
493enum Kind {
494 Mesh,
495 Object,
496 Light,
497 Emitter,
498 Mover,
499 Content,
500 Text,
501 Sound,
502 Rig,
503 Tiles,
504}
505
506impl Kind {
507 const ALL: [Kind; 10] = [
508 Kind::Mesh,
509 Kind::Object,
510 Kind::Light,
511 Kind::Emitter,
512 Kind::Mover,
513 Kind::Content,
514 Kind::Text,
515 Kind::Sound,
516 Kind::Rig,
517 Kind::Tiles,
518 ];
519
520 fn word(self) -> &'static str {
521 match self {
522 Kind::Mesh => "mesh",
523 Kind::Object => "object",
524 Kind::Light => "light",
525 Kind::Emitter => "emitter",
526 Kind::Mover => "mover",
527 Kind::Content => "content",
528 Kind::Text => "text",
529 Kind::Sound => "sound",
530 Kind::Rig => "rig",
531 Kind::Tiles => "tile layer",
532 }
533 }
534}
535
536#[derive(Clone, Debug)]
537struct Single<T> {
538 file: PathBuf,
539 path: KeyPath,
540 value: T,
541}
542
543#[derive(Clone, Debug, Default)]
544struct Scope {
545 prefab: bool,
546 files: Vec<PathBuf>,
547 libraries: Vec<(PathBuf, PathBuf, KeyPath)>,
548 meshes: Vec<Item<Mesh>>,
549 objects: Vec<Item<Object>>,
550 lights: Vec<Item<Light>>,
551 emitters: Vec<Item<Emitter>>,
552 movers: Vec<Item<Mover>>,
553 contents: Vec<Item<Content>>,
554 texts: Vec<Item<Text>>,
555 sounds: Vec<Item<Sound>>,
556 rigs: Vec<Item<Rig>>,
557 tiles: Vec<Item<Tiles>>,
558 fallback: Option<Single<String>>,
559 sun: Option<Single<Sun>>,
560 sky: Option<Single<Sky>>,
561 haze: Option<Single<Haze>>,
562 camera: Option<Single<Camera>>,
563 finish: Option<Single<Finish>>,
564 trace: Option<Single<Trace>>,
565 plates: Option<Single<Plates>>,
566 physics: Option<Single<Physics>>,
567}
568
569enum Lookup {
570 One(usize),
571 None,
572 Many(Vec<usize>),
573}
574
575fn lookup<T>(items: &[Item<T>], reference: &str) -> Lookup {
576 if let Ok(id) = Id::parse(reference)
577 && let Some(at) = items.iter().position(|item| item.id == Some(id))
578 {
579 return Lookup::One(at);
580 }
581 let named: Vec<usize> = items
582 .iter()
583 .enumerate()
584 .filter(|(_, item)| item.name == reference)
585 .map(|(at, _)| at)
586 .collect();
587 match named.as_slice() {
588 [] => Lookup::None,
589 [one] => Lookup::One(*one),
590 _ => Lookup::Many(named),
591 }
592}
593
594impl Scope {
595 fn names(&self, kind: Kind) -> Vec<(&str, Option<Id>, &Path, &KeyPath)> {
596 fn list<T>(items: &[Item<T>]) -> Vec<(&str, Option<Id>, &Path, &KeyPath)> {
597 items
598 .iter()
599 .map(|item| (item.name.as_str(), item.id, item.file.as_path(), &item.path))
600 .collect()
601 }
602 match kind {
603 Kind::Mesh => list(&self.meshes),
604 Kind::Object => list(&self.objects),
605 Kind::Light => list(&self.lights),
606 Kind::Emitter => list(&self.emitters),
607 Kind::Mover => list(&self.movers),
608 Kind::Content => list(&self.contents),
609 Kind::Text => list(&self.texts),
610 Kind::Sound => list(&self.sounds),
611 Kind::Rig => list(&self.rigs),
612 Kind::Tiles => list(&self.tiles),
613 }
614 }
615
616 fn lookup(&self, kind: Kind, reference: &str) -> Lookup {
617 match kind {
618 Kind::Mesh => lookup(&self.meshes, reference),
619 Kind::Object => lookup(&self.objects, reference),
620 Kind::Light => lookup(&self.lights, reference),
621 Kind::Emitter => lookup(&self.emitters, reference),
622 Kind::Mover => lookup(&self.movers, reference),
623 Kind::Content => lookup(&self.contents, reference),
624 Kind::Text => lookup(&self.texts, reference),
625 Kind::Sound => lookup(&self.sounds, reference),
626 Kind::Rig => lookup(&self.rigs, reference),
627 Kind::Tiles => lookup(&self.tiles, reference),
628 }
629 }
630}
631
632#[derive(Clone, Debug)]
633struct Override {
634 file: PathBuf,
635 path: KeyPath,
636 entry: String,
637 keys: Vec<String>,
638 value: toml::Value,
639 own: bool,
640}
641
642#[derive(Clone, Debug)]
643struct Prefix {
644 key: String,
645 id: Option<String>,
646 hidden: bool,
647}
648
649#[derive(Clone, Debug)]
650struct Touch {
651 key: String,
652 file: PathBuf,
653 path: KeyPath,
654}
655
656#[derive(Clone, Debug)]
657struct Out<T> {
658 entry: Entry<T>,
659 own: Option<KeyPath>,
660 at: KeyPath,
661 set: Vec<Touch>,
662}
663
664impl<T> Out<T> {
665 fn blame(&self, key: &str) -> Option<(PathBuf, KeyPath, bool)> {
666 if let Some(path) = &self.own {
667 return Some((self.entry.file.clone(), under(path, &[key.into()]), false));
668 }
669 self.set
670 .iter()
671 .rev()
672 .find(|touch| touch.key == key)
673 .map(|touch| (touch.file.clone(), touch.path.clone(), true))
674 }
675}
676
677#[derive(Clone, Debug, Default)]
678struct Placed {
679 meshes: Vec<Out<Mesh>>,
680 objects: Vec<Out<Object>>,
681 lights: Vec<Out<Light>>,
682 emitters: Vec<Out<Emitter>>,
683 movers: Vec<Out<Mover>>,
684 contents: Vec<Out<Content>>,
685 texts: Vec<Out<Text>>,
686 sounds: Vec<Out<Sound>>,
687 rigs: Vec<Out<Rig>>,
688 libraries: Vec<(PathBuf, PathBuf, KeyPath)>,
689 files: Vec<PathBuf>,
690}
691
692#[derive(Clone, Debug)]
693struct Step {
694 file: PathBuf,
695 link: Option<(PathBuf, KeyPath)>,
696}
697
698impl Step {
699 fn new(file: &Path) -> Self {
700 Self {
701 file: file.to_path_buf(),
702 link: None,
703 }
704 }
705}
706
707struct Frame {
708 scope: Scope,
709 prefix: Option<Prefix>,
710 own: bool,
711 nested: BTreeMap<usize, Vec<Override>>,
712 touched: BTreeMap<(Kind, usize), Vec<Touch>>,
713 placed: Placed,
714 inner: Placed,
715 inner_objects: BTreeMap<usize, Vec<Out<Object>>>,
716 next: usize,
717 placing: usize,
718}
719
720pub(crate) fn trail(names: &[String]) -> String {
721 if names.len() <= 8 {
722 return names.join(" -> ");
723 }
724 let head = names[..3].join(" -> ");
725 let tail = names[names.len() - 3..].join(" -> ");
726 format!("{head} -> ... -> {tail}")
727}
728
729fn name_key(prefix: Option<&Prefix>, name: &str) -> String {
730 match prefix {
731 Some(prefix) => format!("{}/{name}", prefix.key),
732 None => name.to_string(),
733 }
734}
735
736fn full_id(prefix: Option<&Prefix>, id: Option<Id>) -> Option<String> {
737 match (prefix, id) {
738 (Some(prefix), Some(id)) => prefix.id.as_ref().map(|outer| format!("{outer}/{id}")),
739 (None, Some(id)) => Some(id.to_string()),
740 _ => None,
741 }
742}
743
744struct Checker<'p> {
745 project: &'p Project,
746 overlay: BTreeMap<PathBuf, String>,
747 parsed: BTreeMap<PathBuf, Box<Parsed>>,
748 kinds: BTreeMap<PathBuf, FileKind>,
749 findings: Vec<Finding>,
750 listing: Listing,
751 filed: BTreeSet<PathBuf>,
752 scopes: BTreeMap<PathBuf, Rc<Scope>>,
753 prefabs_checked: BTreeSet<PathBuf>,
754 unchecked: Vec<(PathBuf, Vec<Step>)>,
755 bound: BTreeSet<PathBuf>,
756 layers: Option<Option<Rc<Layers>>>,
757}
758
759impl<'p> Checker<'p> {
760 fn new(project: &'p Project, overlay: BTreeMap<PathBuf, String>) -> Self {
761 Self {
762 project,
763 overlay,
764 parsed: BTreeMap::new(),
765 kinds: BTreeMap::new(),
766 findings: Vec::new(),
767 listing: Listing::default(),
768 filed: BTreeSet::new(),
769 scopes: BTreeMap::new(),
770 prefabs_checked: BTreeSet::new(),
771 unchecked: Vec::new(),
772 bound: BTreeSet::new(),
773 layers: None,
774 }
775 }
776
777 fn layers(&mut self) -> Option<Rc<Layers>> {
778 if let Some(layers) = &self.layers {
779 return layers.clone();
780 }
781 let file = Path::new("project.toml");
782 let layers = match self.text(file) {
783 None => Some(Rc::new(Layers::new())),
784 Some(Err(_)) => None,
785 Some(Ok(text)) => {
786 let context = Context {
787 root: &self.project.root,
788 };
789 read::parse(file, &text, FileKind::Project, &context)
790 .doc
791 .filter(|doc| !doc.layers_unread)
792 .map(|doc| Rc::new(doc.layers.unwrap_or_default()))
793 }
794 };
795 self.layers = Some(layers.clone());
796 layers
797 }
798
799 fn layered(&mut self, file: &Path, path: &KeyPath, object: &Object) {
800 let Some(layers) = self.layers() else { return };
801 for (at, name) in rules::layer_names(object) {
802 if let Some(message) = rules::unknown_layer(&layers, &name) {
803 self.issue(file, &under(path, &at), code::BAD_REFERENCE, message);
804 }
805 }
806 }
807
808 fn push(&mut self, finding: Finding) {
809 self.findings.push(finding);
810 }
811
812 fn issue(
813 &mut self,
814 file: &Path,
815 path: &KeyPath,
816 code: &'static str,
817 message: impl Into<String>,
818 ) {
819 self.push(Finding::new(file, path.clone(), Part::Value, code, message));
820 }
821
822 fn issues(&mut self, file: &Path, path: &KeyPath, issues: Issues) {
823 for issue in issues.0 {
824 let at = under(path, &issue.key);
825 let code = if let Some(code) = issue.code {
826 code
827 } else if issue.message.starts_with("missing key") {
828 code::MISSING_KEY
829 } else {
830 code::BAD_VALUE
831 };
832 self.issue(file, &at, code, issue.message);
833 }
834 }
835
836 fn text(&self, file: &Path) -> Option<Result<String, String>> {
837 if let Some(text) = self.overlay.get(file) {
838 return Some(Ok(text.clone()));
839 }
840 if let Some(text) = self.project.texts.get(file) {
841 return Some(text.clone());
842 }
843 let path = self.project.root.join(file);
844 if !path.is_file() {
845 return None;
846 }
847 Some(
848 std::fs::read(&path)
849 .map_err(|error| format!("cannot read {}: {error}", file.display()))
850 .and_then(|bytes| {
851 String::from_utf8(bytes).map_err(|_| "the file is not UTF-8".to_string())
852 }),
853 )
854 }
855
856 fn load(&mut self, file: &Path, kind: FileKind) -> Option<Doc> {
857 self.loaded(file, kind).cloned()
858 }
859
860 fn loaded(&mut self, file: &Path, kind: FileKind) -> Option<&Doc> {
861 if !self.parsed.contains_key(file) {
862 let text = self.text(file)?;
863 self.kinds.insert(file.to_path_buf(), kind);
864 let parsed = match text {
865 Ok(text) => read::parse(
866 file,
867 &text,
868 kind,
869 &Context {
870 root: &self.project.root,
871 },
872 ),
873 Err(message) => {
874 let code = if message.contains("UTF-8") {
875 code::NOT_UTF8
876 } else {
877 code::UNREADABLE
878 };
879 self.push(Finding::new(file, Vec::new(), Part::Value, code, message));
880 return None;
881 }
882 };
883 self.parsed.insert(file.to_path_buf(), Box::new(parsed));
884 self.file_checks(file);
885 }
886 self.parsed.get(file).and_then(|parsed| parsed.doc.as_ref())
887 }
888
889 fn asset(
890 &mut self,
891 file: &Path,
892 path: &KeyPath,
893 raw: &str,
894 extensions: &[&str],
895 ) -> Option<PathBuf> {
896 self.located(file, path, raw, extensions, true)
897 }
898
899 fn located(
900 &mut self,
901 file: &Path,
902 path: &KeyPath,
903 raw: &str,
904 extensions: &[&str],
905 needed: bool,
906 ) -> Option<PathBuf> {
907 let old = self
908 .parsed
909 .get(file)
910 .is_some_and(|parsed| parsed.origin.is_some());
911 let rooted = match rooted(raw) {
912 Ok(rooted) => rooted,
913 Err(error) => {
914 if !(old && matches!(error, PathError::Outside(_))) {
915 self.issue(file, path, error.code(), error.message(raw));
916 }
917 return None;
918 }
919 };
920 if !extensions.is_empty() && !extensions.iter().any(|end| rooted.ends_with(end)) {
921 self.issue(
922 file,
923 path,
924 code::BAD_PATH,
925 format!("path {raw} is not a {} file", extensions.join(" or ")),
926 );
927 return None;
928 }
929 let overlay: BTreeSet<String> = self.overlay.keys().map(|key| slashed(key)).collect();
930 match self
931 .listing
932 .find(&self.project.root, &rooted, &|path| overlay.contains(path))
933 {
934 Found::Exact => Some(PathBuf::from(rooted)),
935 Found::Case(actual) => {
936 self.issue(
937 file,
938 path,
939 code::PATH_CASE,
940 format!("path {raw} is found only as {actual}; paths keep their case"),
941 );
942 None
943 }
944 Found::Missing => {
945 if needed {
946 self.issue(
947 file,
948 path,
949 code::MISSING_FILE,
950 format!("{raw} does not exist"),
951 );
952 }
953 None
954 }
955 }
956 }
957
958 fn missing_id(&mut self, file: &Path, path: KeyPath, what: &str, id: Option<Id>) {
959 if id.is_none() {
960 self.push(Finding::new(
961 file,
962 path,
963 Part::Value,
964 code::MISSING_ID,
965 format!("{what} has no id; the next write through the crate adds one"),
966 ));
967 }
968 }
969
970 fn file_checks(&mut self, file: &Path) {
971 if !self.filed.insert(file.to_path_buf()) {
972 return;
973 }
974 let Some(parsed) = self.parsed.get(file) else {
975 return;
976 };
977 let findings = parsed.findings.clone();
978 let kind = parsed.kind;
979 let doc = parsed.doc.clone();
980 self.findings.extend(findings);
981 let Some(doc) = doc else { return };
982 match kind {
983 FileKind::Scene | FileKind::Prefab => self.scene_checks(file, kind, &doc),
984 FileKind::Materials => {
985 for (name, material) in &doc.library {
986 let path = key(&["materials", name]);
987 if let Some(message) = rules::name("material", name) {
988 self.push(Finding::new(
989 file,
990 path.clone(),
991 Part::Key,
992 code::BAD_NAME,
993 message,
994 ));
995 }
996 self.issues(file, &path, rules::material(name, material));
997 self.missing_id(file, path, &format!("material {name}"), material.id);
998 }
999 }
1000 FileKind::Proxies => {
1001 for (place, proxy) in &doc.proxy {
1002 let path = key(&["proxy", &place.to_string()]);
1003 self.issues(file, &path, rules::proxy(proxy));
1004 self.missing_id(file, path, "a proxy", proxy.id);
1005 }
1006 }
1007 FileKind::Project => {
1008 if let Some(project) = &doc.project {
1009 if let Some(scene) = &project.scene {
1010 self.asset(file, &key(&["project", "scene"]), scene, &[".scene.toml"]);
1011 }
1012 for (place, raw) in project.ignore.iter().enumerate() {
1013 let path = key(&["project", "ignore", &place.to_string()]);
1014 self.located(file, &path, raw, &[], false);
1015 }
1016 }
1017 for (name, tunable) in &doc.tunables {
1018 let path = key(&["tunables", name]);
1019 self.named(file, &path, "tunable", name, Part::Key);
1020 if let Some(group) = &tunable.group {
1021 self.named(
1022 file,
1023 &under(&path, &["group".into()]),
1024 "tunable group",
1025 group,
1026 Part::Value,
1027 );
1028 }
1029 self.issues(file, &path, rules::tunable(name, tunable));
1030 }
1031 if let Some(layers) = &doc.layers {
1032 for name in layers.keys() {
1033 self.named(file, &key(&["layers", name]), "layer", name, Part::Key);
1034 }
1035 self.issues(file, &key(&["layers"]), rules::layers(layers));
1036 }
1037 }
1038 FileKind::Finish => {
1039 if let Some(keys) = &doc.finish_keys {
1040 self.issues(file, &Vec::new(), rules::finish_keys(keys));
1041 }
1042 }
1043 }
1044 }
1045
1046 fn scene_checks(&mut self, file: &Path, kind: FileKind, doc: &Doc) {
1047 let include_end: &[&str] = if kind == FileKind::Prefab {
1048 &[".prefab.toml"]
1049 } else {
1050 &[".scene.toml", "/scene.toml"]
1051 };
1052 for (place, raw) in &doc.include {
1053 self.asset(
1054 file,
1055 &key(&["include", &place.to_string()]),
1056 raw,
1057 include_end,
1058 );
1059 }
1060 for (place, raw) in &doc.materials {
1061 self.asset(
1062 file,
1063 &key(&["materials", &place.to_string()]),
1064 raw,
1065 &[".toml"],
1066 );
1067 }
1068 for (name, mesh) in &doc.mesh {
1069 let path = key(&["mesh", name]);
1070 self.named(file, &path, "mesh", name, Part::Key);
1071 self.asset(file, &under(&path, &["file".into()]), &mesh.file, &[]);
1072 self.missing_id(file, path, &format!("mesh {name}"), mesh.id);
1073 }
1074 for (place, object) in &doc.object {
1075 let path = key(&["object", &place.to_string()]);
1076 self.named(
1077 file,
1078 &under(&path, &["name".into()]),
1079 "object",
1080 &object.name,
1081 Part::Value,
1082 );
1083 let issues = rules::object(object);
1084 for issue in issues.0 {
1085 let at = under(&path, &issue.key);
1086 let code = if let Some(code) = issue.code {
1087 code
1088 } else if object.is_placement()
1089 && issue.key.len() == 1
1090 && issue.message.contains("places a prefab")
1091 {
1092 code::PLACEMENT_KEY
1093 } else if issue.message.starts_with("missing key") {
1094 code::MISSING_KEY
1095 } else {
1096 code::BAD_VALUE
1097 };
1098 let part = if code == code::PLACEMENT_KEY {
1099 Part::Key
1100 } else {
1101 Part::Value
1102 };
1103 self.push(Finding::new(file, at, part, code, issue.message));
1104 }
1105 if let Some(prefab) = &object.prefab
1106 && Id::parse(prefab).is_err()
1107 {
1108 self.asset(
1109 file,
1110 &under(&path, &["prefab".into()]),
1111 prefab,
1112 &[".prefab.toml"],
1113 );
1114 }
1115 if let Some(raw) = object.animation.as_ref().and_then(|a| a.atlas.as_ref()) {
1116 self.asset(
1117 file,
1118 &under(&path, &["animation".into(), "atlas".into()]),
1119 raw,
1120 &[],
1121 );
1122 }
1123 self.layered(file, &path, object);
1124 self.missing_id(file, path, &format!("object {}", object.name), object.id);
1125 }
1126 for (place, light) in &doc.light {
1127 let path = key(&["light", &place.to_string()]);
1128 self.named(
1129 file,
1130 &under(&path, &["name".into()]),
1131 "light",
1132 &light.name,
1133 Part::Value,
1134 );
1135 self.issues(file, &path, rules::light(light));
1136 self.missing_id(file, path, &format!("light {}", light.name), light.id);
1137 }
1138 for (place, emitter) in &doc.emitter {
1139 let path = key(&["emitter", &place.to_string()]);
1140 self.named(
1141 file,
1142 &under(&path, &["name".into()]),
1143 "emitter",
1144 &emitter.name,
1145 Part::Value,
1146 );
1147 self.issues(file, &path, rules::emitter(emitter));
1148 self.missing_id(file, path, &format!("emitter {}", emitter.name), emitter.id);
1149 }
1150 for (place, mover) in &doc.mover {
1151 let path = key(&["mover", &place.to_string()]);
1152 self.named(
1153 file,
1154 &under(&path, &["name".into()]),
1155 "mover",
1156 &mover.name,
1157 Part::Value,
1158 );
1159 self.issues(file, &path, rules::mover(mover));
1160 self.missing_id(file, path, &format!("mover {}", mover.name), mover.id);
1161 }
1162 for (name, content) in &doc.content {
1163 let path = key(&["content", name]);
1164 self.named(file, &path, "content", name, Part::Key);
1165 self.asset(file, &under(&path, &["image".into()]), &content.image, &[]);
1166 self.missing_id(file, path, &format!("content {name}"), content.id);
1167 }
1168 for (name, text) in &doc.text {
1169 let path = key(&["text", name]);
1170 self.named(file, &path, "text", name, Part::Key);
1171 self.issues(file, &path, rules::text(text));
1172 self.asset(file, &under(&path, &["font".into()]), &text.font, &[]);
1173 self.missing_id(file, path, &format!("text {name}"), text.id);
1174 }
1175 for (name, sound) in &doc.sound {
1176 let path = key(&["sound", name]);
1177 self.named(file, &path, "sound", name, Part::Key);
1178 self.issues(file, &path, rules::sound(name, sound));
1179 self.asset(file, &under(&path, &["file".into()]), &sound.file, &[]);
1180 self.missing_id(file, path, &format!("sound {name}"), sound.id);
1181 }
1182 for (place, rig) in &doc.rig {
1183 let path = key(&["rig", &place.to_string()]);
1184 self.named(
1185 file,
1186 &under(&path, &["name".into()]),
1187 "rig",
1188 &rig.name,
1189 Part::Value,
1190 );
1191 self.issues(file, &path, rules::rig(rig));
1192 self.missing_id(file, path, &format!("rig {}", rig.name), rig.id);
1193 }
1194 for (place, tiles) in &doc.tiles {
1195 let path = key(&["tiles", &place.to_string()]);
1196 self.named(
1197 file,
1198 &under(&path, &["name".into()]),
1199 "tile layer",
1200 &tiles.name,
1201 Part::Value,
1202 );
1203 for (tile, raw) in &tiles.palette {
1204 let at = under(&path, &["palette".into(), tile.clone()]);
1205 if let Some(message) = rules::palette_key(tile) {
1206 self.push(Finding::new(
1207 file,
1208 at.clone(),
1209 Part::Key,
1210 code::BAD_NAME,
1211 format!("tile layer {}: {message}", tiles.name),
1212 ));
1213 }
1214 if Id::parse(raw).is_err() {
1215 self.asset(file, &at, raw, &[".prefab.toml"]);
1216 }
1217 }
1218 self.issues(file, &path, rules::tiles(tiles));
1219 self.rows(file, &path, tiles);
1220 self.missing_id(file, path, &format!("tile layer {}", tiles.name), tiles.id);
1221 }
1222 if let Some(sun) = &doc.sun {
1223 self.issues(file, &key(&["sun"]), rules::sun(sun));
1224 }
1225 if let Some(sky) = &doc.sky {
1226 self.issues(file, &key(&["sky"]), rules::sky(sky));
1227 if let Some(raw) = &sky.path {
1228 self.asset(file, &key(&["sky", "path"]), raw, &[]);
1229 }
1230 for (place, layer) in sky.layer.iter().enumerate() {
1231 if let Some(raw) = &layer.path {
1232 self.asset(
1233 file,
1234 &key(&["sky", "layer", &place.to_string(), "path"]),
1235 raw,
1236 &[],
1237 );
1238 }
1239 }
1240 }
1241 if let Some(haze) = &doc.haze {
1242 self.issues(file, &key(&["haze"]), rules::haze(haze));
1243 }
1244 if let Some(camera) = &doc.camera {
1245 self.issues(file, &key(&["camera"]), rules::camera(camera));
1246 }
1247 if let Some(finish) = &doc.finish {
1248 self.issues(file, &key(&["finish"]), rules::finish(finish));
1249 if let Some(raw) = &finish.file
1250 && let Some(target) = self.asset(file, &key(&["finish", "file"]), raw, &[])
1251 {
1252 self.load(&target, FileKind::Finish);
1253 }
1254 }
1255 if let Some(trace) = &doc.trace {
1256 self.issues(file, &key(&["trace"]), rules::trace(trace));
1257 }
1258 if let Some(plates) = &doc.plates {
1259 self.issues(file, &key(&["plates"]), rules::plates(plates));
1260 self.located(file, &key(&["plates", "dir"]), &plates.dir, &[], false);
1261 if let Some(raw) = &plates.proxies
1262 && let Some(target) =
1263 self.asset(file, &key(&["plates", "proxies"]), raw, &[".proxies.toml"])
1264 {
1265 self.load(&target, FileKind::Proxies);
1266 }
1267 }
1268 if let Some(physics) = &doc.physics {
1269 self.issues(file, &key(&["physics"]), rules::physics(physics));
1270 }
1271 }
1272
1273 fn rows(&mut self, file: &Path, path: &KeyPath, tiles: &Tiles) {
1274 let Some(rows) = &tiles.rows else { return };
1275 for (place, row) in rows.iter().enumerate() {
1276 let at = under(path, &["rows".into(), place.to_string()]);
1277 let start = self.parsed.get(file).and_then(|parsed| {
1278 if parsed.origin.is_some() {
1279 return None;
1280 }
1281 let range = parsed.locate(&at, Part::Value);
1282 let raw = parsed.text.get(range.clone())?;
1283 let quoted = (raw.starts_with('"') && !raw.starts_with("\"\"\""))
1284 || (raw.starts_with('\'') && !raw.starts_with("'''"));
1285 (quoted && raw.len() >= 2 && raw[1..raw.len() - 1] == *row)
1286 .then_some(range.start + 1)
1287 });
1288 let mut seen = BTreeSet::new();
1289 for (offset, character) in row.char_indices() {
1290 if character == Tiles::EMPTY {
1291 continue;
1292 }
1293 let tile = character.to_string();
1294 if tiles.palette.contains_key(&tile) || !seen.insert(character) {
1295 continue;
1296 }
1297 let mut finding = Finding::new(
1298 file,
1299 at.clone(),
1300 Part::Value,
1301 code::BAD_REFERENCE,
1302 rules::unknown_tile(&tiles.name, &tile, tiles),
1303 );
1304 finding.span =
1305 start.map(|start| start + offset..start + offset + character.len_utf8());
1306 self.push(finding);
1307 }
1308 }
1309 }
1310
1311 fn named(&mut self, file: &Path, path: &KeyPath, kind: &str, name: &str, part: Part) {
1312 if let Some(message) = rules::name(kind, name) {
1313 self.push(Finding::new(
1314 file,
1315 path.clone(),
1316 part,
1317 code::BAD_NAME,
1318 message,
1319 ));
1320 }
1321 }
1322
1323 fn include_target(&mut self, raw: &str, kind: FileKind) -> Option<PathBuf> {
1324 let rooted = rooted(raw).ok()?;
1325 let path = PathBuf::from(rooted);
1326 let end_ok = match kind {
1327 FileKind::Prefab => raw.ends_with(".prefab.toml"),
1328 _ => raw.ends_with(".scene.toml") || raw.ends_with("scene.toml"),
1329 };
1330 if !end_ok || self.text(&path).is_none() {
1331 return None;
1332 }
1333 Some(path)
1334 }
1335
1336 fn includes(&mut self, file: &Path, kind: FileKind) -> Option<Vec<(usize, String)>> {
1337 self.loaded(file, kind).map(|doc| doc.include.clone())
1338 }
1339
1340 fn gather(&mut self, file: &Path, kind: FileKind) -> Vec<PathBuf> {
1341 let mut order: Vec<PathBuf> = Vec::new();
1342 let Some(includes) = self.includes(file, kind) else {
1343 return order;
1344 };
1345 let mut steps = vec![Step::new(file)];
1346 let mut pending = vec![includes.into_iter()];
1347 while let Some(next) = pending.last_mut() {
1348 let Some((place, raw)) = next.next() else {
1349 pending.pop();
1350 if let Some(step) = steps.pop()
1351 && !order.contains(&step.file)
1352 {
1353 order.push(step.file);
1354 }
1355 continue;
1356 };
1357 let Some(target) = self.include_target(&raw, kind) else {
1358 continue;
1359 };
1360 if let Some(step) = steps.last_mut() {
1361 step.link = Some((step.file.clone(), key(&["include", &place.to_string()])));
1362 }
1363 if let Some(start) = steps.iter().position(|step| step.file == target) {
1364 self.chained(
1365 &steps[start..],
1366 &target,
1367 code::INCLUDE_CYCLE,
1368 format!("the {} includes itself through {raw}", kind.name()),
1369 );
1370 continue;
1371 }
1372 if order.contains(&target) {
1373 continue;
1374 }
1375 if steps.len() > MAX_DEPTH {
1376 self.chained(
1377 &steps,
1378 &target,
1379 code::INCLUDE_DEPTH,
1380 format!(
1381 "including {raw} nests the {}'s includes more than {MAX_DEPTH} deep",
1382 kind.name()
1383 ),
1384 );
1385 continue;
1386 }
1387 let Some(includes) = self.includes(&target, kind) else {
1388 continue;
1389 };
1390 steps.push(Step::new(&target));
1391 pending.push(includes.into_iter());
1392 }
1393 order
1394 }
1395
1396 fn chained(&mut self, steps: &[Step], target: &Path, code: &'static str, message: String) {
1397 let links: Vec<(PathBuf, KeyPath)> =
1398 steps.iter().filter_map(|step| step.link.clone()).collect();
1399 let Some((file, path)) = links.last().cloned() else {
1400 return;
1401 };
1402 let known = self
1403 .findings
1404 .iter()
1405 .any(|finding| finding.code == code && finding.file == file && finding.path == path);
1406 if known {
1407 return;
1408 }
1409 let names: Vec<String> = steps
1410 .iter()
1411 .map(|step| slashed(&step.file))
1412 .chain([slashed(target)])
1413 .collect();
1414 let mut finding = Finding::new(
1415 &file,
1416 path,
1417 Part::Value,
1418 code,
1419 format!("{message}: {}", trail(&names)),
1420 );
1421 for (file, path) in links {
1422 finding = finding.related(&file, path, Part::Value);
1423 }
1424 self.push(finding);
1425 }
1426
1427 fn scope(&mut self, file: &Path, kind: FileKind) -> Rc<Scope> {
1428 if let Some(scope) = self.scopes.get(file) {
1429 return scope.clone();
1430 }
1431 let order = self.gather(file, kind);
1432 let mut scope = Scope {
1433 prefab: kind == FileKind::Prefab,
1434 files: order.clone(),
1435 ..Scope::default()
1436 };
1437 for member in &order {
1438 let Some(doc) = self
1439 .parsed
1440 .get(member)
1441 .and_then(|parsed| parsed.doc.clone())
1442 else {
1443 continue;
1444 };
1445 for (place, raw) in &doc.materials {
1446 if let Ok(rooted) = rooted(raw) {
1447 let library = PathBuf::from(rooted);
1448 if self.text(&library).is_some() {
1449 scope.libraries.push((
1450 library,
1451 member.clone(),
1452 key(&["materials", &place.to_string()]),
1453 ));
1454 }
1455 }
1456 }
1457 let at = |parts: &[&str]| key(parts);
1458 for (name, mesh) in doc.mesh {
1459 scope.meshes.push(Item {
1460 file: member.clone(),
1461 path: at(&["mesh", &name]),
1462 id: mesh.id,
1463 name,
1464 value: mesh,
1465 });
1466 }
1467 for (place, object) in doc.object {
1468 scope.objects.push(Item {
1469 file: member.clone(),
1470 path: at(&["object", &place.to_string()]),
1471 id: object.id,
1472 name: object.name.clone(),
1473 value: object,
1474 });
1475 }
1476 for (place, light) in doc.light {
1477 scope.lights.push(Item {
1478 file: member.clone(),
1479 path: at(&["light", &place.to_string()]),
1480 id: light.id,
1481 name: light.name.clone(),
1482 value: light,
1483 });
1484 }
1485 for (place, emitter) in doc.emitter {
1486 scope.emitters.push(Item {
1487 file: member.clone(),
1488 path: at(&["emitter", &place.to_string()]),
1489 id: emitter.id,
1490 name: emitter.name.clone(),
1491 value: emitter,
1492 });
1493 }
1494 for (place, mover) in doc.mover {
1495 scope.movers.push(Item {
1496 file: member.clone(),
1497 path: at(&["mover", &place.to_string()]),
1498 id: mover.id,
1499 name: mover.name.clone(),
1500 value: mover,
1501 });
1502 }
1503 for (name, content) in doc.content {
1504 scope.contents.push(Item {
1505 file: member.clone(),
1506 path: at(&["content", &name]),
1507 id: content.id,
1508 name,
1509 value: content,
1510 });
1511 }
1512 for (name, text) in doc.text {
1513 scope.texts.push(Item {
1514 file: member.clone(),
1515 path: at(&["text", &name]),
1516 id: text.id,
1517 name,
1518 value: text,
1519 });
1520 }
1521 for (name, sound) in doc.sound {
1522 scope.sounds.push(Item {
1523 file: member.clone(),
1524 path: at(&["sound", &name]),
1525 id: sound.id,
1526 name,
1527 value: sound,
1528 });
1529 }
1530 for (place, rig) in doc.rig {
1531 scope.rigs.push(Item {
1532 file: member.clone(),
1533 path: at(&["rig", &place.to_string()]),
1534 id: rig.id,
1535 name: rig.name.clone(),
1536 value: rig,
1537 });
1538 }
1539 for (place, tiles) in doc.tiles {
1540 scope.tiles.push(Item {
1541 file: member.clone(),
1542 path: at(&["tiles", &place.to_string()]),
1543 id: tiles.id,
1544 name: tiles.name.clone(),
1545 value: tiles,
1546 });
1547 }
1548 self.single(&mut scope.fallback, member, "fallback", doc.fallback);
1549 self.single(&mut scope.sun, member, "sun", doc.sun);
1550 self.single(&mut scope.sky, member, "sky", doc.sky);
1551 self.single(&mut scope.haze, member, "haze", doc.haze);
1552 self.single(&mut scope.camera, member, "camera", doc.camera);
1553 self.single(&mut scope.finish, member, "finish", doc.finish);
1554 self.single(&mut scope.trace, member, "trace", doc.trace);
1555 self.single(&mut scope.plates, member, "plates", doc.plates);
1556 self.single(&mut scope.physics, member, "physics", doc.physics);
1557 }
1558 for kind in Kind::ALL {
1559 let names: Vec<(String, PathBuf, KeyPath)> = scope
1560 .names(kind)
1561 .into_iter()
1562 .map(|(name, _, file, path)| (name.to_string(), file.to_path_buf(), path.clone()))
1563 .collect();
1564 let mut seen: BTreeMap<String, (PathBuf, KeyPath, Part)> = BTreeMap::new();
1565 for (name, file, path) in names {
1566 let (at, part) = if path.len() == 2 && path[1] == name {
1567 (path.clone(), Part::Key)
1568 } else {
1569 (under(&path, &["name".into()]), Part::Value)
1570 };
1571 match seen.get(&name) {
1572 Some((first, first_at, first_part)) => {
1573 let shown = if first == &file {
1574 "this file".to_string()
1575 } else {
1576 first.display().to_string()
1577 };
1578 let finding = Finding::new(
1579 &file,
1580 at,
1581 part,
1582 code::DUPLICATE_NAME,
1583 format!(
1584 "{} {name} is named twice; {shown} has it already",
1585 kind.word()
1586 ),
1587 )
1588 .related(first, first_at.clone(), *first_part);
1589 self.push(finding);
1590 }
1591 None => {
1592 seen.insert(name, (file, at, part));
1593 }
1594 }
1595 }
1596 }
1597 let scope = Rc::new(scope);
1598 self.scopes.insert(file.to_path_buf(), scope.clone());
1599 scope
1600 }
1601
1602 fn single<T>(
1603 &mut self,
1604 slot: &mut Option<Single<T>>,
1605 file: &Path,
1606 word: &str,
1607 value: Option<T>,
1608 ) {
1609 let Some(value) = value else { return };
1610 let path = key(&[word]);
1611 if let Some(first) = slot {
1612 let what = if word == "fallback" {
1613 "fallback".to_string()
1614 } else {
1615 format!("[{word}]")
1616 };
1617 let finding = Finding::new(
1618 file,
1619 path.clone(),
1620 Part::Key,
1621 code::HELD_TWICE,
1622 format!(
1623 "{what} is set again; {} sets it already",
1624 first.file.display()
1625 ),
1626 )
1627 .related(&first.file, first.path.clone(), Part::Key);
1628 self.push(finding);
1629 return;
1630 }
1631 *slot = Some(Single {
1632 file: file.to_path_buf(),
1633 path,
1634 value,
1635 });
1636 }
1637
1638 fn overrides_of(&self, item: &Item<Object>, own: bool) -> Vec<Override> {
1639 item.value
1640 .set
1641 .iter()
1642 .map(|(name, value)| {
1643 let (entry, keys) = match name.split_once('.') {
1644 Some((entry, rest)) => (
1645 entry.to_string(),
1646 rest.split('.').map(str::to_string).collect(),
1647 ),
1648 None => (name.clone(), Vec::new()),
1649 };
1650 Override {
1651 file: item.file.clone(),
1652 path: under(&item.path, &["set".into(), name.clone()]),
1653 entry,
1654 keys,
1655 value: value.clone(),
1656 own,
1657 }
1658 })
1659 .collect()
1660 }
1661
1662 fn apply<T: serde::Serialize + serde::de::DeserializeOwned>(
1663 value: &T,
1664 change: &Override,
1665 ) -> Result<T, String> {
1666 if change.keys.is_empty() {
1667 return Err(format!("{} names an entry and no key of it", change.entry));
1668 }
1669 if change.keys[0] == "id" {
1670 return Err("an override does not change an id".to_string());
1671 }
1672 let mut root = toml::Value::try_from(value).map_err(|error| error.to_string())?;
1673 let mut at = &mut root;
1674 let (last, parents) = change.keys.split_last().unwrap_or((&change.keys[0], &[]));
1675 for (place, segment) in parents.iter().enumerate() {
1676 let next = change.keys[place + 1].as_str();
1677 at = match at {
1678 toml::Value::Table(table) => {
1679 if !table.contains_key(segment) && next.parse::<usize>().is_ok() {
1680 return Err(format!("{segment} has no entry {next}"));
1681 }
1682 table
1683 .entry(segment.clone())
1684 .or_insert_with(|| toml::Value::Table(toml::Table::new()))
1685 }
1686 toml::Value::Array(items) => {
1687 let place: usize = segment
1688 .parse()
1689 .map_err(|_| format!("{segment} is not a list place"))?;
1690 let count = items.len();
1691 items
1692 .get_mut(place)
1693 .ok_or_else(|| format!("the list has {count} entries, not {place}"))?
1694 }
1695 _ => return Err(format!("{segment} is not in a table")),
1696 };
1697 }
1698 match at {
1699 toml::Value::Table(table) => {
1700 table.insert(last.clone(), change.value.clone());
1701 }
1702 toml::Value::Array(items) => {
1703 let place: usize = last
1704 .parse()
1705 .map_err(|_| format!("{last} is not a list place"))?;
1706 let count = items.len();
1707 let slot = items
1708 .get_mut(place)
1709 .ok_or_else(|| format!("the list has {count} entries, not {place}"))?;
1710 *slot = change.value.clone();
1711 }
1712 _ => return Err(format!("{last} is not in a table")),
1713 }
1714 root.try_into::<T>().map_err(|error| {
1715 error
1716 .message()
1717 .trim()
1718 .replace('`', "'")
1719 .replace("unknown field", "unknown key")
1720 })
1721 }
1722
1723 fn reject(&mut self, change: &Override, message: String) {
1724 self.push(Finding::new(
1725 &change.file,
1726 change.path.clone(),
1727 Part::Key,
1728 code::BAD_OVERRIDE,
1729 message,
1730 ));
1731 }
1732
1733 fn prefab_path(&self, scope: &Scope, raw: &str) -> Option<PathBuf> {
1734 if Id::parse(raw).is_ok()
1735 && let Lookup::One(at) = lookup(&scope.objects, raw)
1736 {
1737 let target = scope.objects[at].value.prefab.clone()?;
1738 if Id::parse(&target).is_ok() {
1739 return None;
1740 }
1741 return rooted(&target).ok().map(PathBuf::from);
1742 }
1743 rooted(raw).ok().map(PathBuf::from)
1744 }
1745
1746 fn place(&mut self, scope: &Scope, chain: &mut Vec<Step>) -> Placed {
1747 let mut frames = vec![self.enter(scope, None, &[], true)];
1748 while let Some(mut frame) = frames.pop() {
1749 if let Some(child) = self.descend(&mut frame, chain) {
1750 frames.push(frame);
1751 frames.push(child);
1752 continue;
1753 }
1754 let placed = self.finish(frame);
1755 match frames.last_mut() {
1756 Some(parent) => {
1757 chain.pop();
1758 Self::absorb(parent, placed);
1759 }
1760 None => return placed,
1761 }
1762 }
1763 Placed::default()
1764 }
1765
1766 fn enter(
1767 &mut self,
1768 scope: &Scope,
1769 prefix: Option<Prefix>,
1770 changes: &[Override],
1771 own: bool,
1772 ) -> Frame {
1773 let mut scope = scope.clone();
1774 let layers = self.layers();
1775 let known = |object: &Object| match &layers {
1776 Some(layers) => rules::layer_names(object)
1777 .into_iter()
1778 .find_map(|(_, name)| rules::unknown_layer(layers, &name))
1779 .map_or(Ok(()), Err),
1780 None => Ok(()),
1781 };
1782 let mut nested: BTreeMap<usize, Vec<Override>> = BTreeMap::new();
1783 let mut touched: BTreeMap<(Kind, usize), Vec<Touch>> = BTreeMap::new();
1784 for change in changes {
1785 let mut found: Vec<(Kind, usize)> = Vec::new();
1786 for kind in Kind::ALL {
1787 match scope.lookup(kind, &change.entry) {
1788 Lookup::One(at) => found.push((kind, at)),
1789 Lookup::Many(all) => found.extend(all.into_iter().map(|at| (kind, at))),
1790 Lookup::None => {}
1791 }
1792 }
1793 if found.is_empty()
1794 && let Some((head, rest)) = change.entry.split_once('/')
1795 && let Lookup::One(at) = lookup(&scope.objects, head)
1796 && scope.objects[at].value.is_placement()
1797 {
1798 let mut inner = change.clone();
1799 inner.entry = rest.to_string();
1800 nested.entry(at).or_default().push(inner);
1801 continue;
1802 }
1803 match found.as_slice() {
1804 [] => self.reject(change, format!("the prefab has no entry {}", change.entry)),
1805 [(kind, at)] => {
1806 let checked = |issues: Issues| match issues.0.into_iter().next() {
1807 Some(issue) => Err(issue.message),
1808 None => Ok(()),
1809 };
1810 let result = match kind {
1811 Kind::Mesh => Self::apply(&scope.meshes[*at].value, change)
1812 .map(|v| scope.meshes[*at].value = v),
1813 Kind::Object => Self::apply(&scope.objects[*at].value, change)
1814 .and_then(|v| checked(rules::object(&v)).map(|()| v))
1815 .and_then(|v| known(&v).map(|()| v))
1816 .map(|v| scope.objects[*at].value = v),
1817 Kind::Light => Self::apply(&scope.lights[*at].value, change)
1818 .and_then(|v| checked(rules::light(&v)).map(|()| v))
1819 .map(|v| scope.lights[*at].value = v),
1820 Kind::Emitter => Self::apply(&scope.emitters[*at].value, change)
1821 .and_then(|v| checked(rules::emitter(&v)).map(|()| v))
1822 .map(|v| scope.emitters[*at].value = v),
1823 Kind::Mover => Self::apply(&scope.movers[*at].value, change)
1824 .and_then(|v| checked(rules::mover(&v)).map(|()| v))
1825 .map(|v| scope.movers[*at].value = v),
1826 Kind::Content => Self::apply(&scope.contents[*at].value, change)
1827 .map(|v| scope.contents[*at].value = v),
1828 Kind::Text => Self::apply(&scope.texts[*at].value, change)
1829 .and_then(|v| checked(rules::text(&v)).map(|()| v))
1830 .map(|v| scope.texts[*at].value = v),
1831 Kind::Sound => {
1832 let name = scope.sounds[*at].name.clone();
1833 Self::apply(&scope.sounds[*at].value, change)
1834 .and_then(|v| checked(rules::sound(&name, &v)).map(|()| v))
1835 .map(|v| scope.sounds[*at].value = v)
1836 }
1837 Kind::Rig => Self::apply(&scope.rigs[*at].value, change)
1838 .and_then(|v| checked(rules::rig(&v)).map(|()| v))
1839 .map(|v| scope.rigs[*at].value = v),
1840 Kind::Tiles => Self::apply(&scope.tiles[*at].value, change)
1841 .and_then(|v| checked(rules::tiles(&v)).map(|()| v))
1842 .map(|v| scope.tiles[*at].value = v),
1843 };
1844 match result {
1845 Err(message) => self.reject(
1846 change,
1847 format!("{} {}: {message}", kind.word(), change.entry),
1848 ),
1849 Ok(()) if change.own => {
1850 touched.entry((*kind, *at)).or_default().push(Touch {
1851 key: change.keys[0].clone(),
1852 file: change.file.clone(),
1853 path: change.path.clone(),
1854 });
1855 }
1856 Ok(()) => {}
1857 }
1858 }
1859 _ => self.reject(
1860 change,
1861 format!(
1862 "{} names more than one entry of the prefab; use its id",
1863 change.entry
1864 ),
1865 ),
1866 }
1867 }
1868 Frame {
1869 placed: Placed {
1870 libraries: scope.libraries.clone(),
1871 files: scope.files.clone(),
1872 ..Placed::default()
1873 },
1874 scope,
1875 prefix,
1876 own,
1877 nested,
1878 touched,
1879 inner: Placed::default(),
1880 inner_objects: BTreeMap::new(),
1881 next: 0,
1882 placing: 0,
1883 }
1884 }
1885
1886 fn descend(&mut self, frame: &mut Frame, chain: &mut Vec<Step>) -> Option<Frame> {
1887 while frame.next < frame.scope.objects.len() {
1888 let at = frame.next;
1889 frame.next += 1;
1890 let item = &frame.scope.objects[at];
1891 let Some(raw) = &item.value.prefab else {
1892 continue;
1893 };
1894 let Some(path) = self.prefab_path(&frame.scope, raw) else {
1895 continue;
1896 };
1897 if let Some(step) = chain.last_mut() {
1898 step.link = Some((item.file.clone(), under(&item.path, &["prefab".into()])));
1899 }
1900 if let Some(start) = chain.iter().position(|step| step.file == path) {
1901 self.chained(
1902 &chain[start..],
1903 &path,
1904 code::PREFAB_CYCLE,
1905 format!("object {} places {raw}, which places itself", item.name),
1906 );
1907 continue;
1908 }
1909 if self.text(&path).is_none() {
1910 continue;
1911 }
1912 if chain.len() > MAX_DEPTH {
1913 self.chained(
1914 chain,
1915 &path,
1916 code::PREFAB_DEPTH,
1917 format!(
1918 "object {} places {raw}, which nests prefabs more than {MAX_DEPTH} deep",
1919 item.name
1920 ),
1921 );
1922 continue;
1923 }
1924 if self.prefabs_checked.insert(path.clone()) {
1925 self.unchecked.push((path.clone(), chain.clone()));
1926 }
1927 let prefab_scope = self.scope(&path, FileKind::Prefab);
1928 let mut all = self.overrides_of(item, frame.own);
1929 all.extend(frame.nested.remove(&at).unwrap_or_default());
1930 let prefix = frame.prefix.as_ref();
1931 let child = Prefix {
1932 key: name_key(prefix, &item.name),
1933 id: full_id(prefix, item.id),
1934 hidden: item.value.hidden || prefix.is_some_and(|p| p.hidden),
1935 };
1936 frame.placing = at;
1937 chain.push(Step::new(&path));
1938 return Some(self.enter(&prefab_scope, Some(child), &all, false));
1939 }
1940 None
1941 }
1942
1943 fn absorb(frame: &mut Frame, result: Placed) {
1944 frame.inner_objects.insert(frame.placing, result.objects);
1945 let inner = &mut frame.inner;
1946 inner.meshes.extend(result.meshes);
1947 inner.lights.extend(result.lights);
1948 inner.emitters.extend(result.emitters);
1949 inner.movers.extend(result.movers);
1950 inner.contents.extend(result.contents);
1951 inner.texts.extend(result.texts);
1952 inner.sounds.extend(result.sounds);
1953 inner.rigs.extend(result.rigs);
1954 frame.placed.libraries.extend(result.libraries);
1955 for file in result.files {
1956 if !frame.placed.files.contains(&file) {
1957 frame.placed.files.push(file);
1958 }
1959 }
1960 }
1961
1962 fn finish(&mut self, frame: Frame) -> Placed {
1963 let Frame {
1964 scope,
1965 prefix,
1966 own,
1967 mut touched,
1968 mut placed,
1969 inner,
1970 mut inner_objects,
1971 ..
1972 } = frame;
1973 let prefix = prefix.as_ref();
1974 let name_key = |name: &str| name_key(prefix, name);
1975 let full_id = |id: Option<Id>| full_id(prefix, id);
1976 let mut objects_out: Vec<Out<Object>> = Vec::new();
1977 let all_inner_objects: Vec<Out<Object>> =
1978 inner_objects.values().flatten().cloned().collect();
1979 let resolve = |this: &mut Self,
1980 item_file: &Path,
1981 at: &KeyPath,
1982 kind: Kind,
1983 reference: &str,
1984 namespaced: &dyn Fn(&str) -> Option<String>|
1985 -> Option<String> {
1986 match scope.lookup(kind, reference) {
1987 Lookup::One(index) => {
1988 let names = scope.names(kind);
1989 Some(name_key(names[index].0))
1990 }
1991 Lookup::Many(all) => {
1992 if own {
1993 let names = scope.names(kind);
1994 let mut finding = Finding::new(
1995 item_file,
1996 at.clone(),
1997 Part::Value,
1998 code::AMBIGUOUS_REFERENCE,
1999 format!(
2000 "{reference} names {} {}s; use an id",
2001 all.len(),
2002 kind.word()
2003 ),
2004 );
2005 for index in all {
2006 finding = finding.related(
2007 names[index].2,
2008 names[index].3.clone(),
2009 Part::Value,
2010 );
2011 }
2012 this.push(finding);
2013 }
2014 None
2015 }
2016 Lookup::None => {
2017 if let Some(found) = namespaced(reference) {
2018 return Some(found);
2019 }
2020 if own {
2021 this.issue(
2022 item_file,
2023 at,
2024 code::BAD_REFERENCE,
2025 format!(
2026 "{reference} names no {} of this {}",
2027 kind.word(),
2028 if scope.prefab { "prefab" } else { "scene" }
2029 ),
2030 );
2031 }
2032 None
2033 }
2034 }
2035 };
2036 let inner_object_key = |reference: &str| {
2037 all_inner_objects
2038 .iter()
2039 .find(|out| out.entry.id.as_deref() == Some(reference))
2040 .map(|out| out.entry.key.clone())
2041 };
2042 let inner_key = |list: &[(Option<String>, String)], reference: &str| {
2043 list.iter()
2044 .find(|(id, _)| id.as_deref() == Some(reference))
2045 .map(|(_, key)| key.clone())
2046 };
2047 let inner_meshes: Vec<(Option<String>, String)> = inner
2048 .meshes
2049 .iter()
2050 .map(|out| (out.entry.id.clone(), out.entry.key.clone()))
2051 .collect();
2052 let inner_contents: Vec<(Option<String>, String)> = inner
2053 .contents
2054 .iter()
2055 .map(|out| (out.entry.id.clone(), out.entry.key.clone()))
2056 .collect();
2057 for (at, item) in scope.objects.iter().enumerate() {
2058 let mut value = item.value.clone();
2059 if let Some(mesh) = &value.mesh {
2060 value.mesh = resolve(
2061 self,
2062 &item.file,
2063 &under(&item.path, &["mesh".into()]),
2064 Kind::Mesh,
2065 mesh,
2066 &|r| inner_key(&inner_meshes, r),
2067 );
2068 }
2069 if let Some(parent) = &value.parent {
2070 value.parent = resolve(
2071 self,
2072 &item.file,
2073 &under(&item.path, &["parent".into()]),
2074 Kind::Object,
2075 parent,
2076 &inner_object_key,
2077 );
2078 } else if let Some(prefix) = prefix {
2079 value.parent = Some(prefix.key.clone());
2080 }
2081 if let Some(content) = &value.content {
2082 value.content = resolve(
2083 self,
2084 &item.file,
2085 &under(&item.path, &["content".into()]),
2086 Kind::Content,
2087 content,
2088 &|r| inner_key(&inner_contents, r),
2089 );
2090 }
2091 if let Some(prefix) = prefix {
2092 value.hidden |= prefix.hidden;
2093 }
2094 if value.is_placement() {
2095 value.set.clear();
2096 }
2097 objects_out.push(Out {
2098 entry: Entry {
2099 key: name_key(&item.name),
2100 id: full_id(item.id),
2101 file: item.file.clone(),
2102 value,
2103 },
2104 own: own.then(|| item.path.clone()),
2105 at: item.path.clone(),
2106 set: touched.remove(&(Kind::Object, at)).unwrap_or_default(),
2107 });
2108 if let Some(children) = inner_objects.remove(&at) {
2109 objects_out.extend(children);
2110 }
2111 }
2112 for (at, item) in scope.movers.iter().enumerate() {
2113 let mut value = item.value.clone();
2114 let mut objects = Vec::new();
2115 for (place, reference) in item.value.objects.iter().enumerate() {
2116 let at = under(&item.path, &["objects".into(), place.to_string()]);
2117 if let Some(found) = resolve(
2118 self,
2119 &item.file,
2120 &at,
2121 Kind::Object,
2122 reference,
2123 &inner_object_key,
2124 ) {
2125 objects.push(found);
2126 }
2127 }
2128 value.objects = objects;
2129 placed.movers.push(Out {
2130 entry: Entry {
2131 key: name_key(&item.name),
2132 id: full_id(item.id),
2133 file: item.file.clone(),
2134 value,
2135 },
2136 own: own.then(|| item.path.clone()),
2137 at: item.path.clone(),
2138 set: touched.remove(&(Kind::Mover, at)).unwrap_or_default(),
2139 });
2140 }
2141 for item in &scope.sounds {
2142 let mut value = item.value.clone();
2143 if let Some(object) = &item.value.object {
2144 value.object = resolve(
2145 self,
2146 &item.file,
2147 &under(&item.path, &["object".into()]),
2148 Kind::Object,
2149 object,
2150 &inner_object_key,
2151 );
2152 }
2153 placed.sounds.push(Out {
2154 entry: Entry {
2155 key: name_key(&item.name),
2156 id: full_id(item.id),
2157 file: item.file.clone(),
2158 value,
2159 },
2160 own: own.then(|| item.path.clone()),
2161 at: item.path.clone(),
2162 set: Vec::new(),
2163 });
2164 }
2165 for item in &scope.rigs {
2166 let mut value = item.value.clone();
2167 if let Some(target) = &item.value.target {
2168 value.target = resolve(
2169 self,
2170 &item.file,
2171 &under(&item.path, &["target".into()]),
2172 Kind::Object,
2173 target,
2174 &inner_object_key,
2175 );
2176 }
2177 placed.rigs.push(Out {
2178 entry: Entry {
2179 key: name_key(&item.name),
2180 id: full_id(item.id),
2181 file: item.file.clone(),
2182 value,
2183 },
2184 own: own.then(|| item.path.clone()),
2185 at: item.path.clone(),
2186 set: Vec::new(),
2187 });
2188 }
2189 macro_rules! plain {
2190 ($from:ident) => {
2191 for item in &scope.$from {
2192 placed.$from.push(Out {
2193 entry: Entry {
2194 key: name_key(&item.name),
2195 id: full_id(item.id),
2196 file: item.file.clone(),
2197 value: item.value.clone(),
2198 },
2199 own: own.then(|| item.path.clone()),
2200 at: item.path.clone(),
2201 set: Vec::new(),
2202 });
2203 }
2204 };
2205 }
2206 plain!(meshes);
2207 plain!(lights);
2208 plain!(emitters);
2209 plain!(contents);
2210 plain!(texts);
2211 placed.objects = objects_out;
2212 placed.meshes.extend(inner.meshes);
2213 placed.lights.extend(inner.lights);
2214 placed.emitters.extend(inner.emitters);
2215 placed.movers.extend(inner.movers);
2216 placed.contents.extend(inner.contents);
2217 placed.texts.extend(inner.texts);
2218 placed.sounds.extend(inner.sounds);
2219 placed.rigs.extend(inner.rigs);
2220 placed
2221 }
2222
2223 fn context(&mut self, file: &Path, kind: FileKind) -> Option<Scene> {
2224 if kind == FileKind::Prefab {
2225 self.prefabs_checked.insert(file.to_path_buf());
2226 }
2227 let scene = self.context_in(file, kind, Vec::new());
2228 while let Some((prefab, chain)) = self.unchecked.pop() {
2229 self.context_in(&prefab, FileKind::Prefab, chain);
2230 }
2231 scene
2232 }
2233
2234 fn context_in(&mut self, file: &Path, kind: FileKind, mut chain: Vec<Step>) -> Option<Scene> {
2235 self.loaded(file, kind)?;
2236 let scope = self.scope(file, kind);
2237 chain.push(Step::new(file));
2238 let placed = self.place(&scope, &mut chain);
2239 let mut materials: Vec<Item<Material>> = Vec::new();
2240 let mut library_files: Vec<PathBuf> = Vec::new();
2241 for (library, _, _) in &placed.libraries {
2242 if library_files.contains(library) {
2243 continue;
2244 }
2245 library_files.push(library.clone());
2246 let Some(doc) = self.load(library, FileKind::Materials) else {
2247 continue;
2248 };
2249 for (name, material) in doc.library {
2250 let path = key(&["materials", &name]);
2251 if let Some(first) = materials.iter().find(|item| item.name == name) {
2252 if first.file != *library {
2253 let finding = Finding::new(
2254 library,
2255 path.clone(),
2256 Part::Key,
2257 code::DUPLICATE_NAME,
2258 format!("material {name} is also in {}", first.file.display()),
2259 )
2260 .related(
2261 &first.file,
2262 first.path.clone(),
2263 Part::Key,
2264 );
2265 self.push(finding);
2266 }
2267 continue;
2268 }
2269 materials.push(Item {
2270 file: library.clone(),
2271 path,
2272 id: material.id,
2273 name,
2274 value: material,
2275 });
2276 }
2277 }
2278 let mut scene = Scene {
2279 path: file.to_path_buf(),
2280 ..Scene::default()
2281 };
2282 let material = |this: &mut Self,
2283 own: &Option<(PathBuf, KeyPath)>,
2284 reference: &str|
2285 -> Option<String> {
2286 match lookup(&materials, reference) {
2287 Lookup::One(at) => Some(materials[at].name.clone()),
2288 _ => {
2289 if let Some((file, path)) = own {
2290 this.issue(
2291 file,
2292 path,
2293 code::BAD_REFERENCE,
2294 format!("{reference} names no material of the scene's libraries"),
2295 );
2296 }
2297 None
2298 }
2299 }
2300 };
2301 if let Some(fallback) = &scope.fallback {
2302 let own = Some((fallback.file.clone(), fallback.path.clone()));
2303 scene.fallback = material(self, &own, &fallback.value);
2304 }
2305 for out in &placed.meshes {
2306 let mut entry = out.entry.clone();
2307 for (node, value) in entry.value.nodes.iter_mut() {
2308 if let Some(reference) = value.material.clone() {
2309 let own = out.own.as_ref().map(|path| {
2310 (
2311 entry.file.clone(),
2312 under(path, &["nodes".into(), node.clone(), "material".into()]),
2313 )
2314 });
2315 value.material = material(self, &own, &reference);
2316 }
2317 }
2318 scene.meshes.push(entry);
2319 }
2320 for out in &placed.objects {
2321 let mut entry = out.entry.clone();
2322 if let Some(reference) = entry.value.material.clone() {
2323 let own = out
2324 .own
2325 .as_ref()
2326 .map(|path| (entry.file.clone(), under(path, &["material".into()])));
2327 entry.value.material = material(self, &own, &reference);
2328 }
2329 let mut resolved = BTreeMap::new();
2330 for (node, reference) in &entry.value.materials {
2331 let own = out.own.as_ref().map(|path| {
2332 (
2333 entry.file.clone(),
2334 under(path, &["materials".into(), node.clone()]),
2335 )
2336 });
2337 if let Some(name) = material(self, &own, reference) {
2338 resolved.insert(node.clone(), name);
2339 }
2340 }
2341 entry.value.materials = resolved;
2342 scene.objects.push(entry);
2343 }
2344 self.relations(&placed);
2345 scene.materials = materials
2346 .into_iter()
2347 .map(|item| Entry {
2348 key: item.name,
2349 id: item.id.map(|id| id.to_string()),
2350 file: item.file,
2351 value: item.value,
2352 })
2353 .collect();
2354 scene.lights = placed.lights.into_iter().map(|out| out.entry).collect();
2355 scene.emitters = placed.emitters.into_iter().map(|out| out.entry).collect();
2356 scene.movers = placed.movers.into_iter().map(|out| out.entry).collect();
2357 scene.contents = placed.contents.into_iter().map(|out| out.entry).collect();
2358 scene.texts = placed.texts.into_iter().map(|out| out.entry).collect();
2359 scene.sounds = placed.sounds.into_iter().map(|out| out.entry).collect();
2360 scene.rigs = placed.rigs.into_iter().map(|out| out.entry).collect();
2361 scene.layers = self
2362 .layers()
2363 .map(|layers| (*layers).clone())
2364 .unwrap_or_default();
2365 let dynamic = dynamics(&scene.objects, &scene.movers);
2366 for (entry, dynamic) in scene.objects.iter_mut().zip(dynamic) {
2367 entry.value.dynamic = Some(dynamic);
2368 }
2369 scene.sun = scope.sun.as_ref().map(|single| single.value.clone());
2370 scene.sky = scope.sky.as_ref().map(|single| single.value.clone());
2371 scene.haze = scope.haze.as_ref().map(|single| single.value.clone());
2372 scene.camera = scope.camera.as_ref().map(|single| single.value.clone());
2373 scene.trace = scope.trace.as_ref().map(|single| single.value);
2374 scene.plates = scope.plates.as_ref().map(|single| single.value.clone());
2375 scene.physics = scope.physics.as_ref().map(|single| single.value.clone());
2376 scene.files = placed.files.clone();
2377 scene.tiles = self.tile_layers(&scope, &chain, &mut scene.files);
2378 if let Some(finish) = &scope.finish {
2379 scene.finish = Some(finish.value.clone());
2380 if let Some(raw) = &finish.value.file
2381 && let Ok(rooted) = rooted(raw)
2382 {
2383 let path = PathBuf::from(rooted);
2384 scene.finish_file = self
2385 .load(&path, FileKind::Finish)
2386 .and_then(|doc| doc.finish_keys);
2387 scene.files.push(path);
2388 }
2389 }
2390 if let Some(plates) = &scope.plates {
2391 self.plate_proxies(&plates.value, &placed.objects, &mut scene);
2392 }
2393 scene.files.extend(library_files);
2394 Some(scene)
2395 }
2396
2397 fn tile_layers(
2398 &mut self,
2399 scope: &Scope,
2400 chain: &[Step],
2401 files: &mut Vec<PathBuf>,
2402 ) -> Vec<Entry<Tiles>> {
2403 let mut layers = Vec::new();
2404 for item in &scope.tiles {
2405 let mut value = item.value.clone();
2406 let mut palette = BTreeMap::new();
2407 for (tile, raw) in &item.value.palette {
2408 let at = under(&item.path, &["palette".into(), tile.clone()]);
2409 let path = if Id::parse(raw).is_ok() {
2410 match lookup(&scope.objects, raw) {
2411 Lookup::One(found) if scope.objects[found].value.is_placement() => {
2412 self.prefab_path(scope, raw)
2413 }
2414 _ => {
2415 self.issue(
2416 &item.file,
2417 &at,
2418 code::BAD_REFERENCE,
2419 format!(
2420 "tile layer {}: tile {tile} names {raw}, which is no placement of this scene; a palette names a prefab's path or a placement's id",
2421 item.name
2422 ),
2423 );
2424 None
2425 }
2426 }
2427 } else {
2428 rooted(raw).ok().map(PathBuf::from)
2429 };
2430 let Some(path) = path else { continue };
2431 if FileKind::of(&path) == Some(FileKind::Prefab) && self.text(&path).is_some() {
2432 if self.prefabs_checked.insert(path.clone()) {
2433 let mut steps = chain.to_vec();
2434 if let Some(step) = steps.last_mut() {
2435 step.link = Some((item.file.clone(), at.clone()));
2436 }
2437 self.unchecked.push((path.clone(), steps));
2438 }
2439 if !files.contains(&path) {
2440 files.push(path.clone());
2441 }
2442 }
2443 palette.insert(tile.clone(), slashed(&path));
2444 }
2445 value.palette = palette;
2446 layers.push(Entry {
2447 key: item.name.clone(),
2448 id: item.id.map(|id| id.to_string()),
2449 file: item.file.clone(),
2450 value,
2451 });
2452 }
2453 layers
2454 }
2455
2456 fn plate_proxies(&mut self, plates: &Plates, placed: &[Out<Object>], scene: &mut Scene) {
2457 let Some(raw) = &plates.proxies else { return };
2458 let Ok(rooted) = rooted(raw) else { return };
2459 if !rooted.ends_with(".proxies.toml") {
2460 return;
2461 }
2462 let file = PathBuf::from(rooted);
2463 let Some(doc) = self.load(&file, FileKind::Proxies) else {
2464 return;
2465 };
2466 self.bound.insert(file.clone());
2467 scene.files.push(file.clone());
2468 let shown = scene.path.display().to_string();
2469 for (place, proxy) in &doc.proxy {
2470 let at = key(&["proxy", &place.to_string(), "object"]);
2471 let reference = proxy.object.as_str();
2472 let by_id: Vec<usize> = (0..scene.objects.len())
2473 .filter(|at| scene.objects[*at].id.as_deref() == Some(reference))
2474 .collect();
2475 let found = if by_id.is_empty() {
2476 (0..scene.objects.len())
2477 .filter(|at| scene.objects[*at].key == reference)
2478 .collect()
2479 } else {
2480 by_id
2481 };
2482 let related = |finding: Finding, index: usize| {
2483 finding.related(
2484 &placed[index].entry.file,
2485 placed[index].at.clone(),
2486 Part::Value,
2487 )
2488 };
2489 match found.as_slice() {
2490 [] => self.issue(
2491 &file,
2492 &at,
2493 code::BAD_REFERENCE,
2494 format!("{reference} names no object of scene {shown}"),
2495 ),
2496 [one] => {
2497 let object = &scene.objects[*one];
2498 let refused = if object.value.is_placement() {
2499 Some(format!(
2500 "{reference} names placement {}, which has no mesh; a proxy stands in for an object with a mesh",
2501 object.key
2502 ))
2503 } else if object.value.dynamic == Some(true) {
2504 Some(format!(
2505 "{reference} names object {}, which is dynamic in scene {shown} ({}); a proxy stands in for a static object",
2506 object.key,
2507 why_dynamic(placed, &scene.movers, *one)
2508 ))
2509 } else {
2510 None
2511 };
2512 match refused {
2513 Some(message) => {
2514 let finding =
2515 Finding::new(&file, at, Part::Value, code::BAD_REFERENCE, message);
2516 self.push(related(finding, *one));
2517 }
2518 None => scene.proxies.push(Entry {
2519 key: place.to_string(),
2520 id: proxy.id.map(|id| id.to_string()),
2521 file: file.clone(),
2522 value: Proxy {
2523 object: object.key.clone(),
2524 ..proxy.clone()
2525 },
2526 }),
2527 }
2528 }
2529 many => {
2530 let mut finding = Finding::new(
2531 &file,
2532 at,
2533 Part::Value,
2534 code::AMBIGUOUS_REFERENCE,
2535 format!(
2536 "{reference} names {} objects of scene {shown}; use an id",
2537 many.len()
2538 ),
2539 );
2540 for index in many {
2541 finding = related(finding, *index);
2542 }
2543 self.push(finding);
2544 }
2545 }
2546 }
2547 }
2548
2549 fn relations(&mut self, placed: &Placed) {
2550 let objects = &placed.objects;
2551 let index: BTreeMap<&str, usize> = objects
2552 .iter()
2553 .enumerate()
2554 .map(|(at, out)| (out.entry.key.as_str(), at))
2555 .collect();
2556 for (start, out) in objects.iter().enumerate() {
2557 let Some(path) = &out.own else { continue };
2558 let mut at = start;
2559 let mut steps = 0;
2560 while let Some(parent) = objects[at]
2561 .entry
2562 .value
2563 .parent
2564 .as_deref()
2565 .and_then(|p| index.get(p))
2566 {
2567 steps += 1;
2568 if *parent == start || steps > objects.len() {
2569 self.issue(
2570 &out.entry.file,
2571 &under(path, &["parent".into()]),
2572 code::PARENT_CYCLE,
2573 format!("object {}: its parents form a cycle", out.entry.key),
2574 );
2575 break;
2576 }
2577 at = *parent;
2578 }
2579 }
2580 let mut owner: BTreeMap<&str, &str> = BTreeMap::new();
2581 for mover in &placed.movers {
2582 for object in &mover.entry.value.objects {
2583 if let Some(other) = owner.insert(object, &mover.entry.key)
2584 && let Some(path) = &mover.own
2585 {
2586 self.issue(
2587 &mover.entry.file,
2588 &under(path, &["objects".into()]),
2589 code::BAD_VALUE,
2590 format!(
2591 "object {object} is moved by movers {other} and {}; one at most",
2592 mover.entry.key
2593 ),
2594 );
2595 }
2596 }
2597 }
2598 for (start, out) in objects.iter().enumerate() {
2599 let Some(path) = &out.own else { continue };
2600 let mut planes = out.entry.value.clip.clone();
2601 let mut seen = Vec::new();
2602 let mut at = Some(start);
2603 while let Some(current) = at {
2604 if seen.contains(¤t) {
2605 break;
2606 }
2607 seen.push(current);
2608 let object = &objects[current].entry;
2609 if let Some(mover) = owner
2610 .get(object.key.as_str())
2611 .and_then(|name| placed.movers.iter().find(|m| m.entry.key == *name))
2612 {
2613 for plane in &mover.entry.value.clip {
2614 if !planes.contains(plane) {
2615 planes.push(*plane);
2616 }
2617 }
2618 }
2619 at = object
2620 .value
2621 .parent
2622 .as_deref()
2623 .and_then(|parent| index.get(parent).copied());
2624 }
2625 if planes.len() > 2 {
2626 self.issue(
2627 &out.entry.file,
2628 path,
2629 code::BAD_VALUE,
2630 format!(
2631 "object {} has {} clip planes with its movers'; at most two",
2632 out.entry.key,
2633 planes.len()
2634 ),
2635 );
2636 }
2637 }
2638 self.bodies(placed, &index, &owner);
2639 let mut looping: Option<&str> = None;
2640 for sound in &placed.sounds {
2641 let Some(path) = &sound.own else { continue };
2642 if let Some(object) = &sound.entry.value.object
2643 && let Some(at) = index.get(object.as_str())
2644 && objects[*at].entry.value.body.is_none()
2645 {
2646 self.issue(
2647 &sound.entry.file,
2648 &under(path, &["object".into()]),
2649 code::BAD_REFERENCE,
2650 format!(
2651 "sound {} names object {object}, which has no body",
2652 sound.entry.key
2653 ),
2654 );
2655 }
2656 if sound.entry.value.looping {
2657 if let Some(other) = looping {
2658 self.issue(
2659 &sound.entry.file,
2660 &under(path, &["loop".into()]),
2661 code::BAD_VALUE,
2662 format!(
2663 "sounds {other} and {} both loop; one looping sound at most",
2664 sound.entry.key
2665 ),
2666 );
2667 } else {
2668 looping = Some(&sound.entry.key);
2669 }
2670 }
2671 }
2672 }
2673
2674 fn bodies(
2675 &mut self,
2676 placed: &Placed,
2677 index: &BTreeMap<&str, usize>,
2678 owner: &BTreeMap<&str, &str>,
2679 ) {
2680 let objects = &placed.objects;
2681 let entry = |out: &Out<Object>| (out.entry.file.clone(), out.at.clone());
2682 let keyed =
2683 |out: &Out<Object>, key: &str| (out.entry.file.clone(), under(&out.at, &[key.into()]));
2684 let carried = objects.iter().filter_map(|out| {
2685 if out.entry.value.body.is_some() {
2686 Some((out, "body"))
2687 } else if out.entry.value.character.is_some() {
2688 Some((out, "character"))
2689 } else {
2690 None
2691 }
2692 });
2693 for (body, word) in carried {
2694 let name = &body.entry.key;
2695 let mut current = body;
2696 let mut steps = 0;
2697 loop {
2698 let mut found = Vec::new();
2699 if let Some(mover) = owner
2700 .get(current.entry.key.as_str())
2701 .and_then(|key| placed.movers.iter().find(|mover| mover.entry.key == *key))
2702 {
2703 let held = ¤t.entry.key;
2704 let message = if std::ptr::eq(current, body) {
2705 format!(
2706 "object {name} is moved by mover {}; a {word} or a mover, not both",
2707 mover.entry.key
2708 )
2709 } else {
2710 format!(
2711 "object {name} has a {word} under {} {held}, which mover {} moves; a {word} or a mover, not both",
2712 if current.entry.value.is_placement() {
2713 "placement"
2714 } else {
2715 "object"
2716 },
2717 mover.entry.key
2718 )
2719 };
2720 let mut places = vec![keyed(body, word)];
2721 if !std::ptr::eq(current, body) {
2722 places.push(entry(current));
2723 }
2724 places.push((
2725 mover.entry.file.clone(),
2726 under(&mover.at, &["objects".into()]),
2727 ));
2728 found.push((
2729 body.blame(word).or_else(|| mover.blame("objects")),
2730 places,
2731 message,
2732 ));
2733 }
2734 let parent = current
2735 .entry
2736 .value
2737 .parent
2738 .as_deref()
2739 .and_then(|parent| index.get(parent))
2740 .map(|at| &objects[*at]);
2741 if let Some(parent) = parent
2742 && !parent.entry.value.is_placement()
2743 {
2744 let mut places = vec![keyed(body, word)];
2745 if !std::ptr::eq(current, body) {
2746 places.push(keyed(current, "parent"));
2747 }
2748 places.push(entry(parent));
2749 found.push((
2750 body.blame(word).or_else(|| current.blame("parent")),
2751 places,
2752 format!(
2753 "object {name} has a {word} under object {}, which places no prefab; a {word} sits on a root object or under placements",
2754 parent.entry.key
2755 ),
2756 ));
2757 }
2758 if let Some((Some((file, path, overridden)), places, message)) =
2759 found.into_iter().find(|(blame, _, _)| blame.is_some())
2760 {
2761 let (code, part) = if overridden {
2762 (code::BAD_OVERRIDE, Part::Key)
2763 } else {
2764 (code::BAD_VALUE, Part::Value)
2765 };
2766 let mut finding = Finding::new(&file, path.clone(), part, code, message);
2767 for (other, at) in places {
2768 if other != file || at != path {
2769 finding = finding.related(&other, at, Part::Value);
2770 }
2771 }
2772 self.push(finding);
2773 break;
2774 }
2775 steps += 1;
2776 match parent {
2777 Some(parent) if steps <= objects.len() => current = parent,
2778 _ => break,
2779 }
2780 }
2781 }
2782 }
2783
2784 fn unique_ids(&mut self) {
2785 let mut seen: BTreeMap<Id, (PathBuf, KeyPath)> = BTreeMap::new();
2786 let files: Vec<PathBuf> = self.parsed.keys().cloned().collect();
2787 for file in files {
2788 let Some(doc) = self.parsed.get(&file).and_then(|parsed| parsed.doc.clone()) else {
2789 continue;
2790 };
2791 let mut ids: Vec<(Id, KeyPath)> = Vec::new();
2792 let list = |name: &str, place: usize| key(&[name, &place.to_string(), "id"]);
2793 let map = |name: &str, entry: &str| key(&[name, entry, "id"]);
2794 ids.extend(
2795 doc.mesh
2796 .iter()
2797 .filter_map(|(n, v)| v.id.map(|id| (id, map("mesh", n)))),
2798 );
2799 ids.extend(
2800 doc.object
2801 .iter()
2802 .filter_map(|(p, v)| v.id.map(|id| (id, list("object", *p)))),
2803 );
2804 ids.extend(
2805 doc.light
2806 .iter()
2807 .filter_map(|(p, v)| v.id.map(|id| (id, list("light", *p)))),
2808 );
2809 ids.extend(
2810 doc.emitter
2811 .iter()
2812 .filter_map(|(p, v)| v.id.map(|id| (id, list("emitter", *p)))),
2813 );
2814 ids.extend(
2815 doc.mover
2816 .iter()
2817 .filter_map(|(p, v)| v.id.map(|id| (id, list("mover", *p)))),
2818 );
2819 ids.extend(
2820 doc.content
2821 .iter()
2822 .filter_map(|(n, v)| v.id.map(|id| (id, map("content", n)))),
2823 );
2824 ids.extend(
2825 doc.text
2826 .iter()
2827 .filter_map(|(n, v)| v.id.map(|id| (id, map("text", n)))),
2828 );
2829 ids.extend(
2830 doc.sound
2831 .iter()
2832 .filter_map(|(n, v)| v.id.map(|id| (id, map("sound", n)))),
2833 );
2834 ids.extend(
2835 doc.rig
2836 .iter()
2837 .filter_map(|(p, v)| v.id.map(|id| (id, list("rig", *p)))),
2838 );
2839 ids.extend(
2840 doc.tiles
2841 .iter()
2842 .filter_map(|(p, v)| v.id.map(|id| (id, list("tiles", *p)))),
2843 );
2844 ids.extend(
2845 doc.library
2846 .iter()
2847 .filter_map(|(n, v)| v.id.map(|id| (id, map("materials", n)))),
2848 );
2849 ids.extend(
2850 doc.proxy
2851 .iter()
2852 .filter_map(|(p, v)| v.id.map(|id| (id, list("proxy", *p)))),
2853 );
2854 for (id, path) in ids {
2855 match seen.get(&id) {
2856 Some((first, first_path)) => {
2857 let finding = Finding::new(
2858 &file,
2859 path,
2860 Part::Value,
2861 code::DUPLICATE_ID,
2862 format!(
2863 "id {id} is used twice; {} has it already",
2864 if *first == file {
2865 "this file".to_string()
2866 } else {
2867 first.display().to_string()
2868 }
2869 ),
2870 )
2871 .related(first, first_path.clone(), Part::Value);
2872 self.push(finding);
2873 }
2874 None => {
2875 seen.insert(id, (file.clone(), path));
2876 }
2877 }
2878 }
2879 }
2880 }
2881
2882 fn proxies(&mut self) {
2883 let files: Vec<PathBuf> = self
2884 .parsed
2885 .iter()
2886 .filter(|(file, parsed)| {
2887 parsed.kind == FileKind::Proxies && !self.bound.contains(*file)
2888 })
2889 .map(|(file, _)| file.clone())
2890 .collect();
2891 let mut objects: Vec<(Option<Id>, String)> = Vec::new();
2892 for parsed in self.parsed.values() {
2893 if let Some(doc) = &parsed.doc {
2894 objects.extend(
2895 doc.object
2896 .iter()
2897 .map(|(_, object)| (object.id, object.name.clone())),
2898 );
2899 }
2900 }
2901 for file in files {
2902 let Some(doc) = self.parsed.get(&file).and_then(|parsed| parsed.doc.clone()) else {
2903 continue;
2904 };
2905 for (place, proxy) in &doc.proxy {
2906 let by_id = Id::parse(&proxy.object)
2907 .ok()
2908 .is_some_and(|id| objects.iter().any(|(other, _)| *other == Some(id)));
2909 let named = objects
2910 .iter()
2911 .filter(|(_, name)| *name == proxy.object)
2912 .count();
2913 let at = key(&["proxy", &place.to_string(), "object"]);
2914 if by_id || named == 1 {
2915 continue;
2916 }
2917 if named > 1 {
2918 self.issue(
2919 &file,
2920 &at,
2921 code::AMBIGUOUS_REFERENCE,
2922 format!(
2923 "{} names {named} objects of the project; use an id",
2924 proxy.object
2925 ),
2926 );
2927 } else {
2928 self.issue(
2929 &file,
2930 &at,
2931 code::BAD_REFERENCE,
2932 format!("{} names no object of the project", proxy.object),
2933 );
2934 }
2935 }
2936 }
2937 }
2938
2939 fn all(&mut self) -> Vec<Diagnostic> {
2940 let mut files: Vec<PathBuf> = self.project.texts.keys().cloned().collect();
2941 for file in self.overlay.keys() {
2942 if !files.contains(file) && FileKind::of(file).is_some() {
2943 files.push(file.clone());
2944 }
2945 }
2946 for file in &files {
2947 if let Some(kind) = FileKind::of(file) {
2948 self.load(file, kind);
2949 }
2950 }
2951 let scenes: Vec<PathBuf> = files
2952 .iter()
2953 .filter(|file| FileKind::of(file) == Some(FileKind::Scene))
2954 .cloned()
2955 .collect();
2956 let mut included: BTreeSet<PathBuf> = BTreeSet::new();
2957 for scene in &scenes {
2958 if let Some(doc) = self.parsed.get(scene).and_then(|parsed| parsed.doc.clone()) {
2959 for (_, raw) in &doc.include {
2960 if let Ok(rooted) = rooted(raw) {
2961 included.insert(PathBuf::from(rooted));
2962 }
2963 }
2964 }
2965 }
2966 for scene in &scenes {
2967 if !included.contains(scene) {
2968 self.context(scene, FileKind::Scene);
2969 }
2970 }
2971 for scene in &scenes {
2972 let seen = self
2973 .scopes
2974 .values()
2975 .any(|scope| scope.files.contains(scene));
2976 if !seen {
2977 self.context(scene, FileKind::Scene);
2978 }
2979 }
2980 let prefabs: Vec<PathBuf> = files
2981 .iter()
2982 .filter(|file| FileKind::of(file) == Some(FileKind::Prefab))
2983 .cloned()
2984 .collect();
2985 for prefab in prefabs {
2986 if !self.prefabs_checked.contains(&prefab) {
2987 self.context(&prefab, FileKind::Prefab);
2988 }
2989 }
2990 self.proxies();
2991 self.unique_ids();
2992 self.report()
2993 }
2994
2995 fn report(&self) -> Vec<Diagnostic> {
2996 let mut diagnostics = read::report(&self.findings, &self.parsed);
2997 diagnostics.sort_by(|a, b| {
2998 (&a.file, a.line, a.column, a.code).cmp(&(&b.file, b.line, b.column, b.code))
2999 });
3000 diagnostics
3001 }
3002}