Skip to main content

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}