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