UnityPy is python module that makes it possible to extract/unpack and edit Unity assets
UnityPy
A Unity asset extractor for Python based on AssetStudio.
Next to extraction, UnityPy also supports editing Unity assets. Via the typetree structure all object types can be edited in their native forms.
# modification via dict:
rawdict = obj.parseas_dict()
# modify raw dict
obj.patch(raw_dict)
modification via parsed class
instance = obj.parseasobject()
# modify instance
obj.patch(instance)
If you need advice or if you want to talk about (game) data-mining, feel free to join the UnityPy Discord.
If you're using UnityPy for a commercial project, a donation to a charitable cause or a sponsorship of this project is expected.
As UnityPy is still in active development, breaking changes can happen. These changes are usually limited to minor versions (x.y) and not to patch versions (x.y.z). So in case that you don't want to actively maintain your project, make sure to make a note of the used UnityPy version in your README or add a check in your code. e.g.
if UnityPy.version != '1.9.6':
raise ImportError("Invalid UnityPy version detected. Please use version 1.9.6")
Installation
Python 3.8 or higher is required.
Install via PyPI:
pip install UnityPy
Install from source code:
git clone https://github.com/K0lb3/UnityPy.git
cd UnityPy
python -m pip install .
Notes
Windows
Visual C++ Redistributable is required for the brotli dependency. In case a new(ish) Python version is used, it can happen that the C-dependencies of UnityPy might not be precompiled for this version. In such cases the user either has to report this as issue or follow the steps of this issue to compile it oneself. Another option for the user is downgrading Python to the latest version supported by UnityPy. For this see the Python version badge at the top of the README.
Crash without warning/error
The C-implementation of the typetree reader can directly crash Python. In case this happens, the usage of the C-typetree reader can be disabled. Read this section for more details.
Example
The following is a simple example.
import os
import UnityPy
def unpackallassets(sourcefolder: str, destinationfolder: str): # iterate over all files in source folder for root, dirs, files in os.walk(source_folder): for file_name in files: # generate file_path filepath = os.path.join(root, filename) # load that file via UnityPy.load env = UnityPy.load(file_path)
# iterate over internal objects for obj in env.objects: # process specific object types if obj.type.name in ["Texture2D", "Sprite"]: # parse the object data data = obj.parseasobject()
# create destination path dest = os.path.join(destinationfolder, data.mName)
# make sure that the extension is correct # you probably only want to do so with images/textures dest, ext = os.path.splitext(dest) dest = dest + ".png"
img = data.image img.save(dest)
# alternative way which keeps the original path for path,obj in env.container.items(): if obj.type.name in ["Texture2D", "Sprite"]: data = obj.parseasobject() # create dest based on original path dest = os.path.join(destination_folder, *path.split("/")) # make sure that the dir of that path exists os.makedirs(os.path.dirname(dest), exist_ok = True) # correct extension dest, ext = os.path.splitext(dest) dest = dest + ".png" data.image.save(dest)
You probably have to read Important Classes and Important Object Types to understand how it works.
Users with slightly advanced Python skills should look at UnityPy/tools/extractor.py for a more advanced example. It can also be used as a general template or as an importable tool.
Important Classes
Environment
Environment loads and parses the given files. It can be initialized via:
- a file path - apk files can be loaded as well
- a folder path - loads all files in that folder (bad idea for folders with a lot of files)
- a stream - e.g.,
io.BytesIO, file stream,... - a bytes object - will be loaded into a stream
The unpacked assets will be loaded into .files, a dict consisting of asset-name : asset.
All objects of the loaded assets can be easily accessed via .objects, which itself is a simple recursive iterator.
import io
import UnityPy
all of the following would work
src = "file_path"
src = b"bytes"
src = io.BytesIO(b"Streamable")
env = UnityPy.load(src)
for obj in env.objects: ...
saving an edited file
# apply modifications to the objects
# don't forget to use data.save()
...
with open(dst, "wb") as f:
f.write(env.file.save())
Asset
Assets \(SerializedFile class\) are a container that contains multiple objects. One of these objects can be an AssetBundle, which contains a file path for some of the objects in the same asset.
All objects can be found in the .objects dict - {ID : object}.
The objects with a file path can be found in the .container dict - {path : object}.
Object
Objects \(ObjectReader class\) contain the actual_ files, e.g., textures, text files, meshes, settings, ...
To acquire the actual data of an object it has to be parsed first. This happens via the parse functions mentioned below. This isn't done automatically to save time as only a small part of the objects are usually of interest. Serialized objects can be set with raw data using .setrawdata(data) or modified with .save() function, if supported.
For object types with `mName you can use .peekname() to only read the name of the parsed object without parsing it completely, which is way faster.
There are two general parsing functions, .parseasobject() and .parseasdict(). parseasdict parses the object data into a dict. parseasobject parses the object data into a class. If the class is a Unity class, it's stub class from UnityPy.classes(.generated) will be used, if it's an unknown one, then it will be parsed into an UnknownObject, which simply acts as interface for the otherwise parsed dict. Some special classes, namely those below, have additional handlers added to their class for easier interaction with them.
The .patch(item) function can be used on all object (readers) to replace their data with the changed item, which has to be either a dict or of the class the object represents.
Example
<pre><code class="lang-py">for obj in env.objects: if obj.type.name == "the type you want": if obj.peek_name() != "the specific object you want": continue # parsing instance = obj.parseasobject() dic = obj.parseasdict()
# modifying instance.m_Name = "new name" dic["m_Name"] = "new name"
# saving obj.patch(instance) obj.patch(dic)</code></pre>
Legacy
Following functions are legacy functions that will be removed in the future when major version 2 hits. The modern versions are equivalent to them and have a more correct type hints.
| Legacy | Modern | |---------------|-----------------| | read | parseasobject | | readtypetree | parseas_dict | | save_typetree | patch |
Important Object Types
Now UnityPy uses auto generated classes with some useful extension methods and properties defined in legacypatch. You can search for a specific classes in the module UnityPy.classes with your IDE's autocompletion.
Texture2D
- .m_Name
- .image
converts the texture into aPIL.Image - .m_Width
- texture width (int) - .m_Height
- texture height (int)
<pre><code class="lang-python">from PIL import Image for obj in env.objects: if obj.type.name == "Texture2D": # export texture tex = obj.parseasobject() path = os.path.join(exportdir, f"{tex.mName}.png") tex.image.save(path) # edit texture fp = os.path.join(replacedir, f"{tex.mName}.png") pil_img = Image.open(fp) tex.image = pil_img tex.save()</code></pre>
Sprite
Sprites are part of a texture and can have a separate alpha-image as well. Unlike most other extractors (including AssetStudio), UnityPy merges those two images by itself.
- .m_Name
- .image
- converts the merged texture part into aPIL.Image - .m_Width
- sprite width (int) - .m_Height
- sprite height (int)
<pre><code class="lang-python">for obj in env.objects: if obj.type.name == "Sprite": sprite = obj.parseasobject() path = os.path.join(exportdir, f"{sprite.mName}.png") sprite.image.save(path)</code></pre>
TextAsset
TextAssets are usually normal text files.
- .m_Name
- .m_Script
- str
m_Script gets handled as str by default,
use m_Script.encode("utf-8", "surrogateescape") to retrieve the original binary data.
Export
<pre><code class="lang-python">for obj in env.objects: if obj.type.name == "TextAsset": # export asset txt = obj.parseasobject() path = os.path.join(exportdir, f"{txt.mName}.txt") with open(path, "wb") as f: f.write(txt.m_Script.encode("utf-8", "surrogateescape")) # edit asset fp = os.path.join(replacedir, f"{txt.mName}.txt") with open(fp, "rb") as f: txt.m_Script = f.read().decode("utf-8", "surrogateescape") txt.save()</code></pre>
MonoBehaviour
MonoBehaviour assets are usually used to save the class instances with their values. The structure/typetree for these classes might not be contained in the asset files. In such cases see the 2nd example (TypeTreeGenerator) below.
-
.m_Name
.m_Script
custom data
Export
<pre><code class="lang-python">import json
for obj in env.objects: if obj.type.name == "MonoBehaviour": # export # save decoded data tree = obj.parseasdict() fp = os.path.join(extractdir, f"{tree['mName']}.json") with open(fp, "wt", encoding = "utf8") as f: json.dump(tree, f, ensure_ascii = False, indent = 4)
# edit tree = obj.parseasdict() # apply modifications to the data within the tree obj.patch(tree)</code></pre>
TypeTreeGenerator
UnityPy can generate the typetrees of MonoBehaviours from the game assemblies using an optional package,
TypeTreeGeneratorAPI, which has to be installed via pip. UnityPy will automatically try to generate the typetree of MonoBehaviours if the typetree is missing in the assets and env.typetree_generator is set.
<pre><code class="lang-python">import UnityPy from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
create generator
GAMEROOTDIR: str
e.g. r"D:\Program Files (x86)\Steam\steamapps\common\Aethermancer Demo"
GAMEUNITYVERSION: str
you can get the version via an object
e.g. objects[0].assetsfile.unityversion
generator = TypeTreeGenerator(GAMEUNITYVERSION) generator.loadlocalgame(GAMEROOTDIR)
generator.loadlocalgame(root_dir: str) - for a Windows game
generator.loaddllfolder(dll_dir: str) - for mono / non-il2cpp or generated dummies
generator.load_dll(dll: bytes)
generator.load_il2cpp(il2cpp: bytes, metadata: bytes)
env = UnityPy.load(fp)
assign generator to env
env.typetree_generator = generator for obj in objects: if obj.type.name == "MonoBehaviour": # automatically tries to use the generator in the background if necessary x = obj.parseasobject()</code></pre>
AudioClip
-
.samples - {sample-name : sample-data}
The samples are converted into the .wav format.
The sample data is a .wav file in bytes.
<pre><code class="lang-python">clip: AudioClip for name, data in clip.samples.items(): with open(name, "wb") as f: f.write(data)</code></pre>
Font
Export
<pre><code class="lang-python">if obj.type.name == "Font": font: Font = obj.parseasobject() if font.m_FontData: extension = ".ttf" if font.m_FontData[0:4] == b"OTTO": extension = ".otf"
with open(os.path.join(path, font.m_Name+extension), "wb") as f: f.write(font.m_FontData)</code></pre>
Mesh
-
.export() - mesh exported as .obj (str)
The mesh will be converted to the Wavefront .obj file format.
<pre><code class="lang-python">mesh: Mesh with open(f"{mesh.m_Name}.obj", "wt", newline = "") as f: # newline = "" is important f.write(mesh.export())</code></pre>
Renderer, MeshRenderer, SkinnedMeshRenderer
ALPHA-VERSION
-
.export(export_dir) - exports the associated mesh, materials, and textures into the given directory
The mesh and materials will be in the Wavefront formats.
<pre><code class="lang-python">mesh_renderer: Renderer export_dir: str
if meshrenderer.mGameObject: # get the name of the model gameobjreader = meshrenderer.mGameObject.deref() gameobjname = gameobjreader.peek_name() exportdir = os.path.join(exportdir, gameobjname) meshrenderer.export(exportdir)</code></pre>
Texture2DArray
WARNING - not well tested
-
.m_Name
.image converts the texture2darray into a PIL.Image
.m_Width - texture width (int)
.m_Height - texture height (int)
Export
<pre><code class="lang-python">import os from PIL import Image for obj in env.objects: if obj.type.name == "Texture2DArray": # export texture texarr = obj.parseas_object() for i, image in enumerate(tex_arr.images): image.save(os.path.join(path, f"{texarr.mName}_{i}.png")) # editing isn't supported yet!</code></pre>
Configurations
There're several configurations and interfaces that provide the customizability to UnityPy.
Unity CN Decryption
The Chinese version of Unity has its own builtin option to encrypt AssetBundles/BundleFiles. As it's a feature of Unity itself, and not a game specific protection, it is included in UnityPy as well. To enable encryption simply use the code as follow, with
key being the value that the game that loads the bundles passes to AssetBundle.SetAssetBundleDecryptKey.
<pre><code class="lang-python">import UnityPy UnityPy.setassetbundledecrypt_key(key)</code></pre>
Unity Fallback Version
In case UnityPy failed to detect the Unity version of the game assets, you can set a fallback version. e.g.
<pre><code class="lang-python">import UnityPy.config UnityPy.config.FALLBACKUNITYVERSION = "2.5.0f5"</code></pre>
Disable Typetree C-Implementation
The C-implementation of typetree reader can boost the parsing of typetree by a lot. If you want to disable it and use pure Python reader, you can put the following 2 lines in your main file.
<pre><code class="lang-python">from UnityPy.helpers import TypeTreeHelper TypeTreeHelper.readtypetreeboost = False</code></pre>
Custom Block (De)compression
Some game assets have non-standard compression/decompression algorithm applied on the block data. If you wants to customize the compression/decompression function, you can modify the corresponding function mapping. e.g.
<pre><code class="lang-python">from UnityPy.enums.BundleFile import CompressionFlags flag = CompressionFlags.LZHAM
from UnityPy.helpers import CompressionHelper CompressionHelper.COMPRESSIONMAP[flag] = customcompress CompressionHelper.DECOMPRESSIONMAP[flag] = customdecompress</code></pre>
-
custom_compress(data: bytes) -> bytes (where bytes can also be bytearray or memoryview)
customdecompress(data: bytes, uncompressedsize: int) -> bytes
Custom Filesystem
UnityPy uses fsspec under the hood to manage all filesystem interactions. This allows using various different types of filesystems without having to change UnityPy's code. It also means that you can use your own custom filesystem to e.g. handle indirection via catalog files, load assets on demand from a server, or decrypt files.
Following methods of the filesystem have to be implemented for using it in UnityPy.
-
sep (not a function, just the separator as character)
isfile(self, path: str) -> bool
isdir(self, path: str) -> bool
exists(self, path: str, **kwargs) -> bool
walk(self, path: str, **kwargs) -> Iterable[List[str], List[str], List[str]]
open(self, path: str, mode: str = "rb", **kwargs) -> file ("rb" mode required, "wt" required for ModelExporter)
makedirs(self, path: str, exist_ok: bool = False) -> bool`
Credits
First of all, thanks a lot to all contributors of UnityPy and all of its users.
Also, many thanks to:
- Perfare for creating and maintaining and every contributor of AssetStudio
- ds5678 for the TypeTreeDumps and the custom minimal Tpk format
- Razmoth for figuring out and sharing Unity CN's AssetBundle decryption (src).
- nesrak1 for figuring out the Switch texture swizzling
- xiop_13690 (discord) for figuring out unsolved issues of the ManagedReferencesRegistry
