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 = match base.get("m_Script") {
550                Some(p) => match player.resolve(&r.file, p)? {
551                    Some(s) => {
552                        let s = player.decode(&s)?;
553                        let field = |k: &str| s.get(k).and_then(Value::as_str).unwrap_or("").to_owned();
554                        let (ns, class) = (field("m_Namespace"), field("m_ClassName"));
555                        if ns.is_empty() {
556                            class
557                        } else {
558                            format!("{ns}.{class}")
559                        }
560                    }
561                    None => String::new(),
562                },
563                None => String::new(),
564            };
565            out.push(UnityBehaviour {
566                behaviour: r.clone(),
567                script,
568                enabled: base
569                    .get("m_Enabled")
570                    .and_then(Value::as_i64)
571                    .is_some_and(|x| x != 0)
572                    || base.get("m_Enabled").and_then(Value::as_bool) == Some(true),
573                data,
574            });
575        }
576        Ok(out)
577    }
578
579    /// Every MonoBehaviour of the scene, by GameObject (in path id order), as [`Scene::behaviours_of`] gives them.
580    pub fn behaviours(&self, player: &Player) -> Result<Vec<(i64, UnityBehaviour)>> {
581        let mut out = Vec::new();
582        for &go in self.game_objects.keys() {
583            out.extend(self.behaviours_of(player, go)?.into_iter().map(|b| (go, b)));
584        }
585        Ok(out)
586    }
587
588    /// The world matrices computed so far. A thread that panicked holding the lock can leave nothing half-written (an
589    /// entry is inserted whole), so a poisoned lock is still a valid cache.
590    fn world_cache(&self) -> std::sync::MutexGuard<'_, BTreeMap<i64, Matrix>> {
591        self.world
592            .lock()
593            .unwrap_or_else(std::sync::PoisonError::into_inner)
594    }
595
596    /// The Transform of the GameObject `root` that a query is limited to; `None` for a query of the whole file. A
597    /// GameObject without a Transform is an error, not an empty or an unlimited query.
598    fn root_transform(&self, root: Option<i64>) -> Result<Option<i64>> {
599        root.map(|go| {
600            self.transform_of(go)
601                .ok_or_else(|| invalid(format!("GameObject {go} has no transform")))
602        })
603        .transpose()
604    }
605
606    /// A Transform's world matrix (Unity frame).
607    pub fn world_of(&self, tp: i64) -> Result<Matrix> {
608        let mut chain = Vec::new();
609        let mut t = tp;
610        let base = loop {
611            if let Some(m) = self.world_cache().get(&t) {
612                break *m;
613            }
614            let Some(n) = self.nodes.get(&t) else {
615                break IDENTITY;
616            };
617            chain.push(t);
618            if chain.len() > self.nodes.len() {
619                return Err(invalid(format!("transform {tp}: parents loop")));
620            }
621            if n.father == 0 {
622                break IDENTITY;
623            }
624            t = n.father;
625        };
626        let mut m = base;
627        let mut cache = self.world_cache();
628        for &t in chain.iter().rev() {
629            let node = self
630                .nodes
631                .get(&t)
632                .ok_or_else(|| invalid(format!("transform {t} on the chain of {tp} is not in the scene")))?;
633            m = mul(&m, &node.local);
634            cache.insert(t, m);
635        }
636        Ok(m)
637    }
638
639    /// The off flags of a component on Transform `tp`'s GameObject: the GameObjects stored inactive from the root down,
640    /// then the component (`(path id, enabled)`) when it is stored disabled.
641    pub fn off_flags(&self, tp: i64, component: Option<(i64, bool)>) -> Vec<OffFlag> {
642        let mut out = Vec::new();
643        let mut t = tp;
644        for _ in 0..=self.nodes.len() {
645            let Some(n) = self.nodes.get(&t) else { break };
646            if !self.game_objects.get(&n.game_object).is_some_and(|g| g.active) {
647                out.push(OffFlag::Inactive(n.game_object));
648            }
649            if n.father == 0 {
650                break;
651            }
652            t = n.father;
653        }
654        out.reverse();
655        if let Some((c, false)) = component {
656            out.push(OffFlag::Disabled(c));
657        }
658        out
659    }
660
661    /// Whether a Transform's GameObject and all its ancestors' are active.
662    pub fn active(&self, tp: i64) -> bool {
663        let mut t = tp;
664        for _ in 0..=self.nodes.len() {
665            let Some(n) = self.nodes.get(&t) else { return true };
666            if !self.game_objects.get(&n.game_object).is_some_and(|g| g.active) {
667                return false;
668            }
669            if n.father == 0 {
670                return true;
671            }
672            t = n.father;
673        }
674        false
675    }
676
677    /// The GameObject path of a Transform, names from the root joined by `/`.
678    pub fn path(&self, tp: i64) -> String {
679        let mut parts = Vec::new();
680        let mut t = tp;
681        while let Some(n) = self.nodes.get(&t) {
682            parts.push(
683                self.game_objects
684                    .get(&n.game_object)
685                    .map_or("", |g| g.name.as_str()),
686            );
687            if n.father == 0 || parts.len() > self.nodes.len() {
688                break;
689            }
690            t = n.father;
691        }
692        parts.reverse();
693        parts.join("/")
694    }
695
696    /// The Transform of a GameObject (both path ids).
697    pub fn transform_of(&self, game_object: i64) -> Option<i64> {
698        self.transform_of.get(&game_object).copied()
699    }
700
701    /// A GameObject and the GameObjects above it on its Transform chain, itself first, the root last; a GameObject
702    /// without a Transform is alone.
703    pub fn ancestors(&self, game_object: i64) -> Vec<i64> {
704        let mut out = vec![game_object];
705        let Some(mut t) = self.transform_of(game_object) else {
706            return out;
707        };
708        while let Some(n) = self.nodes.get(&t) {
709            if n.father == 0 || out.len() > self.nodes.len() {
710                break;
711            }
712            t = n.father;
713            match self.nodes.get(&t) {
714                Some(p) => out.push(p.game_object),
715                None => break,
716            }
717        }
718        out
719    }
720
721    /// Transform `tp` as a joint ([`SceneJoint`]); `None` when it is no Transform of this file. A RectTransform's
722    /// position is the one its anchors give (as in its local matrix), its rotation and scale as stored.
723    pub fn joint(&self, tp: i64) -> Result<Option<SceneJoint>> {
724        let Some(n) = self.nodes.get(&tp) else {
725            return Ok(None);
726        };
727        Ok(Some(SceneJoint {
728            name: self
729                .game_objects
730                .get(&n.game_object)
731                .map_or("", |g| g.name.as_str())
732                .to_owned(),
733            father: (n.father != 0).then_some(n.father),
734            translation: [n.local[0][3], n.local[1][3], n.local[2][3]],
735            rotation: n.trs.1,
736            scale: n.trs.2,
737        }))
738    }
739
740    /// The characters of the file: its Animators with skinned renderers below them, in the order of its object table.
741    pub fn characters(&self, player: &Player) -> Result<Vec<Character>> {
742        let file = self.file.as_str();
743        let r = |pid: i64| ObjRef {
744            file: file.to_owned(),
745            path_id: pid,
746        };
747        let mut skinned = Vec::new();
748        for o in &player.file(file)?.objects {
749            if player.class_name(&r(o.path_id)) == Some("SkinnedMeshRenderer") {
750                let go = path_id(&player.decode(&r(o.path_id))?, "m_GameObject")?;
751                skinned.extend(self.transform_of(go));
752            }
753        }
754        let mut out = Vec::new();
755        for animator in self.animators(player)? {
756            let Some(tp) = self.transform_of(animator.game_object) else {
757                continue;
758            };
759            let count = skinned
760                .iter()
761                .filter(|&&s| self.relative_to(s, tp).is_some())
762                .count();
763            if count > 0 {
764                out.push(Character {
765                    animator,
766                    skinned_renderers: count,
767                });
768            }
769        }
770        Ok(out)
771    }
772
773    /// The names of the GameObjects from the child of Transform `root` down to Transform `tp` joined by `/` (empty
774    /// for `root` itself): a path as an Animator's clips name their targets. `None` when `root` is not `tp` or one of
775    /// its ancestors.
776    pub fn path_below(&self, tp: i64, root: i64) -> Option<String> {
777        let mut names = Vec::new();
778        let mut t = tp;
779        while t != root {
780            let n = self.nodes.get(&t)?;
781            names.push(
782                self.game_objects
783                    .get(&n.game_object)
784                    .map_or("", |g| g.name.as_str()),
785            );
786            if n.father == 0 || names.len() > self.nodes.len() {
787                return None;
788            }
789            t = n.father;
790        }
791        names.reverse();
792        Some(names.join("/"))
793    }
794
795    /// The Animators of the file, in the order of its object table.
796    pub fn animators(&self, player: &Player) -> Result<Vec<Animator>> {
797        self.animators_under(player, None)
798    }
799
800    /// The Animators on the GameObject `root` and its descendants (a prefab's), all of the file's with `None`.
801    pub fn animators_under(&self, player: &Player, root: Option<i64>) -> Result<Vec<Animator>> {
802        let root_tp = self.root_transform(root)?;
803        let file = self.file.as_str();
804        let r = |pid: i64| ObjRef {
805            file: file.to_owned(),
806            path_id: pid,
807        };
808        let mut out = Vec::new();
809        for o in &player.file(file)?.objects {
810            if player.class_name(&r(o.path_id)) != Some("Animator") {
811                continue;
812            }
813            let a = player.decode(&r(o.path_id))?;
814            let go = path_id(&a, "m_GameObject")?;
815            let Some(tp) = self.transform_of(go) else {
816                continue;
817            };
818            if root_tp.is_some_and(|r| self.relative_to(tp, r).is_none()) {
819                continue;
820            }
821            let pointer = |k: &str| -> Result<Option<ObjRef>> {
822                a.get(k)
823                    .map(|p| player.resolve(file, p))
824                    .transpose()
825                    .map(Option::flatten)
826            };
827            out.push(Animator {
828                animator: r(o.path_id),
829                game_object: go,
830                path: self.path(tp),
831                controller: pointer("m_Controller")?,
832                avatar: pointer("m_Avatar")?,
833                enabled: a.get("m_Enabled").is_none_or(flag_value),
834                apply_root_motion: a.get("m_ApplyRootMotion").is_some_and(flag_value),
835            });
836        }
837        Ok(out)
838    }
839
840    /// A Transform's local matrix.
841    pub fn local_of(&self, tp: i64) -> Option<Matrix> {
842        self.nodes.get(&tp).map(|n| n.local)
843    }
844
845    /// The matrix of Transform `tp` relative to its ancestor `root` (the product of the locals below `root`), or
846    /// `None` when `root` is not `tp` or an ancestor of it.
847    pub fn relative_to(&self, tp: i64, root: i64) -> Option<Matrix> {
848        let mut chain = Vec::new();
849        let mut t = tp;
850        while t != root {
851            let n = self.nodes.get(&t)?;
852            chain.push(t);
853            if n.father == 0 || chain.len() > self.nodes.len() {
854                return None;
855            }
856            t = n.father;
857        }
858        chain
859            .iter()
860            .rev()
861            .try_fold(IDENTITY, |m, t| Some(mul(&m, &self.nodes.get(t)?.local)))
862    }
863
864    /// The mesh renderers, in the order of the scene's object table, and what was skipped (no transform, no
865    /// MeshFilter, no mesh).
866    pub fn renderers(&self, player: &Player) -> Result<(Vec<Renderer>, Skipped)> {
867        self.renderers_under(player, None)
868    }
869
870    /// The LODGroups of the GameObject `root` and its descendants (a prefab's), all of the file's with `None`, in the
871    /// order of the object table.
872    pub fn lod_groups_under(&self, player: &Player, root: Option<i64>) -> Result<Vec<LodGroup>> {
873        let root_tp = self.root_transform(root)?;
874        let file = self.file.as_str();
875        let f = player.file(file)?;
876        let r = |pid: i64| ObjRef {
877            file: file.to_owned(),
878            path_id: pid,
879        };
880        let mut out = Vec::new();
881        for o in f
882            .objects
883            .iter()
884            .filter(|o| player.class_name(&r(o.path_id)) == Some("LODGroup"))
885        {
886            let g = player.decode(&r(o.path_id))?;
887            let Some(tp) = self.transform_of(path_id(&g, "m_GameObject")?) else {
888                continue;
889            };
890            if root_tp.is_some_and(|root| self.relative_to(tp, root).is_none()) {
891                continue;
892            }
893            let enabled = flag(&g, "m_Enabled")?;
894            let mut levels = Vec::new();
895            for lod in values(&g, "m_LODs")? {
896                let mut level = Vec::new();
897                for x in values(lod, "renderers")? {
898                    let pid = path_id(x, "renderer")?;
899                    if pid != 0 {
900                        level.push(pid);
901                    }
902                }
903                levels.push(level);
904            }
905            out.push(LodGroup {
906                group: r(o.path_id),
907                path: self.path(tp),
908                enabled,
909                off: self.off_flags(tp, Some((o.path_id, enabled))),
910                levels,
911            });
912        }
913        Ok(out)
914    }
915
916    /// The mesh renderers of the GameObject `root` and its descendants (a prefab's), their matrices relative to
917    /// `root`; all of the file's with `None`.
918    pub fn renderers_under(&self, player: &Player, root: Option<i64>) -> Result<(Vec<Renderer>, Skipped)> {
919        let root_tp = self.root_transform(root)?;
920        let file = self.file.as_str();
921        let f = player.file(file)?;
922        let r = |pid: i64| ObjRef {
923            file: file.to_owned(),
924            path_id: pid,
925        };
926        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
927        let mut out = Vec::new();
928        let mut skipped = Skipped::new();
929        for o in f
930            .objects
931            .iter()
932            .filter(|o| matches!(class(o.path_id), "MeshRenderer" | "SkinnedMeshRenderer"))
933        {
934            let skinned = class(o.path_id) == "SkinnedMeshRenderer";
935            let mr = player.decode(&r(o.path_id))?;
936            let go = path_id(&mr, "m_GameObject")?;
937            let Some(tp) = self.transform_of(go) else {
938                bump(&mut skipped, "no transform");
939                continue;
940            };
941            let relative = match root_tp {
942                Some(root) => match self.relative_to(tp, root) {
943                    Some(m) => Some(m),
944                    None => continue,
945                },
946                None => None,
947            };
948            let off = self.off_flags(tp, Some((o.path_id, flag(&mr, "m_Enabled")?)));
949            let g = self.game_objects.get(&go).ok_or_else(|| {
950                invalid(format!(
951                    "GameObject {go} of renderer {} is not in the scene",
952                    o.path_id
953                ))
954            })?;
955            // a skinned renderer holds its mesh itself; a mesh renderer's is on its MeshFilter
956            let filter;
957            let mesh_ptr = if skinned {
958                mr.get("m_Mesh")
959                    .ok_or_else(|| invalid("SkinnedMeshRenderer without m_Mesh"))?
960            } else {
961                let Some(&mf) = g.components.iter().find(|&&c| class(c) == "MeshFilter") else {
962                    bump(&mut skipped, "no MeshFilter");
963                    continue;
964                };
965                filter = player.decode(&r(mf))?;
966                filter
967                    .get("m_Mesh")
968                    .ok_or_else(|| invalid("MeshFilter without m_Mesh"))?
969            };
970            let Some(mesh) = player.resolve(file, mesh_ptr)? else {
971                bump(&mut skipped, "null mesh");
972                continue;
973            };
974            let materials = values(&mr, "m_Materials")?
975                .iter()
976                .map(|m| player.resolve(file, m))
977                .collect::<Result<Vec<_>>>()?;
978            let sb = mr.get("m_StaticBatchInfo");
979            let first = sb
980                .and_then(|s| s.get("firstSubMesh"))
981                .and_then(Value::as_i64)
982                .unwrap_or(0);
983            let count = sb
984                .and_then(|s| s.get("subMeshCount"))
985                .and_then(Value::as_i64)
986                .unwrap_or(0);
987            let submeshes = if count > 0 {
988                let to_u32 = |n: i64, k: &str| {
989                    u32::try_from(n).map_err(|_| {
990                        invalid(format!("renderer {}: {k} {n} is not 32-bit unsigned", o.path_id))
991                    })
992                };
993                Some((to_u32(first, "firstSubMesh")?, to_u32(count, "subMeshCount")?))
994            } else {
995                None
996            };
997            let skin = if skinned {
998                let bone = |p: &Value| -> Result<Option<i64>> {
999                    let pid = p
1000                        .get("m_PathID")
1001                        .and_then(Value::as_i64)
1002                        .ok_or_else(|| invalid("a bone pointer without m_PathID"))?;
1003                    if p.get("m_FileID").and_then(Value::as_i64).unwrap_or(0) != 0 {
1004                        return Err(invalid(format!(
1005                            "SkinnedMeshRenderer {}: a bone in another file",
1006                            o.path_id
1007                        )));
1008                    }
1009                    Ok((pid != 0).then_some(pid))
1010                };
1011                Some(RendererSkin {
1012                    bones: values(&mr, "m_Bones")?.iter().map(bone).collect::<Result<_>>()?,
1013                    root_bone: match mr.get("m_RootBone") {
1014                        Some(p) => bone(p)?,
1015                        None => None,
1016                    },
1017                    blend_weights: match mr.get("m_BlendShapeWeights") {
1018                        Some(Value::Array(Array::F32(w))) => w.clone(),
1019                        Some(Value::Array(a)) if a.is_empty() => Vec::new(),
1020                        None => Vec::new(),
1021                        _ => return Err(invalid("SkinnedMeshRenderer: m_BlendShapeWeights not floats")),
1022                    },
1023                })
1024            } else {
1025                None
1026            };
1027            out.push(Renderer {
1028                renderer: r(o.path_id),
1029                game_object: go,
1030                path: self.path(tp),
1031                layer: g.layer,
1032                mesh,
1033                materials,
1034                world: if submeshes.is_some() {
1035                    IDENTITY
1036                } else {
1037                    match relative {
1038                        Some(m) => m,
1039                        None => self.world_of(tp)?,
1040                    }
1041                },
1042                submeshes,
1043                cast_shadows: mr.get("m_CastShadows").and_then(Value::as_i64).unwrap_or(1),
1044                receive_shadows: mr.get("m_ReceiveShadows").and_then(Value::as_i64).map(|v| v != 0),
1045                off,
1046                skin,
1047            });
1048        }
1049        Ok((out, skipped))
1050    }
1051
1052    /// The Rigidbody on Transform `tp`'s GameObject or its nearest ancestor's: `Some(kinematic)`.
1053    fn rigidbody(&self, player: &Player, tp: i64) -> Result<Option<bool>> {
1054        let r = |pid: i64| ObjRef {
1055            file: self.file.clone(),
1056            path_id: pid,
1057        };
1058        let mut t = tp;
1059        for _ in 0..=self.nodes.len() {
1060            let Some(n) = self.nodes.get(&t) else {
1061                return Ok(None);
1062            };
1063            if let Some(g) = self.game_objects.get(&n.game_object)
1064                && let Some(&rb) = g
1065                    .components
1066                    .iter()
1067                    .find(|&&c| player.class_name(&r(c)) == Some("Rigidbody"))
1068            {
1069                return Ok(Some(flag(&player.decode(&r(rb))?, "m_IsKinematic")?));
1070            }
1071            if n.father == 0 {
1072                return Ok(None);
1073            }
1074            t = n.father;
1075        }
1076        Ok(None)
1077    }
1078
1079    /// Every Box, Sphere, Capsule and Mesh collider of the GameObject `root` and its descendants (a prefab's, matrices
1080    /// relative to `root`), or of the whole file with `None`, in the order of the object table: those that do not
1081    /// collide (inactive, disabled, triggers) included, flagged.
1082    pub fn colliders_under(&self, player: &Player, root: Option<i64>) -> Result<Vec<ColliderComponent>> {
1083        let file = self.file.as_str();
1084        let f = player.file(file)?;
1085        let r = |pid: i64| ObjRef {
1086            file: file.to_owned(),
1087            path_id: pid,
1088        };
1089        let root_tp = self.root_transform(root)?;
1090        let v3 = |v: &Value, k: &str| -> Result<[f64; 3]> { vec3(v, k) };
1091        let mut out = Vec::new();
1092        for o in &f.objects {
1093            let class = player.class_name(&r(o.path_id)).unwrap_or("");
1094            if !matches!(
1095                class,
1096                "BoxCollider" | "SphereCollider" | "CapsuleCollider" | "MeshCollider"
1097            ) {
1098                continue;
1099            }
1100            let c = player.decode(&r(o.path_id))?;
1101            let go = path_id(&c, "m_GameObject")?;
1102            let Some(tp) = self.transform_of(go) else { continue };
1103            let world = match root_tp {
1104                Some(root) => match self.relative_to(tp, root) {
1105                    Some(m) => m,
1106                    None => continue,
1107                },
1108                None => self.world_of(tp)?,
1109            };
1110            let fl = |k: &str| -> Result<f64> { num(&c, k) };
1111            let shape = match class {
1112                "BoxCollider" => ColliderShape::Box {
1113                    center: v3(&c, "m_Center")?,
1114                    size: v3(&c, "m_Size")?,
1115                },
1116                "SphereCollider" => ColliderShape::Sphere {
1117                    center: v3(&c, "m_Center")?,
1118                    radius: fl("m_Radius")?,
1119                },
1120                "CapsuleCollider" => ColliderShape::Capsule {
1121                    center: v3(&c, "m_Center")?,
1122                    radius: fl("m_Radius")?,
1123                    height: fl("m_Height")?,
1124                    direction: match c.get("m_Direction").and_then(Value::as_i64).unwrap_or(1) {
1125                        d @ 0..=2 => usize::try_from(d).unwrap_or_default(),
1126                        d => {
1127                            return Err(invalid(format!(
1128                                "CapsuleCollider {}: m_Direction {d} is not 0, 1 or 2",
1129                                o.path_id
1130                            )));
1131                        }
1132                    },
1133                },
1134                _ => ColliderShape::Mesh {
1135                    mesh: match c.get("m_Mesh") {
1136                        Some(p) => player.resolve(file, p)?,
1137                        None => None,
1138                    },
1139                    convex: flag(&c, "m_Convex")?,
1140                },
1141            };
1142            out.push(ColliderComponent {
1143                collider: r(o.path_id),
1144                path: self.path(tp),
1145                layer: self.game_objects.get(&go).map_or(0, |g| g.layer),
1146                world,
1147                shape,
1148                off: self.off_flags(tp, Some((o.path_id, flag(&c, "m_Enabled")?))),
1149                trigger: c.get("m_IsTrigger").is_some_and(flag_value),
1150                physic_material: match c.get("m_Material") {
1151                    Some(p) => player.resolve(file, p)?,
1152                    None => None,
1153                },
1154                rigidbody: self.rigidbody(player, tp)?,
1155            });
1156        }
1157        Ok(out)
1158    }
1159
1160    /// The scene's RenderSettings: the Light it names as its sun (`m_Sun`; `None` when it names none: Unity then
1161    /// lights its procedural sky by the brightest directional light, a choice of the engine's, not the scene's) and
1162    /// its ambient lighting. `None` when the scene has no RenderSettings.
1163    pub fn render_settings(&self, player: &Player) -> Result<Option<SceneRenderSettings>> {
1164        let file = self.file.as_str();
1165        let f = player.file(file)?;
1166        let r = |pid: i64| ObjRef {
1167            file: file.to_owned(),
1168            path_id: pid,
1169        };
1170        let mut found = None;
1171        for o in &f.objects {
1172            if player.class_name(&r(o.path_id)) != Some("RenderSettings") {
1173                continue;
1174            }
1175            if found.is_some() {
1176                return Err(invalid(format!("{file}: more than one RenderSettings")));
1177            }
1178            let settings = player.decode(&r(o.path_id))?;
1179            let sun = match settings.get("m_Sun") {
1180                Some(p) => player.resolve(file, p)?,
1181                None => None,
1182            };
1183            let rgba = |k: &str| -> Result<[f32; 4]> {
1184                let c = settings
1185                    .get(k)
1186                    .ok_or_else(|| invalid(format!("{file}: RenderSettings without {k}")))?;
1187                Ok([num(c, "r")?, num(c, "g")?, num(c, "b")?, num(c, "a")?].map(crate::narrow))
1188            };
1189            let ambient = match settings.get("m_AmbientMode").and_then(Value::as_i64) {
1190                None => None,
1191                Some(mode) => Some(UnityAmbient {
1192                    mode,
1193                    sky: rgba("m_AmbientSkyColor")?,
1194                    equator: rgba("m_AmbientEquatorColor")?,
1195                    ground: rgba("m_AmbientGroundColor")?,
1196                    intensity: settings
1197                        .get("m_AmbientIntensity")
1198                        .and_then(Value::as_f32)
1199                        .ok_or_else(|| {
1200                            invalid(format!("{file}: RenderSettings without m_AmbientIntensity"))
1201                        })?,
1202                    probe: match settings.get("m_AmbientProbe") {
1203                        None => None,
1204                        Some(sh) => {
1205                            let mut out = [0.0f32; 27];
1206                            for (i, c) in out.iter_mut().enumerate() {
1207                                // the type tree names the coefficients `sh[ 0]` .. `sh[26]`
1208                                *c = sh.get(&format!("sh[{i:2}]")).and_then(Value::as_f32).ok_or_else(
1209                                    || {
1210                                        invalid(format!(
1211                                            "{file}: RenderSettings' m_AmbientProbe without sh[{i:2}]"
1212                                        ))
1213                                    },
1214                                )?;
1215                            }
1216                            Some(out)
1217                        }
1218                    },
1219                }),
1220            };
1221            // fog: absent from a type tree that does not state it
1222            let fog = match settings.get("m_Fog").and_then(Value::as_bool) {
1223                None => None,
1224                Some(enabled) => Some(UnityFog {
1225                    enabled,
1226                    mode: settings
1227                        .get("m_FogMode")
1228                        .and_then(Value::as_i64)
1229                        .ok_or_else(|| invalid(format!("{file}: RenderSettings without m_FogMode")))?,
1230                    color: rgba("m_FogColor")?,
1231                    density: crate::narrow(num(&settings, "m_FogDensity")?),
1232                    start: crate::narrow(num(&settings, "m_LinearFogStart")?),
1233                    end: crate::narrow(num(&settings, "m_LinearFogEnd")?),
1234                }),
1235            };
1236            found = Some(SceneRenderSettings { sun, ambient, fog });
1237        }
1238        Ok(found)
1239    }
1240
1241    /// The lights, whatever their state, in the order of the scene's object table.
1242    pub fn lights(&self, player: &Player) -> Result<Vec<LightComponent>> {
1243        let file = self.file.as_str();
1244        let f = player.file(file)?;
1245        let r = |pid: i64| ObjRef {
1246            file: file.to_owned(),
1247            path_id: pid,
1248        };
1249        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
1250        let mut out = Vec::new();
1251        for o in f.objects.iter().filter(|o| class(o.path_id) == "Light") {
1252            let l = player.decode(&r(o.path_id))?;
1253            let Some(tp) = self.transform_of(path_id(&l, "m_GameObject")?) else {
1254                continue;
1255            };
1256            let off = self.off_flags(tp, Some((o.path_id, flag(&l, "m_Enabled")?)));
1257            let fl = |k: &str| l.get(k).and_then(Value::as_f32);
1258            let int = |k: &str| l.get(k).and_then(Value::as_i64);
1259            let c = l.get("m_Color").ok_or_else(|| invalid("light: no m_Color"))?;
1260            let area = l.get("m_AreaSize");
1261            out.push(LightComponent {
1262                light: r(o.path_id),
1263                path: self.path(tp),
1264                world: self.world_of(tp)?,
1265                kind: int("m_Type").ok_or_else(|| invalid("light: no m_Type"))?,
1266                color: [num(c, "r")?, num(c, "g")?, num(c, "b")?, num(c, "a")?].map(crate::narrow),
1267                intensity: fl("m_Intensity").unwrap_or(1.0),
1268                range: fl("m_Range").unwrap_or(10.0),
1269                spot_angle: fl("m_SpotAngle").unwrap_or(30.0),
1270                inner_spot_angle: fl("m_InnerSpotAngle").unwrap_or(0.0),
1271                shadows: l
1272                    .at(&["m_Shadows", "m_Type"])
1273                    .and_then(Value::as_i64)
1274                    .unwrap_or(0),
1275                color_temperature: if l
1276                    .get("m_UseColorTemperature")
1277                    .is_some_and(|v| v.as_bool() == Some(true) || v.as_i64() == Some(1))
1278                {
1279                    fl("m_ColorTemperature")
1280                } else {
1281                    None
1282                },
1283                bounce_intensity: fl("m_BounceIntensity").unwrap_or(1.0),
1284                light_unit: int("m_LightUnit"),
1285                lux_at_distance: fl("m_LuxAtDistance"),
1286                area_size: [
1287                    area.and_then(|a| a.get("x"))
1288                        .and_then(Value::as_f32)
1289                        .unwrap_or(1.0),
1290                    area.and_then(|a| a.get("y"))
1291                        .and_then(Value::as_f32)
1292                        .unwrap_or(1.0),
1293                ],
1294                render_mode: int("m_RenderMode").unwrap_or(0),
1295                baked: l.at(&["m_BakingOutput", "isBaked"]).is_some_and(flag_value)
1296                    && l.at(&["m_BakingOutput", "lightmapBakeMode", "lightmapBakeType"])
1297                        .and_then(Value::as_i64)
1298                        == Some(2),
1299                culling_mask: culling_mask(&l, o.path_id)?,
1300                cookie: match l.get("m_Cookie") {
1301                    Some(p) => player.resolve(file, p)?,
1302                    None => None,
1303                },
1304                off,
1305            });
1306        }
1307        Ok(out)
1308    }
1309
1310    /// The cameras, whatever their state, in the order of the scene's object table.
1311    pub fn cameras(&self, player: &Player) -> Result<Vec<CameraComponent>> {
1312        let file = self.file.as_str();
1313        let f = player.file(file)?;
1314        let r = |pid: i64| ObjRef {
1315            file: file.to_owned(),
1316            path_id: pid,
1317        };
1318        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
1319        let mut out = Vec::new();
1320        for o in f.objects.iter().filter(|o| class(o.path_id) == "Camera") {
1321            let c = player.decode(&r(o.path_id))?;
1322            let Some(tp) = self.transform_of(path_id(&c, "m_GameObject")?) else {
1323                continue;
1324            };
1325            out.push(CameraComponent {
1326                camera: r(o.path_id),
1327                path: self.path(tp),
1328                world: self.world_of(tp)?,
1329                culling_mask: culling_mask(&c, o.path_id)?,
1330                target_texture: match c.get("m_TargetTexture") {
1331                    Some(p) => player.resolve(file, p)?,
1332                    None => None,
1333                },
1334                off: self.off_flags(tp, Some((o.path_id, flag(&c, "m_Enabled")?))),
1335            });
1336        }
1337        Ok(out)
1338    }
1339
1340    /// The terrains, whatever their state, in the order of the scene's object table, and what was skipped (no transform,
1341    /// no terrain data: a Terrain that names no TerrainData draws nothing).
1342    pub fn terrains(&self, player: &Player) -> Result<(Vec<TerrainComponent>, Skipped)> {
1343        let file = self.file.as_str();
1344        let f = player.file(file)?;
1345        let r = |pid: i64| ObjRef {
1346            file: file.to_owned(),
1347            path_id: pid,
1348        };
1349        let class = |pid: i64| player.class_name(&r(pid)).unwrap_or("");
1350        let mut out = Vec::new();
1351        let mut skipped = Skipped::new();
1352        for o in f.objects.iter().filter(|o| class(o.path_id) == "Terrain") {
1353            let t = player.decode(&r(o.path_id))?;
1354            let go = path_id(&t, "m_GameObject")?;
1355            let Some(tp) = self.transform_of(go) else {
1356                bump(&mut skipped, "no transform");
1357                continue;
1358            };
1359            let off = self.off_flags(tp, Some((o.path_id, flag(&t, "m_Enabled")?)));
1360            let pptr = |v: &Value, k: &str| -> Result<Option<ObjRef>> {
1361                match v.get(k) {
1362                    Some(p) => player.resolve(file, p),
1363                    None => Ok(None),
1364                }
1365            };
1366            let Some(data) = pptr(&t, "m_TerrainData")? else {
1367                bump(&mut skipped, "no terrain data");
1368                continue;
1369            };
1370            let collider = match self
1371                .game_objects
1372                .get(&go)
1373                .ok_or_else(|| {
1374                    invalid(format!(
1375                        "GameObject {go} of terrain {} is not in the scene",
1376                        o.path_id
1377                    ))
1378                })?
1379                .components
1380                .iter()
1381                .find(|&&c| class(c) == "TerrainCollider")
1382            {
1383                Some(&c) => {
1384                    let tc = player.decode(&r(c))?;
1385                    Some(TerrainColliderComponent {
1386                        collider: r(c),
1387                        enabled: flag(&tc, "m_Enabled")?,
1388                        data: pptr(&tc, "m_TerrainData")?,
1389                        material: pptr(&tc, "m_Material")?,
1390                        tree_colliders: flag(&tc, "m_EnableTreeColliders")?,
1391                    })
1392                }
1393                None => None,
1394            };
1395            out.push(TerrainComponent {
1396                terrain: r(o.path_id),
1397                path: self.path(tp),
1398                world: self.world_of(tp)?,
1399                data,
1400                material: pptr(&t, "m_MaterialTemplate")?,
1401                draw_heightmap: flag(&t, "m_DrawHeightmap")?,
1402                draw_trees: flag(&t, "m_DrawTreesAndFoliage")?,
1403                collider,
1404                off,
1405            });
1406        }
1407        Ok((out, skipped))
1408    }
1409}
1410
1411/// A collider's shape as stored, in its GameObject's space (Unity's frame).
1412#[derive(Clone, Debug, PartialEq)]
1413pub enum ColliderShape {
1414    /// A box collider.
1415    Box {
1416        /// `m_Center`: the box's centre in its GameObject's space.
1417        center: [f64; 3],
1418        /// `m_Size`: the full edge lengths along x, y and z (not half-extents), before the GameObject's scale.
1419        size: [f64; 3],
1420    },
1421    /// A sphere collider.
1422    Sphere {
1423        /// `m_Center`: the sphere's centre in its GameObject's space.
1424        center: [f64; 3],
1425        /// `m_Radius`, before the GameObject's scale.
1426        radius: f64,
1427    },
1428    /// A capsule collider.
1429    Capsule {
1430        /// `m_Center`: the capsule's centre in its GameObject's space.
1431        center: [f64; 3],
1432        /// `m_Radius`, before the GameObject's scale.
1433        radius: f64,
1434        /// `m_Height`, including the caps, before the GameObject's scale.
1435        height: f64,
1436        /// `m_Direction`: the axis the height runs along (0 x, 1 y, 2 z).
1437        direction: usize,
1438    },
1439    /// A mesh collider.
1440    Mesh {
1441        /// `m_Mesh`; `None` for a null pointer.
1442        mesh: Option<ObjRef>,
1443        /// `m_Convex`.
1444        convex: bool,
1445    },
1446}
1447
1448/// A collider as stored: its shape, its off flags, whether it is a trigger, its physic material, and the Rigidbody on
1449/// it or its nearest ancestor (`Some(true)`: kinematic).
1450#[derive(Clone, Debug, PartialEq)]
1451pub struct ColliderComponent {
1452    /// The collider object.
1453    pub collider: ObjRef,
1454    /// Its GameObject's path from the scene root, names joined by `/`.
1455    pub path: String,
1456    /// Its GameObject's layer.
1457    pub layer: u32,
1458    /// The matrix of its GameObject: world, or relative to `root` when one was given.
1459    pub world: Matrix,
1460    /// The shape.
1461    pub shape: ColliderShape,
1462    /// What keeps it from colliding; collides when empty.
1463    pub off: Vec<OffFlag>,
1464    /// `m_IsTrigger`.
1465    pub trigger: bool,
1466    /// `m_Material`.
1467    pub physic_material: Option<ObjRef>,
1468    /// The nearest Rigidbody: `Some(kinematic)`; `None` when there is none.
1469    pub rigidbody: Option<bool>,
1470}
1471
1472/// A light as stored: Unity's LightType (0 spot, 1 directional, 2 point, 3 rectangle, 4 disc), colour as stored
1473/// (gamma-encoded), intensity, range, spot angles in degrees (full cone), shadow type, colour temperature (Kelvin, when
1474/// used), bounce intensity, URP's light unit and lux-at-distance, area size, render mode and culling mask.
1475#[derive(Clone, Debug, PartialEq)]
1476pub struct LightComponent {
1477    /// The Light object.
1478    pub light: ObjRef,
1479    /// Its GameObject's path from the scene root, names joined by `/`.
1480    pub path: String,
1481    /// Its GameObject's world matrix.
1482    pub world: Matrix,
1483    /// `m_Type`.
1484    pub kind: i64,
1485    /// `m_Color`, RGBA.
1486    pub color: [f32; 4],
1487    /// `m_Intensity` (1 when not stored).
1488    pub intensity: f32,
1489    /// `m_Range` (10 when not stored).
1490    pub range: f32,
1491    /// `m_SpotAngle` (30 when not stored).
1492    pub spot_angle: f32,
1493    /// `m_InnerSpotAngle` (0 when not stored).
1494    pub inner_spot_angle: f32,
1495    /// `m_Shadows.m_Type` (0 when not stored).
1496    pub shadows: i64,
1497    /// `m_ColorTemperature`, when `m_UseColorTemperature` is on.
1498    pub color_temperature: Option<f32>,
1499    /// `m_BounceIntensity` (1 when not stored).
1500    pub bounce_intensity: f32,
1501    /// `m_LightUnit`, when stored.
1502    pub light_unit: Option<i64>,
1503    /// `m_LuxAtDistance`, when stored.
1504    pub lux_at_distance: Option<f32>,
1505    /// `m_AreaSize` (1 x 1 when not stored).
1506    pub area_size: [f32; 2],
1507    /// `m_RenderMode` (0 when not stored).
1508    pub render_mode: i64,
1509    /// Fully baked: `m_BakingOutput` says baked, as `Baked` (2), not mixed or realtime.
1510    pub baked: bool,
1511    /// `m_CullingMask`'s bits (all layers when not stored).
1512    pub culling_mask: u32,
1513    /// `m_Cookie`.
1514    pub cookie: Option<ObjRef>,
1515    /// What keeps it from shining; shines when empty.
1516    pub off: Vec<OffFlag>,
1517}
1518
1519/// A camera as stored: its culling mask (`m_CullingMask`, the layers it draws; all when absent) and the texture it
1520/// renders into (`m_TargetTexture`; `None`: it draws to the screen).
1521#[derive(Clone, Debug, PartialEq)]
1522pub struct CameraComponent {
1523    /// The Camera object.
1524    pub camera: ObjRef,
1525    /// Its GameObject's path from the scene root, names joined by `/`.
1526    pub path: String,
1527    /// Its GameObject's world matrix.
1528    pub world: Matrix,
1529    /// `m_CullingMask.m_Bits`: bit n set draws layer n (all 32 bits when not stored).
1530    pub culling_mask: u32,
1531    /// `m_TargetTexture`; `None` when it draws to the screen.
1532    pub target_texture: Option<ObjRef>,
1533    /// What keeps it from drawing; draws when empty.
1534    pub off: Vec<OffFlag>,
1535}
1536
1537/// A terrain's collider: its own TerrainData (which may differ from the drawn one), physic material, tree colliders.
1538#[derive(Clone, Debug, PartialEq)]
1539pub struct TerrainColliderComponent {
1540    /// The TerrainCollider object.
1541    pub collider: ObjRef,
1542    /// `m_Enabled`.
1543    pub enabled: bool,
1544    /// `m_TerrainData`.
1545    pub data: Option<ObjRef>,
1546    /// `m_Material`.
1547    pub material: Option<ObjRef>,
1548    /// `m_EnableTreeColliders`.
1549    pub tree_colliders: bool,
1550}
1551
1552/// A terrain, whatever its state: its GameObject's path and world matrix (a terrain is placed by its position only),
1553/// its data, material, collider and off flags (drawn when empty).
1554#[derive(Clone, Debug, PartialEq)]
1555pub struct TerrainComponent {
1556    /// The Terrain object.
1557    pub terrain: ObjRef,
1558    /// Its GameObject's path from the scene root, names joined by `/`.
1559    pub path: String,
1560    /// Its GameObject's world matrix.
1561    pub world: Matrix,
1562    /// `m_TerrainData`, read with [`crate::terrain::TerrainData::read`].
1563    pub data: ObjRef,
1564    /// `m_MaterialTemplate`.
1565    pub material: Option<ObjRef>,
1566    /// `m_DrawHeightmap`.
1567    pub draw_heightmap: bool,
1568    /// `m_DrawTreesAndFoliage`.
1569    pub draw_trees: bool,
1570    /// The terrain's TerrainCollider, if its GameObject has one.
1571    pub collider: Option<TerrainColliderComponent>,
1572    /// What keeps it from being drawn; drawn when empty.
1573    pub off: Vec<OffFlag>,
1574}
1575
1576/// A scene's RenderSettings as read ([`Scene::render_settings`]).
1577#[derive(Clone, Debug, PartialEq)]
1578pub struct SceneRenderSettings {
1579    /// `m_Sun`: the Light the scene names as its sun.
1580    pub sun: Option<ObjRef>,
1581    /// The ambient lighting; `None` when the RenderSettings state no `m_AmbientMode`.
1582    pub ambient: Option<UnityAmbient>,
1583    /// The fog; `None` when the RenderSettings state no `m_Fog`.
1584    pub fog: Option<UnityFog>,
1585}
1586
1587#[cfg(test)]
1588mod tests {
1589    use super::*;
1590
1591    fn chain(len: i64) -> BTreeMap<i64, Node> {
1592        (1..=len)
1593            .map(|id| {
1594                let node = Node {
1595                    game_object: id,
1596                    father: if id == 1 { 0 } else { id - 1 },
1597                    local: IDENTITY,
1598                    trs: ([0.0; 3], [0.0, 0.0, 0.0, 1.0], [1.0; 3]),
1599                };
1600                (id, node)
1601            })
1602            .collect()
1603    }
1604
1605    /// 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
1606    /// walked by every node's path.
1607    #[test]
1608    fn deep_and_looping_parents_are_refused() {
1609        assert!(check_depth(&chain(MAX_DEPTH as i64)).is_ok());
1610        let e = check_depth(&chain(MAX_DEPTH as i64 + 1)).unwrap_err().to_string();
1611        assert!(e.contains("deeper than 1024"), "{e}");
1612        let mut looped = chain(3);
1613        looped.get_mut(&1).unwrap().father = 3;
1614        assert!(check_depth(&looped).is_err());
1615        // a father the scene does not hold ends the chain
1616        let mut orphan = chain(3);
1617        orphan.get_mut(&1).unwrap().father = 99;
1618        assert!(check_depth(&orphan).is_ok());
1619    }
1620
1621    fn scene_of(nodes: BTreeMap<i64, Node>) -> Scene {
1622        Scene {
1623            file: "level0".into(),
1624            game_objects: BTreeMap::new(),
1625            transform_of: nodes.iter().map(|(&tp, n)| (n.game_object, tp)).collect(),
1626            nodes,
1627            world: std::sync::Mutex::new(BTreeMap::new()),
1628        }
1629    }
1630
1631    /// A query limited to a GameObject without a Transform is an error for every query that takes a root, the LODGroup
1632    /// query among them, not an unlimited one.
1633    #[test]
1634    fn a_root_without_a_transform_is_refused() {
1635        let scene = scene_of(chain(2));
1636        assert_eq!(scene.root_transform(None).unwrap(), None);
1637        assert_eq!(scene.root_transform(Some(2)).unwrap(), Some(2));
1638        let e = scene.root_transform(Some(77)).unwrap_err().to_string();
1639        assert!(e.contains("GameObject 77 has no transform"), "{e}");
1640    }
1641
1642    /// A panic while the world cache was locked leaves a usable cache, not a panic in every later call.
1643    #[test]
1644    fn a_poisoned_world_cache_is_still_read() {
1645        let scene = std::sync::Arc::new(scene_of(chain(3)));
1646        let held = scene.clone();
1647        let _ = std::thread::spawn(move || {
1648            let _guard = held.world.lock().unwrap();
1649            panic!("poison the cache");
1650        })
1651        .join();
1652        assert!(scene.world.is_poisoned());
1653        assert_eq!(scene.world_of(3).unwrap(), IDENTITY);
1654    }
1655
1656    fn with(k: &str, v: Value) -> Value {
1657        Value::Struct(vec![(k.into(), v)])
1658    }
1659
1660    /// Layers and culling masks outside 32 bits are refused, not wrapped to another layer set.
1661    #[test]
1662    fn layers_and_masks_are_not_cast() {
1663        assert_eq!(layer_of(&with("m_Layer", Value::Int(5))).unwrap(), 5);
1664        assert_eq!(layer_of(&Value::Struct(vec![])).unwrap(), 0);
1665        for bad in [-1, 1 << 32] {
1666            assert!(layer_of(&with("m_Layer", Value::Int(bad))).is_err());
1667        }
1668        let mask = |bits| culling_mask(&with("m_CullingMask", with("m_Bits", Value::Int(bits))), 9);
1669        assert_eq!(mask(0xffff_fff0).unwrap(), 0xffff_fff0);
1670        assert_eq!(mask(-1).unwrap(), u32::MAX);
1671        assert_eq!(culling_mask(&Value::Struct(vec![]), 9).unwrap(), u32::MAX);
1672        let e = mask(1 << 32).unwrap_err().to_string();
1673        assert!(e.contains("object 9") && e.contains("4294967296"), "{e}");
1674        assert!(mask(-(1 << 40)).is_err());
1675    }
1676}