unity_typetree_gen/lib.rs
1//! Generate Unity [`TypeTreeNode`] trees for `MonoBehaviour` and `ScriptableObject`
2//! scripts directly from a game's .NET assemblies.
3//!
4//! See [`AssemblyTypeTreeGenerator`] for a usage example.
5mod assembly;
6mod generator;
7mod template;
8
9pub use assembly::{AssemblyTypeTreeGenerator, Loader};
10pub use rabex::UnityVersion;
11pub use rabex::typetree::TypeTreeNode;
12
13use template::TemplateField;
14
15/// Align flag set on a node whose value is 4-byte aligned after writing.
16const ALIGN_FLAG: i32 = 0x4000;
17
18/// Wrap a script's serialized fields in the `Base` root (type = the class name)
19/// and build the [`TypeTreeNode`] tree directly.
20pub(crate) fn assemble(children: Vec<TemplateField>, type_name: &str) -> TypeTreeNode {
21 let base = TemplateField {
22 name: "Base".to_string(),
23 ty: type_name.to_string(),
24 aligned: false,
25 children,
26 };
27 let mut root = build_node(&base);
28 // Unity always stores the MonoBehaviour/ScriptableObject root as variable-size (-1), even when every field is fixed.
29 root.m_ByteSize = -1;
30 root
31}
32
33fn build_node(field: &TemplateField) -> TypeTreeNode {
34 let children: Vec<TypeTreeNode> = field.children.iter().map(build_node).collect();
35 let mut node = TypeTreeNode {
36 m_Type: field.ty.clone(),
37 m_Name: field.name.clone(),
38 m_MetaFlag: Some(if field.aligned { ALIGN_FLAG } else { 0 }),
39 children,
40 ..Default::default()
41 };
42 node.m_ByteSize = byte_size(&node);
43 // Unity aligns an `Array` node iff its element is a scalar or a string (validated against Unity's embedded type
44 // trees); arrays of structs/classes/PPtrs are not aligned. Decided here via rabex's `classify()` on the element.
45 if node.m_Type == "Array" {
46 // m_TypeFlags=1 marks this node as an array; without it Unity won't iterate it (re-reads element 0 for every
47 // entry, e.g. a vector<string> comes back as N copies of the first string).
48 node.m_TypeFlags = 1;
49 node.m_MetaFlag = Some(if array_element_aligned(&node) { ALIGN_FLAG } else { 0 });
50 }
51 node
52}
53
54/// Whether an `Array` node's element (`data` child) is the kind Unity 4-byte aligns: a scalar, a `string`, or a pure
55/// POD struct (every field transitively a scalar — e.g. Vector3f/ColorRGBA/Quaternionf/Keyframe). Arrays of structs
56/// that hold a PPtr or string (PPtr itself, or custom classes) are NOT aligned. Validated against Unity's embedded trees.
57fn array_element_aligned(array_node: &TypeTreeNode) -> bool {
58 array_node
59 .children
60 .iter()
61 .find(|c| c.m_Name == "data")
62 .is_some_and(|data| matches!(data.classify(), rabex::typetree::TypetreeNodeKind::String) || is_pod_value_type(data))
63}
64
65/// A scalar, or a struct (not a PPtr) whose every field is a POD value type AND whose serialized size is 4-aligned.
66/// Unity 4-aligns arrays of its built-in value types (Vector3f, ColorRGBA, Keyframe, … — all 4-byte multiples). The
67/// size%4 guard is what keeps this SAFE: it never aligns a struct whose array elements aren't already 4-aligned (a
68/// custom struct ending in a bool/byte), so we never insert padding the data doesn't have → never desync. Aligning a
69/// 4-aligned struct's array is at worst a redundant no-op.
70fn is_pod_value_type(node: &TypeTreeNode) -> bool {
71 use rabex::typetree::TypetreeNodeKind::*;
72 match node.classify() {
73 Bool | U8 | U16 | U32 | U64 | I8 | I16 | I32 | I64 | Float | Double | Char => true,
74 Struct => {
75 !node.m_Type.starts_with("PPtr")
76 && node.m_ByteSize > 0
77 && node.m_ByteSize % 4 == 0
78 && node.children.iter().all(is_pod_value_type)
79 }
80 _ => false,
81 }
82}
83
84/// Unity's `m_ByteSize`: the fixed width of a primitive, -1 for dynamic containers (string/Array/vector/map/…) or any
85/// struct with a variable-size member, else the sum of the children's sizes (no inter-field padding — alignment is
86/// carried by `m_MetaFlag`, not the size). Primitive widths come from rabex's own `classify()`, so every serialized
87/// type-name alias (`short`, `long long`, `FileSize`, `Type*`, …) is covered. Matches Unity's embedded type trees.
88fn byte_size(node: &TypeTreeNode) -> i32 {
89 use rabex::typetree::TypetreeNodeKind::*;
90 match node.classify() {
91 Bool | U8 | I8 | Char => 1,
92 U16 | I16 => 2,
93 U32 | I32 | Float => 4,
94 U64 | I64 | Double => 8,
95 String | Map | Array | Untyped => -1,
96 _ => {
97 let mut total = 0i32;
98 for c in &node.children {
99 if c.m_ByteSize < 0 {
100 return -1;
101 }
102 total += c.m_ByteSize;
103 }
104 total
105 }
106 }
107}