From 40896392e1f34b88af0994bcb6c324081bba9335 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 14 Apr 2026 11:22:14 +0800 Subject: [PATCH 1/5] docs: README - improve environment section, add tips for properly closing files --- README.md | 57 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index baf92789..02d93596 100644 --- a/README.md +++ b/README.md @@ -138,39 +138,58 @@ It can also be used as a general template or as an importable tool. ### Environment [Environment](UnityPy/environment.py) 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 +#### Initialization -UnityPy can detect if the file is a WebFile, BundleFile, Asset, or APK. +An Environment object can be created by providing: -The unpacked assets will be loaded into `.files`, a dict consisting of `asset-name : asset`. +- a file path - loads a single file (typically an asset bundle file, apk file or zip file). +- a folder path - loads all files in the given folder (bad idea for large folders). +- a streamable object - loads the data from the given stream (it can be `io.BytesIO`, file stream returned by `open()` and any other object that extends `io.IOBase`). +- a bytes object - loads the data from memory. + +UnityPy can detect if the file is a WebFile, BundleFile, Asset, or APK. -All objects of the loaded assets can be easily accessed via `.objects`, -which itself is a simple recursive iterator. +The following code shows the different ways to create an Environment object. ```python import io import UnityPy -# all of the following would work -src = "file_path" -src = b"bytes" -src = io.BytesIO(b"Streamable") +# load from file path or folder path +env = UnityPy.load("path/to/your/file") + +# note that the best way to pass a file to UnityPy is +# using with-statement to ensure the file can be properly closed +with open("path/to/your/file", "rb") as f: + env = UnityPy.load(f) + +# load from bytes io stream +data = io.BytesIO(b"streamable-data") +env = UnityPy.load(data) + +# load from bytes object +data = b"some-bytes-data" +env = UnityPy.load(data) +``` + +#### Attributes + +The unpacked assets will be loaded into `.files`, a dict consisting of `asset-name : asset`. -env = UnityPy.load(src) +All objects of the loaded assets can be easily accessed via `.objects`, which itself is a simple recursive iterator. +If you want, you can modify the objects and save the edited file later. +See [Object](#object) section to learn how to apply modifications to the objects. + +```python +# assumes that you have already created an `env` for obj in env.objects: + # your code for processing each object ... -# saving an edited file - # apply modifications to the objects - # don't forget to use data.save() - ... -with open(dst, "wb") as f: +# don't forget to save an edited file +with open("path/to/save", "wb") as f: f.write(env.file.save()) ``` From 111c142a12fd2679d222117c6b900f5f1b06aba4 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 14 Apr 2026 11:22:14 +0800 Subject: [PATCH 2/5] docs: README - improve example section, use with statement as a best practice --- README.md | 73 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 02d93596..4ce879d6 100644 --- a/README.md +++ b/README.md @@ -90,41 +90,44 @@ def unpack_all_assets(source_folder: str, destination_folder: 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 + # generate full file path file_path = os.path.join(root, file_name) - # 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.parse_as_object() - - # create destination path - dest = os.path.join(destination_folder, data.m_Name) - - # 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.parse_as_object() - # 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) + # open and load that file + with open(file_path, "rb") as f: + env = UnityPy.load(f) + + # 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.parse_as_object() + + # generate destination path + dest = os.path.join(destination_folder, data.m_Name) + + # 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.parse_as_object() + + # generate destination 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](#important-classes) @@ -246,7 +249,7 @@ Following functions are legacy functions that will be removed in the future when The modern versions are equivalent to them and have a more correct type hints. | Legacy | Modern | -|---------------|-----------------| +| ------------- | --------------- | | read | parse_as_object | | read_typetree | parse_as_dict | | save_typetree | patch | From 4bb61622ca2733b70659181e18d73220665d66d2 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 14 Apr 2026 11:22:14 +0800 Subject: [PATCH 3/5] docs: README - improve objects section, add details for object modification --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index 4ce879d6..1f28dc49 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,6 @@ A Unity asset extractor for Python based on [AssetStudio](https://github.com/Perfare/AssetStudio). Next to extraction, UnityPy also supports editing Unity assets. -Via the typetree structure all object types can be edited in their native forms. - -```python -# modification via dict: - raw_dict = obj.parse_as_dict() - # modify raw dict - obj.patch(raw_dict) -# modification via parsed class - instance = obj.parse_as_object() - # 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](https://discord.gg/C6txv7M). From e1d9b048f2628ca2e0d58d38ebcc140729ae3936 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 14 Apr 2026 21:25:17 +0800 Subject: [PATCH 4/5] docs: README - improve env reading --- README.md | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1f28dc49..b8f02fa3 100644 --- a/README.md +++ b/README.md @@ -143,26 +143,37 @@ UnityPy can detect if the file is a WebFile, BundleFile, Asset, or APK. The following code shows the different ways to create an Environment object. -```python -import io -import UnityPy - -# load from file path or folder path -env = UnityPy.load("path/to/your/file") - -# note that the best way to pass a file to UnityPy is -# using with-statement to ensure the file can be properly closed -with open("path/to/your/file", "rb") as f: - env = UnityPy.load(f) - -# load from bytes io stream -data = io.BytesIO(b"streamable-data") -env = UnityPy.load(data) - -# load from bytes object -data = b"some-bytes-data" -env = UnityPy.load(data) -``` +1. Load from file path or folder path (most frequently used). + ```python + import UnityPy + + env = UnityPy.load("path/to/your/file") + + # it's suggested to keep the file open while using `env` + f = open("path/to/your/file", "rb") + try: + env = UnityPy.load(f) + # use env here ... + finally: + f.close() + ``` + +2. Load from streamable object. + ```python + import io + import UnityPy + + data = io.BytesIO(b"streamable-data") + env = UnityPy.load(data) + ``` + +3. Load from bytes object. + ```python + import UnityPy + + data = b"some-bytes-data" + env = UnityPy.load(data) + ``` #### Attributes From 9866fd0be26c18956c77e6d6aeaf0243cdbae496 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Tue, 14 Apr 2026 21:25:48 +0800 Subject: [PATCH 5/5] docs: README - format and quote blocks --- README.md | 57 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b8f02fa3..a69ebecb 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,24 @@ feel free to join the [UnityPy Discord](https://discord.gg/C6txv7M). 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. +> [!NOTE] +> **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). +> +>
+> In case you don't want to actively maintain your project... +> +> Make a note of the used UnityPy version in your README or add a check in your code. +> +> ```python +> if UnityPy.__version__ != '1.9.6': +> raise ImportError("Invalid UnityPy version detected. Please use version 1.9.6") +> ``` +> +> You can also pin the version in your `requirements.txt` or `pyproject.toml` file, which is the best practice. +> +>
-```python -if UnityPy.__version__ != '1.9.6': - raise ImportError("Invalid UnityPy version detected. Please use version 1.9.6") -``` 1. [Installation](#installation) 2. [Example](#example) @@ -208,12 +216,17 @@ The objects with a file path can be found in the `.container` dict - `{path : ob Objects \([ObjectReader class](UnityPy/files/ObjectReader.py)\) 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. +> [!IMPORTANT] +> +> 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 `.set_raw_data(data)` or modified with `.save()` function, if supported. -For object types with ``m_Name`` you can use ``.peek_name()`` to only read the name of the parsed object without parsing it completely, which is way faster. +> [!TIP] +> +> For object types with attribute `.m_Name` you can use `.peek_name()` to only read the name of the parsed object without parsing it completely, which is way faster. + +Serialized objects can be set with raw data using `.set_raw_data(data)` or modified with `.save()` function, if supported. There are two general parsing functions, ``.parse_as_object()`` and ``.parse_as_dict()``. ``parse_as_dict`` parses the object data into a dict. @@ -269,6 +282,7 @@ Now UnityPy uses [auto generated classes](UnityPy/classes/generated.py) with som ```python from PIL import Image + for obj in env.objects: if obj.type.name == "Texture2D": # export texture @@ -352,7 +366,7 @@ for obj in env.objects: tree = obj.parse_as_dict() fp = os.path.join(extract_dir, f"{tree['m_Name']}.json") with open(fp, "wt", encoding = "utf8") as f: - json.dump(tree, f, ensure_ascii = False, indent = 4) + json.dump(tree, f, ensure_ascii=False, indent=4) # edit tree = obj.parse_as_dict() @@ -431,14 +445,16 @@ The mesh will be converted to the Wavefront .obj file format. ```python mesh: Mesh -with open(f"{mesh.m_Name}.obj", "wt", newline = "") as f: +with open(f"{mesh.m_Name}.obj", "wt", newline="") as f: # newline = "" is important f.write(mesh.export()) ``` ### Renderer, MeshRenderer, SkinnedMeshRenderer -ALPHA-VERSION +> [!WARNING] +> +> This feature is in alpha version. - `.export(export_dir)` - exports the associated mesh, materials, and textures into the given directory @@ -458,7 +474,9 @@ mesh_renderer.export(export_dir) ### Texture2DArray -WARNING - not well tested +> [!WARNING] +> +> This feature isn't well tested. - `.m_Name` - `.image` converts the texture2darray into a `PIL.Image` @@ -490,6 +508,7 @@ To enable encryption simply use the code as follow, with `key` being the value t ```python import UnityPy + UnityPy.set_assetbundle_decrypt_key(key) ``` @@ -499,6 +518,7 @@ In case UnityPy failed to detect the Unity version of the game assets, you can s ```python import UnityPy.config + UnityPy.config.FALLBACK_UNITY_VERSION = "2.5.0f5" ``` @@ -508,6 +528,7 @@ The [C-implementation](UnityPyBoost/) of typetree reader can boost the parsing o ```python from UnityPy.helpers import TypeTreeHelper + TypeTreeHelper.read_typetree_boost = False ``` @@ -517,9 +538,9 @@ Some game assets have non-standard compression/decompression algorithm applied o ```python from UnityPy.enums.BundleFile import CompressionFlags -flag = CompressionFlags.LZHAM - from UnityPy.helpers import CompressionHelper + +flag = CompressionFlags.LZHAM CompressionHelper.COMPRESSION_MAP[flag] = custom_compress CompressionHelper.DECOMPRESSION_MAP[flag] = custom_decompress ```