unity-formats 0.1.0

Reads Unity games' data files: SerializedFiles, scenes, meshes, textures, shader programs and Animator data
Documentation
//! Readers for the data files of Unity players: the SerializedFiles a built game keeps its scenes and assets in, and
//! the objects inside them (scenes, meshes, textures, materials, shader programs, terrains, Animator data).
//!
//! A built Unity game keeps its scenes and assets in *SerializedFiles* (`globalgamemanagers`, `*.assets`, `levelN`),
//! with the bulk data (textures, meshes, audio) in `.resS` stream files beside them. A player build strips the type
//! trees that say how each class is laid out, so objects are decoded with a *type tree package* (a TPK file) that the
//! caller supplies. The crate was written against Unity 2019.4 and Unity 6 players (SkateBIRD, Sledding Game, PEAK);
//! what it refuses to read is named in each module.
//!
//! ```no_run
//! use std::path::Path;
//! use unity_formats::{TypeTreePackage, listing::list, mesh::UnityMeshData, player::Player};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let package = TypeTreePackage::parse(&std::fs::read("typetrees.tpk")?)?;
//! let player = Player::open(Path::new("Game_Data"), package)?;
//! for scene in player.scenes()? {
//!     println!("{}: {}", scene.file, scene.name);
//! }
//! for entry in list(&player, Some(&["Mesh"]))?.entries.iter().take(3) {
//!     let mesh = UnityMeshData::read(&player, &entry.object)?;
//!     println!("{}: {} vertices", mesh.name, mesh.vertex_count);
//! }
//! # Ok(())
//! # }
//! ```
//!
//! The modules:
//!
//! - [`serialized`]: the SerializedFile container: header, types, the object table, script references, external
//!   files. Strict and version-gated: only container versions verified against real data are read.
//! - [`tpk`]: a type tree package (the TPK format), the class layouts per Unity version that a player build strips
//!   from its files. The caller supplies the package; none is bundled.
//! - [`typetree`]: decoding an object's bytes by its class's type tree into a [`Value`] tree.
//! - [`value`]: the decoded value tree.
//! - [`version`]: Unity version strings.
//! - [`resource`]: streamed resources (`.resS`), the bulk data that objects point at.
//! - [`player`]: a player's data folder: its files, pointers resolved across them, its quality levels, its build's
//!   scene list and render pipeline.
//! - [`scene`]: a scene's hierarchy (GameObjects, Transforms composed to world matrices) and its renderers, lights,
//!   cameras, colliders and terrains, with the scene's ambient and fog settings.
//! - [`mesh`]: meshes (vertex streams, compressed meshes, index buffers, submeshes, skins and blend shapes).
//! - [`texture`]: textures (formats, mip chains, sampler state).
//! - [`program`]: a shader's compiled programs (variants, constant-buffer layouts, bind slots, code).
//! - [`animator`]: Avatars and AnimatorControllers (state machines, blend trees, transitions).
//! - [`clip`]: AnimationClips (curves decoded to keys with tangents).
//! - [`listing`]: a player's objects by class and name, read from the object tables.
//! - [`material`]: materials as stored (shader, saved properties, keywords).
//! - [`shader`]: a Shader's render state as stored (subshaders, passes, blend, depth and stencil state).
//! - [`terrain`]: terrains as stored (heightmap, holes, splat layers and alphamaps, trees).
//! - [`pipeline`]: the lighting and shadow settings of a Universal Render Pipeline asset.
//!
//! Reads are bounds-checked, and a file the readers do not understand is refused with an [`Error`] that names what
//! was found and where.
#![deny(clippy::disallowed_methods, clippy::disallowed_types, missing_docs)]
// Every module reads stored bytes (the files themselves, or values decoded from them), so a panic is a defect: indexes
// are `get`s, sums are checked or saturating, and numbers are narrowed with `try_from`. Tests index freely.
#![cfg_attr(
    not(test),
    deny(
        clippy::indexing_slicing,
        clippy::arithmetic_side_effects,
        clippy::cast_possible_truncation
    )
)]

pub mod animator;
pub mod clip;
pub mod listing;
pub mod material;
pub mod mesh;
pub mod pipeline;
pub mod player;
pub mod program;
mod reader;
pub mod resource;
pub mod scene;
pub mod serialized;
pub mod shader;
pub mod terrain;
pub mod texture;
pub mod tpk;
pub mod typetree;
pub mod value;
pub mod version;

pub use serialized::{ObjectInfo, SerializedFile};
pub use tpk::TypeTreePackage;
pub use typetree::TypeTree;
pub use value::Value;
pub use version::UnityVersion;

/// The README's code blocks, compiled as doc tests so they cannot drift from the API.
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;

/// Why a read was refused.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// The bytes are not what the format allows (a length past the data, a bad tag, a broken reference).
    #[error("{0}")]
    Invalid(String),
    /// The bytes are well formed but use something the reader does not read (an unverified container version, a
    /// feature of a newer Unity).
    #[error("not supported: {0}")]
    Unsupported(String),
    /// A file could not be read from disk.
    #[error("{}: {source}", path.display())]
    Io {
        /// The file.
        path: std::path::PathBuf,
        /// What the operating system said.
        source: std::io::Error,
    },
}

/// A read's result.
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// A double narrowed to the nearest `f32`: the rounding is the intent, so the truncation lint does not apply.
#[allow(
    clippy::cast_possible_truncation,
    reason = "float narrowing rounds to the nearest f32 by design"
)]
pub(crate) fn narrow(x: f64) -> f32 {
    x as f32
}

/// Count one more of `key` in `counts`, stopping at `u64::MAX`.
pub(crate) fn bump<K: Ord>(counts: &mut std::collections::BTreeMap<K, u64>, key: K) {
    let n = counts.entry(key).or_default();
    *n = n.saturating_add(1);
}

pub(crate) fn invalid(msg: impl Into<String>) -> Error {
    Error::Invalid(msg.into())
}