Skip to main content

pfx_scene/
project.rs

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(&current) {
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 = &current.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}