A Zig library and CLI for reading, extracting, and editing Unity assets.
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.unity3dLinux (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.cppdefinessprintf_s/vsprintf_sfor every compiler that is not MSVC, and mingw-w64'ssec_api/stdio_s.halready defines both, so-Dtarget=x86_64-windows-gnuwould fail to compile anyway, deep in vendored C++. - Big-endian hosts.
lzham_core.hhardcodesLZHAM_LITTLE_ENDIAN_CPUand 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- Look inside any Unity asset - open a bundle,
.assetsfile, 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
.assetsfile, 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
--verifyto 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.
info- what unityz can read from a file (--objectslists the object table,--dumpprints every object as JSON)extract- pull out embedded assets (filters:--class,--path-id,--name,--raw;--jsonvalue trees + manifest;--summarydry run)edit- change fields, patch sidecars,--verifyto round-trip-check before writingverify- byte-exact round-trip check of every object, non-zero exit on failurestats- per-class sizes and duplicate-object detection (--dupsprints only the duplicate report)find- name/class search over objects (--exactfor a whole-name match,--anysearches every string field)show- one object as JSON, or a hex dump with--raw; missing or undecodable targets return non-zerodiff- 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 fingerprintsskin- whether every Shader's vertex stage applies bone matriceshierarchy- the GameObject/Transform tree of a sceneshader- a Shader's decoded compiled sub-program blob tablefsb- inspect a raw FSB5 bank with read-only--json, or decode it to playable WAV/OGG with--outdirmanaged- read a Mono build's assemblies and list every MonoBehaviour's serialized field layout, straight from the .NET metadata with no runtimetrees- export the type trees embedded in a file as a--treesJSON table (--out <file.json>), so a game's own AssetBundles, which keep their trees, supply version-exact trees for its stripped.assetsfilestrees --builtin <release>- export the built-in engine-class trees unityz ships for one exact Unity release (--class <id>for one class);--builtinon any--trees-taking command decodes a stripped file's built-in classes through themcreate- build a UnityFS bundle from scratch (<spec.json> --out <file>): a format-22 SerializedFile with the declared type trees and objects plus an optional.resourcesidecar, re-read and round-trip checked before it is written
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.assetsfiles, 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 (itsManaged/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.pyconverts AssetRipper's public type-tree dumps into the format, andscripts/merge-trees.pyjoins 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.
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.
- 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 --helpis 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.
src/lib.zig- library root: public API surfacesrc/main.zig- CLI entry point and subcommand dispatchsrc/streams.zig- endian-aware binary reader/writersrc/container.zig- file type detection by magic/headersrc/webfile.zig- WebFile container parser and rebuildersrc/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 (.assetsand friends)src/serialized_writer.zig- SerializedFile rebuild and from-scratch writersrc/typetree.zig- TypeTree parsing + Unity common-string tablesrc/builtin_trees.zig- built-in engine-class type trees indexed by exact Unity release, embedded fromsrc/builtin_trees/<release>.bin(packed byscripts/structsdump-to-builtin.pyfrom an AssetRipper dump)src/value.zig- generic object value model + JSON output and parsingsrc/object_reader.zig- type-tree-driven object readersrc/object_writer.zig- type-tree-driven object writersrc/classes.zig- typed views for the common classessrc/dotnet.zig- .NET assembly metadata reader (themanagedcommand)src/managed_trees.zig- MonoBehaviour script trees built from assemblies (managed --trees)src/fsb5.zig- FSB5 audio bank metadata parsersrc/audio.zig- FSB5 sample decoding to 16-bit PCM, no external toolssrc/vorbis.zig- FSB5 Vorbis to playable Ogg reconstruction, no external tools; the setup headers it keys by CRC32 are embedded fromsrc/vorbis_headers.binwith the generated offset tables insrc/vorbis_headers_index.zigsrc/wav.zig- minimal WAV encoder for the decoded PCMsrc/shader.zig- Shader sub-program blob decoding and skinning detectionsrc/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 encoderssrc/vendor/unitycrunch/- vendored unitycrunch decompressor (ZLIB), reached through the hand-writtensrc/vendor/unitycrunch_shim.cppsrc/vendor/lzham/- vendored LZHAM decompressor (MIT), reached through the hand-writtensrc/vendor/lzham/lzham_shim.cppscripts/- offline tooling that produces inputs rather than shipping in the binary: the--treesand built-in-tree generators, the trees merger, and the docs index generatortrees/- hand-derived--treesJSON 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 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.ZThe 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/features.md - the full capability reference
- docs/ROADMAP.md - what is current, planned, shipped
- docs/WORKFLOW.md - the delivery lifecycle
- docs/INDEX.md - index of all durable records