Skip to content
hordeforgePublic

About

Zig library and CLI for reading, extracting, and editing Unity assets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

unityz

A Zig library and CLI for reading, extracting, and editing Unity assets.

Quick start

Requires Zig 0.16.0.

git clone https://github.com/hordeforge/unityz
cd unityz
zig build test
zig build
./zig-out/bin/unityz --help
./zig-out/bin/unityz info path/to/asset
./zig-out/bin/unityz extract path/to/asset
./zig-out/bin/unityz edit path/to/asset 100 m_Count 99
./zig-out/bin/unityz verify path/to/asset
./zig-out/bin/unityz stats path/to/asset
./zig-out/bin/unityz find path/to/asset Player
./zig-out/bin/unityz show path/to/asset 100
./zig-out/bin/unityz diff asset_a asset_b
./zig-out/bin/unityz create spec.json --out new.unity3d

Linux (x86_64) and macOS (aarch64) are built and tested in CI. The Zig parsers make no host-endianness or word-size assumptions - every integer read and written names its byte order - so other little-endian POSIX targets zig build -Dtarget=... accepts should work. CI cross-compiles x86_64-linux-musl, aarch64-linux-gnu, x86-linux-gnu (32-bit) and x86_64-macos on every push, so those four are known to build; nothing beyond CI's two runner targets is known to pass tests, because nothing runs the suite on them.

Two families are not supported, both because of the vendored LZHAM decoder (src/vendor/lzham), which UnityFS block compression type 4 needs. build.zig refuses both at configure time, so -Dtarget= for either fails with a message naming the reason rather than a C++ error or wrong output:

  • Windows. lzham_platform.cpp defines sprintf_s / vsprintf_s for every compiler that is not MSVC, and mingw-w64's sec_api/stdio_s.h already defines both, so -Dtarget=x86_64-windows-gnu would fail to compile anyway, deep in vendored C++.
  • Big-endian hosts. lzham_core.h hardcodes LZHAM_LITTLE_ENDIAN_CPU and unaligned integer loads for every non-MSVC target, so type 4 blocks would decode wrong there - a build that succeeds and silently produces wrong bytes.

CI also blocks on formatting, shell lint, and Python lint for the generator scripts:

zig fmt --check build.zig build.zig.zon src
shellcheck scripts/*.sh
pipx run ruff==0.16.8 check scripts

What you can do

  • Look inside any Unity asset - open a bundle, .assets file, or sidecar and see its containers, objects, and type trees (info, show).
  • Extract everything - textures and sprites as images, meshes as OBJ and glTF/GLB (skinned rigs included), straight out of a bundle or a bare .assets file, audio as playable files (OGG/FSB/WAV), video cutscenes as MP4, terrain heightmaps as PGM, readable ShaderLab for shaders, fonts, and structured JSON for most other classes (animations, animator controllers, mixers, particle systems, materials, script registries).
  • Edit in place - change any field of any object, patch streamed sidecar bytes, and get byte-exact output, with --verify to round-trip-check the result before it writes (edit).
  • Create from scratch - build a whole bundle from type trees and JSON object values, with no source file (create).
  • Check and compare - round-trip every object byte-exactly, validate streamed references, and diff two files down to the changed pixel, audio sample, or field path (verify, diff).
  • Understand a game - per-class stats, name/class search, scene hierarchy, shader skinning analysis, and the script registry (stats, find, hierarchy, skin).
  • Work with Mono builds - decode typeless files via injected type trees, or read the game's .NET assemblies directly to list every MonoBehaviour's serialized fields (--trees, managed).

Every command accepts a directory and processes all files in it - except diff, which compares two trees, and managed, which reads a Mono build's assembly folder - and --json modes cover the machine-readable commands. Over a directory, --json emits one line per file: {"file":"<path>","results":[...]} holding that file's documents, plus "error" when it failed. Usage errors exit 2, read or check failures exit 1, always with the diagnostic on stderr. See docs/features.md for the full capability reference.

Commands

  • info - what unityz can read from a file (--objects lists the object table, --dump prints every object as JSON)
  • extract - pull out embedded assets (filters: --class, --path-id, --name, --raw; --json value trees + manifest; --summary dry run)
  • edit - change fields, patch sidecars, --verify to round-trip-check before writing
  • verify - byte-exact round-trip check of every object, non-zero exit on failure
  • stats - per-class sizes and duplicate-object detection (--dups prints only the duplicate report)
  • find - name/class search over objects (--exact for a whole-name match, --any searches every string field)
  • show - one object as JSON, or a hex dump with --raw; missing or undecodable targets return non-zero
  • diff - compare two files or directories by content hash, with optional decoded passes: --pixels (texture/sprite pixel diffs), --audio (streamed audio), --fields (exact changed field paths)
  • hash - per-object content fingerprints
  • skin - whether every Shader's vertex stage applies bone matrices
  • hierarchy - the GameObject/Transform tree of a scene
  • shader - a Shader's decoded compiled sub-program blob table
  • fsb - inspect a raw FSB5 bank with read-only --json, or decode it to playable WAV/OGG with --outdir
  • managed - read a Mono build's assemblies and list every MonoBehaviour's serialized field layout, straight from the .NET metadata with no runtime
  • trees - export the type trees embedded in a file as a --trees JSON table (--out <file.json>), so a game's own AssetBundles, which keep their trees, supply version-exact trees for its stripped .assets files
  • trees --builtin <release> - export the built-in engine-class trees unityz ships for one exact Unity release (--class <id> for one class); --builtin on any --trees-taking command decodes a stripped file's built-in classes through them
  • create - build a UnityFS bundle from scratch (<spec.json> --out <file>): a format-22 SerializedFile with the declared type trees and objects plus an optional .resource sidecar, re-read and round-trip checked before it is written

Typeless files

Mono builds strip class type trees, leaving typeless objects undecodable. --trees <file.json> supplies them, and extract, show, verify, find, skin, hierarchy, stats, edit, and diff --fields all decode with the injected trees.

The trees JSON shape is what TypeTreeGeneratorAPI.get_nodes_as_json() emits. unityz can build one itself three ways:

  • trees <bundle> --out <out.json> exports the trees a file already carries. Unity keeps type trees in AssetBundles even when it strips them from the player's .assets files, so a game's bundles are the closest version-exact source.
  • managed <data-dir> --trees <out.json> derives script trees from the game's own assemblies (its Managed/ folder) and the MonoScript objects in its top-level serialized files, so the trees match that specific game's layouts.
  • For version-generic trees instead, scripts/structsdump-to-trees.py converts AssetRipper's public type-tree dumps into the format, and scripts/merge-trees.py joins the two halves for full coverage.

unityz also ships the built-in engine-class trees of specific Unity releases (currently 2019.4–2023.2 LTS dumps including 2022.3.62f2) inside the binary. --builtin on any of the commands above decodes a stripped file's built-in classes through the shipped trees for the file's own exact release, and trees --builtin <release> exports them; MonoBehaviour script fields still need --trees.

Without trees, a typeless file reports how many objects were skipped.

Editing

edit sets fields by dotted-indexed path, byte-array fields take base64 values, --verify round-trip-checks the result before writing, and --trees makes typeless files editable. edit --patch <file> applies a JSON patch atomically. An entry whose object does not exist fails the whole patch and nothing is written. Edits reserialize byte-exactly; rebuilt bundles keep their compression.

What it is

  • a library (src/lib.zig, imported as @import("unityz")) with parsers for Unity's asset formats, and
  • a CLI (src/main.zig, unityz) with the subcommands above; unityz --help is the authoritative flag reference.

Targeted formats: SerializedFile (.assets), asset bundles (.unity3d / .bundle), WebFile (UnityWebData1.0), and .resources / .resource / .resS sidecar files. The rare UnityArchive container is detected but not yet parsed.

Layout

  • src/lib.zig - library root: public API surface
  • src/main.zig - CLI entry point and subcommand dispatch
  • src/streams.zig - endian-aware binary reader/writer
  • src/container.zig - file type detection by magic/header
  • src/webfile.zig - WebFile container parser and rebuilder
  • src/bundle.zig - UnityFS bundle parser and writer (with LZ4/LZMA blocks)
  • src/lz4.zig - LZ4 block decompression and compression (bundle rebuilds re-encode compressed blocks)
  • src/serialized.zig - SerializedFile parser (.assets and friends)
  • src/serialized_writer.zig - SerializedFile rebuild and from-scratch writer
  • src/typetree.zig - TypeTree parsing + Unity common-string table
  • src/builtin_trees.zig - built-in engine-class type trees indexed by exact Unity release, embedded from src/builtin_trees/<release>.bin (packed by scripts/structsdump-to-builtin.py from an AssetRipper dump)
  • src/value.zig - generic object value model + JSON output and parsing
  • src/object_reader.zig - type-tree-driven object reader
  • src/object_writer.zig - type-tree-driven object writer
  • src/classes.zig - typed views for the common classes
  • src/dotnet.zig - .NET assembly metadata reader (the managed command)
  • src/managed_trees.zig - MonoBehaviour script trees built from assemblies (managed --trees)
  • src/fsb5.zig - FSB5 audio bank metadata parser
  • src/audio.zig - FSB5 sample decoding to 16-bit PCM, no external tools
  • src/vorbis.zig - FSB5 Vorbis to playable Ogg reconstruction, no external tools; the setup headers it keys by CRC32 are embedded from src/vorbis_headers.bin with the generated offset tables in src/vorbis_headers_index.zig
  • src/wav.zig - minimal WAV encoder for the decoded PCM
  • src/shader.zig - Shader sub-program blob decoding and skinning detection
  • src/texture.zig - texture format decoding to RGBA8 (DXT, BC, PVRTC, ATC, EAC, ETC, ASTC, crunch, and the raw half/float/16-bit formats)
  • src/png.zig, src/tga.zig, src/bmp.zig - minimal image encoders
  • src/vendor/unitycrunch/ - vendored unitycrunch decompressor (ZLIB), reached through the hand-written src/vendor/unitycrunch_shim.cpp
  • src/vendor/lzham/ - vendored LZHAM decompressor (MIT), reached through the hand-written src/vendor/lzham/lzham_shim.cpp
  • scripts/ - offline tooling that produces inputs rather than shipping in the binary: the --trees and built-in-tree generators, the trees merger, and the docs index generator
  • trees/ - hand-derived --trees JSON checked in because it cannot be generated from a dump (currently 2021.x TerrainData)
  • build.zig, build.zig.zon - package metadata and build steps

Releases

Releases are tag-driven. Bump .version in build.zig.zon (its one canonical home; unityz --version reads it at build time), move the [Unreleased] notes in CHANGELOG.md under a ## [X.Y.Z] - date heading, merge that to main, then push the matching tag:

git tag vX.Y.Z
git push origin vX.Y.Z

The release workflow rejects a tag that disagrees with build.zig.zon or has no changelog section, creates the GitHub Release with that section as its notes, then re-runs the tests on the tagged tree and attaches a ReleaseSafe build for each platform CI covers (Linux x86_64, macOS arm64) with a SHA-256 checksum. The Linux tarball is built against glibc 2.28 (RHEL 8 / Debian 10), so that is its floor; it will not run on a musl distro such as Alpine. Other targets - musl included - build from source with zig build -Dtarget=....

Docs

About

Zig library and CLI for reading, extracting, and editing Unity assets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages