unity_formats/lib.rs
1//! Readers for the data files of Unity players: the SerializedFiles a built game keeps its scenes and assets in, and
2//! the objects inside them (scenes, meshes, textures, materials, shader programs, terrains, Animator data).
3//!
4//! A built Unity game keeps its scenes and assets in *SerializedFiles* (`globalgamemanagers`, `*.assets`, `levelN`),
5//! with the bulk data (textures, meshes, audio) in `.resS` stream files beside them. A player build strips the type
6//! trees that say how each class is laid out, so objects are decoded with a *type tree package* (a TPK file) that the
7//! caller supplies. The crate was written against Unity 2019.4 and Unity 6 players (SkateBIRD, Sledding Game, PEAK);
8//! what it refuses to read is named in each module.
9//!
10//! ```no_run
11//! use std::path::Path;
12//! use unity_formats::{TypeTreePackage, listing::list, mesh::UnityMeshData, player::Player};
13//!
14//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
15//! let package = TypeTreePackage::parse(&std::fs::read("typetrees.tpk")?)?;
16//! let player = Player::open(Path::new("Game_Data"), package)?;
17//! for scene in player.scenes()? {
18//! println!("{}: {}", scene.file, scene.name);
19//! }
20//! for entry in list(&player, Some(&["Mesh"]))?.entries.iter().take(3) {
21//! let mesh = UnityMeshData::read(&player, &entry.object)?;
22//! println!("{}: {} vertices", mesh.name, mesh.vertex_count);
23//! }
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! The modules:
29//!
30//! - [`bundle`]: UnityFS bundles (AssetBundles, Addressables content, a player's `data.unity3d`): the block table and
31//! directory, nodes read by range.
32//! - [`serialized`]: the SerializedFile container: header, types, the object table, script references, external
33//! files. Strict and version-gated: only container versions verified against real data are read.
34//! - [`tpk`]: a type tree package (the TPK format), the class layouts per Unity version that a player build strips
35//! from its files. The caller supplies the package; none is bundled.
36//! - [`typetree`]: decoding an object's bytes by its class's type tree into a [`Value`] tree.
37//! - [`value`]: the decoded value tree.
38//! - [`version`]: Unity version strings.
39//! - [`resource`]: streamed resources (`.resS`), the bulk data that objects point at.
40//! - [`player`]: a player's data folder: its files, pointers resolved across them, its quality levels, its build's
41//! scene list and render pipeline.
42//! - [`scene`]: a scene's hierarchy (GameObjects, Transforms composed to world matrices) and its renderers, lights,
43//! cameras, colliders and terrains, with the scene's ambient and fog settings.
44//! - [`mesh`]: meshes (vertex streams, compressed meshes, index buffers, submeshes, skins and blend shapes).
45//! - [`texture`]: textures (formats, mip chains, sampler state).
46//! - [`program`]: a shader's compiled programs (variants, constant-buffer layouts, bind slots, code).
47//! - [`animator`]: Avatars and AnimatorControllers (state machines, blend trees, transitions).
48//! - [`clip`]: AnimationClips (curves decoded to keys with tangents).
49//! - [`listing`]: a player's objects by class and name, read from the object tables.
50//! - [`material`]: materials as stored (shader, saved properties, keywords).
51//! - [`shader`]: a Shader's render state as stored (subshaders, passes, blend, depth and stencil state).
52//! - [`terrain`]: terrains as stored (heightmap, holes, splat layers and alphamaps, trees).
53//! - [`pipeline`]: the lighting and shadow settings of a Universal Render Pipeline asset.
54//!
55//! Reads are bounds-checked, and a file the readers do not understand is refused with an [`Error`] that names what
56//! was found and where.
57#![deny(clippy::disallowed_methods, clippy::disallowed_types, missing_docs)]
58// Every module reads stored bytes (the files themselves, or values decoded from them), so a panic is a defect: indexes
59// are `get`s, sums are checked or saturating, and numbers are narrowed with `try_from`. Tests index freely.
60#![cfg_attr(
61 not(test),
62 deny(
63 clippy::indexing_slicing,
64 clippy::arithmetic_side_effects,
65 clippy::cast_possible_truncation
66 )
67)]
68
69pub mod animator;
70pub mod bundle;
71pub mod clip;
72pub mod listing;
73pub mod material;
74pub mod mesh;
75pub mod pipeline;
76pub mod player;
77pub mod program;
78mod reader;
79pub mod resource;
80pub mod scene;
81pub mod serialized;
82pub mod shader;
83pub mod terrain;
84pub mod texture;
85pub mod tpk;
86pub mod typetree;
87pub mod value;
88pub mod version;
89
90pub use serialized::{ObjectInfo, SerializedFile};
91pub use tpk::TypeTreePackage;
92pub use typetree::TypeTree;
93pub use value::Value;
94pub use version::UnityVersion;
95
96/// The README's code blocks, compiled as doc tests so they cannot drift from the API.
97#[cfg(doctest)]
98#[doc = include_str!("../README.md")]
99struct ReadmeDoctests;
100
101/// Why a read was refused.
102#[derive(Debug, thiserror::Error)]
103pub enum Error {
104 /// The bytes are not what the format allows (a length past the data, a bad tag, a broken reference).
105 #[error("{0}")]
106 Invalid(String),
107 /// The bytes are well formed but use something the reader does not read (an unverified container version, a
108 /// feature of a newer Unity).
109 #[error("not supported: {0}")]
110 Unsupported(String),
111 /// A file could not be read from disk.
112 #[error("{}: {source}", path.display())]
113 Io {
114 /// The file.
115 path: std::path::PathBuf,
116 /// What the operating system said.
117 source: std::io::Error,
118 },
119}
120
121/// A read's result.
122pub type Result<T, E = Error> = std::result::Result<T, E>;
123
124/// A double narrowed to the nearest `f32`: the rounding is the intent, so the truncation lint does not apply.
125#[allow(
126 clippy::cast_possible_truncation,
127 reason = "float narrowing rounds to the nearest f32 by design"
128)]
129pub(crate) fn narrow(x: f64) -> f32 {
130 x as f32
131}
132
133/// Count one more of `key` in `counts`, stopping at `u64::MAX`.
134pub(crate) fn bump<K: Ord>(counts: &mut std::collections::BTreeMap<K, u64>, key: K) {
135 let n = counts.entry(key).or_default();
136 *n = n.saturating_add(1);
137}
138
139pub(crate) fn invalid(msg: impl Into<String>) -> Error {
140 Error::Invalid(msg.into())
141}