Skip to main content

unity_formats/
scene.rs

1//! A scene's hierarchy: GameObjects, their Transforms (local TRS composed to world matrices in Unity's frame), and
2//! its mesh renderers (skinned ones with their bones), LODGroups, colliders, lights and terrains, each whatever its
3//! state.
4//!
5//! State is data: each object carries its [`OffFlag`]s, the GameObjects on its Transform chain stored inactive and the
6//! component itself stored disabled; Unity draws or uses it when it has none. A LODGroup lists its levels' renderers
7//! ([`LodGroup`]); which level draws is Unity's choice by screen size. A static-batched renderer draws a slice of a
8//! combined mesh whose vertices are already in world space (its matrix is the identity). A RectTransform's local x
9//! and y come from its anchors (the stored position can be stale): the parent rect's minimum plus its size times the
10//! anchors interpolated at the pivot, plus the anchored position.
11
12use std::collections::BTreeMap;
13
14use serde::Serialize;
15
16use crate::bump;
17use crate::player::{ObjRef, Player};
18use crate::value::{Array, Value};
19use crate::{Result, invalid};
20
21/// A MonoBehaviour as stored: its script (namespace and class) and the script's serialized fields, kept as bytes
22/// (a stripped build has no type tree for them).
23#[derive(Clone, Debug, PartialEq, Serialize)]
24pub struct UnityBehaviour {
25    /// The MonoBehaviour object.
26    pub behaviour: ObjRef,
27    /// The script's class, `namespace.class`; empty when the script cannot be resolved.
28    pub script: String,
29    /// `m_Enabled`.
30    pub enabled: bool,
31    /// The script's serialized fields, as bytes.
32    #[serde(skip)]
33    pub data: Vec<u8>,
34}
35
36/// A scene's ambient lighting as RenderSettings stores it: `mode` (`m_AmbientMode`: 0 skybox, 1 trilight, a gradient
37/// of the sky, equator and ground colours, 3 flat, the sky colour alone, 4 custom), the three colours as stored,
38/// `intensity` (`m_AmbientIntensity`) and the ambient probe as stored (`m_AmbientProbe`, Unity's SphericalHarmonicsL2:
39/// nine coefficients per channel, red 0..8, green 9..17, blue 18..26; `None` when the RenderSettings store none). The
40/// probe is what the editor last generated: all zeros when it never did, and not necessarily from the colours stored
41/// beside it.
42#[derive(Clone, Debug, PartialEq, Serialize)]
43pub struct UnityAmbient {
44    /// `m_AmbientMode`.
45    pub mode: i64,
46    /// `m_AmbientSkyColor`, RGBA.
47    pub sky: [f32; 4],
48    /// `m_AmbientEquatorColor`, RGBA.
49    pub equator: [f32; 4],
50    /// `m_AmbientGroundColor`, RGBA.
51    pub ground: [f32; 4],
52    /// `m_AmbientIntensity`.
53    pub intensity: f32,
54    /// `m_AmbientProbe`: 27 coefficients, red then green then blue; `None` when none is stored.
55    pub probe: Option<[f32; 27]>,
56}
57
58/// The scene's fog as RenderSettings stores it: whether it is on (`m_Fog`), its `mode` (`m_FogMode`: 1 linear, 2
59/// exponential, 3 exponential squared), its colour, `density` (exponential modes) and the linear mode's `start` and
60/// `end`.
61#[derive(Clone, Debug, PartialEq, Serialize)]
62pub struct UnityFog {
63    /// `m_Fog`.
64    pub enabled: bool,
65    /// `m_FogMode`.
66    pub mode: i64,
67    /// `m_FogColor`, RGBA.
68    pub color: [f32; 4],
69    /// `m_FogDensity`.
70    pub density: f32,
71    /// `m_LinearFogStart`.
72    pub start: f32,
73    /// `m_LinearFogEnd`.
74    pub end: f32,
75}
76
77/// A 4x4 matrix, rows of columns (`m[row][col]`), column vectors: Unity's `Matrix4x4` layout, in f64.
78pub type Matrix = [[f64; 4]; 4];
79
80/// The identity matrix.
81pub const IDENTITY: Matrix = [
82    [1.0, 0.0, 0.0, 0.0],
83    [0.0, 1.0, 0.0, 0.0],
84    [0.0, 0.0, 1.0, 0.0],
85    [0.0, 0.0, 0.0, 1.0],
86];
87
88/// The product `a * b`.
89pub fn mul(a: &Matrix, b: &Matrix) -> Matrix {
90    let mut m = [[0.0; 4]; 4];
91    for (row, a_row) in m.iter_mut().zip(a) {
92        for (j, x) in row.iter_mut().enumerate() {
93            *x = a_row
94                .iter()
95                .zip(b)
96                .map(|(&av, b_row)| av * b_row.get(j).copied().unwrap_or(0.0))
97                .sum();
98        }
99    }
100    m
101}
102
103/// Unity's `Matrix4x4.TRS`: translation, rotation (the quaternion as stored), scale.
104pub fn trs(p: [f64; 3], q: [f64; 4], s: [f64; 3]) -> Matrix {
105    let [x, y, z, w] = q;
106    let r = [
107        [
108            1.0 - 2.0 * (y * y + z * z),
109            2.0 * (x * y - z * w),
110            2.0 * (x * z + y * w),
111        ],
112        [
113            2.0 * (x * y + z * w),
114            1.0 - 2.0 * (x * x + z * z),
115            2.0 * (y * z - x * w),
116        ],
117        [
118            2.0 * (x * z - y * w),
119            2.0 * (y * z + x * w),
120            1.0 - 2.0 * (x * x + y * y),
121        ],
122    ];
123    let mut m = IDENTITY;
124    // the rotation's three rows scaled by column; the fourth row stays (0, 0, 0, 1)
125    for ((row, r_row), &position) in m.iter_mut().zip(&r).zip(&p) {
126        for ((x, &rv), &sv) in row.iter_mut().zip(r_row).zip(&s) {
127            *x = rv * sv;
128        }
129        if let Some(last) = row.get_mut(3) {
130            *last = position;
131        }
132    }
133    m
134}
135
136fn num(v: &Value, k: &str) -> Result<f64> {
137    v.get(k)
138        .and_then(Value::as_f32)
139        .map(f64::from)
140        .ok_or_else(|| invalid(format!("no float {k}")))
141}
142
143fn vec3(v: &Value, k: &str) -> Result<[f64; 3]> {
144    let x = v.get(k).ok_or_else(|| invalid(format!("no {k}")))?;
145    Ok([num(x, "x")?, num(x, "y")?, num(x, "z")?])
146}
147
148fn vec2(v: &Value, k: &str) -> Result<[f64; 2]> {
149    let x = v.get(k).ok_or_else(|| invalid(format!("no {k}")))?;
150    Ok([num(x, "x")?, num(x, "y")?])
151}
152
153/// A flag stored as a bool or as an integer (`m_IsActive` and `m_Enabled` are `UInt8` in some layouts).
154fn flag_value(v: &Value) -> bool {
155    match v {
156        Value::Bool(b) => *b,
157        x => x.as_i64().is_some_and(|i| i != 0),
158    }
159}
160
161fn flag(v: &Value, k: &str) -> Result<bool> {
162    match v.get(k) {
163        Some(Value::Bool(b)) => Ok(*b),
164        Some(x) => x
165            .as_i64()
166            .map(|i| i != 0)
167            .ok_or_else(|| invalid(format!("{k} is not a flag"))),
168        None => Err(invalid(format!("no {k}"))),
169    }
170}
171
172fn path_id(v: &Value, k: &str) -> Result<i64> {
173    v.at(&[k, "m_PathID"])
174        .and_then(Value::as_i64)
175        .ok_or_else(|| invalid(format!("no {k}.m_PathID")))
176}
177
178fn values<'a>(v: &'a Value, k: &str) -> Result<&'a [Value]> {
179    match v.get(k) {
180        Some(Value::Array(Array::Values(items))) => Ok(items),
181        Some(Value::Array(a)) if a.is_empty() => Ok(&[]),
182        _ => Err(invalid(format!("no list {k}"))),
183    }
184}
185
186/// A GameObject as stored.
187#[derive(Clone, Debug)]
188pub struct GameObject {
189    /// `m_Name`.
190    pub name: String,
191    /// `m_IsActive`.
192    pub active: bool,
193    /// `m_Layer`.
194    pub layer: u32,
195    /// Its components' path ids, in order.
196    pub components: Vec<i64>,
197}
198
199/// A stored "off" between an object and being drawn or used: a GameObject on its Transform chain stored inactive
200/// (`m_IsActive`), or the component itself stored disabled (`m_Enabled`); path ids in the scene's file.
201#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
202pub enum OffFlag {
203    /// The GameObject (path id) is stored inactive.
204    Inactive(i64),
205    /// The component (path id) is stored disabled.
206    Disabled(i64),
207}
208
209#[derive(Clone, Debug)]
210struct Node {
211    game_object: i64,
212    father: i64,
213    local: Matrix,
214    /// The stored local position, rotation (a quaternion `[x, y, z, w]`) and scale (a RectTransform's `local`
215    /// position comes from its anchors instead).
216    trs: ([f64; 3], [f64; 4], [f64; 3]),
217}
218
219/// A Transform as a joint of a skeleton: its GameObject's name, its father (a Transform's path id) and its local
220/// position, rotation (`[x, y, z, w]`) and scale as stored, in Unity's frame.
221#[derive(Clone, Debug, PartialEq)]
222pub struct SceneJoint {
223    /// The GameObject's name.
224    pub name: String,
225    /// The parent Transform's path id; `None` for a root.
226    pub father: Option<i64>,
227    /// Local position.
228    pub translation: [f64; 3],
229    /// Local rotation, a quaternion `[x, y, z, w]`.
230    pub rotation: [f64; 4],
231    /// Local scale.
232    pub scale: [f64; 3],
233}
234
235/// An Animator component: the GameObject it animates (its Transform is the root of the paths its clips name), the
236/// controller that lists its clips and its avatar; a null pointer is `None`.
237#[derive(Clone, Debug, PartialEq)]
238pub struct Animator {
239    /// The Animator object.
240    pub animator: ObjRef,
241    /// The path id of the GameObject it animates.
242    pub game_object: i64,
243    /// That GameObject's path from the scene root, names joined by `/`.
244    pub path: String,
245    /// `m_Controller`.
246    pub controller: Option<ObjRef>,
247    /// `m_Avatar`.
248    pub avatar: Option<ObjRef>,
249    /// `m_Enabled` (true when the Animator does not store one).
250    pub enabled: bool,
251    /// `m_ApplyRootMotion`.
252    pub apply_root_motion: bool,
253}
254
255/// A character: an Animator with skinned renderers on its GameObject or below it.
256#[derive(Clone, Debug, PartialEq)]
257pub struct Character {
258    /// The Animator.
259    pub animator: Animator,
260    /// The SkinnedMeshRenderers on the Animator's GameObject and its descendants.
261    pub skinned_renderers: usize,
262}
263
264/// A skinned renderer's bones: the Transforms (path ids in the renderer's file) its mesh's bone slots name, in slot
265/// order, and its root bone; a null entry stays `None`.
266#[derive(Clone, Debug, PartialEq)]
267pub struct RendererSkin {
268    /// `m_Bones`: the bone Transforms (path ids) in slot order.
269    pub bones: Vec<Option<i64>>,
270    /// `m_RootBone`.
271    pub root_bone: Option<i64>,
272    /// `m_BlendShapeWeights`: the weight of each of the mesh's blend shape channels (0 to 100 for a full shape).
273    pub blend_weights: Vec<f32>,
274}
275
276/// A mesh renderer with a mesh, whatever its state.
277#[derive(Clone, Debug, PartialEq)]
278pub struct Renderer {
279    /// The renderer object.
280    pub renderer: ObjRef,
281    /// Its GameObject, in the scene file.
282    pub game_object: i64,
283    /// The GameObject path, names from the root joined by `/`.
284    pub path: String,
285    /// Its GameObject's layer.
286    pub layer: u32,
287    /// The mesh (a SkinnedMeshRenderer's own, else its MeshFilter's).
288    pub mesh: ObjRef,
289    /// `m_Materials`, one entry per submesh slot; `None` for a null pointer.
290    pub materials: Vec<Option<ObjRef>>,
291    /// World matrix in Unity's frame (the identity for a static-batched renderer).
292    pub world: Matrix,
293    /// A static-batched renderer's slice of its combined mesh: first submesh, count.
294    pub submeshes: Option<(u32, u32)>,
295    /// `m_CastShadows`: 0 off, 1 on, 2 two-sided, 3 shadows only (1 when absent, Unity's default).
296    pub cast_shadows: i64,
297    /// `m_ReceiveShadows`, when stored.
298    pub receive_shadows: Option<bool>,
299    /// What keeps it from being drawn; drawn when empty.
300    pub off: Vec<OffFlag>,
301    /// A SkinnedMeshRenderer's bones; `None` for a MeshRenderer.
302    pub skin: Option<RendererSkin>,
303}
304
305/// A LODGroup: its levels' renderers (path ids in the scene's file, a level's in stored order, null entries left out),
306/// finest first; whether the component is enabled (a disabled LODGroup switches no level off) and its own off flags.
307#[derive(Clone, Debug, PartialEq)]
308pub struct LodGroup {
309    /// The LODGroup object.
310    pub group: ObjRef,
311    /// Its GameObject's path from the scene root, names joined by `/`.
312    pub path: String,
313    /// `m_Enabled`.
314    pub enabled: bool,
315    /// What keeps the group from switching levels.
316    pub off: Vec<OffFlag>,
317    /// Per level, the path ids of its renderers.
318    pub levels: Vec<Vec<i64>>,
319}
320
321/// The deepest chain of parents a scene may have. A Unity hierarchy is a few dozen deep.
322const MAX_DEPTH: usize = 1024;
323
324/// What the scene reading skipped, by reason.
325pub type Skipped = std::collections::BTreeMap<&'static str, u64>;
326
327/// A scene file's hierarchy: its GameObjects and Transforms, and from them the paths and world matrices of what the
328/// scene holds.
329#[derive(Debug)]
330pub struct Scene {
331    /// The scene's file (`level1`).
332    pub file: String,
333    /// The scene's GameObjects by path id.
334    pub game_objects: BTreeMap<i64, GameObject>,
335    nodes: BTreeMap<i64, Node>,
336    /// GameObject -> its Transform.
337    transform_of: BTreeMap<i64, i64>,
338    world: std::sync::Mutex<BTreeMap<i64, Matrix>>,
339}
340
341/// The chains of parents are at most [`MAX_DEPTH`] long (a loop is a chain that does not end): a path, a world matrix
342/// and the off flags are walked up the chain for each node, so a chain of d transforms costs d^2 of them and d paths
343/// of d names.
344/// A GameObject's `m_Layer`: 0 when it states none; a stored value outside 32 bits unsigned is refused.
345fn layer_of(g: &Value) -> Result<u32> {
346    let layer = g.get("m_Layer").and_then(Value::as_i64).unwrap_or(0);
347    u32::try_from(layer).map_err(|_| invalid(format!("m_Layer {layer} is not a 32-bit unsigned number")))
348}
349
350/// A component's `m_CullingMask.m_Bits`: every layer when it states none. The 32 bits may be stored unsigned or as a
351/// signed `int` (-1 is every layer); a number that fits neither is refused, not wrapped.
352fn culling_mask(c: &Value, path_id: i64) -> Result<u32> {
353    match c.at(&["m_CullingMask", "m_Bits"]).and_then(Value::as_i64) {
354        None => Ok(u32::MAX),
355        Some(bits) => u32::try_from(bits)
356            .or_else(|_| i32::try_from(bits).map(|signed| u32::from_ne_bytes(signed.to_ne_bytes())))
357            .map_err(|_| {
358                invalid(format!(
359                    "object {path_id}: m_CullingMask {bits} does not fit 32 bits"
360                ))
361            }),
362    }
363}
364
365fn check_depth(nodes: &BTreeMap<i64, Node>) -> Result<()> {
366    let mut depths: BTreeMap<i64, usize> = BTreeMap::new();
367    for &start in nodes.keys() {
368        let mut chain = Vec::new();
369        let mut t = start;
370        let base = loop {
371            if let Some(&d) = depths.get(&t) {
372                break d;
373            }
374            let Some(n) = nodes.get(&t) else { break 0 };
375            chain.push(t);
376            if chain.len() > MAX_DEPTH {
377                return Err(invalid(format!(
378                    "transform {start}: its parents are deeper than {MAX_DEPTH} (or loop)"
379                )));
380            }
381            if n.father == 0 {
382                break 0;
383            }
384            t = n.father;
385        };
386        if base.saturating_add(chain.len()) > MAX_DEPTH {
387            return Err(invalid(format!(
388                "transform {start}: its parents are deeper than {MAX_DEPTH}"
389            )));
390        }
391        for (k, &t) in chain.iter().rev().enumerate() {
392            depths.insert(t, base.saturating_add(k).saturating_add(1));
393        }
394    }
395    Ok(())
396}
397
398impl Scene {
399    /// The hierarchy of the scene file `file` (`level1`).
400    pub fn read(player: &Player, file: &str) -> Result<Scene> {
401        let f = player.file(file)?;
402        let r = |file: &str, pid: i64| ObjRef {
403            file: file.to_owned(),
404            path_id: pid,
405        };
406        let name_of =
407            |o: &crate::serialized::ObjectInfo| player.class_name(&r(file, o.path_id)).unwrap_or("");
408        let mut nodes = BTreeMap::new();
409        let mut rects: BTreeMap<i64, Value> = BTreeMap::new();
410        let mut game_objects = BTreeMap::new();
411        for o in &f.objects {
412            match name_of(o) {
413                "Transform" | "RectTransform" => {
414                    let t = player.decode(&r(file, o.path_id))?;
415                    let p = vec3(&t, "m_LocalPosition")?;
416                    let q = t
417                        .get("m_LocalRotation")
418                        .ok_or_else(|| invalid("no m_LocalRotation"))?;
419                    let q = [num(q, "x")?, num(q, "y")?, num(q, "z")?, num(q, "w")?];
420                    let s = vec3(&t, "m_LocalScale")?;
421                    nodes.insert(
422                        o.path_id,
423                        Node {
424                            game_object: path_id(&t, "m_GameObject")?,
425                            father: path_id(&t, "m_Father")?,
426                            local: trs(p, q, s),
427                            trs: (p, q, s),
428                        },
429                    );
430                    if name_of(o) == "RectTransform" {
431                        rects.insert(o.path_id, t);
432                    }
433                }
434                "GameObject" => {
435                    let g = player.decode(&r(file, o.path_id))?;
436                    let components = values(&g, "m_Component")?
437                        .iter()
438                        .map(|c| path_id(c, "component"))
439                        .collect::<Result<Vec<_>>>()?;
440                    game_objects.insert(
441                        o.path_id,
442                        GameObject {
443                            name: g.get("m_Name").and_then(Value::as_str).unwrap_or("").to_owned(),
444                            active: flag(&g, "m_IsActive")?,
445                            layer: layer_of(&g)?,
446                            components,
447                        },
448                    );
449                }
450                _ => {}
451            }
452        }
453        check_depth(&nodes)?;
454        // RectTransforms: local x, y from the anchors
455        let mut sizes: BTreeMap<i64, ([f64; 2], [f64; 2])> = BTreeMap::new();
456        fn rect_size(
457            tp: i64,
458            rects: &BTreeMap<i64, Value>,
459            nodes: &BTreeMap<i64, Node>,
460            sizes: &mut BTreeMap<i64, ([f64; 2], [f64; 2])>,
461            depth: u32,
462        ) -> Result<([f64; 2], [f64; 2])> {
463            if let Some(s) = sizes.get(&tp) {
464                return Ok(*s);
465            }
466            let Some(t) = rects.get(&tp) else {
467                return Ok(([0.0; 2], [0.0; 2]));
468            };
469            if depth > 4096 {
470                return Err(invalid("RectTransform parents loop"));
471            }
472            let (sd, amin, amax, piv) = (
473                vec2(t, "m_SizeDelta")?,
474                vec2(t, "m_AnchorMin")?,
475                vec2(t, "m_AnchorMax")?,
476                vec2(t, "m_Pivot")?,
477            );
478            let fa = nodes.get(&tp).map_or(0, |n| n.father);
479            let (ps, _) = if fa != 0 {
480                rect_size(fa, rects, nodes, sizes, depth.saturating_add(1))?
481            } else {
482                ([0.0; 2], [0.0; 2])
483            };
484            let s = (
485                [
486                    ps[0] * (amax[0] - amin[0]) + sd[0],
487                    ps[1] * (amax[1] - amin[1]) + sd[1],
488                ],
489                piv,
490            );
491            sizes.insert(tp, s);
492            Ok(s)
493        }
494        let rect_ids: Vec<i64> = rects.keys().copied().collect();
495        for tp in rect_ids {
496            let missing = || invalid(format!("RectTransform {tp} is not a transform of the scene"));
497            let t = rects.get(&tp).ok_or_else(missing)?;
498            let fa = nodes.get(&tp).ok_or_else(missing)?.father;
499            let (ps, ppiv) = if fa != 0 {
500                rect_size(fa, &rects, &nodes, &mut sizes, 0)?
501            } else {
502                ([0.0; 2], [0.0; 2])
503            };
504            let (amin, amax, piv, ap) = (
505                vec2(t, "m_AnchorMin")?,
506                vec2(t, "m_AnchorMax")?,
507                vec2(t, "m_Pivot")?,
508                vec2(t, "m_AnchoredPosition")?,
509            );
510            let n = nodes.get_mut(&tp).ok_or_else(missing)?;
511            let anchored = ppiv
512                .iter()
513                .zip(&ps)
514                .zip(&amin)
515                .zip(&amax)
516                .zip(&piv)
517                .zip(&ap)
518                .map(|(((((&pp, &p), &lo), &hi), &pv), &a)| -pp * p + p * (lo + (hi - lo) * pv) + a);
519            for (row, value) in n.local.iter_mut().zip(anchored) {
520                if let Some(x) = row.get_mut(3) {
521                    *x = value;
522                }
523            }
524        }
525        let transform_of = nodes.iter().map(|(&tp, n)| (n.game_object, tp)).collect();
526        Ok(Scene {
527            file: file.to_owned(),
528            game_objects,
529            nodes,
530            transform_of,
531            world: std::sync::Mutex::new(BTreeMap::new()),
532        })
533    }
534
535    /// The MonoBehaviours of GameObject `go`, in its component order: the script (`namespace.class`, empty when it
536    /// can't be resolved), `m_Enabled`, and the script's serialized fields as bytes (a stripped build has no type tree
537    /// for them).
538    pub fn behaviours_of(&self, player: &Player, go: i64) -> Result<Vec<UnityBehaviour>> {
539        let mut out = Vec::new();
540        for &c in self.game_objects.get(&go).map_or(&[][..], |g| &g.components) {
541            let r = ObjRef {
542                file: self.file.clone(),
543                path_id: c,
544            };
545            if player.class_name(&r) != Some("MonoBehaviour") {
546                continue;
547            }
548            let (base, data) = player.behaviour(&r)?;
549            let script = player.script_class_of(&r.file, &base)?.unwrap_or_default();
550            out.push(UnityBehaviour {
551                behaviour: r.clone(),
552                script,
553                enabled: base
554                    .get("m_Enabled")
555                    .and_then(Value::as_i64)
556                    .is_some_and(|x| x != 0)
557                    || base.get("m_Enabled").and_then(Value::as_bool) == Some(true),
558                data,
559            });
560        }
561        Ok(out)
562    }
563
564    /// Every MonoBehaviour of the scene, by GameObject (in path id order), as [`Scene::behaviours_of`] gives them.
565    pub fn behaviours(&self, player: &Player) -> Result<Vec<(i64, UnityBehaviour)>> {
566        let mut out = Vec::new();
567        for &go in self.game_objects.keys() {
568            out.extend(self.behaviours_of(player, go)?.into_iter().map(|b| (go, b)));
569        }
570        Ok(out)
571    }
572
573    /// The world matrices computed so far. A thread that panicked holding the lock can leave nothing half-written (an
574    /// entry is inserted whole), so a poisoned lock is still a valid cache.
575    fn world_cache(&self) -> std::sync::MutexGuard<'_, BTreeMap<i64, Matrix>> {
576        self.world
577            .lock()
578            .unwrap_or_else(std::sync::PoisonError::into_inner)
579    }
580
581    /// The Transform of the GameObject `root` that a query is limited to; `None` for a query of the whole file. A
582    /// GameObject without a Transform is an error, not an empty or an unlimited query.
583    fn root_transform(&self, root: Option<i64>) -> Result<Option<i64>> {
584        root.map(|go| {
585            self.transform_of(go)
586                .ok_or_else(|| invalid(format!("GameObject {go} has no transform")))
587        })
588        .transpose()
589    }
590
591    /// A Transform's world matrix (Unity frame).
592    pub fn world_of(&self, tp: i64) -> Result<Matrix> {
593        let mut chain = Vec::new();
594        let mut t = tp;
595        let base = loop {
596            if let Some(m) = self.world_cache().get(&t) {
597                break *m;
598            }
599            let Some(n) = self.nodes.get(&t) else {
600                break IDENTITY;
601            };
602            chain.push(t);
603            if chain.len() > self.nodes.len() {
604                return Err(invalid(format!("transform {tp}: parents loop")));
605            }
606            if n.father == 0 {
607                break IDENTITY;
608            }
609            t = n.father;
610        };
611        let mut m = base;
612        let mut cache = self.world_cache();
613        for &t in chain.iter().rev() {
614            let node = self
615                .nodes
616                .get(&t)
617                .ok_or_else(|| invalid(format!("transform {t} on the chain of {tp} is not in the scene")))?;
618            m = mul(&m, &node.local);
619            cache.insert(t, m);
620        }
621        Ok(m)
622    }
623
624    /// The off flags of a component on Transform `tp`'s GameObject: the GameObjects stored inactive from the root down,
625    /// then the component (`(path id, enabled)`) when it is stored disabled.
626    pub fn off_flags(&self, tp: i64, component: Option<(i64, bool)>) -> Vec<OffFlag> {
627        let mut out = Vec::new();
628        let mut t = tp;
629        for _ in 0..=self.nodes.len() {
630            let Some(n) = self.nodes.get(&t) else { break };
631            if !self.game_objects.get(&n.game_object).is_some_and(|g| g.active) {
632                out.push(OffFlag::Inactive(n.game_object));
633            }
634            if n.father == 0 {
635                break;
636            }
637            t = n.father;
638        }
639        out.reverse();
640        if let Some((c, false)) = component {
641            out.push(OffFlag::Disabled(c));
642        }
643        out
644    }
645
646    /// Whether a Transform's GameObject and all its ancestors' are active.
647    pub fn active(&self, tp: i64) -> bool {
648        let mut t = tp;
649        for _ in 0..=self.nodes.len() {
650            let Some(n) = self.nodes.get(&t) else { return true };
651            if !self.game_objects.get(&n.game_object).is_some_and(|g| g.active) {
652                return false;
653            }
654            if n.father == 0 {
655                return true;
656            }
657            t = n.father;
658        }
659        false
660    }
661
662    /// The GameObject path of a Transform, names from the root joined by `/`.
663    pub fn path(&self, tp: i64) -> String {
664        let mut parts = Vec::new();
665        let mut t = tp;
666        while let Some(n) = self.nodes.get(&t) {
667            parts.push(
668                self.game_objects
669                    .get(&n.game_object)
670                    .map_or("", |g| g.name.as_str()),
671            );
672            if n.father == 0 || parts.len() > self.nodes.len() {
673                break;
674            }
675            t = n.father;
676        }
677        parts.reverse();
678        parts.join("/")
679    }
680
681    /// The Transform of a GameObject (both path ids).
682    pub fn transform_of(&self, game_object: i64) -> Option<i64> {
683        self.transform_of.get(&game_object).copied()
684    }
685
686    /// A GameObject and the GameObjects above it on its Transform chain, itself first, the root last; a GameObject
687    /// without a Transform is alone.
688    pub fn ancestors(&self, game_object: i64) -> Vec<i64> {
689        let mut out = vec![game_object];
690        let Some(mut t) = self.transform_of(game_object) else {
691            return out;
692        };
693        while let Some(n) = self.nodes.get(&t) {
694            if n.father == 0 || out.len() > self.nodes.len() {
695                break;
696            }
697            t = n.father;
698            match self.nodes.get(&t) {
699                Some(p) => out.push(p.game_object),
700                None => break,
701            }
702        }
703        out
704    }
705
706    /// Transform `tp` as a joint ([`SceneJoint`]); `None` when it is no Transform of this file. A RectTransform's
707    /// position is the one its anchors give (as in its local matrix), its rotation and scale as stored.
708    pub fn joint(&self, tp: i64) -> Result<Option<SceneJoint>> {
709        let Some(n) = self.nodes.get(&tp) else {
710            return Ok(None);
711        };
712        Ok(Some(SceneJoint {
713            name: self
714                .game_objects
715                .get(&n.game_object)
716                .map_or("", |g| g.name.as_str())
717                .to_owned(),
718            father: (n.father != 0).then_some(n.father),
719            translation: [n.local[0][3], n.local[1][3], n.local[2][3]],
720            rotation: n.trs.1,
721            scale: n.trs.2,
722        }))
723    }
724
725    /// The characters of the file: its Animators with skinned renderers below them, in the order of its object table.
726    pub fn characters(&self, player: &Player) -> Result<Vec<Character>> {
727        let file = self.file.as_str();
728        let r = |pid: i64| ObjRef {
729            file: file.to_owned(),
730            path_id: pid,
731        };
732        let mut skinned = Vec::new();
733        for o in &player.file(file)?.objects {
734            if player.class_name(&r(o.path_id)) == Some("SkinnedMeshRenderer") {
735                let go = path_id(&player.decode(&r(o.path_id))?, "m_GameObject")?;
736                skinned.extend(self.transform_of(go));
737            }
738        }
739        let mut out = Vec::new();
740        for animator in self.animators(player)? {
741            let Some(tp) = self.transform_of(animator.game_object) else {
742                continue;
743            };
744            let count = skinned
745                .iter()
746                .filter(|&&s| self.relative_to(s, tp).is_some())
747                .count();
748            if count > 0 {
749                out.push(Character {
750                    animator,
751                    skinned_renderers: count,
752                });
753            }
754        }
755        Ok(out)
756    }
757
758    /// The names of the GameObjects from the child of Transform `root` down to Transform `tp` joined by `/` (empty
759    /// for `root` itself): a path as an Animator's clips name their targets. `None` when `root` is not `tp` or one of
760    /// its ancestors.
761    pub fn path_below(&self, tp: i64, root: i64) -> Option<String> {
762        let mut names = Vec::new();
763        let mut t = tp;
764        while t != root {
765            let n = self.nodes.get(&t)?;
766            names.push(
767                self.game_objects
768                    .get(&n.game_object)
769                    .map_or("", |g| g.name.as_str()),
770            );
771            if n.father == 0 || names.len() > self.nodes.len() {
772                return None;
773            }
774            t = n.father;
775        }
776        names.reverse();
777        Some(names.join("/"))
778    }
779
780    /// The Animators of the file, in the order of its object table.
781    pub fn animators(&self, player: &Player) -> Result<Vec<Animator>> {
782        self.animators_under(player, None)
783    }
784
785    /// The Animators on the GameObject `root` and its descendants (a prefab's), all of the file's with `None`.
786    pub fn animators_under(&self, player: &Player, root: Option<i64>) -> Result<Vec<Animator>> {
787        let root_tp = self.root_transform(root)?;
788        let file = self.file.as_str();
789        let r = |pid: i64| ObjRef {
790            file: file.to_owned(),
791            path_id: pid,
792        };
793        let mut out = Vec::new();
794        for o in &player.file(file)?.objects {
795            if player.class_name(&r(o.path_id)) != Some("Animator") {
796                continue;
797            }
798            let a = player.decode(&r(o.path_id))?;
799            let go = path_id(&a, "m_GameObject")?;
800            let Some(tp) = self.transform_of(go) else {
801                continue;
802            };
803            if root_tp.is_some_and(|r| self.relative_to(tp, r).is_none()) {
804                continue;
805            }
806            let pointer = |k: &str| -> Result<Option<ObjRef>> {
807                a.get(k)
808                    .map(|p| player.resolve(file, p))
809                    .transpose()
810                    .map(Option::flatten)
811            };
812            out.push(Animator {
813                animator: r(o.path_id),
814                game_object: go,
815                path: self.path(tp),
816                controller: pointer("m_Controller")?,
817                avatar: pointer("m_Avatar")?,
818                enabled: a.get("m_Enabled").is_none_or(flag_value),
819                apply_root_motion: a.get("m_ApplyRootMotion").is_some_and(flag_value),
820            });
821        }
822        Ok(out)
823    }
824
825    /// A Transform's local matrix.
826    pub fn local_of(&self, tp: i64) -> Option<Matrix> {
827        self.nodes.get(&tp).map(|n| n.local)
828    }
829
830    /// The matrix of Transform `tp` relative to its ancestor `root` (the product of the locals below `root`), or
831    /// `None` when `root` is not `tp` or an ancestor of it.
832    pub fn relative_to(&self, tp: i64, root: i64) -> Option<Matrix> {
833        let mut chain = Vec::new();
834        let mut t = tp;
835        while t != root {
836            let n = self.nodes.get(&t)?;
837            chain.push(t);
838            if n.father == 0 || chain.len() > self.nodes.len() {
839                return None;
840            }
841            t = n.father;
842        }
843        chain
844            .iter()
845            .rev()
846            .try_fold(IDENTITY, |m, t| Some(mul(&m, &self.nodes.get(t)?.local)))
847    }
848
849    /// The mesh renderers, in the order of the scene's object table, and what was skipped (no transform, no
850    /// MeshFilter, no mesh).
851    pub fn renderers(&self, player: &Player) -> Result<(Vec<Renderer>, Skipped)> {
852        self.renderers_under(player, None)
853    }
854
855    /// The LODGroups of the GameObject `root` and its descendants (a prefab's), all of the file's with `None`, in the
856    /// order of the object table.
857    pub fn lod_groups_under(&self, player: &Player, root: Option<i64>) -> Result<Vec<LodGroup>> {
858        let root_tp = self.root_transform(root)?;
859        let file = self.file.as_str();
860        let f = player.file(file)?;
861        let r = |pid: i64| ObjRef {
862            file: file.to_owned(),
863            path_id: pid,
864        };
865        let mut out = Vec::new();
866        for o in f
867            .objects
868            .iter()
869            .filter(|o| player.class_name(&r(o.path_id)) == Some("LODGroup"))
870        {
871            let g = player.decode(&r(o.path_id))?;
872            let Some(tp) = self.transform_of(path_id(&g, "m_GameObject")?) else {
873                continue;
874            };
875            if root_tp.is_some_and(|root| self.relative_to(tp, root).is_none()) {
876                continue;
877            }
878            let enabled = flag(&g, "m_Enabled")?;
879            let mut levels = Vec::new();
880            for lod in values(&g, "m_LODs")? {
881                let mut level = Vec::new();
882                for x in values(lod, "renderers")? {
883                    let pid = path_id(x, "renderer")?;
884                    if pid != 0 {
885                        level.push(pid);
886                    }
887                }
888                levels.push(level);
889            }
890            out.push(LodGroup {
891                group: r(o.path_id),
892                path: self.path(tp),
893                enabled,
894                off: self.off_flags(tp, Some((o.path_id, enabled))),
895                levels,
896            });
897        }
898        Ok(out)
899    }
900
901    /// The mesh renderers of the GameObject `root` and its descendants (a prefab's), their matrices relative to
902    /// `root`; all of the file's with `None`.
903    pub fn renderers_under(&self, player: &Player, root: Option<i64>) -> Result<(Vec<Renderer>, Skipped)> {
904        let root_tp = self.root_transform(root)?;
905        let file = self.file.as_str();
906        let f = player.file(file)?;
907        let r = |pid: i64| ObjRef {
908            file: file.to_owned(),
909            path_id: pid,
910        };
911        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
912        let mut out = Vec::new();
913        let mut skipped = Skipped::new();
914        for o in f
915            .objects
916            .iter()
917            .filter(|o| matches!(class(o.path_id), "MeshRenderer" | "SkinnedMeshRenderer"))
918        {
919            let skinned = class(o.path_id) == "SkinnedMeshRenderer";
920            let mr = player.decode(&r(o.path_id))?;
921            let go = path_id(&mr, "m_GameObject")?;
922            let Some(tp) = self.transform_of(go) else {
923                bump(&mut skipped, "no transform");
924                continue;
925            };
926            let relative = match root_tp {
927                Some(root) => match self.relative_to(tp, root) {
928                    Some(m) => Some(m),
929                    None => continue,
930                },
931                None => None,
932            };
933            let off = self.off_flags(tp, Some((o.path_id, flag(&mr, "m_Enabled")?)));
934            let g = self.game_objects.get(&go).ok_or_else(|| {
935                invalid(format!(
936                    "GameObject {go} of renderer {} is not in the scene",
937                    o.path_id
938                ))
939            })?;
940            // a skinned renderer holds its mesh itself; a mesh renderer's is on its MeshFilter
941            let filter;
942            let mesh_ptr = if skinned {
943                mr.get("m_Mesh")
944                    .ok_or_else(|| invalid("SkinnedMeshRenderer without m_Mesh"))?
945            } else {
946                let Some(&mf) = g.components.iter().find(|&&c| class(c) == "MeshFilter") else {
947                    bump(&mut skipped, "no MeshFilter");
948                    continue;
949                };
950                filter = player.decode(&r(mf))?;
951                filter
952                    .get("m_Mesh")
953                    .ok_or_else(|| invalid("MeshFilter without m_Mesh"))?
954            };
955            let Some(mesh) = player.resolve(file, mesh_ptr)? else {
956                bump(&mut skipped, "null mesh");
957                continue;
958            };
959            let materials = values(&mr, "m_Materials")?
960                .iter()
961                .map(|m| player.resolve(file, m))
962                .collect::<Result<Vec<_>>>()?;
963            let sb = mr.get("m_StaticBatchInfo");
964            let first = sb
965                .and_then(|s| s.get("firstSubMesh"))
966                .and_then(Value::as_i64)
967                .unwrap_or(0);
968            let count = sb
969                .and_then(|s| s.get("subMeshCount"))
970                .and_then(Value::as_i64)
971                .unwrap_or(0);
972            let submeshes = if count > 0 {
973                let to_u32 = |n: i64, k: &str| {
974                    u32::try_from(n).map_err(|_| {
975                        invalid(format!("renderer {}: {k} {n} is not 32-bit unsigned", o.path_id))
976                    })
977                };
978                Some((to_u32(first, "firstSubMesh")?, to_u32(count, "subMeshCount")?))
979            } else {
980                None
981            };
982            let skin = if skinned {
983                let bone = |p: &Value| -> Result<Option<i64>> {
984                    let pid = p
985                        .get("m_PathID")
986                        .and_then(Value::as_i64)
987                        .ok_or_else(|| invalid("a bone pointer without m_PathID"))?;
988                    if p.get("m_FileID").and_then(Value::as_i64).unwrap_or(0) != 0 {
989                        return Err(invalid(format!(
990                            "SkinnedMeshRenderer {}: a bone in another file",
991                            o.path_id
992                        )));
993                    }
994                    Ok((pid != 0).then_some(pid))
995                };
996                Some(RendererSkin {
997                    bones: values(&mr, "m_Bones")?.iter().map(bone).collect::<Result<_>>()?,
998                    root_bone: match mr.get("m_RootBone") {
999                        Some(p) => bone(p)?,
1000                        None => None,
1001                    },
1002                    blend_weights: match mr.get("m_BlendShapeWeights") {
1003                        Some(Value::Array(Array::F32(w))) => w.clone(),
1004                        Some(Value::Array(a)) if a.is_empty() => Vec::new(),
1005                        None => Vec::new(),
1006                        _ => return Err(invalid("SkinnedMeshRenderer: m_BlendShapeWeights not floats")),
1007                    },
1008                })
1009            } else {
1010                None
1011            };
1012            out.push(Renderer {
1013                renderer: r(o.path_id),
1014                game_object: go,
1015                path: self.path(tp),
1016                layer: g.layer,
1017                mesh,
1018                materials,
1019                world: if submeshes.is_some() {
1020                    IDENTITY
1021                } else {
1022                    match relative {
1023                        Some(m) => m,
1024                        None => self.world_of(tp)?,
1025                    }
1026                },
1027                submeshes,
1028                cast_shadows: mr.get("m_CastShadows").and_then(Value::as_i64).unwrap_or(1),
1029                receive_shadows: mr.get("m_ReceiveShadows").and_then(Value::as_i64).map(|v| v != 0),
1030                off,
1031                skin,
1032            });
1033        }
1034        Ok((out, skipped))
1035    }
1036
1037    /// The Rigidbody on Transform `tp`'s GameObject or its nearest ancestor's: `Some(kinematic)`.
1038    fn rigidbody(&self, player: &Player, tp: i64) -> Result<Option<bool>> {
1039        let r = |pid: i64| ObjRef {
1040            file: self.file.clone(),
1041            path_id: pid,
1042        };
1043        let mut t = tp;
1044        for _ in 0..=self.nodes.len() {
1045            let Some(n) = self.nodes.get(&t) else {
1046                return Ok(None);
1047            };
1048            if let Some(g) = self.game_objects.get(&n.game_object)
1049                && let Some(&rb) = g
1050                    .components
1051                    .iter()
1052                    .find(|&&c| player.class_name(&r(c)) == Some("Rigidbody"))
1053            {
1054                return Ok(Some(flag(&player.decode(&r(rb))?, "m_IsKinematic")?));
1055            }
1056            if n.father == 0 {
1057                return Ok(None);
1058            }
1059            t = n.father;
1060        }
1061        Ok(None)
1062    }
1063
1064    /// Every Box, Sphere, Capsule and Mesh collider of the GameObject `root` and its descendants (a prefab's, matrices
1065    /// relative to `root`), or of the whole file with `None`, in the order of the object table: those that do not
1066    /// collide (inactive, disabled, triggers) included, flagged.
1067    pub fn colliders_under(&self, player: &Player, root: Option<i64>) -> Result<Vec<ColliderComponent>> {
1068        let file = self.file.as_str();
1069        let f = player.file(file)?;
1070        let r = |pid: i64| ObjRef {
1071            file: file.to_owned(),
1072            path_id: pid,
1073        };
1074        let root_tp = self.root_transform(root)?;
1075        let v3 = |v: &Value, k: &str| -> Result<[f64; 3]> { vec3(v, k) };
1076        let mut out = Vec::new();
1077        for o in &f.objects {
1078            let class = player.class_name(&r(o.path_id)).unwrap_or("");
1079            if !matches!(
1080                class,
1081                "BoxCollider" | "SphereCollider" | "CapsuleCollider" | "MeshCollider"
1082            ) {
1083                continue;
1084            }
1085            let c = player.decode(&r(o.path_id))?;
1086            let go = path_id(&c, "m_GameObject")?;
1087            let Some(tp) = self.transform_of(go) else { continue };
1088            let world = match root_tp {
1089                Some(root) => match self.relative_to(tp, root) {
1090                    Some(m) => m,
1091                    None => continue,
1092                },
1093                None => self.world_of(tp)?,
1094            };
1095            let fl = |k: &str| -> Result<f64> { num(&c, k) };
1096            let shape = match class {
1097                "BoxCollider" => ColliderShape::Box {
1098                    center: v3(&c, "m_Center")?,
1099                    size: v3(&c, "m_Size")?,
1100                },
1101                "SphereCollider" => ColliderShape::Sphere {
1102                    center: v3(&c, "m_Center")?,
1103                    radius: fl("m_Radius")?,
1104                },
1105                "CapsuleCollider" => ColliderShape::Capsule {
1106                    center: v3(&c, "m_Center")?,
1107                    radius: fl("m_Radius")?,
1108                    height: fl("m_Height")?,
1109                    direction: match c.get("m_Direction").and_then(Value::as_i64).unwrap_or(1) {
1110                        d @ 0..=2 => usize::try_from(d).unwrap_or_default(),
1111                        d => {
1112                            return Err(invalid(format!(
1113                                "CapsuleCollider {}: m_Direction {d} is not 0, 1 or 2",
1114                                o.path_id
1115                            )));
1116                        }
1117                    },
1118                },
1119                _ => ColliderShape::Mesh {
1120                    mesh: match c.get("m_Mesh") {
1121                        Some(p) => player.resolve(file, p)?,
1122                        None => None,
1123                    },
1124                    convex: flag(&c, "m_Convex")?,
1125                },
1126            };
1127            out.push(ColliderComponent {
1128                collider: r(o.path_id),
1129                path: self.path(tp),
1130                layer: self.game_objects.get(&go).map_or(0, |g| g.layer),
1131                world,
1132                shape,
1133                off: self.off_flags(tp, Some((o.path_id, flag(&c, "m_Enabled")?))),
1134                trigger: c.get("m_IsTrigger").is_some_and(flag_value),
1135                physic_material: match c.get("m_Material") {
1136                    Some(p) => player.resolve(file, p)?,
1137                    None => None,
1138                },
1139                rigidbody: self.rigidbody(player, tp)?,
1140            });
1141        }
1142        Ok(out)
1143    }
1144
1145    /// The scene's RenderSettings: the Light it names as its sun (`m_Sun`; `None` when it names none: Unity then
1146    /// lights its procedural sky by the brightest directional light, a choice of the engine's, not the scene's) and
1147    /// its ambient lighting. `None` when the scene has no RenderSettings.
1148    pub fn render_settings(&self, player: &Player) -> Result<Option<SceneRenderSettings>> {
1149        let file = self.file.as_str();
1150        let f = player.file(file)?;
1151        let r = |pid: i64| ObjRef {
1152            file: file.to_owned(),
1153            path_id: pid,
1154        };
1155        let mut found = None;
1156        for o in &f.objects {
1157            if player.class_name(&r(o.path_id)) != Some("RenderSettings") {
1158                continue;
1159            }
1160            if found.is_some() {
1161                return Err(invalid(format!("{file}: more than one RenderSettings")));
1162            }
1163            let settings = player.decode(&r(o.path_id))?;
1164            let sun = match settings.get("m_Sun") {
1165                Some(p) => player.resolve(file, p)?,
1166                None => None,
1167            };
1168            let rgba = |k: &str| -> Result<[f32; 4]> {
1169                let c = settings
1170                    .get(k)
1171                    .ok_or_else(|| invalid(format!("{file}: RenderSettings without {k}")))?;
1172                Ok([num(c, "r")?, num(c, "g")?, num(c, "b")?, num(c, "a")?].map(crate::narrow))
1173            };
1174            let ambient = match settings.get("m_AmbientMode").and_then(Value::as_i64) {
1175                None => None,
1176                Some(mode) => Some(UnityAmbient {
1177                    mode,
1178                    sky: rgba("m_AmbientSkyColor")?,
1179                    equator: rgba("m_AmbientEquatorColor")?,
1180                    ground: rgba("m_AmbientGroundColor")?,
1181                    intensity: settings
1182                        .get("m_AmbientIntensity")
1183                        .and_then(Value::as_f32)
1184                        .ok_or_else(|| {
1185                            invalid(format!("{file}: RenderSettings without m_AmbientIntensity"))
1186                        })?,
1187                    probe: match settings.get("m_AmbientProbe") {
1188                        None => None,
1189                        Some(sh) => {
1190                            let mut out = [0.0f32; 27];
1191                            for (i, c) in out.iter_mut().enumerate() {
1192                                // the type tree names the coefficients `sh[ 0]` .. `sh[26]`
1193                                *c = sh.get(&format!("sh[{i:2}]")).and_then(Value::as_f32).ok_or_else(
1194                                    || {
1195                                        invalid(format!(
1196                                            "{file}: RenderSettings' m_AmbientProbe without sh[{i:2}]"
1197                                        ))
1198                                    },
1199                                )?;
1200                            }
1201                            Some(out)
1202                        }
1203                    },
1204                }),
1205            };
1206            // fog: absent from a type tree that does not state it
1207            let fog = match settings.get("m_Fog").and_then(Value::as_bool) {
1208                None => None,
1209                Some(enabled) => Some(UnityFog {
1210                    enabled,
1211                    mode: settings
1212                        .get("m_FogMode")
1213                        .and_then(Value::as_i64)
1214                        .ok_or_else(|| invalid(format!("{file}: RenderSettings without m_FogMode")))?,
1215                    color: rgba("m_FogColor")?,
1216                    density: crate::narrow(num(&settings, "m_FogDensity")?),
1217                    start: crate::narrow(num(&settings, "m_LinearFogStart")?),
1218                    end: crate::narrow(num(&settings, "m_LinearFogEnd")?),
1219                }),
1220            };
1221            found = Some(SceneRenderSettings { sun, ambient, fog });
1222        }
1223        Ok(found)
1224    }
1225
1226    /// The lights, whatever their state, in the order of the scene's object table.
1227    pub fn lights(&self, player: &Player) -> Result<Vec<LightComponent>> {
1228        let file = self.file.as_str();
1229        let f = player.file(file)?;
1230        let r = |pid: i64| ObjRef {
1231            file: file.to_owned(),
1232            path_id: pid,
1233        };
1234        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
1235        let mut out = Vec::new();
1236        for o in f.objects.iter().filter(|o| class(o.path_id) == "Light") {
1237            let l = player.decode(&r(o.path_id))?;
1238            let Some(tp) = self.transform_of(path_id(&l, "m_GameObject")?) else {
1239                continue;
1240            };
1241            let off = self.off_flags(tp, Some((o.path_id, flag(&l, "m_Enabled")?)));
1242            let fl = |k: &str| l.get(k).and_then(Value::as_f32);
1243            let int = |k: &str| l.get(k).and_then(Value::as_i64);
1244            let c = l.get("m_Color").ok_or_else(|| invalid("light: no m_Color"))?;
1245            let area = l.get("m_AreaSize");
1246            out.push(LightComponent {
1247                light: r(o.path_id),
1248                path: self.path(tp),
1249                world: self.world_of(tp)?,
1250                kind: int("m_Type").ok_or_else(|| invalid("light: no m_Type"))?,
1251                color: [num(c, "r")?, num(c, "g")?, num(c, "b")?, num(c, "a")?].map(crate::narrow),
1252                intensity: fl("m_Intensity").unwrap_or(1.0),
1253                range: fl("m_Range").unwrap_or(10.0),
1254                spot_angle: fl("m_SpotAngle").unwrap_or(30.0),
1255                inner_spot_angle: fl("m_InnerSpotAngle").unwrap_or(0.0),
1256                shadows: l
1257                    .at(&["m_Shadows", "m_Type"])
1258                    .and_then(Value::as_i64)
1259                    .unwrap_or(0),
1260                color_temperature: if l
1261                    .get("m_UseColorTemperature")
1262                    .is_some_and(|v| v.as_bool() == Some(true) || v.as_i64() == Some(1))
1263                {
1264                    fl("m_ColorTemperature")
1265                } else {
1266                    None
1267                },
1268                bounce_intensity: fl("m_BounceIntensity").unwrap_or(1.0),
1269                light_unit: int("m_LightUnit"),
1270                lux_at_distance: fl("m_LuxAtDistance"),
1271                area_size: [
1272                    area.and_then(|a| a.get("x"))
1273                        .and_then(Value::as_f32)
1274                        .unwrap_or(1.0),
1275                    area.and_then(|a| a.get("y"))
1276                        .and_then(Value::as_f32)
1277                        .unwrap_or(1.0),
1278                ],
1279                render_mode: int("m_RenderMode").unwrap_or(0),
1280                baked: l.at(&["m_BakingOutput", "isBaked"]).is_some_and(flag_value)
1281                    && l.at(&["m_BakingOutput", "lightmapBakeMode", "lightmapBakeType"])
1282                        .and_then(Value::as_i64)
1283                        == Some(2),
1284                culling_mask: culling_mask(&l, o.path_id)?,
1285                cookie: match l.get("m_Cookie") {
1286                    Some(p) => player.resolve(file, p)?,
1287                    None => None,
1288                },
1289                off,
1290            });
1291        }
1292        Ok(out)
1293    }
1294
1295    /// The cameras, whatever their state, in the order of the scene's object table.
1296    pub fn cameras(&self, player: &Player) -> Result<Vec<CameraComponent>> {
1297        let file = self.file.as_str();
1298        let f = player.file(file)?;
1299        let r = |pid: i64| ObjRef {
1300            file: file.to_owned(),
1301            path_id: pid,
1302        };
1303        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
1304        let mut out = Vec::new();
1305        for o in f.objects.iter().filter(|o| class(o.path_id) == "Camera") {
1306            let c = player.decode(&r(o.path_id))?;
1307            let game_object = path_id(&c, "m_GameObject")?;
1308            let Some(tp) = self.transform_of(game_object) else {
1309                continue;
1310            };
1311            out.push(CameraComponent {
1312                camera: r(o.path_id),
1313                game_object,
1314                path: self.path(tp),
1315                world: self.world_of(tp)?,
1316                culling_mask: culling_mask(&c, o.path_id)?,
1317                target_texture: match c.get("m_TargetTexture") {
1318                    Some(p) => player.resolve(file, p)?,
1319                    None => None,
1320                },
1321                off: self.off_flags(tp, Some((o.path_id, flag(&c, "m_Enabled")?))),
1322            });
1323        }
1324        Ok(out)
1325    }
1326
1327    /// The terrains, whatever their state, in the order of the scene's object table, and what was skipped (no transform,
1328    /// no terrain data: a Terrain that names no TerrainData draws nothing).
1329    pub fn terrains(&self, player: &Player) -> Result<(Vec<TerrainComponent>, Skipped)> {
1330        let file = self.file.as_str();
1331        let f = player.file(file)?;
1332        let r = |pid: i64| ObjRef {
1333            file: file.to_owned(),
1334            path_id: pid,
1335        };
1336        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
1337        let mut out = Vec::new();
1338        let mut skipped = Skipped::new();
1339        for o in f.objects.iter().filter(|o| class(o.path_id) == "Terrain") {
1340            let t = player.decode(&r(o.path_id))?;
1341            let go = path_id(&t, "m_GameObject")?;
1342            let Some(tp) = self.transform_of(go) else {
1343                bump(&mut skipped, "no transform");
1344                continue;
1345            };
1346            let off = self.off_flags(tp, Some((o.path_id, flag(&t, "m_Enabled")?)));
1347            let pptr = |v: &Value, k: &str| -> Result<Option<ObjRef>> {
1348                match v.get(k) {
1349                    Some(p) => player.resolve(file, p),
1350                    None => Ok(None),
1351                }
1352            };
1353            let Some(data) = pptr(&t, "m_TerrainData")? else {
1354                bump(&mut skipped, "no terrain data");
1355                continue;
1356            };
1357            let collider = match self
1358                .game_objects
1359                .get(&go)
1360                .ok_or_else(|| {
1361                    invalid(format!(
1362                        "GameObject {go} of terrain {} is not in the scene",
1363                        o.path_id
1364                    ))
1365                })?
1366                .components
1367                .iter()
1368                .find(|&&c| class(c) == "TerrainCollider")
1369            {
1370                Some(&c) => {
1371                    let tc = player.decode(&r(c))?;
1372                    Some(TerrainColliderComponent {
1373                        collider: r(c),
1374                        enabled: flag(&tc, "m_Enabled")?,
1375                        data: pptr(&tc, "m_TerrainData")?,
1376                        material: pptr(&tc, "m_Material")?,
1377                        tree_colliders: flag(&tc, "m_EnableTreeColliders")?,
1378                    })
1379                }
1380                None => None,
1381            };
1382            out.push(TerrainComponent {
1383                terrain: r(o.path_id),
1384                path: self.path(tp),
1385                world: self.world_of(tp)?,
1386                data,
1387                material: pptr(&t, "m_MaterialTemplate")?,
1388                draw_heightmap: flag(&t, "m_DrawHeightmap")?,
1389                draw_trees: flag(&t, "m_DrawTreesAndFoliage")?,
1390                collider,
1391                off,
1392            });
1393        }
1394        Ok((out, skipped))
1395    }
1396}
1397
1398/// A collider's shape as stored, in its GameObject's space (Unity's frame).
1399#[derive(Clone, Debug, PartialEq)]
1400pub enum ColliderShape {
1401    /// A box collider.
1402    Box {
1403        /// `m_Center`: the box's centre in its GameObject's space.
1404        center: [f64; 3],
1405        /// `m_Size`: the full edge lengths along x, y and z (not half-extents), before the GameObject's scale.
1406        size: [f64; 3],
1407    },
1408    /// A sphere collider.
1409    Sphere {
1410        /// `m_Center`: the sphere's centre in its GameObject's space.
1411        center: [f64; 3],
1412        /// `m_Radius`, before the GameObject's scale.
1413        radius: f64,
1414    },
1415    /// A capsule collider.
1416    Capsule {
1417        /// `m_Center`: the capsule's centre in its GameObject's space.
1418        center: [f64; 3],
1419        /// `m_Radius`, before the GameObject's scale.
1420        radius: f64,
1421        /// `m_Height`, including the caps, before the GameObject's scale.
1422        height: f64,
1423        /// `m_Direction`: the axis the height runs along (0 x, 1 y, 2 z).
1424        direction: usize,
1425    },
1426    /// A mesh collider.
1427    Mesh {
1428        /// `m_Mesh`; `None` for a null pointer.
1429        mesh: Option<ObjRef>,
1430        /// `m_Convex`.
1431        convex: bool,
1432    },
1433}
1434
1435/// A collider as stored: its shape, its off flags, whether it is a trigger, its physic material, and the Rigidbody on
1436/// it or its nearest ancestor (`Some(true)`: kinematic).
1437#[derive(Clone, Debug, PartialEq)]
1438pub struct ColliderComponent {
1439    /// The collider object.
1440    pub collider: ObjRef,
1441    /// Its GameObject's path from the scene root, names joined by `/`.
1442    pub path: String,
1443    /// Its GameObject's layer.
1444    pub layer: u32,
1445    /// The matrix of its GameObject: world, or relative to `root` when one was given.
1446    pub world: Matrix,
1447    /// The shape.
1448    pub shape: ColliderShape,
1449    /// What keeps it from colliding; collides when empty.
1450    pub off: Vec<OffFlag>,
1451    /// `m_IsTrigger`.
1452    pub trigger: bool,
1453    /// `m_Material`.
1454    pub physic_material: Option<ObjRef>,
1455    /// The nearest Rigidbody: `Some(kinematic)`; `None` when there is none.
1456    pub rigidbody: Option<bool>,
1457}
1458
1459/// A light as stored: Unity's LightType (0 spot, 1 directional, 2 point, 3 rectangle, 4 disc), colour as stored
1460/// (gamma-encoded), intensity, range, spot angles in degrees (full cone), shadow type, colour temperature (Kelvin, when
1461/// used), bounce intensity, URP's light unit and lux-at-distance, area size, render mode and culling mask.
1462#[derive(Clone, Debug, PartialEq)]
1463pub struct LightComponent {
1464    /// The Light object.
1465    pub light: ObjRef,
1466    /// Its GameObject's path from the scene root, names joined by `/`.
1467    pub path: String,
1468    /// Its GameObject's world matrix.
1469    pub world: Matrix,
1470    /// `m_Type`.
1471    pub kind: i64,
1472    /// `m_Color`, RGBA.
1473    pub color: [f32; 4],
1474    /// `m_Intensity` (1 when not stored).
1475    pub intensity: f32,
1476    /// `m_Range` (10 when not stored).
1477    pub range: f32,
1478    /// `m_SpotAngle` (30 when not stored).
1479    pub spot_angle: f32,
1480    /// `m_InnerSpotAngle` (0 when not stored).
1481    pub inner_spot_angle: f32,
1482    /// `m_Shadows.m_Type` (0 when not stored).
1483    pub shadows: i64,
1484    /// `m_ColorTemperature`, when `m_UseColorTemperature` is on.
1485    pub color_temperature: Option<f32>,
1486    /// `m_BounceIntensity` (1 when not stored).
1487    pub bounce_intensity: f32,
1488    /// `m_LightUnit`, when stored.
1489    pub light_unit: Option<i64>,
1490    /// `m_LuxAtDistance`, when stored.
1491    pub lux_at_distance: Option<f32>,
1492    /// `m_AreaSize` (1 x 1 when not stored).
1493    pub area_size: [f32; 2],
1494    /// `m_RenderMode` (0 when not stored).
1495    pub render_mode: i64,
1496    /// Fully baked: `m_BakingOutput` says baked, as `Baked` (2), not mixed or realtime.
1497    pub baked: bool,
1498    /// `m_CullingMask`'s bits (all layers when not stored).
1499    pub culling_mask: u32,
1500    /// `m_Cookie`.
1501    pub cookie: Option<ObjRef>,
1502    /// What keeps it from shining; shines when empty.
1503    pub off: Vec<OffFlag>,
1504}
1505
1506/// A camera as stored: its culling mask (`m_CullingMask`, the layers it draws; all when absent) and the texture it
1507/// renders into (`m_TargetTexture`; `None`: it draws to the screen).
1508#[derive(Clone, Debug, PartialEq)]
1509pub struct CameraComponent {
1510    /// The Camera object.
1511    pub camera: ObjRef,
1512    /// Its GameObject's path id in the scene's file (whose MonoBehaviours, [`Scene::behaviours_of`], hold a Universal
1513    /// Render Pipeline camera's URP data).
1514    pub game_object: i64,
1515    /// Its GameObject's path from the scene root, names joined by `/`.
1516    pub path: String,
1517    /// Its GameObject's world matrix.
1518    pub world: Matrix,
1519    /// `m_CullingMask.m_Bits`: bit n set draws layer n (all 32 bits when not stored).
1520    pub culling_mask: u32,
1521    /// `m_TargetTexture`; `None` when it draws to the screen.
1522    pub target_texture: Option<ObjRef>,
1523    /// What keeps it from drawing; draws when empty.
1524    pub off: Vec<OffFlag>,
1525}
1526
1527/// A terrain's collider: its own TerrainData (which may differ from the drawn one), physic material, tree colliders.
1528#[derive(Clone, Debug, PartialEq)]
1529pub struct TerrainColliderComponent {
1530    /// The TerrainCollider object.
1531    pub collider: ObjRef,
1532    /// `m_Enabled`.
1533    pub enabled: bool,
1534    /// `m_TerrainData`.
1535    pub data: Option<ObjRef>,
1536    /// `m_Material`.
1537    pub material: Option<ObjRef>,
1538    /// `m_EnableTreeColliders`.
1539    pub tree_colliders: bool,
1540}
1541
1542/// A terrain, whatever its state: its GameObject's path and world matrix (a terrain is placed by its position only),
1543/// its data, material, collider and off flags (drawn when empty).
1544#[derive(Clone, Debug, PartialEq)]
1545pub struct TerrainComponent {
1546    /// The Terrain object.
1547    pub terrain: ObjRef,
1548    /// Its GameObject's path from the scene root, names joined by `/`.
1549    pub path: String,
1550    /// Its GameObject's world matrix.
1551    pub world: Matrix,
1552    /// `m_TerrainData`, read with [`crate::terrain::TerrainData::read`].
1553    pub data: ObjRef,
1554    /// `m_MaterialTemplate`.
1555    pub material: Option<ObjRef>,
1556    /// `m_DrawHeightmap`.
1557    pub draw_heightmap: bool,
1558    /// `m_DrawTreesAndFoliage`.
1559    pub draw_trees: bool,
1560    /// The terrain's TerrainCollider, if its GameObject has one.
1561    pub collider: Option<TerrainColliderComponent>,
1562    /// What keeps it from being drawn; drawn when empty.
1563    pub off: Vec<OffFlag>,
1564}
1565
1566/// A scene's RenderSettings as read ([`Scene::render_settings`]).
1567#[derive(Clone, Debug, PartialEq)]
1568pub struct SceneRenderSettings {
1569    /// `m_Sun`: the Light the scene names as its sun.
1570    pub sun: Option<ObjRef>,
1571    /// The ambient lighting; `None` when the RenderSettings state no `m_AmbientMode`.
1572    pub ambient: Option<UnityAmbient>,
1573    /// The fog; `None` when the RenderSettings state no `m_Fog`.
1574    pub fog: Option<UnityFog>,
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579    use super::*;
1580
1581    fn chain(len: i64) -> BTreeMap<i64, Node> {
1582        (1..=len)
1583            .map(|id| {
1584                let node = Node {
1585                    game_object: id,
1586                    father: if id == 1 { 0 } else { id - 1 },
1587                    local: IDENTITY,
1588                    trs: ([0.0; 3], [0.0, 0.0, 0.0, 1.0], [1.0; 3]),
1589                };
1590                (id, node)
1591            })
1592            .collect()
1593    }
1594
1595    /// A chain of parents is as deep as it is long, and a loop or a chain past the limit is refused at the read, not
1596    /// walked by every node's path.
1597    #[test]
1598    fn deep_and_looping_parents_are_refused() {
1599        assert!(check_depth(&chain(MAX_DEPTH as i64)).is_ok());
1600        let e = check_depth(&chain(MAX_DEPTH as i64 + 1)).unwrap_err().to_string();
1601        assert!(e.contains("deeper than 1024"), "{e}");
1602        let mut looped = chain(3);
1603        looped.get_mut(&1).unwrap().father = 3;
1604        assert!(check_depth(&looped).is_err());
1605        // a father the scene does not hold ends the chain
1606        let mut orphan = chain(3);
1607        orphan.get_mut(&1).unwrap().father = 99;
1608        assert!(check_depth(&orphan).is_ok());
1609    }
1610
1611    fn scene_of(nodes: BTreeMap<i64, Node>) -> Scene {
1612        Scene {
1613            file: "level0".into(),
1614            game_objects: BTreeMap::new(),
1615            transform_of: nodes.iter().map(|(&tp, n)| (n.game_object, tp)).collect(),
1616            nodes,
1617            world: std::sync::Mutex::new(BTreeMap::new()),
1618        }
1619    }
1620
1621    /// A query limited to a GameObject without a Transform is an error for every query that takes a root, the LODGroup
1622    /// query among them, not an unlimited one.
1623    #[test]
1624    fn a_root_without_a_transform_is_refused() {
1625        let scene = scene_of(chain(2));
1626        assert_eq!(scene.root_transform(None).unwrap(), None);
1627        assert_eq!(scene.root_transform(Some(2)).unwrap(), Some(2));
1628        let e = scene.root_transform(Some(77)).unwrap_err().to_string();
1629        assert!(e.contains("GameObject 77 has no transform"), "{e}");
1630    }
1631
1632    /// A panic while the world cache was locked leaves a usable cache, not a panic in every later call.
1633    #[test]
1634    fn a_poisoned_world_cache_is_still_read() {
1635        let scene = std::sync::Arc::new(scene_of(chain(3)));
1636        let held = scene.clone();
1637        let _ = std::thread::spawn(move || {
1638            let _guard = held.world.lock().unwrap();
1639            panic!("poison the cache");
1640        })
1641        .join();
1642        assert!(scene.world.is_poisoned());
1643        assert_eq!(scene.world_of(3).unwrap(), IDENTITY);
1644    }
1645
1646    fn with(k: &str, v: Value) -> Value {
1647        Value::Struct(vec![(k.into(), v)])
1648    }
1649
1650    /// Layers and culling masks outside 32 bits are refused, not wrapped to another layer set.
1651    #[test]
1652    fn layers_and_masks_are_not_cast() {
1653        assert_eq!(layer_of(&with("m_Layer", Value::Int(5))).unwrap(), 5);
1654        assert_eq!(layer_of(&Value::Struct(vec![])).unwrap(), 0);
1655        for bad in [-1, 1 << 32] {
1656            assert!(layer_of(&with("m_Layer", Value::Int(bad))).is_err());
1657        }
1658        let mask = |bits| culling_mask(&with("m_CullingMask", with("m_Bits", Value::Int(bits))), 9);
1659        assert_eq!(mask(0xffff_fff0).unwrap(), 0xffff_fff0);
1660        assert_eq!(mask(-1).unwrap(), u32::MAX);
1661        assert_eq!(culling_mask(&Value::Struct(vec![]), 9).unwrap(), u32::MAX);
1662        let e = mask(1 << 32).unwrap_err().to_string();
1663        assert!(e.contains("object 9") && e.contains("4294967296"), "{e}");
1664        assert!(mask(-(1 << 40)).is_err());
1665    }
1666}