Skip to content

Type trees

An object in a serialized file is a run of bytes whose layout is given by its class’s type tree: a list of nodes, each with a type name, a field name, a byte size (or −1 when variable), a version and meta flags. A reader that has the tree decodes the object into named fields. A Unity player build strips the trees from its files (the has-type-tree byte is 0), so a reader needs the layouts from outside; bundles keep their trees. Observed · SkateBIRD, Sledding, PEAK, Big Walk

The layout of each built-in class changes between Unity versions, so a type tree package (TPK) lists the class layouts for every Unity version and a reader looks the class up by the file’s version string. AssetRipper publishes one. A package stores its tree data uncompressed or LZ4-compressed (format versions 1 and 2); LZMA- and Brotli-compressed packages have to be re-packed before a reader that only does LZ4 can use them. Observed

Game Unity version Package coverage Confidence
SkateBIRD 2019.4.20f1 AssetRipper’s package covers this version exactly Observed · SkateBIRD
Sledding Game 6000.3.12f1 AssetRipper’s package has a layout for the version; UnityPy 1.25.4’s bundled lzma.tpk has no 6000.3.12f1 entry Observed · Sledding

UnityPy 1.25.4’s package gives the 6000.3.11f1 PlayerSettings layout 936 bytes. Sledding Game’s globalgamemanagers PlayerSettings object is 940 bytes, so the layout is 4 bytes short of the data, and UnityPy’s own read check reports the mismatch. Observed · Sledding PEAK’s 6000.3.15f1 PlayerSettings is 896 bytes against a layout of 892: the same 4-byte shortfall. Observed · PEAK Big Walk’s 6000.3.17f1 PlayerSettings has it too. Observed · Big Walk A decoder that requires a tree to cover an object exactly refuses that one object and decodes the rest. Observed · Sledding, PEAK

An embedded tree stores each node’s type name and field name as a 32-bit offset. With the top bit clear the offset is into the tree’s own string buffer; with it set, the low 31 bits are an offset into the engine’s built-in common string table, a run of NUL-terminated names (AABB, Array, m_Name, Vector3f, …) that every tree of the Unity version shares. Sourced · UnityPy The table grows between versions, always by appending, so an offset keeps its meaning. The type tree package lists it per version: 2019.4 has 107 strings (1,152 bytes), 2022.3 109 (1,169 bytes), 6000.0 110 (1,188 bytes) and 6000.3 112 (1,209 bytes), the last adding fixed_array at offset 1,188 and EntityId at offset 1,200. The package AssetRipper publishes and the one UnityPy ships give the same table for each of those versions. Observed The embedded trees of the games use it: the 50 Sledding Game bundles (6000.3.12f1) use 52 distinct offsets, at most 1,161, and PEAK’s bundles (6000.3.15f1; the 195 serialized files up to 40 MB of its seven level bundles, and its maps bundle) use 65 distinct offsets, at most 1,200 (EntityId, six times). Every offset resolves against the 6000.3 table, and a reader that carries only the 6000.0 table (110 strings) cannot resolve PEAK’s offset 1,200. Observed · Sledding, PEAK Across all 466 files that store trees (PEAK’s 416 level files and Sledding Game’s 50) every common-string offset of every one of the 83,867 trees resolves against the package’s table for the file’s Unity version, and the resulting names equal an independent reader’s, node for node. Proven · PEAK, Sledding No loose player file of SkateBIRD (31), PEAK (13) or Sledding Game (7) stores a tree, so none uses the table. Proven · SkateBIRD, Sledding, PEAK Reading the seven serialized files of Big Walk’s data.unity3d up to 40 MB looks up no common string. Observed · Big Walk

Decoding walks the tree against the object’s bytes. A reader that applies these rules decodes the objects of Sledding Game’s serialized files to exactly their stored sizes, except the one object whose layout is short. Observed · Sledding

Node Bytes Confidence
a primitive by its type name (SInt32, float, bool, …) Sourced · UnityPy
string a 32-bit length, the UTF-8 bytes, then padding to 4 bytes Sourced · UnityPy
TypelessData a 32-bit length and the bytes Sourced · UnityPy
pair its two children, first then second Sourced · UnityPy
a node whose first child is Array a vector: a 32-bit count, then the elements, each described by the array’s second child Sourced · UnityPy
any other node a struct: its children in order Sourced · UnityPy
a node whose meta flag has the align bit (0x4000) the position is aligned to 4 bytes after the node, counted from the object’s start Sourced · UnityPy

An Array child’s own flag aligns its vector. Sourced · UnityPy A decoder can bound a vector’s count by the bytes left (a count whose elements would not fit is an error, not an allocation) because each node type has a least size. Inferred

The tree a bundled file stores for a class other than MonoBehaviour equals the type tree package’s release tree for the file’s Unity version, node for node, for every such type in PEAK’s level bundles and Sledding Game’s Addressables bundles (8,367 and 173 types, none differs). Proven · PEAK, Sledding A tree stored for a MonoBehaviour is that script’s own, with its fields after the base fields; the package has only the base fields. Proven · PEAK, Sledding Decoding every object of the 466 files by its stored tree consumes exactly the object’s bytes for 136,275,632 of PEAK’s 136,474,698 objects (2 have no tree: the manifest bundle’s) and 1,333 of Sledding Game’s 1,427. Proven · PEAK, Sledding All the others, 199,064 and 94, are MonoBehaviours with a [SerializeReference] field (a managed reference node in their tree), which a decoder that has only the tree cannot read: it needs the file’s reference types. Proven · PEAK, Sledding

A MonoBehaviour’s tree covers only its base fields (m_GameObject, m_Enabled, m_Script, m_Name); the script’s own fields follow in the object, and a stripped build has no tree for them. The bytes are kept raw and decoded by a layout measured from the data, see script fields. Observed

A class declared with [SerializeReference] needs the file’s reference type trees, which a stripped build does not have. Sourced · UnityPy

The same field has different names in different Unity versions, so a layout is a family of names per version, not one set. The physic material is an example. Observed

Field SkateBIRD (2019.4) Sledding Game (6000.3)
dynamic friction dynamicFriction m_DynamicFriction
static friction staticFriction m_StaticFriction
bounciness bounciness m_Bounciness
friction combine frictionCombine m_FrictionCombine
bounce combine bounceCombine m_BounceCombine

Observed · SkateBIRD, Sledding A reader strict about one family of names rejects the other game’s objects. Observed · SkateBIRD, Sledding

Do the layouts hold on other minor versions

The 6000.3 layouts here are those of 6000.3.12f1 (Sledding), 6000.3.15f1 (PEAK) and 6000.3.17f1 (Big Walk). A package entry exists for each as far as the readers used here decode their files, but a layout change between two 6000.3 patch versions is not excluded.