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