1use 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#[derive(Clone, Debug, PartialEq, Serialize)]
24pub struct UnityBehaviour {
25 pub behaviour: ObjRef,
27 pub script: String,
29 pub enabled: bool,
31 #[serde(skip)]
33 pub data: Vec<u8>,
34}
35
36#[derive(Clone, Debug, PartialEq, Serialize)]
43pub struct UnityAmbient {
44 pub mode: i64,
46 pub sky: [f32; 4],
48 pub equator: [f32; 4],
50 pub ground: [f32; 4],
52 pub intensity: f32,
54 pub probe: Option<[f32; 27]>,
56}
57
58#[derive(Clone, Debug, PartialEq, Serialize)]
62pub struct UnityFog {
63 pub enabled: bool,
65 pub mode: i64,
67 pub color: [f32; 4],
69 pub density: f32,
71 pub start: f32,
73 pub end: f32,
75}
76
77pub type Matrix = [[f64; 4]; 4];
79
80pub 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
88pub 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
103pub 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 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
153fn 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#[derive(Clone, Debug)]
188pub struct GameObject {
189 pub name: String,
191 pub active: bool,
193 pub layer: u32,
195 pub components: Vec<i64>,
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
202pub enum OffFlag {
203 Inactive(i64),
205 Disabled(i64),
207}
208
209#[derive(Clone, Debug)]
210struct Node {
211 game_object: i64,
212 father: i64,
213 local: Matrix,
214 trs: ([f64; 3], [f64; 4], [f64; 3]),
217}
218
219#[derive(Clone, Debug, PartialEq)]
222pub struct SceneJoint {
223 pub name: String,
225 pub father: Option<i64>,
227 pub translation: [f64; 3],
229 pub rotation: [f64; 4],
231 pub scale: [f64; 3],
233}
234
235#[derive(Clone, Debug, PartialEq)]
238pub struct Animator {
239 pub animator: ObjRef,
241 pub game_object: i64,
243 pub path: String,
245 pub controller: Option<ObjRef>,
247 pub avatar: Option<ObjRef>,
249 pub enabled: bool,
251 pub apply_root_motion: bool,
253}
254
255#[derive(Clone, Debug, PartialEq)]
257pub struct Character {
258 pub animator: Animator,
260 pub skinned_renderers: usize,
262}
263
264#[derive(Clone, Debug, PartialEq)]
267pub struct RendererSkin {
268 pub bones: Vec<Option<i64>>,
270 pub root_bone: Option<i64>,
272 pub blend_weights: Vec<f32>,
274}
275
276#[derive(Clone, Debug, PartialEq)]
278pub struct Renderer {
279 pub renderer: ObjRef,
281 pub game_object: i64,
283 pub path: String,
285 pub layer: u32,
287 pub mesh: ObjRef,
289 pub materials: Vec<Option<ObjRef>>,
291 pub world: Matrix,
293 pub submeshes: Option<(u32, u32)>,
295 pub cast_shadows: i64,
297 pub receive_shadows: Option<bool>,
299 pub off: Vec<OffFlag>,
301 pub skin: Option<RendererSkin>,
303}
304
305#[derive(Clone, Debug, PartialEq)]
308pub struct LodGroup {
309 pub group: ObjRef,
311 pub path: String,
313 pub enabled: bool,
315 pub off: Vec<OffFlag>,
317 pub levels: Vec<Vec<i64>>,
319}
320
321const MAX_DEPTH: usize = 1024;
323
324pub type Skipped = std::collections::BTreeMap<&'static str, u64>;
326
327#[derive(Debug)]
330pub struct Scene {
331 pub file: String,
333 pub game_objects: BTreeMap<i64, GameObject>,
335 nodes: BTreeMap<i64, Node>,
336 transform_of: BTreeMap<i64, i64>,
338 world: std::sync::Mutex<BTreeMap<i64, Matrix>>,
339}
340
341fn 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
350fn 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 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 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 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 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 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 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 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 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 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 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 pub fn transform_of(&self, game_object: i64) -> Option<i64> {
698 self.transform_of.get(&game_object).copied()
699 }
700
701 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 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 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 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 pub fn animators(&self, player: &Player) -> Result<Vec<Animator>> {
797 self.animators_under(player, None)
798 }
799
800 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 pub fn local_of(&self, tp: i64) -> Option<Matrix> {
842 self.nodes.get(&tp).map(|n| n.local)
843 }
844
845 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 pub fn renderers(&self, player: &Player) -> Result<(Vec<Renderer>, Skipped)> {
867 self.renderers_under(player, None)
868 }
869
870 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 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 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 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 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 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 *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 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 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 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 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#[derive(Clone, Debug, PartialEq)]
1413pub enum ColliderShape {
1414 Box {
1416 center: [f64; 3],
1418 size: [f64; 3],
1420 },
1421 Sphere {
1423 center: [f64; 3],
1425 radius: f64,
1427 },
1428 Capsule {
1430 center: [f64; 3],
1432 radius: f64,
1434 height: f64,
1436 direction: usize,
1438 },
1439 Mesh {
1441 mesh: Option<ObjRef>,
1443 convex: bool,
1445 },
1446}
1447
1448#[derive(Clone, Debug, PartialEq)]
1451pub struct ColliderComponent {
1452 pub collider: ObjRef,
1454 pub path: String,
1456 pub layer: u32,
1458 pub world: Matrix,
1460 pub shape: ColliderShape,
1462 pub off: Vec<OffFlag>,
1464 pub trigger: bool,
1466 pub physic_material: Option<ObjRef>,
1468 pub rigidbody: Option<bool>,
1470}
1471
1472#[derive(Clone, Debug, PartialEq)]
1476pub struct LightComponent {
1477 pub light: ObjRef,
1479 pub path: String,
1481 pub world: Matrix,
1483 pub kind: i64,
1485 pub color: [f32; 4],
1487 pub intensity: f32,
1489 pub range: f32,
1491 pub spot_angle: f32,
1493 pub inner_spot_angle: f32,
1495 pub shadows: i64,
1497 pub color_temperature: Option<f32>,
1499 pub bounce_intensity: f32,
1501 pub light_unit: Option<i64>,
1503 pub lux_at_distance: Option<f32>,
1505 pub area_size: [f32; 2],
1507 pub render_mode: i64,
1509 pub baked: bool,
1511 pub culling_mask: u32,
1513 pub cookie: Option<ObjRef>,
1515 pub off: Vec<OffFlag>,
1517}
1518
1519#[derive(Clone, Debug, PartialEq)]
1522pub struct CameraComponent {
1523 pub camera: ObjRef,
1525 pub path: String,
1527 pub world: Matrix,
1529 pub culling_mask: u32,
1531 pub target_texture: Option<ObjRef>,
1533 pub off: Vec<OffFlag>,
1535}
1536
1537#[derive(Clone, Debug, PartialEq)]
1539pub struct TerrainColliderComponent {
1540 pub collider: ObjRef,
1542 pub enabled: bool,
1544 pub data: Option<ObjRef>,
1546 pub material: Option<ObjRef>,
1548 pub tree_colliders: bool,
1550}
1551
1552#[derive(Clone, Debug, PartialEq)]
1555pub struct TerrainComponent {
1556 pub terrain: ObjRef,
1558 pub path: String,
1560 pub world: Matrix,
1562 pub data: ObjRef,
1564 pub material: Option<ObjRef>,
1566 pub draw_heightmap: bool,
1568 pub draw_trees: bool,
1570 pub collider: Option<TerrainColliderComponent>,
1572 pub off: Vec<OffFlag>,
1574}
1575
1576#[derive(Clone, Debug, PartialEq)]
1578pub struct SceneRenderSettings {
1579 pub sun: Option<ObjRef>,
1581 pub ambient: Option<UnityAmbient>,
1583 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 #[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 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 #[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 #[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 #[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}