diff --git a/.editorconfig b/.editorconfig index c425d68b..83ffe4cd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ root = true charset = utf-8 end_of_line = lf indent_size = 4 -indent_style = tab +indent_style = space quote_type = double insert_final_newline = true tab_width = 4 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..88ad43f5 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '37 5 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 999ffdc2..88425c25 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -2,9 +2,9 @@ name: Test and Publish on: [push, pull_request] + jobs: deploy: - strategy: fail-fast: false matrix: @@ -14,6 +14,7 @@ jobs: "windows-latest" ] python-version: [ + "3.9", "3.8", "3.7", "3.6", @@ -24,6 +25,8 @@ jobs: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} ${{ matrix.architecture }} - ${{ matrix.python-version }} + env: + DEPENDENCIES: lz4 brotli Pillow texture2ddecoder tabulate steps: - uses: actions/checkout@v2 @@ -36,22 +39,34 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install --upgrade pip wheel setuptools twine - pip install pytest lz4 brotli Pillow fsb5 texture2ddecoder + pip install --upgrade wheel setuptools twine + pip install pytest ${{ env.DEPENDENCIES }} - name: Install module run: | - python setup.py install + pip install . --use-feature=in-tree-build - name: Test with pytest run: | pytest -v -s - - name: Build and publish + - name: Build Python wheel if: success() && runner.os != 'Linux' && github.event_name == 'push' + run: | + python setup.py sdist bdist_wheel + + - name: Build manylinux Python wheels + if: success() && runner.os == 'Linux' && github.event_name == 'push' + uses: RalfG/python-wheels-manylinux-build@v0.3.3-manylinux2010_x86_64 + with: + python-versions: 'cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39' + build-requirements: ${{ env.DEPENDENCIES }} + + - name: Publish wheels to PyPI + if: success() && github.event_name == 'push' && env.TWINE_PASSWORD != '' env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | - python setup.py sdist bdist_wheel twine upload dist/* --skip-existing + diff --git a/.gitignore b/.gitignore index f88deb46..6fb3e554 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ # Byte-compiled / optimized / DLL files __pycache__/ +.dump .idea/ *.py[cod] *$py.class test.py AssetStudio/ -# C extensions -*.so # Distribution / packaging .Python @@ -16,8 +15,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ parts/ sdist/ var/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6a8766a7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: python # this works for Linux but is an error on macOS or Windows -jobs: - include: - - name: "Python 3.8.0 on Xenial Linux" - python: 3.8 # this works for Linux but is ignored on macOS or Windows - - name: "Python 3.7.4 on macOS" - os: osx - osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 - language: shell # 'language: python' is an error on Travis CI macOS - - name: "Python 3.8.0 on Windows" - os: windows # Windows 10.0.17134 N/A Build 17134 - language: shell # 'language: python' is an error on Travis CI Windows - before_install: - - choco install python --version 3.8.0 - - python -m pip install --upgrade pip - env: PATH=/c/Python38:/c/Python38/Scripts:$PATH -install: - - pip3 install --upgrade pip # all three OSes agree about 'pip3' - - pip3 install pytest - - pip3 install lz4 brotli Pillow fsb5 texture2ddecoder -# 'python' points to Python 2.7 on macOS but points to Python 3.8 on Linux and Windows -# 'python3' is a 'command not found' error on Windows but 'py' works on Windows only -script: - - python3 setup.py install || python setup.py install - - pytest -v diff --git a/AssetBatchConverter.py b/AssetBatchConverter.py deleted file mode 100644 index e8be2d9e..00000000 --- a/AssetBatchConverter.py +++ /dev/null @@ -1,109 +0,0 @@ -import os -from UnityPy import AssetsManager -from collections import Counter -import zipfile - -TYPES = ['Sprite', 'Texture2D', 'TextAsset'] - -ROOT = os.path.dirname(os.path.realpath(__file__)) - -# source folder -ASSETS = os.path.join(ROOT, 'assets') -# destination folder -DST = os.path.join(ROOT, 'extracted') -# number of dirs to ignore -# e.g. IGNOR_DIR_COUNT = 2 will reduce -# 'assets/assetbundles/images/story_picture/small/15.png' -# to -# 'images/story_picture/small/15.png' - -IGNOR_DIR_COUNT = 2 - -os.makedirs(DST, exist_ok=True) - - -def main(): - for root, dirs, files in os.walk(ASSETS, topdown=False): - if '.git' in root: - continue - for f in files: - print(f) - extension = os.path.splitext(f)[1] - src = os.path.realpath(os.path.join(root, f)) - - if extension == ".zip": - archive = zipfile.ZipFile(src, 'r') - for zf in archive.namelist(): - extract_assets(archive.open(zf)) - else: - extract_assets(src) - - -def extract_assets(src): - # load source - am = AssetsManager(src) - - # iterate over assets - for asset in am.assets.values(): - # assets without container / internal path will be ignored for now - if not asset.container: - continue - - # check which mode we will have to use - num_cont = sum(1 for obj in asset.container.values() if obj.type in TYPES) - num_objs = sum(1 for obj in asset.objects.values() if obj.type in TYPES) - - # check if container contains all important assets, if yes, just ignore the container - if num_objs <= num_cont * 2: - for asset_path, obj in asset.container.items(): - fp = os.path.join(DST, *asset_path.split('/')[IGNOR_DIR_COUNT:]) - export_obj(obj, fp) - - # otherwise use the container to generate a path for the normal objects - else: - extracted = [] - # find the most common path - occurence_count = Counter(os.path.splitext(asset_path)[0] for asset_path in asset.container.keys()) - local_path = os.path.join(DST, *occurence_count.most_common(1)[0][0].split('/')[IGNOR_DIR_COUNT:]) - - for obj in asset.objects.values(): - if obj.path_id not in extracted: - extracted.extend(export_obj(obj, local_path, append_name=True)) - - -def export_obj(obj, fp: str, append_name: bool = False) -> list: - if obj.type not in TYPES: - return [] - data = obj.read() - if append_name: - fp = os.path.join(fp, data.name) - - fp, extension = os.path.splitext(fp) - os.makedirs(os.path.dirname(fp), exist_ok=True) - - if obj.type == 'TextAsset': - if not extension: - extension = '.txt' - with open(f"{fp}{extension}", 'wb') as f: - f.write(data.script) - - elif obj.type == "Sprite": - extension = ".png" - data.image.save(f"{fp}{extension}") - - return [obj.path_id, data.m_RD.texture.path_id, getattr(data.m_RD.alphaTexture, 'path_id', None)] - - elif obj.type == "Texture2D": - extension = ".png" - fp = f"{fp}{extension}" - if not os.path.exists(fp): - try: - data.image.save(fp) - except EOFError: - pass - - return [obj.path_id] - - -if __name__ == '__main__': - main() diff --git a/LICENSE b/LICENSE index ddb36697..6bc7be1f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2020 K0lb3 +Copyright (c) 2019-2021 K0lb3 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 40794bdc..c6bf4863 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ -include UnityPy/* \ No newline at end of file +include UnityPy/* + +recursive-include UnityPy/lib *.dll *.lib *.so *.dylib \ No newline at end of file diff --git a/README.md b/README.md index 8831057b..b115693a 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,24 @@ [![PyPI supported Python versions](https://img.shields.io/pypi/pyversions/UnityPy.svg)](https://pypi.python.org/pypi/UnityPy) [![Win/Mac/Linux](https://img.shields.io/badge/platform-windows%20%7C%20macos%20%7C%20linux-informational)]() [![MIT](https://img.shields.io/pypi/l/UnityPy.svg)](https://github.com/K0lb3/UnityPy/blob/master/LICENSE) -[![Build Status](https://travis-ci.com/K0lb3/UnityPy.svg?branch=master)](https://travis-ci.com/K0lb3/UnityPy) +![Test and Publish](https://github.com/K0lb3/UnityPy/workflows/Test%20and%20Publish/badge.svg) A Unity asset extractor for Python based on [AssetStudio](https://github.com/Perfare/AssetStudio). +Next to extraction it also supports editing Unity assets. +So far following obj types can be edited: + - Texture2D + - Sprite(indirectly via linked Texture2D) + - TextAsset + - MonoBehaviour + +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). + 1. [Installation](#installation) 2. [Example](#example) 3. [Important Classes](#important-classes) 4. [Important Object Types](#important-object-types) -5. [Goals](#goals) -6. [Motivation](#motivation) -7. [Community](#community) ## Installation @@ -29,13 +36,21 @@ or download/clone the git and use python setup.py install ``` +### Notes + +#### Windows + +Visual C++ Redistributable is required for the brotli dependency. + + ## Example The following is a simple example. + ```python import os -from UnityPy import AssetsManager +import UnityPy def unpack_all_assets(source_folder : str, destination_folder : str): # iterate over all files in source folder @@ -43,40 +58,53 @@ def unpack_all_assets(source_folder : str, destination_folder : str): for file_name in files: # generate file_path file_path = os.path.join(root, file_name) - # load that file via AssetsManager - am = AssetsManager(file_path) - - # iterate over all assets and named objects - for asset in am.assets.values(): - for obj in asset.objects.values(): - # only process specific object types - if obj.type in ["Texture2D", "Sprite"]: - # parse the object data - data = obj.read() - - # create destination path - dest = os.path.join(destination_folder, data.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) + # 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 in ["Texture2D", "Sprite"]: + # parse the object data + data = obj.read() + + # create destination path + dest = os.path.join(destination_folder, data.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 in ["Texture2D", "Sprite"]: + data = obj.read() + # 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](#important-classes) and [Important Object Types](#important-object-types) to understand how it works. -People who have slightly advanced python skills should take a look at [AssetBatchConverter.py](AssetBatchConverter.py) for a more advanced example. +People who have slightly advanced python skills should take a look at [examples/AssetBatchConverter.py](examples/AssetBatchConverter.py) for a more advanced example. +It can also be used as general template. ## Important Classes -### [AssetsManager](UnityPy/AssetsManager.py) +### [Environment](UnityPy/environment.py) -AssetsManager loads and parses the files that are given to it. +Environment loads and parses the files that are given to it. It can be initialized via: * a file path - apk files can be loaded as well @@ -86,14 +114,31 @@ It can be initialized via: UnityPy can detect itself if the file is a WebFile, BundleFile, Asset or APK itself. -The unpacked assets will be loaded into ``.assets``, which is a dict consisting of ``asset-name : asset``. +The unpacked assets will be loaded into ``.files``, which is a dict consisting of ``asset-name : asset``. + +The all objects of the loaded assets can be easily accessed via ``.objects``, +which itself is a simple recursive iterator. ```python -from UnityPy import AssetsManager -am = AssetsManager(src) +import io +import UnityPy -for asset_name, asset in am.assets.items(): - pass +# 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](UnityPy/files/SerializedFile.py) @@ -105,7 +150,7 @@ All objects can be found in the ``.objects`` dict - ``{ID : object}``. The objects which have a file path can be found in the ``.container`` dict - ``{path : object}``. -### [Object](UnityPy/ObjectReader.py) +### [Object](UnityPy/files/ObjectReader.py) Objects contain the *actual* files which, e.g. textures, text files, meshes, settings, ... @@ -122,6 +167,21 @@ All object types can be found in [UnityPy/classes](UnityPy/classes/). * ``.m_Width`` - texture width (int) * ``.m_Height`` - texture height (int) +__Export__ +```python +from PIL import Image +for obj in env.objects: + if obj.type == "Texture2D": + # export texture + data = image.read() + data.image.save(path) + # edit texture + fp = os.path.join(replace_dir, data.name) + pil_img = Image.open(fp) + data.image = pil_img + data.save() +``` + ### [Sprite](UnityPy/classes/Sprite.py) Sprites are part of a texture and can have a separate alpha-image as well. @@ -132,6 +192,14 @@ Unlike most other extractors (including AssetStudio) UnityPy merges those two im * ``.m_Width`` - sprite width (int) * ``.m_Height`` - sprite height (int) +__Export__ +```python +for obj in env.objects: + if obj.type == "Sprite": + data = image.read() + data.image.save(path) +``` + ### [TextAsset](UnityPy/classes/TextAsset.py) TextAssets are usually normal text files. @@ -142,39 +210,99 @@ TextAssets are usually normal text files. Some games save binary data as TextFile, so it's usually better to use ``.script``. +__Export__ +```python +for obj in env.objects: + if obj.type == "TextAsset": + # export asset + data = image.read() + with open(path, "wb") as f: + f.write(bytes(data.script)) + # edit asset + fp = os.path.join(replace_dir, data.name) + with open(fp, "rb") as f: + data.script = f.read() + data.save() +``` + ### [MonoBehaviour](UnityPy/classes/MonoBehaviour.py) -MonoBehaviour assets are usually binary data that has to be decoded. -(e.g. via msgpack, protobuf, kaitaistruct etc) +MonoBehaviour assets are usually used to save the class instances with their values. +If a type tree exists it can be used to read the whole data, +but if it doesn't, then it is usually necessary to investigate the class that loads the specific MonoBehaviour to extract the data. +([example](examples/CustomMonoBehaviour/get_scriptable_texture.py)) * ``.name`` -* ``.script``- binary data (bytes) +* ``.script`` +* ``.raw_data`` - data after the basic initialisation -### [AudioClip](UnityPy/classes/AudioClip.py) +__Export__ +```python +import json + +for obj in env.objects: + if obj.type == "MonoBehaviour": + # export + if obj.serialized_type.nodes: + # save decoded data + tree = obj.read_typetree() + fp = os.path.join(extract_dir, f"{data.name}.json"): + with open(fp, "wt", encoding = "utf8") as f: + json.dump(tree, f, ensure_ascii = False, indent = 4) + else: + # save raw relevant data (without Unity MonoBehaviour header) + data = obj.read() + fp = os.path.join(extract_dir, f"{data.name}.bin"): + with open(fp, "wb") as f: + f.write(data.raw_data) + + # edit + if obj.serialized_type.nodes: + tree = obj.read_typetree() + # apply modifications to the data within the tree + obj.save_typetree(tree) + else: + with open(replace_dir, data.name) as f: + data.save(raw_data = f.read()) +``` -* ``.samples`` - ``{sample-name : sample-data}`` dict +### [AudioClip](UnityPy/classes/AudioClip.py) -## Goals +* ``.samples`` - ``{sample-name : sample-data}`` -### WIP +The samples are converted into the .wav format. +The sample-data is a .wav file in bytes. -- [x] A documentation -- [x] The ability to edit assets (like in UABE) +```python +clip : AudioClip +for name, data in clip.samples.items(): + with open(name, "wb") as f: + f.write(data) +``` -### TODO +### [Mesh](UnityPy/classes/Mesh.py) -- [ ] Support for more object types -- [ ] Code optimization -- [ ] Speed-ups via C-extensions -- [ ] Multiprocessing +* ``.export()`` - mesh exported as .obj (str) -## Motivation +The mesh is converted into an Wavefront .obj file. -I'm an active data-miner and noticed that unitypack has problems with new unity assets. -The problem in unitypack isn't that easy to fix and the undocumented code is a bit hard to understand. -That's why I tried other tools like UABE and AssetStudio. Sadly none of these tools can be used like unitypack. -That's why I started this project. +```python +mesh : Mesh +with open(f"{mesh.name}.obj", "wt", newline = "") as f: + # newline = "" is important + f.write(mesh.export()) +``` -## Community +### [Font](UnityPy/classes/Font.py) -### [Discord](https://discord.gg/C6txv7M) +```python +if obj.type == "Font": + font : Font = obj.read() + if font.m_FontData: + extension = ".ttf" + if font.m_FontData[0:4] == b"OTTO": + extension = ".otf" + + with open(os.path.join(path, font.name+extension), "wb") as f: + f.write(font.m_FontData) +``` diff --git a/UnityPy/AssetsManager.py b/UnityPy/AssetsManager.py deleted file mode 100644 index 8fbfeb15..00000000 --- a/UnityPy/AssetsManager.py +++ /dev/null @@ -1,234 +0,0 @@ -import io -import logging -import os -from zipfile import ZipFile - -from .EndianBinaryReader import EndianBinaryReader -from .Progress import Progress -from .files import BundleFile, SerializedFile, WebFile -from .helpers import ImportHelper - -FileType = ImportHelper.FileType - -# create logger -logger = logging.getLogger("UnityPy") -logger.setLevel(logging.DEBUG) - -# create console handler and set level to debug -ch = logging.StreamHandler() -ch.setLevel(logging.DEBUG) - -# create formatter -formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - -# add formatter to ch -ch.setFormatter(formatter) - -# add ch to logger -logger.addHandler(ch) - - -class AssetsManager: - assets: dict - resource_file_readers: dict - import_files: dict - Progress: Progress - - def __init__(self, *args): - self.assets = {} - self.resource_file_readers = {} - self.import_files = {} - self.path = "." - self.Progress = Progress() - - if args: - for arg in args: - if isinstance(arg, str): - if os.path.isfile(arg): - if os.path.splitext(arg)[-1] in [".apk", ".zip"]: - self.load_zip_file(arg) - else: - self.path = os.path.dirname(arg) - self.load_file(arg) - elif os.path.isdir(arg): - self.path = arg - self.load_folder(arg) - else: - self.path = None - self.load_file(arg) - - def load_files(self, files: list): - """Loads all files (list) into the AssetsManager and merges .split files for common usage.""" - path = os.path.dirname(files[0]) - ImportHelper.merge_split_assets(path) - to_read_file = ImportHelper.processing_split_files(files) - self.load(to_read_file) - - def load_folder(self, path: str): - """Loads all files in the given path and its subdirs into the AssetsManager.""" - ImportHelper.merge_split_assets(path, True) - files = ImportHelper.list_all_files(path) - to_read_file = ImportHelper.processing_split_files(files) - self.load(to_read_file) - - def load(self, files: list): - """Loads all files into the AssetsManager.""" - for f in files: - self.import_files[os.path.basename(f)] = f - - self.Progress.reset() - # use a for loop because list size can change - for i, f in enumerate(self.import_files.values()): - self.load_file(f) - self.Progress.report(i + 1, len(self.import_files)) - - def load_file(self, full_name: str = "", data=None): - typ, reader = ImportHelper.check_file_type(data if data else full_name) - if not full_name: - full_name = str(data)[:256] - if typ == FileType.AssetsFile: - self.load_assets_file(full_name, reader) - elif typ == FileType.BundleFile: - self.load_bundle_file(full_name, reader) - elif typ == FileType.WebFile: - self.load_web_file(full_name, reader) - elif typ == FileType.ZIP: - self.load_zip_file(reader.stream) - - def load_zip_file(self, value): - buffer = None - if isinstance(value, str) and os.path.exists(value): - buffer = open(value, "rb") - elif isinstance(value, (bytes, bytearray)): - buffer = ZipFile(io.BytesIO(value)) - elif isinstance(value, (io.BufferedReader, io.BufferedIOBase)): - buffer = value - - z = ZipFile(buffer) - - for path in z.namelist(): - self.load_file(path, z.open(path).read()) - - def load_assets_file(self, full_name: str, reader: EndianBinaryReader): - file_name = os.path.basename(full_name) - if file_name not in self.assets: - logging.info(f"Loading {full_name}") - try: - assets_file = SerializedFile(self, reader) - self.assets[file_name] = assets_file - - for sharedFile in assets_file.externals: - shared_file_path = os.path.join( - os.path.dirname(full_name), sharedFile.name - ) - shared_file_name = sharedFile.name - - if shared_file_name not in self.import_files: - if not os.path.exists(shared_file_path): - find_files = [ - f - for f in ImportHelper.list_all_files( - os.path.dirname(full_name) - ) - if shared_file_name in f - ] - if find_files: - shared_file_path = find_files[0] - - if os.path.exists(shared_file_path): - self.import_files[shared_file_name] = shared_file_path - - return assets_file - - except Exception as e: - reader.dispose() - logging.error(f"Unable to load assets file {file_name}", e) - else: - reader.dispose() - - def load_assets_from_memory( - self, - full_name: str, - reader: EndianBinaryReader, - original_path: str, - unity_version=None, - ): - file_name = os.path.basename(full_name) - if file_name.endswith((".resS", ".resource", ".config", ".xml", ".dat")): - self.resource_file_readers[file_name] = reader - return False - elif file_name not in self.assets: - try: - assets_file = SerializedFile(self, full_name, reader) - assets_file.original_path = original_path - if assets_file.header.version < 7: - assets_file.set_version(unity_version) - self.assets[file_name] = assets_file - except Exception as e: - logging.error( - f"Unable to load assets file {file_name} from {original_path}", e - ) - self.resource_file_readers[file_name] = reader - return False - return True - - def load_bundle_file( - self, full_name: str, reader: EndianBinaryReader, parent_path=None - ): - file_name = os.path.basename(full_name) - logging.info(f"Loading {full_name}") - bundle_file = None - try: - bundle_file = BundleFile(reader, full_name) - for f in bundle_file.files: - dummy_path = os.path.join(os.path.dirname(full_name), f.name) - self.load_assets_from_memory( - dummy_path, - EndianBinaryReader(f.stream), - full_name if parent_path else bundle_file.version_engine, - ) - except Exception as e: - string = f"Unable to load bundle file {file_name}" - if parent_path: - string += f" from {os.path.basename(parent_path)}" - string += "\n" + str(e) - logging.error(string, e) - finally: - reader.dispose() - return bundle_file - - def load_web_file(self, full_name: str, reader: EndianBinaryReader): - file_name = os.path.basename(full_name) - logging.info(f"Loading {full_name}") - dispose = True - web_file = None - try: - web_file = WebFile(reader) - for f in web_file.files: - dummy_path = os.path.join(os.path.dirname(full_name), f.name) - typ, reader = ImportHelper.check_file_type(f.stream) - if typ == FileType.AssetsFile: - dispose = self.load_assets_from_memory( - dummy_path, reader, full_name - ) - elif typ == FileType.BundleFile: - self.load_bundle_file(dummy_path, reader, full_name) - elif typ == FileType.WebFile: - self.load_web_file(dummy_path, reader) - except Exception as e: - logging.error(f"Unable to load web file {file_name}", e) - finally: - if dispose: - reader.dispose() - return web_file - - def clear(self): - for assetsFile in self.assets: - assetsFile.Objects = [] - assetsFile.reader.Close() - self.assets = {} - - for resourceFileReader in self.resource_file_readers: - resourceFileReader.Value.Close() - - self.resource_file_readers = {} diff --git a/UnityPy/ObjectReader.py b/UnityPy/ObjectReader.py deleted file mode 100644 index d387d142..00000000 --- a/UnityPy/ObjectReader.py +++ /dev/null @@ -1,37 +0,0 @@ -from . import classes - - -class ObjectReader: - def __init__(self, reader, assets_file, object_info): - self.assets_file = assets_file - self.path_id = object_info.path_id - self.byte_start = object_info.byte_start - self.byte_size = object_info.byte_size - self.serialized_type = object_info.serialized_type - self.platform = assets_file.target_platform - self.version2 = assets_file.header.version - self.type = object_info.class_id - self.version = assets_file.version - self.build_type = assets_file.build_type - self.reader = reader - - @property - def container(self): - return ( - self.assets_file._container[self.path_id] - if self.path_id in self.assets_file._container - else None - ) - - def reset(self): - self.reader.Position = self.byte_start - - def read(self): - return getattr(classes, self.type.name, classes.Object)(self) - - def __getattr__(self, item: str): - if hasattr(self.reader, item): - return getattr(self.reader, item) - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.type.name) diff --git a/UnityPy/Progress.py b/UnityPy/Progress.py deleted file mode 100644 index 48a0dea1..00000000 --- a/UnityPy/Progress.py +++ /dev/null @@ -1,16 +0,0 @@ -class Progress: - pre_value: int - - def reset(self): - self.pre_value = 0 - self.report(0) - - def report(self, current: int, total: int = None): - if total: - value = current * 100 // total - else: - value = current - - if value > self.pre_value: - self.pre_value = value - print(f"{value}%") diff --git a/UnityPy/ResourceReader.py b/UnityPy/ResourceReader.py deleted file mode 100644 index ac0749c9..00000000 --- a/UnityPy/ResourceReader.py +++ /dev/null @@ -1,67 +0,0 @@ -import os - -from .helpers import ImportHelper - - -class ResourceReader: - need_search: bool - path: str - assets_file: None - offset: int - size: int - - def __init__(self, *args): - """ - Input: - Option 1: - path - file path - assets_file - SerializedFile - offset - - size - - Option 2: - reader - EndianBinaryReader - offset - - size - - """ - if len(args) == 4: - self.need_search = True - self.path = args[0] - self.assets_file = args[1] - elif len(args) == 3: - self.need_search = False - self.reader = args[0] - self.offset = args[-2] - self.size = args[-1] - - def get_data(self): - if self.need_search: - resource_file_name = os.path.basename(self.path) - - reader = self.assets_file.assets_manager.resource_file_readers.get( - resource_file_name - ) - if reader: - reader.Position = self.offset - return reader.read_bytes(self.size) - - current_directory = os.path.dirname(self.assets_file.full_name) - resource_file_path = os.path.join(current_directory, resource_file_name) - if not os.path.isfile(resource_file_path): - find_files = ImportHelper.find_all_files( - current_directory, resource_file_name - ) - if find_files: - resource_file_path = find_files[0] - - if os.path.isfile(resource_file_path): - with open(resource_file_path, "rb") as f: - f.seek(self.offset) - return f.read(self.size) - else: - raise FileNotFoundError( - f"Can't find the resource file {resource_file_name}" - ) - - else: - self.reader.Position = self.offset - return self.reader.read_bytes(self.size) diff --git a/UnityPy/__init__.py b/UnityPy/__init__.py index 223c1226..565fba62 100644 --- a/UnityPy/__init__.py +++ b/UnityPy/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.5.1" +__version__ = "1.7.11" from .environment import Environment diff --git a/UnityPy/classes/Animation.py b/UnityPy/classes/Animation.py index d9bc05eb..3006fcee 100644 --- a/UnityPy/classes/Animation.py +++ b/UnityPy/classes/Animation.py @@ -5,6 +5,6 @@ class Animation(Behaviour): def __init__(self, reader): super().__init__(reader=reader) - self.animation = PPtr(reader) + self.m_Animation = PPtr(reader) num_animations = reader.read_int() - self.animations = [PPtr(reader) for i in range(num_animations)] + self.m_Animations = [PPtr(reader) for _ in range(num_animations)] diff --git a/UnityPy/classes/AnimationClip.py b/UnityPy/classes/AnimationClip.py index 1632c035..afea6efe 100644 --- a/UnityPy/classes/AnimationClip.py +++ b/UnityPy/classes/AnimationClip.py @@ -3,7 +3,7 @@ from .NamedObject import NamedObject from .PPtr import PPtr -from ..enums import ClassIDType +from ..enums import ClassIDType, makeClassID from ..math import Quaternion, Vector3 from ..streams import EndianBinaryReader @@ -80,12 +80,12 @@ def UnpackFloats( indexPos: int = bitPos // 8 bitPos %= 8 - scale: float = 1.0 / self.m_Range + scale: float = (1.0 / self.m_Range) if self.m_Range else float('inf') if numChunks == -1: numChunks = self.m_NumItems // itemCountInChunk - end = chunkStride * numChunks / 4 + end = int(chunkStride * numChunks / 4) data = [] - for index in (0, end, chunkStride // 4): + for index in range(0, end, chunkStride // 4): for i in range(itemCountInChunk): x = 0 # uint bits = 0 @@ -101,7 +101,8 @@ def UnpackFloats( bitPos = 0 x &= uint((1 << self.m_BitSize) - 1) # (uint)(1 << m_BitSize) - 1u - data.append(x / (scale * ((1 << self.m_BitSize) - 1)) + self.m_Start) + denomi = scale * ((1 << self.m_BitSize) - 1) + data.append( (x / denomi if denomi else float('inf')) + self.m_Start) return data @@ -128,7 +129,7 @@ def save(self, writer): writer.align_stream() def UnpackInts(self): - data = [] + data = [0] * self.m_NumItems indexPos = 0 bitPos = 0 m_BitSize = self.m_BitSize @@ -136,7 +137,7 @@ def UnpackInts(self): for i in range(self.m_NumItems): bits = 0 entry = 0 - while bits << m_BitSize: + while bits < m_BitSize: entry |= (self.m_Data[indexPos] >> bitPos) << bits num = min(m_BitSize - bits, 8 - bitPos) bitPos += num @@ -157,7 +158,7 @@ def __init__(self, reader): def UnpackQuats(self): m_Data = self.m_Data - data = [] + data = [None]*self.m_NumItems indexPos = 0 bitPos = 0 @@ -224,7 +225,7 @@ def __init__(self, reader): self.curve = AnimationCurve(reader, reader.read_float) # Float self.attribute = reader.read_aligned_string() self.path = reader.read_aligned_string() - self.classID = ClassIDType(reader.read_int()) + self.classID = makeClassID(reader.read_int()) self.script = PPtr(reader) # MonoScript @@ -341,7 +342,7 @@ def CalculateNextInSlope(self, dx: float, rhs): """ # Stepped if self.coeff[0] == 0 and self.coeff[1] == 0 and self.coeff[2] == 0: - return float.PositiveInfinity + return float('inf') dx = max(dx, 0.0001) dy = rhs.value - self.value @@ -365,7 +366,9 @@ def __init__(self, reader): def ReadData(self): frameList = [] - buffer = self.data[0 : len(self.data) * 4] + buffer = b"".join( + val.to_bytes(4, 'big') for val in self.data + ) reader = EndianBinaryReader(buffer) while reader.Position < reader.Length: frameList.append(StreamedFrame(reader)) @@ -493,9 +496,9 @@ def __init__(self, reader): self.attribute = reader.read_u_int() self.script = PPtr(reader) # Object if version >= (5, 6): # 5.6 and up - self.typeID = ClassIDType(reader.read_int()) + self.typeID = makeClassID(reader.read_int()) else: - self.typeID = ClassIDType(reader.read_u_short()) + self.typeID = makeClassID(reader.read_u_short()) self.customType = reader.read_byte() self.isPPtrCurve = reader.read_byte() reader.align_stream() @@ -509,26 +512,26 @@ def __init__(self, reader): numMappings = reader.read_int() self.pptrCurveMapping = [PPtr(reader) for _ in range(numMappings)] # Object - def FindBinding(self, index): - curves = 0 - for b in self.genericBindings: - if b.typeID == ClassIDType.Transform: # - switch = b.attribute - - if switch in [1, 3, 4]: - # case 1: #kBindTransformPosition - # case 3: #kBindTransformScale - # case 4: #kBindTransformEuler - curves += 3 - elif switch == 2: # kBindTransformRotation - curves += 4 - else: - curves += 1 + def FindBinding(self, index): + curves = 0 + for b in self.genericBindings: + if b.typeID == ClassIDType.Transform: # + switch = b.attribute + + if switch in [1, 3, 4]: + # case 1: #kBindTransformPosition + # case 3: #kBindTransformScale + # case 4: #kBindTransformEuler + curves += 3 + elif switch == 2: # kBindTransformRotation + curves += 4 else: curves += 1 - if curves > index: - return b - return None + else: + curves += 1 + if curves > index: + return b + return None class AnimationType(IntEnum): diff --git a/UnityPy/classes/Animator.py b/UnityPy/classes/Animator.py index 7fd0f006..a134cb86 100644 --- a/UnityPy/classes/Animator.py +++ b/UnityPy/classes/Animator.py @@ -6,30 +6,30 @@ class Animator(Behaviour): def __init__(self, reader): super().__init__(reader=reader) - self.avatar = PPtr(reader) # Avatar - self.controller = PPtr(reader) # RuntimeAnimatorController - self.culling_mode = reader.read_int() + self.m_Avatar = PPtr(reader) # Avatar + self.m_Controller = PPtr(reader) # RuntimeAnimatorController + self.m_CullingMode = reader.read_int() version = self.version - if version[:2] > (4, 4): - self.update_mode = reader.read_int() + if version >= (4, 5): # 4.5 and up + self.m_UpdateMode = reader.read_int() self.m_ApplyRootMotion = reader.read_boolean() - if version >= (4, 5): # 4.5 and up - 5.0 down + if (4, 5) < version[2:] <= (5, 0): # 4.5 and up - 5.0 down reader.align_stream() if version >= (5,): # 5.0 and up - self.linear_velocity_blending = reader.read_boolean() + self.m_LinearVelocityBlending = reader.read_boolean() reader.align_stream() - if version < (4, 5): # 4.5 down - self.animate_physics = reader.read_boolean() + if version[2:] < (4, 5): # 4.5 down + self.m_AnimatePhysics = reader.read_boolean() if version >= (4, 3): # 4.3 and up - self.has_transform_hierarchy = reader.read_boolean() + self.m_HasTransformHierarchy = reader.read_boolean() if version >= (4, 5): # 4.5 and up - self.allow_constant_clip_sampling_optimization = reader.read_boolean() + self.m_AllowConstantClipSamplingOptimization = reader.read_boolean() if (4,) < version[:1] < (2018,): # 5.0 and up - 2018 down reader.align_stream() diff --git a/UnityPy/classes/AnimatorController.py b/UnityPy/classes/AnimatorController.py index edfa303b..0b8999e3 100644 --- a/UnityPy/classes/AnimatorController.py +++ b/UnityPy/classes/AnimatorController.py @@ -171,7 +171,8 @@ def __init__(self, reader): version = reader.version numNodes = reader.read_int() - self.m_NodeArray = [BlendTreeNodeConstant(reader) for _ in range(numNodes)] + self.m_NodeArray = [BlendTreeNodeConstant( + reader) for _ in range(numNodes)] if version < (4, 5): # 4.5 down self.m_BlendEventArrayConstant = ValueArrayConstant(reader) @@ -190,7 +191,8 @@ def __init__(self, reader): if version < (5, 2): # 5.2 down numInfos = reader.read_int() - self.m_LeafInfoArray = [LeafInfoConstant(reader) for _ in range(numInfos)] + self.m_LeafInfoArray = [LeafInfoConstant( + reader) for _ in range(numInfos)] numBlends = reader.read_int() self.m_BlendTreeConstantArray = [ @@ -252,7 +254,8 @@ def __init__(self, reader): version = reader.version numStates = reader.read_int() - self.m_StateConstantArray = [StateConstant(reader) for _ in range(numStates)] + self.m_StateConstantArray = [ + StateConstant(reader) for _ in range(numStates)] numAnyStates = reader.read_int() self.m_AnyStateTransitionConstantArray = [ diff --git a/UnityPy/classes/AnimatorOverrideController.py b/UnityPy/classes/AnimatorOverrideController.py index 8274be25..0544d314 100644 --- a/UnityPy/classes/AnimatorOverrideController.py +++ b/UnityPy/classes/AnimatorOverrideController.py @@ -4,13 +4,14 @@ class AnimationClipOverride: def __init__(self, reader): - self.original_clip = PPtr(reader) - self.override_clip = PPtr(reader) + self.m_OriginalClip = PPtr(reader) + self.m_OverrideClip = PPtr(reader) class AnimatorOverrideController(RuntimeAnimatorController): def __init__(self, reader): super().__init__(reader=reader) - self.controller = PPtr(reader) + self.m_Controller = PPtr(reader) num_overrides = reader.read_int() - self.clips = [AnimationClipOverride(reader) for i in range(num_overrides)] + self.m_Clips = [AnimationClipOverride( + reader) for _ in range(num_overrides)] diff --git a/UnityPy/classes/AssetBundle.py b/UnityPy/classes/AssetBundle.py index f0d178dc..d4fd87c8 100644 --- a/UnityPy/classes/AssetBundle.py +++ b/UnityPy/classes/AssetBundle.py @@ -13,9 +13,10 @@ class AssetBundle(NamedObject): def __init__(self, reader): super().__init__(reader=reader) preload_table_size = reader.read_int() - self.preload_table = [PPtr(reader) for _ in range(preload_table_size)] + self.m_PreloadTable = [PPtr(reader) for _ in range(preload_table_size)] container_size = reader.read_int() - self.container = {} + self.m_Container = {} + # TODO - m_Container is a multi-dict, multiple values can have the same key for i in range(container_size): key = reader.read_aligned_string() - self.container[key] = AssetInfo(reader) + self.m_Container[key] = AssetInfo(reader) diff --git a/UnityPy/classes/AudioClip.py b/UnityPy/classes/AudioClip.py index 36983901..3f8bb012 100644 --- a/UnityPy/classes/AudioClip.py +++ b/UnityPy/classes/AudioClip.py @@ -18,15 +18,14 @@ def __init__(self, reader): if version >= (3, 2): # and version <= (5,): # 3.2.0 to 5 self.m_Stream = reader.read_int() - m_Size = reader.read_int() - tsize = m_Size + 4 - m_Size % 4 if (m_Size % 4 != 0) else m_Size + self.m_Size = reader.read_int() + tsize = self.m_Size + 4 - self.m_Size % 4 if (self.m_Size % 4 != 0) else self.m_Size if reader.byte_size + reader.byte_start - reader.Position != tsize: - m_Offset = reader.read_u_int() + self.m_Offset = reader.read_u_int() self.m_Source = self.assets_file.full_name + ".resS" else: - m_Size = reader.read_int() + self.m_Size = reader.read_int() - self.extension = AUDIO_TYPE_EXTEMSION.get(self.m_Type, ".audioclip") else: self.m_LoadType = reader.read_int() self.m_Channels = reader.read_int() @@ -41,20 +40,20 @@ def __init__(self, reader): self.m_Legacy3D = reader.read_boolean() reader.align_stream() self.m_Source = reader.read_aligned_string() - m_Offset = reader.read_u_long() - m_Size = reader.read_long() + self.m_Offset = reader.read_u_long() + self.m_Size = reader.read_long() self.m_CompressionFormat = AudioCompressionFormat(reader.read_int()) - self.extension = AUDIO_TYPE_EXTEMSION.get( - self.m_CompressionFormat, ".audioclip" - ) - if self.m_Source: self.m_AudioData = get_resource_data( - self.m_Source, self.assets_file, m_Offset, m_Size + self.m_Source, self.assets_file, self.m_Offset, self.m_Size ) else: - self.m_AudioData = reader.read_bytes(m_Size) + self.m_AudioData = reader.read_bytes(self.m_Size) + + @property + def extension(self): + return AUDIO_TYPE_EXTEMSION.get(self.m_CompressionFormat, ".audioclip") @property def samples(self) -> dict: diff --git a/UnityPy/classes/Avatar.py b/UnityPy/classes/Avatar.py index c32789a4..973770b6 100644 --- a/UnityPy/classes/Avatar.py +++ b/UnityPy/classes/Avatar.py @@ -106,7 +106,8 @@ def __init__(self, reader): numHandles = reader.read_int() self.m_Handles = [Handle(reader) for _ in range(numHandles)] numColliders = reader.read_int() - self.m_ColliderArray = [Collider(reader) for _ in range(numColliders)] + self.m_ColliderArray = [Collider(reader) + for _ in range(numColliders)] self.m_HumanBoneIndex = reader.read_int_array() self.m_HumanBoneMass = reader.read_float_array() diff --git a/UnityPy/classes/Behaviour.py b/UnityPy/classes/Behaviour.py index 1a7ec5e5..0e0bb4ba 100644 --- a/UnityPy/classes/Behaviour.py +++ b/UnityPy/classes/Behaviour.py @@ -1,8 +1,16 @@ from .Component import Component - +from ..streams import EndianBinaryReader, EndianBinaryWriter class Behaviour(Component): - def __init__(self, reader): + def __init__(self, reader : EndianBinaryReader): super().__init__(reader=reader) - self.enabled = reader.read_byte() - self._dummy = reader.align_stream() + self.m_Enabled = reader.read_byte() + reader.align_stream() + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + version = self.version + super().save(writer) + writer.write_byte(self.m_Enabled) + writer.align_stream() \ No newline at end of file diff --git a/UnityPy/classes/Component.py b/UnityPy/classes/Component.py index cd6c1c10..f36abfe8 100644 --- a/UnityPy/classes/Component.py +++ b/UnityPy/classes/Component.py @@ -1,8 +1,16 @@ from .EditorExtension import EditorExtension -from .PPtr import PPtr +from .PPtr import PPtr, save_ptr +from ..streams import EndianBinaryReader, EndianBinaryWriter class Component(EditorExtension): - def __init__(self, reader): + def __init__(self, reader: EndianBinaryReader): super().__init__(reader=reader) - self.GameObject = PPtr(reader) # GameObject + self.m_GameObject = PPtr(reader) # GameObject + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + version = self.version + super().save(writer) + save_ptr(self.m_GameObject, writer) diff --git a/UnityPy/classes/EditorExtension.py b/UnityPy/classes/EditorExtension.py index 24292d0f..4d01b55b 100644 --- a/UnityPy/classes/EditorExtension.py +++ b/UnityPy/classes/EditorExtension.py @@ -1,18 +1,20 @@ from .Object import Object from .PPtr import PPtr, save_ptr from ..enums import BuildTarget -from ..streams import EndianBinaryWriter, EndianBinaryReader +from ..streams import EndianBinaryWriter class EditorExtension(Object): - def __init__(self, reader: EndianBinaryReader): + def __init__(self, reader): super().__init__(reader=reader) if self.platform == BuildTarget.NoTarget: - self.prefab_parent_object = PPtr(reader) - self.prefab_internal = PPtr(reader) + self.m_PrefabParentObject = PPtr(reader) + self.m_PrefabInternal = PPtr(reader) - def save(self, writer: EndianBinaryWriter): + def save(self, writer: EndianBinaryWriter = None): + if not writer: + writer = EndianBinaryWriter(endian = self.reader.endian) super().save(writer, intern_call=True) if self.platform == BuildTarget.NoTarget: - save_ptr(self.prefab_parent_object, writer, self.reader.version2) - save_ptr(self.prefab_internal, writer, self.reader.version2) + save_ptr(self.m_PrefabParentObject, writer) + save_ptr(self.m_PrefabInternal, writer) diff --git a/UnityPy/classes/Font.py b/UnityPy/classes/Font.py index a4ba5dcb..0241b478 100644 --- a/UnityPy/classes/Font.py +++ b/UnityPy/classes/Font.py @@ -7,34 +7,34 @@ def __init__(self, reader): super().__init__(reader=reader) version = self.version if version >= (5, 5): # 5.5 and up: - self.LineSpacing = reader.read_float() - self.DefaultMaterial = PPtr(reader) - self.FontSize = reader.read_float() - self.Texture = PPtr(reader) - self.AsciiStartOffset = reader.read_int() - self.Tracking = reader.read_float() - self.CharacterSpacing = reader.read_int() - self.CharacterPadding = reader.read_int() - self.ConvertCase = reader.read_int() + self.m_LineSpacing = reader.read_float() + self.m_DefaultMaterial = PPtr(reader) + self.m_FontSize = reader.read_float() + self.m_Texture = PPtr(reader) + self.m_AsciiStartOffset = reader.read_int() + self.m_Tracking = reader.read_float() + self.m_CharacterSpacing = reader.read_int() + self.m_CharacterPadding = reader.read_int() + self.m_ConvertCase = reader.read_int() CharacterRects_size = reader.read_int() for i in range(CharacterRects_size): reader.Position += 44 # CharacterInfo data 41 KerningValues_size = reader.read_int() for i in range(KerningValues_size): reader.Position += 8 - self.PixelScale = reader.read_float() + self.m_PixelScale = reader.read_float() FontData_size = reader.read_int() if FontData_size > 0: - self.FontData = reader.read_bytes(FontData_size) + self.m_FontData = reader.read_bytes(FontData_size) else: - self.AsciiStartOffset = reader.read_int() + self.m_AsciiStartOffset = reader.read_int() if version[:1] <= (3,): - self.FontCountX = reader.read_int() - self.FontCountY = reader.read_int() + self.m_FontCountX = reader.read_int() + self.m_FontCountY = reader.read_int() - self.Kerning = reader.read_float() - self.LineSpacing = reader.read_float() + self.m_Kerning = reader.read_float() + self.m_LineSpacing = reader.read_float() if version[:1] <= (3,): PerCharacterKerning_size = reader.read_int() @@ -42,11 +42,11 @@ def __init__(self, reader): first = reader.read_int() second = reader.read_float() else: - self.CharacterSpacing = reader.read_int() - self.CharacterPadding = reader.read_int() + self.m_CharacterSpacing = reader.read_int() + self.m_CharacterPadding = reader.read_int() - self.ConvertCase = reader.read_int() - self.DefaultMaterial = PPtr(reader) + self.m_ConvertCase = reader.read_int() + self.m_DefaultMaterial = PPtr(reader) CharacterRects_size = reader.read_int() for i in range(CharacterRects_size): @@ -67,7 +67,7 @@ def __init__(self, reader): flipped = reader.read_boolean() reader.align_stream() - self.Texture = PPtr(reader) + self.m_Texture = PPtr(reader) KerningValues_size = reader.read_int() for i in range(KerningValues_size): @@ -76,10 +76,10 @@ def __init__(self, reader): second = reader.read_float() if version[:1] <= (3,): - self.GridFont = reader.read_boolean() + self.m_GridFont = reader.read_boolean() reader.align_stream() else: - self.PixelScale = reader.read_float() + self.m_PixelScale = reader.read_float() FontData_size = reader.read_int() if FontData_size > 0: - self.FontData = reader.read_bytes(FontData_size) + self.m_FontData = reader.read_bytes(FontData_size) diff --git a/UnityPy/classes/GameObject.py b/UnityPy/classes/GameObject.py index c53f12e6..838f649d 100644 --- a/UnityPy/classes/GameObject.py +++ b/UnityPy/classes/GameObject.py @@ -3,24 +3,13 @@ class GameObject(EditorExtension): - """ - public PPtr[] m_Components; - public string m_Name; - public Transform m_Transform; - public MeshRenderer m_MeshRenderer; - public MeshFilter m_MeshFilter; - public SkinnedMeshRenderer m_SkinnedMeshRenderer; - public Animator m_Animator; - public Animation m_Animation; - """ - def __init__(self, reader): super().__init__(reader=reader) component_size = reader.read_int() - self.components = [] + self.m_Components = [None]*component_size for i in range(component_size): if self.version < (5, 5): first = reader.read_int() - self.components.append(PPtr(reader)) - self.layer = reader.read_int() + self.m_Components[i] = PPtr(reader) + self.m_Layer = reader.read_int() self.name = reader.read_aligned_string() diff --git a/UnityPy/classes/Mesh.py b/UnityPy/classes/Mesh.py index 4778ea44..bcb8a490 100644 --- a/UnityPy/classes/Mesh.py +++ b/UnityPy/classes/Mesh.py @@ -1,4 +1,5 @@ import math +import traceback from .AnimationClip import AABB, PackedFloatVector, PackedIntVector from .NamedObject import NamedObject @@ -6,6 +7,11 @@ from ..helpers.ResourceReader import get_resource_data from ..math import Matrix4x4, Vector3 from ..streams import EndianBinaryWriter +from ..enums import GfxPrimitiveType +import struct +from enum import IntEnum +from ..export.MeshExporter import export_mesh + class MinMaxAABB: def __init__(self, reader): @@ -22,7 +28,7 @@ def __init__(self, reader): version = reader.version self.m_Vertices = PackedFloatVector(reader) self.m_UV = PackedFloatVector(reader) - if version < (5,): # 5 down + if version[0] < 5: # 5 down self.m_BindPoses = PackedFloatVector(reader) self.m_Normals = PackedFloatVector(reader) self.m_Tangents = PackedFloatVector(reader) @@ -34,7 +40,7 @@ def __init__(self, reader): self.m_BoneIndices = PackedIntVector(reader) self.m_Triangles = PackedIntVector(reader) if version >= (3, 5): # 3.5 and up - if version < (5,): # 5 down + if version[0] < 5: # 5 down self.m_Colors = PackedIntVector(reader) else: self.m_UVInfo = reader.read_u_int() @@ -42,7 +48,7 @@ def __init__(self, reader): def save(self, writer, version): self.m_Vertices.save(writer) self.m_UV.save(writer) - if version < (5,): # 5 down + if version < (5,): # 5 down self.m_BindPoses.save(writer) self.m_Normals.save(writer) self.m_Tangents.save(writer) @@ -54,10 +60,11 @@ def save(self, writer, version): self.m_BoneIndices.save(writer) self.m_Triangles.save(writer) if version >= (3, 5): # 3.5 and up - if version < (5,): # 5 down + if version < (5,): # 5 down self.m_Colors.save(writer) else: - writer.write_u_int(m_UVInfo) + writer.write_u_int(self.m_UVInfo) + class StreamInfo: def __init__(self, **kwargs): @@ -77,7 +84,7 @@ def __init__(self, **kwargs): else: self.__dict__ = kwargs - def save(self, writer, version): + def save(self, writer: EndianBinaryWriter, version: tuple): writer.write_u_int(self.channelMask) writer.write_u_int(self.offset) @@ -89,6 +96,7 @@ def save(self, writer, version): writer.write_byte(self.dividerOp) writer.write_u_short(self.frequency) + class ChannelInfo: def __init__(self, reader): self.stream = reader.read_byte() @@ -102,6 +110,7 @@ def save(self, writer): writer.write_byte(self.format) writer.write_byte(self.dimension) + class VertexData: def __init__(self, reader): self.reader = reader @@ -114,7 +123,8 @@ def __init__(self, reader): if version >= (4,): # 4.0 and up m_ChannelsSize = reader.read_int() - self.m_Channels = [ChannelInfo(reader) for _ in range(m_ChannelsSize)] + self.m_Channels = [ChannelInfo(reader) + for _ in range(m_ChannelsSize)] if version < (5,): # 5.0 down if version < (4,): # 4.0 down @@ -122,7 +132,8 @@ def __init__(self, reader): else: m_StreamsSize = reader.read_int() - self.m_Streams = [StreamInfo(reader=reader) for _ in range(m_StreamsSize)] + self.m_Streams = [StreamInfo(reader=reader) + for _ in range(m_StreamsSize)] if version < (4,): # 4.0 down self.GetChannels() @@ -144,15 +155,15 @@ def save(self, writer: EndianBinaryWriter, version): ch.save(writer) if (4,) <= version[:2] < (5,): # 4.0 and up to 5.0 - writer.write_int(len(m_Streams)) + writer.write_int(len(self.m_Streams)) for stream in self.m_Streams: - stream.save(writer=writer, varsion=version) + stream.save(writer=writer, version=version) if version < (4,): # 4.0 down raise Exception("Unsupported version") else: # 5.0 and up - #for stream in self.m_Streams: + # for stream in self.m_Streams: # stream.save(writer) pass @@ -160,11 +171,11 @@ def save(self, writer: EndianBinaryWriter, version): writer.write_bytes(self.m_DataSize) writer.align_stream() - def GetStreams(self): - streamCount = 1 + ( - 0 if not self.m_Channels else max([x.stream for x in self.m_Channels]) - ) + streamCount = 1 + if self.m_Channels: + streamCount += max(x.stream for x in self.m_Channels) + self.m_Streams = {} offset = 0 for s in range(streamCount): @@ -174,9 +185,10 @@ def GetStreams(self): if m_Channel.stream == s: if m_Channel.dimension > 0: chnMask |= 1 << chn # Shift 1UInt << chn - stride += m_Channel.dimension * MeshHelper.GetChannelFormatSize( - m_Channel.format - ) + stride += m_Channel.dimension * MeshHelper.GetFormatSize( + self.reader.version, + m_Channel.format + ) self.m_Streams[s] = StreamInfo( channelMask=chnMask, offset=offset, @@ -218,38 +230,10 @@ def GetChannels(self): elif i == 5: # kShaderChannelTangent m_Channel.format = 0 # kChannelFormatFloat m_Channel.dimension = 4 - offset += m_Channel.dimension * MeshHelper.GetChannelFormatSize( - m_Channel.format + offset += m_Channel.dimension * MeshHelper.GetFormatSize( + self.reader.version, m_Channel.format ) - def FixChannel(self): - if any( - [x.dimension > 4 for x in self.m_Channels] - ): # m_Channels.FirstOrDefault(x => x.dimension > 4) != null: # - fixStream = max( - [x.stream for x in self.m_Channels] - ) # m_Channels.Max(x => x.stream) - fixChannels = [ - x for x in self.m_Channels if x.dimension > 0 and x.stream == fixStream - ] - stride = 0 - for i, curChannel in enumerate(fixChannels): - preChannel = fixChannels[i - 1] - offset = curChannel.offset - preChannel.offset - preChannel.dimension = offset / MeshHelper.GetChannelFormatSize( - preChannel.format - ) - stride += offset - # Fix Last - m_Channel = fixChannels[-1] - streamSize = len(self.m_DataSize) - self.m_Streams[fixStream].offset - totalStride = streamSize / self.m_VertexCount - channelStride = totalStride - stride - m_Channel.dimension = channelStride / MeshHelper.GetChannelFormatSize( - m_Channel.format - ) - self.GetStreams() - class BoneWeights4: def __init__(self, reader=None): @@ -310,11 +294,13 @@ def __init__(self, reader): self.shapes = [MeshBlendShape(reader) for _ in range(numShapes)] numChannels = reader.read_int() - self.channels = [MeshBlendShapeChannel(reader) for _ in range(numChannels)] + self.channels = [MeshBlendShapeChannel( + reader) for _ in range(numChannels)] self.fullWeights = reader.read_float_array() else: m_ShapesSize = reader.read_int() - self.m_Shapes = [MeshBlendShape(reader) for _ in range(m_ShapesSize)] + self.m_Shapes = [MeshBlendShape(reader) + for _ in range(m_ShapesSize)] reader.align_stream() m_ShapeVerticesSize = reader.read_int() self.m_ShapeVertices = [ @@ -323,10 +309,11 @@ def __init__(self, reader): class SubMesh: - def __init__(self, reader, version): + def __init__(self, reader): + version = reader.version self.firstByte = reader.read_u_int() self.indexCount = reader.read_u_int() - self.topology = reader.read_int() + self.topology = GfxPrimitiveType(reader.read_int()) if version < (4,): # 4.0 down self.triangleCount = reader.read_u_int() @@ -342,7 +329,7 @@ def __init__(self, reader, version): def save(self, writer, version): writer.write_u_int(self.firstByte) writer.write_u_int(self.indexCount) - writer.write_int(self.topology) + writer.write_int(self.topology.value) if version < (4,): # 4.0 down writer.write_u_int(self.triangleCount) @@ -357,7 +344,9 @@ def save(self, writer, version): class Mesh(NamedObject): - m_StreamData: None + + def export(self): + return export_mesh(self) def __init__(self, reader): super().__init__(reader=reader) @@ -365,6 +354,21 @@ def __init__(self, reader): self.m_Use16BitIndices = True self.m_Indices = [] + self.m_BindPose = [] + self.m_BoneNameHashes = [] + self.m_Vertices = [] + self.m_Skin = [] + self.m_Normals = [] + self.m_Colors = [] + self.m_UV0 = [] + self.m_UV1 = [] + self.m_UV2 = [] + self.m_UV3 = [] + self.m_UV4 = [] + self.m_UV5 = [] + self.m_UV6 = [] + self.m_UV7 = [] + self.m_Tangents = [] if version < (3, 5): # 3.5 down self.m_Use16BitIndices = reader.read_int() > 0 @@ -372,14 +376,15 @@ def __init__(self, reader): if version[:2] <= (2, 5): # 2.5 and down m_IndexBuffer_size = reader.read_int() - if self.m_Use16BitIndices: # + if self.m_Use16BitIndices: self.m_IndexBuffer = [ reader.read_u_short() for _ in range(math.ceil(m_IndexBuffer_size / 2)) ] reader.align_stream() else: - self.m_IndexBuffer = reader.read_u_int_array(m_IndexBuffer_size // 4) + self.m_IndexBuffer = reader.read_u_int_array( + math.ceil(m_IndexBuffer_size / 4)) m_SubMeshesSize = reader.read_int() self.m_SubMeshes = [SubMesh(reader) for _ in range(m_SubMeshesSize)] @@ -395,7 +400,8 @@ def __init__(self, reader): if version >= (2, 6): # 2.6.0 and up if version >= (2019,): # 2019 and up m_BonesAABBSize = reader.read_int() - self.m_BonesAABB = [MinMaxAABB(reader) for _ in range(m_BonesAABBSize)] + self.m_BonesAABB = [MinMaxAABB(reader) + for _ in range(m_BonesAABBSize)] self.m_VariableBoneCountWeights = reader.read_u_int_array() self.m_MeshCompression = reader.read_byte() @@ -409,31 +415,39 @@ def __init__(self, reader): # Unity fixed it in 2017.3.1p1 and later versions if ( - version >= (2017, 4) # 2017.4 - or version[:3] == (2017, 3, 1) and self.build_type.IsPatch # fixed after 2017.3.1px - or version[:2] == (2017, 3) and self.m_MeshCompression == 0 # 2017.3.xfx with no compression + version >= (2017, 4) # 2017.4 + # fixed after 2017.3.1px + or version[:3] == (2017, 3, 1) and self.build_type.IsPatch + # 2017.3.xfx with no compression + or version[:2] == (2017, 3) and self.m_MeshCompression == 0 ): self.m_IndexFormat = reader.read_int() + self.m_Use16BitIndices = self.m_IndexFormat == 0 m_IndexBuffer_size = reader.read_int() if self.m_Use16BitIndices: self.m_IndexBuffer = [ - reader.read_u_short() for _ in range(m_IndexBuffer_size // 2) + reader.read_u_short() for _ in range(math.ceil(m_IndexBuffer_size / 2)) ] reader.align_stream() else: - self.m_IndexBuffer = reader.read_u_int_array(m_IndexBuffer_size // 4) + self.m_IndexBuffer = reader.read_u_int_array( + math.ceil(m_IndexBuffer_size / 4)) if version < (3, 5): # 3.4.2 and earlier - m_VertexCount = reader.read_int() - self.m_Vertices = reader.read_float_array(m_VertexCount * 3) # Vector3 + self.m_VertexCount = reader.read_int() + self.m_Vertices = reader.read_float_array( + self.m_VertexCount * 3) # Vector3 - m_SkinSize = reader.read_int() - self.m_Skin = [BoneWeights4(reader) for _ in range(m_SkinSize)] + self.m_SkinSize = reader.read_int() + self.m_Skin = [BoneWeights4(reader) + for _ in range(self.m_SkinSize)] self.m_BindPose = reader.read_matrix_array() - self.m_UV0 = reader.read_float_array(reader.read_int() * 2) # Vector2 - self.m_UV1 = reader.read_float_array(reader.read_int() * 2) # Vector2 + self.m_UV0 = reader.read_float_array( + reader.read_int() * 2) # Vector2 + self.m_UV1 = reader.read_float_array( + reader.read_int() * 2) # Vector2 if version[:2] <= (2, 5): # 2.5 and down m_TangentSpace_size = reader.read_int() @@ -446,7 +460,8 @@ def __init__(self, reader): self.m_Tangents[v * 3] = reader.read_float() self.m_Tangents[v * 3 + 1] = reader.read_float() self.m_Tangents[v * 3 + 2] = reader.read_float() - self.m_Tangents[v * 3 + 3] = reader.read_float() # handedness + # handedness + self.m_Tangents[v * 3 + 3] = reader.read_float() else: # 2.6.0 and later self.m_Tangents = reader.read_float_array( reader.read_int() * 4 @@ -455,7 +470,7 @@ def __init__(self, reader): reader.read_int() * 3 ) # Vector3 else: - if version < (2018, 2): # 2018.2 down + if version[:2] < (2018, 2): # 2018.2 down m_SkinSize = reader.read_int() self.m_Skin = [BoneWeights4(reader) for _ in range(m_SkinSize)] @@ -472,7 +487,7 @@ def __init__(self, reader): if version[:2] <= (3, 4): # 3.4.2 and earlier m_Colors_size = reader.read_int() self.mColors = [ - reader.read_byte() # (float)reader.read_byte() / 0xFF + reader.read_byte() / 0xFF for _ in range(m_Colors_size * 4) ] @@ -481,11 +496,13 @@ def __init__(self, reader): m_CollisionVertexCount = reader.read_int() self.m_MeshUsageFlags = reader.read_int() - if version >= (5,): # 5.0 and up - self.m_BakedConvexCollisionMesh = reader.read_bytes(reader.read_int()) + self.m_BakedConvexCollisionMesh = reader.read_bytes( + reader.read_int() + ) reader.align_stream() - self.m_BakedTriangleCollisionMesh = reader.read_bytes(reader.read_int()) + self.m_BakedTriangleCollisionMesh = reader.read_bytes( + reader.read_int()) reader.align_stream() if version >= (2018, 2): # 2018.2 and up @@ -493,84 +510,82 @@ def __init__(self, reader): if version >= (2018, 3): # 2018.3 and up reader.align_stream() - self.m_StreamData = StreamingInfo(reader) + self.m_StreamData = StreamingInfo(reader, version) - # try: - # self.ProcessData() - # except: - # pass + try: + self.ProcessData() + except Exception as e: + print(traceback.format_exc()) + pass def ProcessData(self): - if self.m_StreamData and self.m_StreamData.path: - if self.m_VertexData.m_VertexCount > 0: # - self.m_VertexData.m_DataSize = get_resource_data( - self.m_StreamData.path, - self.assets_file, - self.m_StreamData.offset, - self.m_StreamData.size, - ) + # `m_StreamData` attribute may not exists for some assets + if hasattr(self, "m_StreamData"): + path = self.m_StreamData.path + else: + path = None + if path and self.m_VertexData.m_VertexCount > 0: + self.m_VertexData.m_DataSize = get_resource_data( + self.m_StreamData.path, + self.assets_file, + self.m_StreamData.offset, + self.m_StreamData.size, + ) + # Fix channel after 2018.3 version = self.version - if version >= (2018, 3): # - self.m_VertexData.FixChannel() if version >= (3, 5): # 3.5 and up self.ReadVertexData() if version >= (2, 6): # 2.6.0 and later self.DecompressCompressedMesh() - self.BuildFaces() + self.GetTriangles() def ReadVertexData(self): - m_VertexCount = self.m_VertexData.m_VertexCount version = self.version - for chn, m_Channel in enumerate(self.m_VertexData.m_Channels): + m_VertexData = self.m_VertexData + m_VertexCount = self.m_VertexCount = m_VertexData.m_VertexCount + + for chn, m_Channel in enumerate(m_VertexData.m_Channels): if m_Channel.dimension > 0: - m_Stream = self.m_VertexData.m_Streams[m_Channel.stream] - channelMask = bytearray(m_Stream.channelMask) - if channelMask[chn]: - if version < (2018,) and chn == 2 and m_Channel.format == 2: # 2018.0 down + m_Stream = m_VertexData.m_Streams[m_Channel.stream] + channelMask = bin(m_Stream.channelMask)[::-1] + if channelMask[chn] == "1": + if version[0] < 2018 and chn == 2 and m_Channel.format == 2: m_Channel.dimension = 4 - componentByteSize = MeshHelper.GetChannelFormatSize( - m_Channel.format - ) + componentByteSize = MeshHelper.GetFormatSize( + version, m_Channel.format) componentBytes = bytearray( - m_VertexCount * m_Channel.dimension * componentByteSize - ) + m_VertexCount * m_Channel.dimension * componentByteSize) for v in range(m_VertexCount): - vertexOffset = ( - m_Stream.offset + m_Channel.offset + m_Stream.stride * v - ) + vertexOffset = int( + m_Stream.offset) + m_Channel.offset + int(m_Stream.stride) * v for d in range(m_Channel.dimension): - componentOffset = vertexOffset + componentByteSize * d - dstOffset = componentByteSize * ( - v * m_Channel.dimension + d - ) - componentBytes[ - dstOffset : dstOffset + componentByteSize - ] += self.m_VertexData.m_DataSize[ - componentOffset : componentOffset + componentByteSize - ] - - if ( - self.reader.endian == ">" and componentByteSize > 1 - ): # if big endian, swap bytes - for i in range(0, len(componentBytes), componentByteSize): - componentBytes[i : i + componentByteSize] = componentBytes[ - i : i + componentByteSize - ][::-1] - - componentsIntArray = [] - componentsFloatArray = [] - if m_Channel.format == 10 or m_Channel.format == 11: # - componentsIntArray = MeshHelper.BytesToIntArray(componentBytes) + componentOffsetSrc = vertexOffset + componentByteSize * d # src offset + componentOffsetDst = componentByteSize * \ + (v * m_Channel.dimension + d) # dst offst + + buff = m_VertexData.m_DataSize[componentOffsetSrc: + componentOffsetSrc + componentByteSize] + + if self.reader.endian == "<" and componentByteSize > 1: # swap bytes + buff = buff[::-1] + + componentBytes[componentOffsetDst: componentOffsetDst + + componentByteSize] = buff + # Buffer.BlockCopy(m_VertexData.m_DataSize, componentOffset, componentBytes, componentByteSize * (v * m_Channel.dimension + d), componentByteSize); + # (Array src, int srcOffset, Array dst, int dstOffset, int count); + + if MeshHelper.IsIntFormat(version, m_Channel.format): + componentsIntArray = MeshHelper.BytesToIntArray( + componentBytes, componentByteSize) else: componentsFloatArray = MeshHelper.BytesToFloatArray( - componentBytes, componentByteSize - ) + componentBytes, componentByteSize) - if version >= (2018,): # 2018.0 and up + if version[0] >= 2018: if chn == 0: # kShaderChannelVertex self.m_Vertices = componentsFloatArray elif chn == 1: # kShaderChannelNormal @@ -587,28 +602,29 @@ def ReadVertexData(self): self.m_UV2 = componentsFloatArray elif chn == 7: # kShaderChannelTexCoord3 self.m_UV3 = componentsFloatArray - # kShaderChannelTexCoord4 8 - # kShaderChannelTexCoord5 9 - # kShaderChannelTexCoord6 10 - # kShaderChannelTexCoord7 11 + elif chn == 8: # kShaderChannelTexCoord4 + self.m_UV4 = componentsFloatArray + elif chn == 9: # kShaderChannelTexCoord5 + self.m_UV5 = componentsFloatArray + elif chn == 10: # kShaderChannelTexCoord6 + self.m_UV6 = componentsFloatArray + elif chn == 11: # kShaderChannelTexCoord7 + self.m_UV7 = componentsFloatArray # 2018.2 and up elif chn == 12: # kShaderChannelBlendWeight - if getattr(self, "m_Skin", None): + if not self.m_Skin: self.InitMSkin() for i in range(m_VertexCount): for j in range(m_Channel.dimension): - self.m_Skin[i].weight[j] = componentsFloatArray[ - i * m_Channel.dimension + j - ] + self.m_Skin[i].weight[j] = componentsFloatArray[i * + m_Channel.dimension + j] elif chn == 13: # kShaderChannelBlendIndices - if getattr(self, "m_Skin", None): + if not self.m_Skin: self.InitMSkin() for i in range(m_VertexCount): for j in range(m_Channel.dimension): - self.m_Skin[i].boneIndex[j] = componentsIntArray[ - i * m_Channel.dimension + j - ] - + self.m_Skin[i].boneIndex[j] = componentsIntArray[i * + m_Channel.dimension + j] else: if chn == 0: # kShaderChannelVertex self.m_Vertices = componentsFloatArray @@ -621,7 +637,7 @@ def ReadVertexData(self): elif chn == 4: # kShaderChannelTexCoord1 self.m_UV1 = componentsFloatArray elif chn == 5: - if version >= (5,): # kShaderChannelTexCoord2 + if version[0] >= 5: # kShaderChannelTexCoord2 self.m_UV2 = componentsFloatArray else: # kShaderChannelTangent self.m_Tangents = componentsFloatArray @@ -635,42 +651,57 @@ def DecompressCompressedMesh(self): version = self.version m_CompressedMesh = self.m_CompressedMesh if m_CompressedMesh.m_Vertices.m_NumItems > 0: - m_VertexCount = m_CompressedMesh.m_Vertices.m_NumItems / 3 - m_Vertices = m_CompressedMesh.m_Vertices.UnpackFloats(3, 4) + self.m_VertexCount = int( + m_CompressedMesh.m_Vertices.m_NumItems / 3) + self.m_Vertices = m_CompressedMesh.m_Vertices.UnpackFloats( + 3, 3 * 4) + m_VertexCount = self.m_VertexCount # UV if m_CompressedMesh.m_UV.m_NumItems > 0: # - m_UV0 = m_CompressedMesh.m_UV.UnpackFloats(2, 4, 0, m_VertexCount) - if m_CompressedMesh.m_UV.m_NumItems >= m_VertexCount * 4: # - m_UV1 = m_CompressedMesh.m_UV.UnpackFloats( - 2, 4, m_VertexCount * 2, m_VertexCount - ) - if m_CompressedMesh.m_UV.m_NumItems >= m_VertexCount * 6: # - m_UV2 = m_CompressedMesh.m_UV.UnpackFloats( - 2, 4, m_VertexCount * 4, m_VertexCount - ) - if m_CompressedMesh.m_UV.m_NumItems >= m_VertexCount * 8: # - m_UV3 = m_CompressedMesh.m_UV.UnpackFloats( - 2, 4, m_VertexCount * 6, m_VertexCount - ) + + m_UVInfo = m_CompressedMesh.m_UVInfo + if m_UVInfo != 0: + kInfoBitsPerUV = 4 + kUVDimensionMask = 3 + kUVChannelExists = 4 + kMaxTexCoordShaderChannels = 8 + + uvSrcOffset = 0 + + for uv in range(kMaxTexCoordShaderChannels): + texCoordBits = m_UVInfo >> (uv * kInfoBitsPerUV) + texCoordBits &= (1 << kInfoBitsPerUV) - 1 + if (texCoordBits & kUVChannelExists) != 0: + uvDim = 1 + int(texCoordBits & kUVDimensionMask) + m_UV = m_CompressedMesh.m_UV.UnpackFloats( + uvDim, uvDim * 4, uvSrcOffset, self.m_VertexCount) + self.SetUV(uv, m_UV) + else: + self.m_UV0 = m_CompressedMesh.m_UV.UnpackFloats( + 2, 2*4, 0, m_VertexCount) + if m_CompressedMesh.m_UV.m_NumItems >= m_VertexCount * 4: # + self.m_UV1 = m_CompressedMesh.m_UV.UnpackFloats( + 2, 2 * 4, m_VertexCount * 2, m_VertexCount + ) + # BindPose - if version < (5,): # 5.0 down + if version < (5,): # 5.0 down if m_CompressedMesh.m_BindPoses.m_NumItems > 0: # m_BindPoses_Unpacked = m_CompressedMesh.m_BindPoses.UnpackFloats( 16, 4 * 16 ) self.m_BindPose = [ - Matrix4x4(m_BindPoses_Unpacked[i : i + 16]) + Matrix4x4(m_BindPoses_Unpacked[i: i + 16]) for i in range(0, m_CompressedMesh.m_BindPoses.m_NumItems, 16) ] - # Normal if m_CompressedMesh.m_Normals.m_NumItems > 0: normalData = m_CompressedMesh.m_Normals.UnpackFloats(2, 4 * 2) signs = m_CompressedMesh.m_NormalSigns.UnpackInts() self.m_Normals = [] # float[m_CompressedMesh.m_Normals.m_NumItems / 2 * 3] for i in range(0, math.ceil(m_CompressedMesh.m_Normals.m_NumItems / 2)): - x = normalData[i + 0] - y = normalData[i + 1] + x = normalData[i * 2 + 0] + y = normalData[i * 2 + 1] zsqr = 1 - x * x - y * y if zsqr >= 0: z = math.sqrt(zsqr) @@ -700,7 +731,7 @@ def DecompressCompressedMesh(self): else: z = 0 vector3f = Vector3(x, y, z) - vector3f.Normalize() + vector3f.normalize() x = vector3f.X y = vector3f.Y z = vector3f.Z @@ -710,16 +741,16 @@ def DecompressCompressedMesh(self): self.m_Tangents.extend([x, y, z, w]) # FloatColor - if version >= (5,): # 5.0 and up + if version >= (5,): # 5.0 and up if m_CompressedMesh.m_FloatColors.m_NumItems > 0: # - self.m_Colors = m_CompressedMesh.m_FloatColors.UnpackFloats(1, 4) + self.m_Colors = m_CompressedMesh.m_FloatColors.UnpackFloats( + 1, 4) + # Skin if m_CompressedMesh.m_Weights.m_NumItems > 0: weights = m_CompressedMesh.m_Weights.UnpackInts() boneIndices = m_CompressedMesh.m_BoneIndices.UnpackInts() - self.InitMSkin() - bonePos = 0 boneIndexPos = 0 j = 0 @@ -738,8 +769,9 @@ def DecompressCompressedMesh(self): while j < 4: self.m_Skin[bonePos].weight[j] = 0 self.m_Skin[bonePos].boneIndex[j] = 0 - bonePos += 1 j += 1 + + bonePos += 1 j = 0 sum = 0 # we read three weights, but they don't add up to one. calculate the fourth one, and read @@ -751,90 +783,243 @@ def DecompressCompressedMesh(self): bonePos += 1 j = 0 sum = 0 - # IndexBuffer if m_CompressedMesh.m_Triangles.m_NumItems > 0: # - self.m_IndexBuffer = m_CompressedMesh.m_Triangles.UnpackUInts() + self.m_IndexBuffer = m_CompressedMesh.m_Triangles.UnpackInts() # Color if ( - hasattr(m_CompressedMesh, "m_Colors") - and m_CompressedMesh.m_Colors.m_NumItems > 0 + hasattr(m_CompressedMesh, "m_Colors") + and m_CompressedMesh.m_Colors.m_NumItems > 0 ): m_CompressedMesh.m_Colors.m_NumItems *= 4 m_CompressedMesh.m_Colors.m_BitSize /= 4 tempColors = m_CompressedMesh.m_Colors.UnpackInts() self.m_Colors = [color / 255 for color in tempColors] - def BuildFaces(self): + def GetTriangles(self): + m_IndexBuffer = self.m_IndexBuffer + m_Indices = self.m_Indices + for m_SubMesh in self.m_SubMeshes: firstIndex = m_SubMesh.firstByte // 2 if not self.m_Use16BitIndices: - firstIndex /= 2 + firstIndex //= 2 + + indexCount = m_SubMesh.indexCount + topology = m_SubMesh.topology + if topology == GfxPrimitiveType.kPrimitiveTriangles: + m_Indices.extend( + m_IndexBuffer[firstIndex: firstIndex + indexCount * 3]) + + elif self.version[0] < 4 or topology == GfxPrimitiveType.kPrimitiveTriangleStrip: + # de-stripify : + triIndex = 0 + for i in range(indexCount - 2): + a, b, c = m_IndexBuffer[firstIndex + i: firstIndex + i + 3] + + # skip degenerates + if a == b or a == c or b == c: + continue + + # do the winding flip-flop of strips : + m_Indices.extend( + [b, a, c] if ((i & 1) == 1) else [a, b, c] + ) + triIndex += 3 + # fix indexCount + m_SubMesh.indexCount = triIndex + + elif topology == GfxPrimitiveType.kPrimitiveQuads: + for q in range(0, indexCount, 4): + m_Indices.extend([ + m_IndexBuffer[firstIndex + q], + m_IndexBuffer[firstIndex + q + 1], + m_IndexBuffer[firstIndex + q + 2], + m_IndexBuffer[firstIndex + q], + m_IndexBuffer[firstIndex + q + 2], + m_IndexBuffer[firstIndex + q + 3] + ]) + # fix indexCount + m_SubMesh.indexCount = indexCount // 2 * 3 - if m_SubMesh.topology == 0: - for i in range(0, m_SubMesh.indexCount, 3): - self.m_Indices.append(self.m_IndexBuffer[firstIndex + i]) - self.m_Indices.append(self.m_IndexBuffer[firstIndex + i + 1]) - self.m_Indices.append(self.m_IndexBuffer[firstIndex + i + 2]) else: - j = 0 - for i in range(0, m_SubMesh.indexCount - 2): - fa = self.m_IndexBuffer[firstIndex + i] - fb = self.m_IndexBuffer[firstIndex + i + 1] - fc = self.m_IndexBuffer[firstIndex + i + 2] - - if (fa != fb) and (fa != fc) and (fc != fb): - self.m_Indices.append(fa) - self.m_Indices.extend([fc, fb] if i % 2 else [fb, fc]) - j += 1 - # fix indexCount - m_SubMesh.indexCount = j * 3 + raise NotImplementedError( + "Failed getting triangles. Submesh topology is lines or points.") def InitMSkin(self): self.m_Skin = [BoneWeights4() for _ in range(self.m_VertexCount)] + def SetUV(self, uv: int, m_UV): + if uv == 0: + self.m_UV0 = m_UV + elif uv == 1: + self.m_UV1 = m_UV + elif uv == 2: + self.m_UV2 = m_UV + elif uv == 3: + self.m_UV3 == m_UV + elif uv == 4: + self.m_UV4 == m_UV + elif uv == 5: + self.m_UV5 == m_UV + elif uv == 6: + self.m_UV6 == m_UV + elif uv == 7: + self.m_UV7 == m_UV + else: + raise IndexError("Out of Range") + + def GetUV(self, uv: int): + if uv == 0: + return self.m_UV0 + elif uv == 1: + return self.m_UV1 + elif uv == 2: + return self.m_UV2 + elif uv == 3: + return self.m_UV3 + elif uv == 4: + return self.m_UV4 + elif uv == 5: + return self.m_UV5 + elif uv == 6: + return self.m_UV6 + elif uv == 7: + return self.m_UV7 + else: + raise IndexError("Out of Range") + class MeshHelper: @staticmethod - def GetChannelFormatSize(format): - if format == 0: # kChannelFormatFloat - return 4 - elif format == 1: # kChannelFormatFloat16 - return 2 - elif format == 2: # kChannelFormatColor - return 1 - elif format == 3: # kChannelFormatByte - return 1 - elif format == 4: # kChannelFormatUInt32 - return 4 - elif format == 10: # kChannelFormatInt32 - return 4 - elif format == 11: # kChannelFormatInt32 - return 4 + def GetFormatSize(version, format: int) -> int: + if version[0] < 2017: + if format == VertexChannelFormat.kChannelFormatFloat: + return 4 + elif format == VertexChannelFormat.kChannelFormatFloat16: + return 2 + elif format == VertexChannelFormat.kChannelFormatColor: # in 4.x is size 4 + return 1 + elif format == VertexChannelFormat.kChannelFormatByte: + return 1 + elif format == VertexChannelFormat.kChannelFormatUInt32: # in 5.x + return 4 + elif version[0] < 2019: + if format == VertexFormat.kVertexFormatFloat: + return 4 + elif format == VertexFormat.kVertexFormatFloat16: + return 2 + elif format == VertexFormat.kVertexFormatColor: + return 1 + elif format == VertexFormat.kVertexFormatUNorm8: + return 1 + elif format == VertexFormat.kVertexFormatSNorm8: + return 1 + elif format == VertexFormat.kVertexFormatUNorm16: + return 2 + elif format == VertexFormat.kVertexFormatSNorm16: + return 2 + elif format == VertexFormat.kVertexFormatUInt8: + return 1 + elif format == VertexFormat.kVertexFormatSInt8: + return 1 + elif format == VertexFormat.kVertexFormatUInt16: + return 2 + elif format == VertexFormat.kVertexFormatSInt16: + return 2 + elif format == VertexFormat.kVertexFormatUInt32: + return 4 + elif format == VertexFormat.kVertexFormatSInt32: + return 4 else: - return 0 + if format == VertexFormatV2019.kVertexFormatFloat: + return 4 + elif format == VertexFormatV2019.kVertexFormatFloat16: + return 2 + elif format == VertexFormatV2019.kVertexFormatUNorm8: + return 1 + elif format == VertexFormatV2019.kVertexFormatSNorm8: + return 1 + elif format == VertexFormatV2019.kVertexFormatUNorm16: + return 2 + elif format == VertexFormatV2019.kVertexFormatSNorm16: + return 2 + elif format == VertexFormatV2019.kVertexFormatUInt8: + return 1 + elif format == VertexFormatV2019.kVertexFormatSInt8: + return 1 + elif format == VertexFormatV2019.kVertexFormatUInt16: + return 2 + elif format == VertexFormatV2019.kVertexFormatSInt16: + return 2 + elif format == VertexFormatV2019.kVertexFormatUInt32: + return 4 + elif format == VertexFormatV2019.kVertexFormatSInt32: + return 4 + raise ValueError(format) + + @staticmethod + def IsIntFormat(version, format: int) -> bool: + if version[0] < 2017: + return format == 4 + elif version[0] < 2019: + return format >= 7 + else: + return format >= 6 @staticmethod def BytesToFloatArray(inputBytes, size): - result = [] - for i in range(math.ceil(len(inputBytes) / size)): - value = 0 - if size == 1: - value = inputBytes[i] / 255.0 - elif size == 2: - value = float.from_bytes( - inputBytes[i * 2 : i * 2 + 2], byteorder="big", signed=True - ) # return Half.ToHalf((ushort)BitConverter.ToInt16(value, startIndex)); - elif size == 4: - value = float.from_bytes( - inputBytes[i * 4 : i * 4 + 4], byteorder="big", signed=True - ) - result[i] = value - return result + return [ + inputBytes[i] / 255.0 if size == 1 else + struct.unpack(">e", inputBytes[i * 2: i * 2 + 2])[0] if size == 2 else + struct.unpack(">f", inputBytes[i * 4: i * 4 + 4])[0] if size == 4 else + 0 + for i in range(len(inputBytes) // size) + ] @staticmethod - def BytesToIntArray(inputBytes): + def BytesToIntArray(inputBytes, size): return [ - int.from_bytes(inputBytes[i : i + 4], byteorder="big", signed=True) - for i in range(0, len(inputBytes), 4) + int.from_bytes(inputBytes[i: i + size], + byteorder="big", signed=True) + for i in range(0, len(inputBytes), size) ] + + +class VertexChannelFormat(IntEnum): + kChannelFormatFloat = 0 + kChannelFormatFloat16 = 1 + kChannelFormatColor = 2 + kChannelFormatByte = 3 + kChannelFormatUInt32 = 4 + + +class VertexFormat(IntEnum): + kVertexFormatFloat = 0 + kVertexFormatFloat16 = 1 + kVertexFormatColor = 2 + kVertexFormatUNorm8 = 3 + kVertexFormatSNorm8 = 4 + kVertexFormatUNorm16 = 5 + kVertexFormatSNorm16 = 6 + kVertexFormatUInt8 = 7 + kVertexFormatSInt8 = 8 + kVertexFormatUInt16 = 9 + kVertexFormatSInt16 = 10 + kVertexFormatUInt32 = 11 + kVertexFormatSInt32 = 12 + + +class VertexFormatV2019(IntEnum): + kVertexFormatFloat = 0 + kVertexFormatFloat16 = 1 + kVertexFormatUNorm8 = 2 + kVertexFormatSNorm8 = 3 + kVertexFormatUNorm16 = 4 + kVertexFormatSNorm16 = 5 + kVertexFormatUInt8 = 6 + kVertexFormatSInt8 = 7 + kVertexFormatUInt16 = 8 + kVertexFormatSInt16 = 9 + kVertexFormatUInt32 = 10 + kVertexFormatSInt32 = 11 diff --git a/UnityPy/classes/MonoBehaviour.py b/UnityPy/classes/MonoBehaviour.py index e98399ea..183ecf41 100644 --- a/UnityPy/classes/MonoBehaviour.py +++ b/UnityPy/classes/MonoBehaviour.py @@ -1,11 +1,42 @@ from .Behaviour import Behaviour -from .PPtr import PPtr - +from .PPtr import PPtr, save_ptr +from ..streams import EndianBinaryReader, EndianBinaryWriter +from ..exceptions import TypeTreeError as TypeTreeError class MonoBehaviour(Behaviour): - def __init__(self, reader): + def __init__(self, reader: EndianBinaryReader): super().__init__(reader=reader) - self.script = PPtr(reader) + self.m_Script = PPtr(reader) self.name = reader.read_aligned_string() - self.read_type_tree() + self._raw_offset = reader.Position + if self.assets_file._enable_type_tree: + try: + self.read_typetree() + except TypeTreeError as e: + print("Failed to read TypeTree:\n", e.message) + self.assets_file._enable_type_tree = False + + def save(self, writer: EndianBinaryWriter = None, raw_data: bytes = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + if not raw_data: + ValueError("No raw data given") + + super().save(writer) + save_ptr(self.m_Script, writer) + writer.write_aligned_string(self.name) + writer.write(raw_data) + + self.set_raw_data(writer.bytes) + + @property + def raw_data(self) -> bytes: + """ + Reads the undocumentated data following the default init. + This is usefull for classes that are stored via MonoBehaviours. + """ + reader = self.reader + reader.Position = self._raw_offset + return reader.read_bytes(reader.byte_size - (self._raw_offset - reader.byte_start)) + diff --git a/UnityPy/classes/MonoScript.py b/UnityPy/classes/MonoScript.py index c5c043fd..6635100a 100644 --- a/UnityPy/classes/MonoScript.py +++ b/UnityPy/classes/MonoScript.py @@ -6,17 +6,18 @@ def __init__(self, reader): super().__init__(reader=reader) version = self.version if version >= (3, 4): # 3.4 and up - self.execution_order = reader.read_int() + self.m_ExecutionOrder = reader.read_int() if version < (5,): # 5.0 down - self.properties_hash = reader.read_u_int() + self.m_PropertiesHash = reader.read_u_int() else: - self.properties_hash = reader.read_bytes(16) + self.m_PropertiesHash = reader.read_bytes(16) if version < (3,): # 3.0 down - self.path_name = reader.read_aligned_string() + self.m_PathName = reader.read_aligned_string() - self.class_name = reader.read_aligned_string() + self.m_ClassName = reader.read_aligned_string() if version >= (3,): # 3.0 and up - self.namespace = reader.read_aligned_string() - self.assembly_name = reader.read_aligned_string() + self.m_Namespace = reader.read_aligned_string() + + self.m_AssemblyName = reader.read_aligned_string() if version < (2018, 2): # 2018.2 down - self.is_editor_script = reader.read_boolean() + self.m_IsEditorScript = reader.read_boolean() diff --git a/UnityPy/classes/MovieTexture.py b/UnityPy/classes/MovieTexture.py index 9a1943ae..e27af18d 100644 --- a/UnityPy/classes/MovieTexture.py +++ b/UnityPy/classes/MovieTexture.py @@ -5,7 +5,7 @@ class MovieTexture(Texture): def __init__(self, reader): super().__init__(reader=reader) - self.Loop = reader.read_boolean() + self.m_Loop = reader.read_boolean() reader.align_stream() - self.AudioClip = PPtr(reader) # AudioClip + self.m_AudioClip = PPtr(reader) # AudioClip self.m_MovieData = reader.read_bytes(reader.read_int()) diff --git a/UnityPy/classes/NamedObject.py b/UnityPy/classes/NamedObject.py index 5feac3bd..fd36ca45 100644 --- a/UnityPy/classes/NamedObject.py +++ b/UnityPy/classes/NamedObject.py @@ -8,6 +8,8 @@ def __init__(self, reader): self.reader.reset() self.name = self.reader.read_aligned_string() - def save(self, writer: EndianBinaryWriter): + def save(self, writer: EndianBinaryWriter = None): + if not writer: + writer = EndianBinaryWriter(endian=self.reader.endian) super().save(writer) - writer.write_aligned_string(self.name) # self.name if self.name else '' ? + writer.write_aligned_string(self.name) diff --git a/UnityPy/classes/Object.py b/UnityPy/classes/Object.py index 17169cb6..e6c0b037 100644 --- a/UnityPy/classes/Object.py +++ b/UnityPy/classes/Object.py @@ -1,12 +1,15 @@ +from .PPtr import PPtr from ..enums import BuildTarget from ..helpers import TypeTreeHelper -from ..streams import EndianBinaryWriter, EndianBinaryReader - +from ..streams import EndianBinaryWriter +from ..files import ObjectReader +import types +from ..exceptions import TypeTreeError as TypeTreeError -class Object: +class Object(object): type_tree: dict - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.reader = reader self.assets_file = reader.assets_file self.type = reader.type @@ -16,7 +19,7 @@ def __init__(self, reader): self.platform = reader.platform self.serialized_type = reader.serialized_type self.byte_size = reader.byte_size - self.environment = reader.assets_file.environment + self.assets_file = reader.assets_file if self.platform == BuildTarget.NoTarget: self._object_hide_flags = reader.read_u_int() @@ -29,64 +32,101 @@ def __init__(self, reader): self.reader.reset() if type(self) == Object: - self.__dict__.update(self.read_type_tree().__dict__) + self.read_typetree() def has_struct_member(self, name: str) -> bool: return self.serialized_type.m_Nodes and any( [x.name == name for x in self.serialized_type.m_Nodes] ) - def dump(self) -> str: - self.reader.reset() - if getattr(self.serialized_type, "nodes", None): - sb = [] - TypeTreeHelper(self.reader).read_type_string(sb, self.serialized_type.nodes) - return "".join(sb) - return "" - - def read_type_tree(self) -> dict: - self.reader.reset() - if self.serialized_type.nodes: - self.type_tree = TypeTreeHelper(self.reader).read_value( - self.serialized_type.nodes, 0 - ) - else: - self.type_tree = {} - return NodeHelper(self.type_tree, self.assets_file) + def dump_typetree(self, nodes: list = None) -> str: + return self.reader.dump_typetree(nodes=nodes) + + def dump_typetree_structure(self) -> str: + return self.reader.dump_typetree_structure() + + def read_typetree(self, nodes: list = None) -> dict: + tree = dict() + try: + tree = self.reader.read_typetree(nodes) + except TypeTreeError as e: + print("Failed to read TypeTree:\n", e.message) + return dict() + self.type_tree = NodeHelper(tree, self.assets_file) + return tree + + def save_typetree(self, nodes: list = None, writer: EndianBinaryWriter = None): + if not writer: + writer = EndianBinaryWriter(endian=self.reader.endian) + + def class_to_dict(value): + if isinstance(value, list): + return [class_to_dict(val) for val in value] + elif isinstance(value, dict): + return { + key: class_to_dict(val) + for key, val in value.items() + } + elif hasattr(value, "__dict__"): + if isinstance(value, PPtr): + return {"m_PathID": value.path_id, "m_FileID": value.file_id} + return { + key: class_to_dict(val) + for key, val in value.__dict__.items() + if not isinstance(value, (types.FunctionType, types.MethodType)) and not key in ["type_tree", "assets_file"] + } + else: + return value + + obj = class_to_dict(self if not self.type_tree else self.type_tree) + return self.reader.save_typetree(obj, nodes, writer) def get_raw_data(self) -> bytes: - self.reader.reset() - return self.reader.read_bytes(self.byte_size) + return self.reader.get_raw_data() def set_raw_data(self, data): - self.reader.data = data + self.reader.set_raw_data(data) - def save(self, writer: EndianBinaryWriter, intern_call=False): + def save(self, writer: EndianBinaryWriter = None, intern_call=False): + if not writer: + writer = EndianBinaryWriter(endian=self.reader.endian) if intern_call: if self.platform == BuildTarget.NoTarget: writer.write_u_int(self._object_hide_flags) + elif self.serialized_type.nodes: + # save for objects WITHOUT specific save function + # so we have to use the typetree if it exists + self.save_typetree() + else: + raise NotImplementedError( + "There is no save function for this obj.type nor has it any typetree nodes that could be used.") def _save(self, writer): # the reader is actually an ObjectReader, # the data value is written back into the asset self.reader.data = writer.bytes - def __getattr__(self, item): + def __getattr__(self, name): """ If item not found in __dict__, read type_tree and check if it is in there. """ - self.read_type_tree() - if item in self.type_tree: - return self.type_tree[item] + if name == "type_tree" or self.type_tree == None: + old_pos = self.reader.Position + self.read_typetree() + self.reader.Position = old_pos + if name == "type_tree": + return self.type_tree - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.name) + return getattr(self.type_tree, name) + def get(self, key, default=None): + return getattr(self, key, default) -from .PPtr import PPtr + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) -class NodeHelper(object): +class NodeHelper: def __init__(self, data, assets_file): if "m_PathID" in data and "m_FileID" in data: # used to make pointers directly useable @@ -94,10 +134,11 @@ def __init__(self, data, assets_file): self.file_id = data["m_FileID"] self.index = data.get("m_Index", -2) self.assets_file = assets_file + self._obj = None self.__class__ = PPtr else: self.__dict__ = { - str(key): NodeHelper(val, assets_file) for key, val in data.items() + key: NodeHelper(val, assets_file) for key, val in data.items() } def __new__(cls, data, assets_file): @@ -107,8 +148,8 @@ def __new__(cls, data, assets_file): return [NodeHelper(x, assets_file) for x in data] return data - def __item__(self, key): - return self.__dict__[key] + def __getitem__(self, item): + return getattr(self, item) def to_dict(self): def dump(val): @@ -126,6 +167,15 @@ def dump(val): return {key: dump(val) for key, val in self.__dict__.items()} + def items(self): + return self.__dict__.items() + + def values(self): + return self.__dict__.values() + + def keys(self): + return self.__dict__.keys() + def __repr__(self): return "" % self.__dict__.__repr__() diff --git a/UnityPy/classes/PPtr.py b/UnityPy/classes/PPtr.py index afb39e90..68612332 100644 --- a/UnityPy/classes/PPtr.py +++ b/UnityPy/classes/PPtr.py @@ -1,14 +1,17 @@ -from ..streams import EndianBinaryReader, EndianBinaryWriter +from ..files import ObjectReader +from ..streams import EndianBinaryWriter from ..helpers import ImportHelper from .. import files -from ..enums import FileType +from ..enums import FileType, ClassIDType +import os +from .. import environment -def save_ptr(obj, writer: EndianBinaryWriter, version): +def save_ptr(obj, writer: EndianBinaryWriter): if isinstance(obj, PPtr): writer.write_int(obj.file_id) else: writer.write_int(0) # it's usually 0...... - if version < 14: + if obj._version < 14: writer.write_int(obj.path_id) else: writer.write_long(obj.path_id) @@ -16,7 +19,7 @@ def save_ptr(obj, writer: EndianBinaryWriter, version): cached_managers = dict() class PPtr: - def __init__(self, reader: EndianBinaryReader): + def __init__(self, reader: ObjectReader): self._version = reader.version2 self.index = -2 self.file_id = reader.read_int() @@ -25,7 +28,7 @@ def __init__(self, reader: EndianBinaryReader): self._obj = None def save(self, writer: EndianBinaryWriter): - save_ptr(self, writer, self._version) + save_ptr(self, writer) def get_obj(self): if self._obj != None: @@ -39,10 +42,19 @@ def get_obj(self): external_name = self.assets_file.externals[self.file_id - 1].name parent = self.assets_file.parent if parent is not None: - if external_name not in parent.files: - external_name = external_name.upper() if external_name in parent.files: manager = parent.files[external_name] + elif external_name.upper() in parent.files: + manager = parent.files[external_name.upper()] + else: + while not isinstance(parent, environment.Environment): + parent = parent.parent + if parent.path: + path = parent.path + files = os.listdir(path) + if external_name in files: + parent.load_files([os.path.join(path, external_name)]) + manager = parent.files[external_name] else: if external_name not in cached_managers: typ, reader = ImportHelper.check_file_type(external_name) @@ -51,7 +63,6 @@ def get_obj(self): if external_name in cached_managers: manager = cached_managers[external_name] - if manager and self.path_id in manager.objects: self._obj = manager.objects[self.path_id] else: @@ -62,6 +73,8 @@ def get_obj(self): def __getattr__(self, key): obj = self.get_obj() if obj is None: + if key == "type": + return ClassIDType.UnknownType raise AttributeError(key) return getattr(obj, key) diff --git a/UnityPy/classes/PlayerSettings.py b/UnityPy/classes/PlayerSettings.py index e291d7ea..f171eac4 100644 --- a/UnityPy/classes/PlayerSettings.py +++ b/UnityPy/classes/PlayerSettings.py @@ -1,5 +1,5 @@ from .Object import Object - +from ..streams import EndianBinaryWriter class PlayerSettings(Object): def __init__(self, reader): @@ -27,3 +27,39 @@ def __init__(self, reader): self.accelerometerFrequency = reader.read_int() self.companyName = reader.read_aligned_string() self.productName = reader.read_aligned_string() + self.the_rest = reader.read_the_rest(reader.byte_size, reader.byte_start) + + def save(self, writer: EndianBinaryWriter = None, raw_data: bytes = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + if not raw_data: + ValueError("No raw data given") + + super().save(writer, intern_call=True) + version = self.version + if version >= (5, 4): # 5.4.0 nad up + writer.write_bytes(self.productGUID) + + writer.write_boolean(self.AndroidProfiler) + # bool AndroidFilterTouchesWhenObscured 2017.2 and up + # bool AndroidEnableSustainedPerformanceMode 2018 and up + writer.align_stream() + writer.write_int(self.defaultScreenOrientation) + writer.write_int(self.targetDevice) + if version < (5, 3): # 5.3 down + if version < (5,): # 5.0 down + writer.write_int(self.targetPlatform) # 4.0 and up targetGlesGraphics + if version >= (4, 6): # 4.6 and up + writer.write_int(self.targetIOSGraphics) + writer.write_int(self.targetResolution) + else: + writer.write_boolean(self.useOnDemandResources) + writer.align_stream() + if version >= (3, 5): # 3.5 and up + writer.write_int(self.accelerometerFrequency) + writer.write_aligned_string(self.companyName) + writer.write_aligned_string(self.productName) + writer.write_bytes(self.the_rest) + + self.set_raw_data(writer.bytes) + diff --git a/UnityPy/classes/Renderer.py b/UnityPy/classes/Renderer.py index 21fc334f..5cdc9ad4 100644 --- a/UnityPy/classes/Renderer.py +++ b/UnityPy/classes/Renderer.py @@ -28,7 +28,7 @@ def __init__(self, reader): self.m_LightProbeUsage = reader.read_byte() self.m_ReflectionProbeUsage = reader.read_byte() if version >= (2019, 3): # 2019.3 and up - self.m_RayTracingMode = reader.ReadByte() + self.m_RayTracingMode = reader.read_byte() if version >= (2020,): # 2020.1 and up self.m_RayTraceProcedural = reader.read_byte() reader.align_stream() @@ -55,7 +55,8 @@ def __init__(self, reader): self.m_LightmapTilingOffsetDynamic = reader.read_vector4() m_MaterialsSize = reader.read_int() - self.m_Materials = [PPtr(reader) for _ in range(m_MaterialsSize)] # Material + self.m_Materials = [PPtr(reader) + for _ in range(m_MaterialsSize)] # Material if version < (3,): # 3.0 down self.m_LightmapTilingOffset = reader.read_vector4() diff --git a/UnityPy/classes/ResourceManager.py b/UnityPy/classes/ResourceManager.py new file mode 100644 index 00000000..2f490caf --- /dev/null +++ b/UnityPy/classes/ResourceManager.py @@ -0,0 +1,23 @@ +from .Object import Object +from .PPtr import PPtr, save_ptr +from ..streams import EndianBinaryReader, EndianBinaryWriter + + +class ResourceManager(Object): + def __init__(self, reader): + super().__init__(reader) + m_ContainerSize = reader.read_int() + self.m_Container = { + reader.read_aligned_string(): PPtr(reader) + for _ in range(m_ContainerSize) + } + + def save(self, writer: EndianBinaryWriter = None): + if not writer: + writer = EndianBinaryWriter(endian=self.reader.endian) + super().save(writer, intern_call=True) + writer.write_int(len(self.m_Container)) + for key, val in self.m_Container.items(): + writer.write_aligned_string(key) + save_ptr(val, writer) + return writer diff --git a/UnityPy/classes/Shader.py b/UnityPy/classes/Shader.py index a6aae90d..10e74a32 100644 --- a/UnityPy/classes/Shader.py +++ b/UnityPy/classes/Shader.py @@ -1,9 +1,15 @@ from enum import IntEnum from .NamedObject import NamedObject +from ..export.ShaderConverter import export_shader +from ..enums import ShaderCompilerPlatform, ShaderGpuProgramType, SerializedPropertyType +from ..enums import TextureDimension, PassType class Shader(NamedObject): + def export(self): + return export_shader(self) + def __init__(self, reader): super().__init__(reader=reader) version = reader.version @@ -14,9 +20,9 @@ def __init__(self, reader): ] if version >= (2019, 3): # 2019.3 and up - offsets = reader.read_u_int_array_array()[0] - compressedLengths = reader.read_u_int_array_array()[0] - decompressedLengths = reader.read_u_int_array_array()[0] + self.offsets = reader.read_u_int_array_array()[0] + self.compressedLengths = reader.read_u_int_array_array()[0] + self.decompressedLengths = reader.read_u_int_array_array()[0] else: self.offsets = reader.read_u_int_array() self.compressedLengths = reader.read_u_int_array() @@ -39,10 +45,12 @@ def __init__(self, reader): self.m_StructSize = reader.read_int() numVectorParams = reader.read_int() - self.m_VectorParams = [VectorParameter(reader) for _ in range(numVectorParams)] + self.m_VectorParams = [VectorParameter( + reader) for _ in range(numVectorParams)] numMatrixParams = reader.read_int() - self.m_MatrixParams = [MatrixParameter(reader) for _ in range(numMatrixParams)] + self.m_MatrixParams = [MatrixParameter( + reader) for _ in range(numMatrixParams)] class SamplerParameter: @@ -51,32 +59,12 @@ def __init__(self, reader): self.bindPoint = reader.read_int() -class TextureDimension(IntEnum): - kTexDimUnknown = (-1,) - kTexDimNone = (0,) - kTexDimAny = (1,) - kTexDim2D = (2,) - kTexDim3D = (3,) - kTexDimCUBE = (4,) - kTexDim2DArray = (5,) - kTexDimCubeArray = (6,) - kTexDimForce32Bit = (2147483647,) - - class SerializedTextureProperty: def __init__(self, reader): self.m_DefaultName = reader.read_aligned_string() self.m_TexDim = TextureDimension(reader.read_int()) -class SerializedPropertyType(IntEnum): - kColor = (0,) - kVector = (1,) - kFloat = (2,) - kRange = (3,) - kTexture = (4,) - - class SerializedProperty: def __init__(self, reader): self.m_Name = reader.read_aligned_string() @@ -149,6 +137,8 @@ def __init__(self, reader): self.zTest = SerializedShaderFloatValue(reader) self.zWrite = SerializedShaderFloatValue(reader) self.culling = SerializedShaderFloatValue(reader) + if version >= (2020,): # 2020.1 and up + self.conservative = SerializedShaderFloatValue(reader) self.offsetFactor = SerializedShaderFloatValue(reader) self.offsetUnits = SerializedShaderFloatValue(reader) self.alphaToMask = SerializedShaderFloatValue(reader) @@ -179,7 +169,8 @@ def __init__(self, reader): class ParserBindChannels: def __init__(self, reader): numChannels = reader.read_int() - self.m_Channels = [ShaderBindChannel(reader) for _ in range(numChannels)] + self.m_Channels = [ShaderBindChannel( + reader) for _ in range(numChannels)] reader.align_stream() self.m_SourceMap = reader.read_u_int() @@ -220,6 +211,8 @@ class BufferBinding: def __init__(self, reader): self.m_NameIndex = reader.read_int() self.m_Index = reader.read_int() + if reader.version >= (2020,): # 2020.1 and up + m_ArraySize = reader.read_int() class ConstantBuffer: @@ -229,10 +222,12 @@ def __init__(self, reader): self.m_NameIndex = reader.read_int() numMatrixParams = reader.read_int() - self.m_MatrixParams = [MatrixParameter(reader) for _ in range(numMatrixParams)] + self.m_MatrixParams = [MatrixParameter( + reader) for _ in range(numMatrixParams)] numVectorParams = reader.read_int() - self.m_VectorParams = [VectorParameter(reader) for _ in range(numVectorParams)] + self.m_VectorParams = [VectorParameter( + reader) for _ in range(numVectorParams)] if version >= (2017, 3): # 2017.3 and up numStructParams = reader.read_int() self.m_StructParams = [ @@ -248,36 +243,6 @@ def __init__(self, reader): self.m_OriginalIndex = reader.read_int() -class ShaderGpuProgramType(IntEnum): - kShaderGpuProgramUnknown = (0,) - kShaderGpuProgramGLLegacy = (1,) - kShaderGpuProgramGLES31AEP = (2,) - kShaderGpuProgramGLES31 = (3,) - kShaderGpuProgramGLES3 = (4,) - kShaderGpuProgramGLES = (5,) - kShaderGpuProgramGLCore32 = (6,) - kShaderGpuProgramGLCore41 = (7,) - kShaderGpuProgramGLCore43 = (8,) - kShaderGpuProgramDX9VertexSM20 = (9,) - kShaderGpuProgramDX9VertexSM30 = (10,) - kShaderGpuProgramDX9PixelSM20 = (11,) - kShaderGpuProgramDX9PixelSM30 = (12,) - kShaderGpuProgramDX10Level9Vertex = (13,) - kShaderGpuProgramDX10Level9Pixel = (14,) - kShaderGpuProgramDX11VertexSM40 = (15,) - kShaderGpuProgramDX11VertexSM50 = (16,) - kShaderGpuProgramDX11PixelSM40 = (17,) - kShaderGpuProgramDX11PixelSM50 = (18,) - kShaderGpuProgramDX11GeometrySM40 = (19,) - kShaderGpuProgramDX11GeometrySM50 = (20,) - kShaderGpuProgramDX11HullSM50 = (21,) - kShaderGpuProgramDX11DomainSM50 = (22,) - kShaderGpuProgramMetalVS = (23,) - kShaderGpuProgramMetalFS = (24,) - kShaderGpuProgramSPIRV = (25,) - kShaderGpuProgramConsole = (26,) - - class SerializedSubProgram: def __init__(self, reader): version = reader.version @@ -300,10 +265,12 @@ def __init__(self, reader): reader.align_stream() numVectorParams = reader.read_int() - self.m_VectorParams = [VectorParameter(reader) for _ in range(numVectorParams)] + self.m_VectorParams = [VectorParameter( + reader) for _ in range(numVectorParams)] numMatrixParams = reader.read_int() - self.m_MatrixParams = [MatrixParameter(reader) for _ in range(numMatrixParams)] + self.m_MatrixParams = [MatrixParameter( + reader) for _ in range(numMatrixParams)] numTextureParams = reader.read_int() self.m_TextureParams = [ @@ -311,7 +278,8 @@ def __init__(self, reader): ] numBufferParams = reader.read_int() - self.m_BufferParams = [BufferBinding(reader) for _ in range(numBufferParams)] + self.m_BufferParams = [BufferBinding( + reader) for _ in range(numBufferParams)] numConstantBuffers = reader.read_int() self.m_ConstantBuffers = [ @@ -328,7 +296,8 @@ def __init__(self, reader): if version >= (2017,): # 2017 and up numSamplers = reader.read_int() - self.m_Samplers = [SamplerParameter(reader) for _ in range(numSamplers)] + self.m_Samplers = [SamplerParameter( + reader) for _ in range(numSamplers)] if version >= (2017, 2): # 2017.2 and up self.m_ShaderRequirements = reader.read_int() @@ -341,15 +310,24 @@ def __init__(self, reader): ] -class PassType(IntEnum): - kPassTypeNormal = (0,) - kPassTypeUse = (1,) - kPassTypeGrab = 2 - - class SerializedPass: def __init__(self, reader): version = reader.version + + if version >= (2020, 2): # 2020.2 and up + numEditorDataHash = reader.read_int() + m_EditorDataHash = [ + reader.read_bytes(16) # Hash128(reader) + for _ in range(numEditorDataHash) + ] + reader.align_stream() + m_Platforms = reader.read_byte_array() + reader.align_stream() + m_LocalKeywordMask = reader.read_u_short_array() + reader.align_stream() + m_GlobalKeywordMask = reader.read_u_short_array() + reader.align_stream() + numIndices = reader.read_int() self.m_NameIndices = {} for _ in range(numIndices): @@ -402,7 +380,8 @@ class SerializedShader: def __init__(self, reader): self.m_PropInfo = SerializedProperties(reader) numSubShaders = reader.read_int() - self.m_SubShaders = [SerializedSubShader(reader) for _ in range(numSubShaders)] + self.m_SubShaders = [SerializedSubShader( + reader) for _ in range(numSubShaders)] self.m_Name = reader.read_aligned_string() self.m_CustomEditorName = reader.read_aligned_string() self.m_FallbackName = reader.read_aligned_string() @@ -412,28 +391,3 @@ def __init__(self, reader): ] self.m_DisableNoSubshadersMessage = reader.read_boolean() reader.align_stream() - - -class ShaderCompilerPlatform(IntEnum): - kShaderCompPlatformNone = (-1,) - kShaderCompPlatformGL = (0,) - kShaderCompPlatformD3D9 = (1,) - kShaderCompPlatformXbox360 = (2,) - kShaderCompPlatformPS3 = (3,) - kShaderCompPlatformD3D11 = (4,) - kShaderCompPlatformGLES20 = (5,) - kShaderCompPlatformNaCl = (6,) - kShaderCompPlatformFlash = (7,) - kShaderCompPlatformD3D11_9x = (8,) - kShaderCompPlatformGLES3Plus = (9,) - kShaderCompPlatformPSP2 = (10,) - kShaderCompPlatformPS4 = (11,) - kShaderCompPlatformXboxOne = (12,) - kShaderCompPlatformPSM = (13,) - kShaderCompPlatformMetal = (14,) - kShaderCompPlatformOpenGLCore = (15,) - kShaderCompPlatformN3DS = (16,) - kShaderCompPlatformWiiU = (17,) - kShaderCompPlatformVulkan = (18,) - kShaderCompPlatformSwitch = (19,) - kShaderCompPlatformXboxOneD3D12 = (20,) diff --git a/UnityPy/classes/Sprite.py b/UnityPy/classes/Sprite.py index 53f38ddb..f85be9f0 100644 --- a/UnityPy/classes/Sprite.py +++ b/UnityPy/classes/Sprite.py @@ -2,7 +2,7 @@ from .Mesh import BoneWeights4, SubMesh, VertexData from .NamedObject import NamedObject -from .PPtr import PPtr, save_ptr +from .PPtr import PPtr from ..export import SpriteHelper from ..enums import SpriteMeshType from ..streams import EndianBinaryWriter @@ -122,24 +122,17 @@ class SpritePackingMode(IntEnum): class SpriteSettings: def __init__(self, reader): - self.value = reader.read_u_int() - - @property - def value(self): - return self.m_settingsRaw - - @value.setter - def value(self, _value): - self.m_settingsRaw = _value - - self.packed = self.m_settingsRaw & 1 # 1 - self.packingMode = SpritePackingMode((self.m_settingsRaw >> 1) & 1) # 1 - self.packingRotation = SpritePackingRotation((self.m_settingsRaw >> 2) & 0xF) # 4 - self.meshType = SpriteMeshType((self.m_settingsRaw >> 6) & 1) # 1 + self.settingsRaw = reader.read_u_int() + self.packed = self.settingsRaw & 1 # 1 + self.packingMode = SpritePackingMode( + (self.settingsRaw >> 1) & 1) # 1 + self.packingRotation = SpritePackingRotation( + (self.settingsRaw >> 2) & 0xF) # 4 + self.meshType = SpriteMeshType((self.settingsRaw >> 6) & 1) # 1 # rest of the bits are reserved def save(self, writer): - writer.write_u_int(self.m_settingsRaw) + writer.write_u_int(self.settingsRaw) class SpriteVertex: @@ -155,6 +148,7 @@ def save(self, writer, version): if version[:2] <= (4, 3): # 4.3 and down writer.write__vector2(self.uv) + class SpriteRenderData: def __init__(self, reader): version = reader.version @@ -171,7 +165,7 @@ def __init__(self, reader): if version >= (5, 6): # 5.6 and up SubMeshesSize = reader.read_int() - self.m_SubMeshes = [SubMesh(reader, version) for _ in range(SubMeshesSize)] + self.m_SubMeshes = [SubMesh(reader) for _ in range(SubMeshesSize)] IndexBufferSize = reader.read_int() self.m_IndexBuffer = reader.read_bytes(IndexBufferSize) reader.align_stream() diff --git a/UnityPy/classes/SpriteAtlas.py b/UnityPy/classes/SpriteAtlas.py index 82797dd3..01c62381 100644 --- a/UnityPy/classes/SpriteAtlas.py +++ b/UnityPy/classes/SpriteAtlas.py @@ -7,16 +7,17 @@ class SpriteAtlas(NamedObject): def __init__(self, reader): super().__init__(reader=reader) packed_sprites_size = reader.read_int() - self.PackSprites = [PPtr(reader) for _ in range(packed_sprites_size)] + self.m_PackedSprites = [PPtr(reader) + for _ in range(packed_sprites_size)] - self.packed_sprite_names_to_index = reader.read_string_array() + self.m_PackedSpriteNamesToIndex = reader.read_string_array() m_render_data_map_size = reader.read_int() - self.render_data_map = {} + self.m_RenderDataMap = {} for _ in range(m_render_data_map_size): first = reader.read_bytes(16) # GUID second = reader.read_long() value = SpriteAtlasData(reader) - self.render_data_map[(first, second)] = value + self.m_RenderDataMap[(first, second)] = value class SpriteAtlasData: diff --git a/UnityPy/classes/TextAsset.py b/UnityPy/classes/TextAsset.py index 2c82b7a1..8c1a0165 100644 --- a/UnityPy/classes/TextAsset.py +++ b/UnityPy/classes/TextAsset.py @@ -1,11 +1,20 @@ from .NamedObject import NamedObject -from ..streams import EndianBinaryWriter, EndianBinaryReader +from ..streams import EndianBinaryWriter class TextAsset(NamedObject): - def __init__(self, reader: EndianBinaryReader): + def __init__(self, reader): super().__init__(reader=reader) - self.script = reader.read_bytes(reader.read_int()) + self.m_Script = reader.read_bytes(reader.read_int()) + + @property + def script(self): + # required for backward compatibility + return self.m_Script + + @script.setter + def script(self, value): + self.m_Script = value @property def text(self): @@ -19,8 +28,8 @@ def save(self, writer: EndianBinaryWriter = None): if writer is None: writer = EndianBinaryWriter(endian=self.reader.endian) super().save(writer) - writer.write_int(len(self.script)) - writer.write_bytes(self.script) + writer.write_int(len(self.m_Script)) + writer.write_bytes(self.m_Script) writer.align_stream() self.set_raw_data(writer.bytes) diff --git a/UnityPy/classes/Texture.py b/UnityPy/classes/Texture.py index 3399d466..652ea1b6 100644 --- a/UnityPy/classes/Texture.py +++ b/UnityPy/classes/Texture.py @@ -6,13 +6,17 @@ class Texture(NamedObject): def __init__(self, reader): super().__init__(reader=reader) if self.version >= (2017, 3): # 2017.3 and up - self.forced_fallback_format = reader.read_int() - self.downscale_fallback = reader.read_boolean() + self.m_ForcedFallbackFormat = reader.read_int() + self.m_DownscaleFallback = reader.read_boolean() + if self.version >= (2020,2): # 2020.2 and up + self.m_IsAlphaChannelOptional = reader.read_boolean() reader.align_stream() def save(self, writer: EndianBinaryWriter): super().save(writer) if self.version >= (2017, 3): # 2017.3 and up - writer.write_int(self.forced_fallback_format) - writer.write_boolean(self.downscale_fallback) + writer.write_int(self.m_ForcedFallbackFormat) + writer.write_boolean(self.m_DownscaleFallback) + if self.version >= (2020,2): # 2020.2 and up + writer.write_boolean(self.m_IsAlphaChannelOptional) writer.align_stream() diff --git a/UnityPy/classes/Texture2D.py b/UnityPy/classes/Texture2D.py index 98b05fe3..0fb6d225 100644 --- a/UnityPy/classes/Texture2D.py +++ b/UnityPy/classes/Texture2D.py @@ -3,6 +3,8 @@ from ..export import Texture2DConverter from ..helpers.ResourceReader import get_resource_data from ..streams import EndianBinaryWriter +from PIL import Image +from io import BufferedIOBase, RawIOBase, IOBase class Texture2D(Texture): @@ -14,22 +16,58 @@ def image(self): def image(self, img): # img is PIL.Image / image path / opened file # overwrite original image data with the RGB(A) image data and sets the correct new format - img = Texture2DConverter.prepare_raw_image(img) if img is None: raise Exception("No image provided") - writer = EndianBinaryWriter() - for pix in img: - for val in pix: - writer.write_u_byte(val) - self.image_data = writer.bytes - self.m_CompleteImageSize = len(self.image_data) + if (isinstance(img, str) or isinstance(img, BufferedIOBase) or + isinstance(img, RawIOBase) or isinstance(img, IOBase)): + img = Image.open(img) + + img_data, tex_format = Texture2DConverter.image_to_texture2d(img) + self.image_data = img_data + # width * height * channel count + self.m_CompleteImageSize = len(self._image_data)# img.width * img.height * len(img.getbands()) + self.m_TextureFormat = tex_format + + @property + def image_data(self): + return self._image_data + + def reset_streamdata(self): + if not self.m_StreamData: return self.m_StreamData.offset = 0 self.m_StreamData.size = 0 - self.m_StreamData.path = '' - self.m_TextureFormat = ( - TextureFormat.RGBA32 if len(pix) == 4 else TextureFormat.RGB24 - ) + self.m_StreamData.path = "" + + @image_data.setter + def image_data(self, data: bytes): + self._image_data = data + # prefer writing to cab if possible + if self.m_StreamData: + cab = self.assets_file.get_writeable_cab() + if cab: + self.m_StreamData.offset = cab.Position + cab.write(data) + self.m_StreamData.size = len(data) + self.m_StreamData.path = cab.path + else: + self.reset_streamdata() + + def set_image( + self, img, target_format: TextureFormat = None, in_cab: bool = False + ): + if img is None: + raise Exception("No image provided") + img_data, tex_format = Texture2DConverter.image_to_texture2d(img) + + if in_cab: + self.image_data = img_data + else: + self._image_data = img_data + self.reset_streamdata() + # width * height * channel count + self.m_CompleteImageSize = len(self._image_data)#img.width * img.height * len(img.getbands()) + self.m_TextureFormat = tex_format def __init__(self, reader): super().__init__(reader=reader) @@ -39,7 +77,7 @@ def __init__(self, reader): self.m_Height = reader.read_int() self.m_CompleteImageSize = reader.read_int() if version >= (2020,): # 2020.1 and up - self.mips_stripped = reader.read_int() + self.m_MipsStripped = reader.read_int() self.m_TextureFormat = TextureFormat(reader.read_int()) if version[:2] < (5, 2): # 5.2 down self.m_MipMap = reader.read_boolean() @@ -49,13 +87,13 @@ def __init__(self, reader): if version >= (2, 6): # 2.6 and up self.m_IsReadable = reader.read_boolean() # 2.6 and up if version >= (2020,): # 2020.1 and up - self.is_pre_processed = reader.read_boolean() + self.m_IsPreProcessed = reader.read_boolean() if version >= (2019, 3): # 2019.3 and up - self.ignore_master_texture_limit = reader.read_boolean() + self.m_IgnoreMasterTextureLimit = reader.read_boolean() if (3,) <= version[:2] <= (5, 4): # 3.0 - 5.4 self.m_ReadAllowed = reader.read_boolean() if version >= (2018, 2): # 2018.2 and up - self.m_streaming_mipmaps = reader.read_boolean() + self.m_StreamingMipmaps = reader.read_boolean() reader.align_stream() if version >= (2018, 2): # 2018.2 and up @@ -67,19 +105,27 @@ def __init__(self, reader): self.m_LightmapFormat = reader.read_int() if version >= (3, 5): # 3.5 and up self.m_ColorSpace = reader.read_int() + if version >= (2020, 2): # 2020.2 and up + self.m_PlatformBlob = reader.read_byte_array() + reader.align_stream() image_data_size = reader.read_int() - self.image_data = b"" - if image_data_size == 0 and version >= (5, 3): # 5.3 and up + self._image_data = b"" + + if image_data_size != 0: + self._image_data = reader.read_bytes(image_data_size) + + self.m_StreamData = None + if version >= (5, 3): # 5.3 and up + # always read the StreamingInfo for resaving self.m_StreamData = StreamingInfo(reader, version) - self.image_data = get_resource_data( - self.m_StreamData.path, - self.assets_file, - self.m_StreamData.offset, - self.m_StreamData.size, - ) - else: - self.image_data = reader.read_bytes(image_data_size) + if image_data_size == 0 and self.m_StreamData.path: + self._image_data = get_resource_data( + self.m_StreamData.path, + self.assets_file, + self.m_StreamData.offset, + self.m_StreamData.size, + ) def save(self, writer: EndianBinaryWriter = None): if writer is None: @@ -91,21 +137,23 @@ def save(self, writer: EndianBinaryWriter = None): writer.write_int(self.m_Height) writer.write_int(self.m_CompleteImageSize) if version >= (2020,): # 2020.1 and up - writer.write_int(self.mips_stripped) - writer.write_int(int(self.m_TextureFormat)) - if version < (5, 2): # 5.2 down + writer.write_int(self.m_MipsStripped) + writer.write_int(self.m_TextureFormat.value) + if version[:2] < (5, 2): # 5.2 down writer.write_boolean(self.m_MipMap) else: writer.write_int(self.m_MipCount) - writer.write_boolean(self.m_IsReadable) # 2.6 and up + + if version >= (2, 6): # 2.6 and up + writer.write_boolean(self.m_IsReadable) # 2.6 and up if version >= (2020,): # 2020.1 and up - writer.write_boolean(self.is_pre_processed ) + writer.write_boolean(self.m_IsPreProcessed) if version >= (2019, 3): # 2019.3 and up - writer.write_boolean(self.ignore_master_texture_limit) + writer.write_boolean(self.m_IgnoreMasterTextureLimit) if (3,) <= version[:2] <= (5, 4): # 3.0 - 5.4 writer.write_boolean(self.m_ReadAllowed) # 3.0 - 5.4 if version >= (2018, 2): # 2018.2 and up - writer.write_boolean(self.m_streaming_mipmaps) + writer.write_boolean(self.m_StreamingMipmaps) writer.align_stream() if version >= (2018, 2): # 2018.2 and up @@ -117,19 +165,32 @@ def save(self, writer: EndianBinaryWriter = None): writer.write_int(self.m_LightmapFormat) if version >= (3, 5): # 3.5 and up writer.write_int(self.m_ColorSpace) + if version >= (2020, 2): # 2020.2 and up + writer.write_byte_array(self.m_PlatformBlob) + writer.align_stream() + + if version[:2] < (5, 3): + # version without m_StreamData + writer.write_int(len(self.image_data)) + writer.write_bytes(self.image_data) + else: + # decide if m_StreamData is used + if self.m_StreamData.path: + # used -> don't save the image_data + writer.write_int(0) + else: + writer.write_int(len(self.image_data)) + writer.write_bytes(self.image_data) - writer.write_int(len(self.image_data)) - writer.write_bytes(self.image_data) - - self.m_StreamData.save(writer, version) + self.m_StreamData.save(writer, version) self.set_raw_data(writer.bytes) class StreamingInfo: - offset: int = 0 - size: int = 0 - path: str = "" + offset: int + size: int + path: str def __init__(self, reader, version): if version >= (2020,): # 2020.1 and up diff --git a/UnityPy/classes/Transform.py b/UnityPy/classes/Transform.py index 90341605..df8419d2 100644 --- a/UnityPy/classes/Transform.py +++ b/UnityPy/classes/Transform.py @@ -5,10 +5,10 @@ class Transform(Component): def __init__(self, reader): super().__init__(reader=reader) - self.local_rotation = reader.read_quaternion() - self.local_position = reader.read_vector3() - self.local_scale = reader.read_vector3() + self.m_LocalRotation = reader.read_quaternion() + self.m_LocalPosition = reader.read_vector3() + self.m_LocalScale = reader.read_vector3() children_count = reader.read_int() - self.children = [PPtr(reader) for _ in range(children_count)] - self.father = PPtr(reader) + self.m_Children = [PPtr(reader) for _ in range(children_count)] + self.m_Father = PPtr(reader) diff --git a/UnityPy/classes/VideoClip.py b/UnityPy/classes/VideoClip.py index 1584b433..66ceb03d 100644 --- a/UnityPy/classes/VideoClip.py +++ b/UnityPy/classes/VideoClip.py @@ -6,37 +6,39 @@ class VideoClip(NamedObject): def __init__(self, reader): super().__init__(reader=reader) - self.original_path = reader.read_aligned_string() - self.proxy_width = reader.read_u_int() - self.proxy_height = reader.read_u_int() - self.width = reader.read_u_int() - self.height = reader.read_u_int() + self.m_OriginalPath = reader.read_aligned_string() + self.m_ProxyWidth = reader.read_u_int() + self.m_ProxyHeight = reader.read_u_int() + self.Width = reader.read_u_int() + self.Height = reader.read_u_int() if self.version >= (2017, 2): # 2017.2 and up - self.pixel_aspec_ratio_num = reader.read_u_int() - self.pixel_aspec_ratio_den = reader.read_u_int() - self.frame_rate = reader.read_double() - self.frame_count = reader.read_u_long() - self.format = reader.read_int() - self.audio_channel_count = reader.read_u_short_array() + self.m_PixelAspecRatioNum = reader.read_u_int() + self.m_PixelAspecRatioDen = reader.read_u_int() + self.m_FrameRate = reader.read_double() + self.m_FrameCount = reader.read_u_long() + self.m_Format = reader.read_int() + self.m_AudioChannelCount = reader.read_u_short_array() reader.align_stream() - self.audio_sample_rate = reader.read_u_int_array() - self.audio_language = reader.read_string_array() - # StreamedResource m_ExternalResources + self.m_AudioSampleRate = reader.read_u_int_array() + self.m_AudioLanguage = reader.read_string_array() if self.version >= (2020,): # 2020.1 and up - video_shader_size = reader.ReadInt32() - self.video_shaders = [PPtr(reader) for _ in range(video_shader_size)] + m_VideoShadersSize = reader.read_int() + self.m_VideoShaders = [ + PPtr(reader) for _ in range(m_VideoShadersSize) + ] - # StreamedResource - source = reader.read_aligned_string() - offset = reader.read_u_long() - size = reader.read_u_long() - - self.has_split_alpha = reader.read_boolean() + # m_ExternalResources = new StreamedResource(reader) + self.source = reader.read_aligned_string() + self.offset = reader.read_u_long() + self.size = reader.read_u_long() + self.m_HasSplitAlpha = reader.read_boolean() if self.version >= (2020,): # 2020.1 and up self.m_sRGB = reader.read_boolean() - if source: - self.VideoData = get_resource_data(source, self.assets_file, offset, size) + if self.source: + self.m_VideoData = get_resource_data( + self.source, self.assets_file, self.offset, self.size) else: - self.VideoData = reader.read(size) + self.reader.Position = self.data_offset + self.m_VideoData = self.reader.read_bytes(self.size) diff --git a/UnityPy/classes/__init__.py b/UnityPy/classes/__init__.py index 89babcbc..301aac1f 100644 --- a/UnityPy/classes/__init__.py +++ b/UnityPy/classes/__init__.py @@ -26,6 +26,7 @@ from .RectTransform import RectTransform from .Renderer import Renderer from .RuntimeAnimatorController import RuntimeAnimatorController +from .ResourceManager import ResourceManager from .Shader import Shader from .SkinnedMeshRenderer import SkinnedMeshRenderer from .Sprite import Sprite diff --git a/UnityPy/enums/Audio.py b/UnityPy/enums/Audio.py index 726837ab..e9765582 100644 --- a/UnityPy/enums/Audio.py +++ b/UnityPy/enums/Audio.py @@ -2,31 +2,31 @@ class AudioType(IntEnum): - UNKNOWN = (0,) - ACC = (1,) - AIFF = (2,) - IT = (10,) - MOD = (12,) - MPEG = (13,) - OGGVORBIS = (14,) - S3M = (17,) - WAV = (20,) - XM = (21,) - XMA = (22,) - VAG = (23,) + UNKNOWN = 0 + ACC = 1 + AIFF = 2 + IT = 10 + MOD = 12 + MPEG = 13 + OGGVORBIS = 14 + S3M = 17 + WAV = 20 + XM = 21 + XMA = 22 + VAG = 23 AUDIOQUEUE = 24 class AudioCompressionFormat(IntEnum): - PCM = (0,) - Vorbis = (1,) - ADPCM = (2,) - MP3 = (3,) - VAG = (4,) - HEVAG = (5,) - XMA = (6,) - AAC = (7,) - GCADPCM = (8,) + PCM = 0 + Vorbis = 1 + ADPCM = 2 + MP3 = 3 + VAG = 4 + HEVAG = 5 + XMA = 6 + AAC = 7 + GCADPCM = 8 ATRAC9 = 9 diff --git a/UnityPy/enums/BuildTarget.py b/UnityPy/enums/BuildTarget.py index bd45942d..0ff7eeff 100644 --- a/UnityPy/enums/BuildTarget.py +++ b/UnityPy/enums/BuildTarget.py @@ -2,43 +2,68 @@ class BuildTarget(IntEnum): - UnknownPlatform = (3716,) - DashboardWidget = (1,) - StandaloneOSX = (2,) - StandaloneOSXPPC = (3,) - StandaloneOSXIntel = (4,) - StandaloneWindows = (5,) - WebPlayer = (6,) - WebPlayerStreamed = (7,) - Wii = (8,) - iOS = (9,) - PS3 = (10,) - XBOX360 = (11,) - Android = (13,) - StandaloneGLESEmu = (14,) - NaCl = (16,) - StandaloneLinux = (17,) - FlashPlayer = (18,) - StandaloneWindows64 = (19,) - WebGL = (20,) - WSAPlayer = (21,) - StandaloneLinux64 = (24,) - StandaloneLinuxUniversal = (25,) - WP8Player = (26,) - StandaloneOSXIntel64 = (27,) - BlackBerry = (28,) - Tizen = (29,) - PSP2 = (30,) - PS4 = (31,) - PSM = (32,) - XboxOne = (33,) - SamsungTV = (34,) - N3DS = (35,) - WiiU = (36,) - tvOS = (37,) - Switch = (38,) + UnknownPlatform = 3716 + DashboardWidget = 1 + StandaloneOSX = 2 + StandaloneOSXPPC = 3 + StandaloneOSXIntel = 4 + StandaloneWindows = 5 + WebPlayer = 6 + WebPlayerStreamed = 7 + Wii = 8 + iOS = 9 + PS3 = 10 + XBOX360 = 11 + Android = 13 + StandaloneGLESEmu = 14 + NaCl = 16 + StandaloneLinux = 17 + FlashPlayer = 18 + StandaloneWindows64 = 19 + WebGL = 20 + WSAPlayer = 21 + StandaloneLinux64 = 24 + StandaloneLinuxUniversal = 25 + WP8Player = 26 + StandaloneOSXIntel64 = 27 + BlackBerry = 28 + Tizen = 29 + PSP2 = 30 + PS4 = 31 + PSM = 32 + XboxOne = 33 + SamsungTV = 34 + N3DS = 35 + WiiU = 36 + tvOS = 37 + Switch = 38 NoTarget = -2 + def __str__(self): + return self.name + + def __format__(self, fmt): + return self.name + + def __eq__(self, value): + if isinstance(value, str): + return self.name == value + elif isinstance(value, BuildTarget): + return self._value_ == value._value_ + elif isinstance(value, int): + if self.has_value(value): + return self._value_ == value + return False + return self._value_ == value + @classmethod - def _missing_(cls, value): - return BuildTarget.UnknownPlatform + def has_value(cls, value): + return value in cls._value2member_map_ + + +def makeBuildTarget(_bt): + if isinstance(_bt, BuildTarget): + return _bt + elif BuildTarget.has_value(_bt): + return BuildTarget(_bt) + return _bt \ No newline at end of file diff --git a/UnityPy/enums/ClassIDType.py b/UnityPy/enums/ClassIDType.py index 5b09c168..53c166a7 100644 --- a/UnityPy/enums/ClassIDType.py +++ b/UnityPy/enums/ClassIDType.py @@ -1,297 +1,406 @@ -from enum import IntEnum +# https://docs.unity3d.com/Manual/ClassIDReference.html +from enum import IntEnum class ClassIDType(IntEnum): - UnknownType = (-1,) - Object = (0,) - GameObject = (1,) - Component = (2,) - LevelGameManager = (3,) - Transform = (4,) - TimeManager = (5,) - GlobalGameManager = (6,) - Behaviour = (8,) - GameManager = (9,) - AudioManager = (11,) - ParticleAnimator = (12,) - InputManager = (13,) - EllipsoidParticleEmitter = (15,) - Pipeline = (17,) - EditorExtension = (18,) - Physics2DSettings = (19,) - Camera = (20,) - Material = (21,) - MeshRenderer = (23,) - Renderer = (25,) - ParticleRenderer = (26,) - Texture = (27,) - Texture2D = (28,) - SceneSettings = (29,) - GraphicsSettings = (30,) - MeshFilter = (33,) - OcclusionPortal = (41,) - Mesh = (43,) - Skybox = (45,) - QualitySettings = (47,) - Shader = (48,) - TextAsset = (49,) - Rigidbody2D = (50,) - Physics2DManager = (51,) - Collider2D = (53,) - Rigidbody = (54,) - PhysicsManager = (55,) - Collider = (56,) - Joint = (57,) - CircleCollider2D = (58,) - HingeJoint = (59,) - PolygonCollider2D = (60,) - BoxCollider2D = (61,) - PhysicsMaterial2D = (62,) - MeshCollider = (64,) - BoxCollider = (65,) - SpriteCollider2D = (66,) - EdgeCollider2D = (68,) - CapsuleCollider2D = (70,) - ComputeShader = (72,) - AnimationClip = (74,) - ConstantForce = (75,) - WorldParticleCollider = (76,) - TagManager = (78,) - AudioListener = (81,) - AudioSource = (82,) - AudioClip = (83,) - RenderTexture = (84,) - CustomRenderTexture = (86,) - MeshParticleEmitter = (87,) - ParticleEmitter = (88,) - Cubemap = (89,) - Avatar = (90,) - AnimatorController = (91,) - GUILayer = (92,) - RuntimeAnimatorController = (93,) - ScriptMapper = (94,) - Animator = (95,) - TrailRenderer = (96,) - DelayedCallManager = (98,) - TextMesh = (102,) - RenderSettings = (104,) - Light = (108,) - CGProgram = (109,) - BaseAnimationTrack = (110,) - Animation = (111,) - MonoBehaviour = (114,) - MonoScript = (115,) - MonoManager = (116,) - Texture3D = (117,) - NewAnimationTrack = (118,) - Projector = (119,) - LineRenderer = (120,) - Flare = (121,) - Halo = (122,) - LensFlare = (123,) - FlareLayer = (124,) - HaloLayer = (125,) - NavMeshAreas = (126,) - HaloManager = (127,) - Font = (128,) - PlayerSettings = (129,) - NamedObject = (130,) - GUITexture = (131,) - GUIText = (132,) - GUIElement = (133,) - PhysicMaterial = (134,) - SphereCollider = (135,) - CapsuleCollider = (136,) - SkinnedMeshRenderer = (137,) - FixedJoint = (138,) - RaycastCollider = (140,) - BuildSettings = (141,) - AssetBundle = (142,) - CharacterController = (143,) - CharacterJoint = (144,) - SpringJoint = (145,) - WheelCollider = (146,) - ResourceManager = (147,) - NetworkView = (148,) - NetworkManager = (149,) - PreloadData = (150,) - MovieTexture = (152,) - ConfigurableJoint = (153,) - TerrainCollider = (154,) - MasterServerInterface = (155,) - TerrainData = (156,) - LightmapSettings = (157,) - WebCamTexture = (158,) - EditorSettings = (159,) - InteractiveCloth = (160,) - ClothRenderer = (161,) - EditorUserSettings = (162,) - SkinnedCloth = (163,) - AudioReverbFilter = (164,) - AudioHighPassFilter = (165,) - AudioChorusFilter = (166,) - AudioReverbZone = (167,) - AudioEchoFilter = (168,) - AudioLowPassFilter = (169,) - AudioDistortionFilter = (170,) - SparseTexture = (171,) - AudioBehaviour = (180,) - AudioFilter = (181,) - WindZone = (182,) - Cloth = (183,) - SubstanceArchive = (184,) - ProceduralMaterial = (185,) - ProceduralTexture = (186,) - Texture2DArray = (187,) - CubemapArray = (188,) - OffMeshLink = (191,) - OcclusionArea = (192,) - Tree = (193,) - NavMeshObsolete = (194,) - NavMeshAgent = (195,) - NavMeshSettings = (196,) - LightProbesLegacy = (197,) - ParticleSystem = (198,) - ParticleSystemRenderer = (199,) - ShaderVariantCollection = (200,) - LODGroup = (205,) - BlendTree = (206,) - Motion = (207,) - NavMeshObstacle = (208,) - TerrainInstance = (210,) - SpriteRenderer = (212,) - Sprite = (213,) - CachedSpriteAtlas = (214,) - ReflectionProbe = (215,) - ReflectionProbes = (216,) - Terrain = (218,) - LightProbeGroup = (220,) - AnimatorOverrideController = (221,) - CanvasRenderer = (222,) - Canvas = (223,) - RectTransform = (224,) - CanvasGroup = (225,) - BillboardAsset = (226,) - BillboardRenderer = (227,) - SpeedTreeWindAsset = (228,) - AnchoredJoint2D = (229,) - Joint2D = (230,) - SpringJoint2D = (231,) - DistanceJoint2D = (232,) - HingeJoint2D = (233,) - SliderJoint2D = (234,) - WheelJoint2D = (235,) - ClusterInputManager = (236,) - BaseVideoTexture = (237,) - NavMeshData = (238,) - AudioMixer = (240,) - AudioMixerController = (241,) - AudioMixerGroupController = (243,) - AudioMixerEffectController = (244,) - AudioMixerSnapshotController = (245,) - PhysicsUpdateBehaviour2D = (246,) - ConstantForce2D = (247,) - Effector2D = (248,) - AreaEffector2D = (249,) - PointEffector2D = (250,) - PlatformEffector2D = (251,) - SurfaceEffector2D = (252,) - BuoyancyEffector2D = (253,) - RelativeJoint2D = (254,) - FixedJoint2D = (255,) - FrictionJoint2D = (256,) - TargetJoint2D = (257,) - LightProbes = (258,) - LightProbeProxyVolume = (259,) - SampleClip = (271,) - AudioMixerSnapshot = (272,) - AudioMixerGroup = (273,) - NScreenBridge = (280,) - AssetBundleManifest = (290,) - UnityAdsManager = (292,) - RuntimeInitializeOnLoadManager = (300,) - CloudWebServicesManager = (301,) - UnityAnalyticsManager = (303,) - CrashReportManager = (304,) - PerformanceReportingManager = (305,) - UnityConnectSettings = (310,) - AvatarMask = (319,) - VideoPlayer = (328,) - VideoClip = (329,) - OcclusionCullingData = (363,) - # kLargestRuntimeClassID = 364 - SmallestEditorClassID = (1000,) - Prefab = (1001,) - EditorExtensionImpl = (1002,) - AssetImporter = (1003,) - AssetDatabase = (1004,) - Mesh3DSImporter = (1005,) - TextureImporter = (1006,) - ShaderImporter = (1007,) - ComputeShaderImporter = (1008,) - AudioImporter = (1020,) - HierarchyState = (1026,) - GUIDSerializer = (1027,) - AssetMetaData = (1028,) - DefaultAsset = (1029,) - DefaultImporter = (1030,) - TextScriptImporter = (1031,) - SceneAsset = (1032,) - NativeFormatImporter = (1034,) - MonoImporter = (1035,) - AssetServerCache = (1037,) - LibraryAssetImporter = (1038,) - ModelImporter = (1040,) - FBXImporter = (1041,) - TrueTypeFontImporter = (1042,) - MovieImporter = (1044,) - EditorBuildSettings = (1045,) - DDSImporter = (1046,) - InspectorExpandedState = (1048,) - AnnotationManager = (1049,) - PluginImporter = (1050,) - EditorUserBuildSettings = (1051,) - PVRImporter = (1052,) - ASTCImporter = (1053,) - KTXImporter = (1054,) - IHVImageFormatImporter = (1055,) - AnimatorStateTransition = (1101,) - AnimatorState = (1102,) - HumanTemplate = (1105,) - AnimatorStateMachine = (1107,) - PreviewAssetType = (1108,) - AnimatorTransition = (1109,) - SpeedTreeImporter = (1110,) - AnimatorTransitionBase = (1111,) - SubstanceImporter = (1112,) - LightmapParameters = (1113,) - LightmapSnapshot = (1120,) - GISRaster = (1121,) - GISRasterImporter = (1122,) - CadImporter = (1123,) - SketchUpImporter = (1124,) - BuildReport = (1125,) - PackedAssets = (1126,) - VideoClipImporter = (1127,) - ActivationLogComponent = (2000,) - # kLargestEditorClassID = 2001 - # kClassIdOutOfHierarchy = 100000 - SubDerived = (367388927,) - SiblingDerived = (334799969,) - SpriteAtlas = (687078895,) - Derived = (1091556383,) - LowerResBlitTexture = (1480428607,) + UnknownType = -1 + Object = 0 + GameObject = 1 + Component = 2 + LevelGameManager = 3 + Transform = 4 + TimeManager = 5 + GlobalGameManager = 6 + Behaviour = 8 + GameManager = 9 + AudioManager = 11 + ParticleAnimator = 12 + InputManager = 13 + EllipsoidParticleEmitter = 15 + Pipeline = 17 + EditorExtension = 18 + Physics2DSettings = 19 + Camera = 20 + Material = 21 + MeshRenderer = 23 + Renderer = 25 + ParticleRenderer = 26 + Texture = 27 + Texture2D = 28 + OcclusionCullingSettings = 29 + GraphicsSettings = 30 + MeshFilter = 33 + OcclusionPortal = 41 + Mesh = 43 + Skybox = 45 + QualitySettings = 47 + Shader = 48 + TextAsset = 49 + Rigidbody2D = 50 + Physics2DManager = 51 + Collider2D = 53 + Rigidbody = 54 + PhysicsManager = 55 + Collider = 56 + Joint = 57 + CircleCollider2D = 58 + HingeJoint = 59 + PolygonCollider2D = 60 + BoxCollider2D = 61 + PhysicsMaterial2D = 62 + MeshCollider = 64 + BoxCollider = 65 + CompositeCollider2D = 66 + EdgeCollider2D = 68 + CapsuleCollider2D = 70 + ComputeShader = 72 + AnimationClip = 74 + ConstantForce = 75 + WorldParticleCollider = 76 + TagManager = 78 + AudioListener = 81 + AudioSource = 82 + AudioClip = 83 + RenderTexture = 84 + CustomRenderTexture = 86 + MeshParticleEmitter = 87 + ParticleEmitter = 88 + Cubemap = 89 + Avatar = 90 + AnimatorController = 91 + GUILayer = 92 + RuntimeAnimatorController = 93 + ScriptMapper = 94 + Animator = 95 + TrailRenderer = 96 + DelayedCallManager = 98 + TextMesh = 102 + RenderSettings = 104 + Light = 108 + CGProgram = 109 + BaseAnimationTrack = 110 + Animation = 111 + MonoBehaviour = 114 + MonoScript = 115 + MonoManager = 116 + Texture3D = 117 + NewAnimationTrack = 118 + Projector = 119 + LineRenderer = 120 + Flare = 121 + Halo = 122 + LensFlare = 123 + FlareLayer = 124 + HaloLayer = 125 + NavMeshProjectSettings = 126 + HaloManager = 127 + Font = 128 + PlayerSettings = 129 + NamedObject = 130 + GUITexture = 131 + GUIText = 132 + GUIElement = 133 + PhysicMaterial = 134 + SphereCollider = 135 + CapsuleCollider = 136 + SkinnedMeshRenderer = 137 + FixedJoint = 138 + RaycastCollider = 140 + BuildSettings = 141 + AssetBundle = 142 + CharacterController = 143 + CharacterJoint = 144 + SpringJoint = 145 + WheelCollider = 146 + ResourceManager = 147 + NetworkView = 148 + NetworkManager = 149 + PreloadData = 150 + MovieTexture = 152 + ConfigurableJoint = 153 + TerrainCollider = 154 + MasterServerInterface = 155 + TerrainData = 156 + LightmapSettings = 157 + WebCamTexture = 158 + EditorSettings = 159 + InteractiveCloth = 160 + ClothRenderer = 161 + EditorUserSettings = 162 + SkinnedCloth = 163 + AudioReverbFilter = 164 + AudioHighPassFilter = 165 + AudioChorusFilter = 166 + AudioReverbZone = 167 + AudioEchoFilter = 168 + AudioLowPassFilter = 169 + AudioDistortionFilter = 170 + SparseTexture = 171 + AudioBehaviour = 180 + AudioFilter = 181 + WindZone = 182 + Cloth = 183 + SubstanceArchive = 184 + ProceduralMaterial = 185 + ProceduralTexture = 186 + Texture2DArray = 187 + CubemapArray = 188 + OffMeshLink = 191 + OcclusionArea = 192 + Tree = 193 + NavMeshObsolete = 194 + NavMeshAgent = 195 + NavMeshSettings = 196 + LightProbesLegacy = 197 + ParticleSystem = 198 + ParticleSystemRenderer = 199 + ShaderVariantCollection = 200 + LODGroup = 205 + BlendTree = 206 + Motion = 207 + NavMeshObstacle = 208 + SortingGroup = 210 + SpriteRenderer = 212 + Sprite = 213 + CachedSpriteAtlas = 214 + ReflectionProbe = 215 + ReflectionProbes = 216 + Terrain = 218 + LightProbeGroup = 220 + AnimatorOverrideController = 221 + CanvasRenderer = 222 + Canvas = 223 + RectTransform = 224 + CanvasGroup = 225 + BillboardAsset = 226 + BillboardRenderer = 227 + SpeedTreeWindAsset = 228 + AnchoredJoint2D = 229 + Joint2D = 230 + SpringJoint2D = 231 + DistanceJoint2D = 232 + HingeJoint2D = 233 + SliderJoint2D = 234 + WheelJoint2D = 235 + ClusterInputManager = 236 + BaseVideoTexture = 237 + NavMeshData = 238 + AudioMixer = 240 + AudioMixerController = 241 + AudioMixerGroupController = 243 + AudioMixerEffectController = 244 + AudioMixerSnapshotController = 245 + PhysicsUpdateBehaviour2D = 246 + ConstantForce2D = 247 + Effector2D = 248 + AreaEffector2D = 249 + PointEffector2D = 250 + PlatformEffector2D = 251 + SurfaceEffector2D = 252 + BuoyancyEffector2D = 253 + RelativeJoint2D = 254 + FixedJoint2D = 255 + FrictionJoint2D = 256 + TargetJoint2D = 257 + LightProbes = 258 + LightProbeProxyVolume = 259 + SampleClip = 271 + AudioMixerSnapshot = 272 + AudioMixerGroup = 273 + NScreenBridge = 280 + AssetBundleManifest = 290 + UnityAdsManager = 292 + RuntimeInitializeOnLoadManager = 300 + CloudWebServicesManager = 301 + UnityAnalyticsManager = 303 + CrashReportManager = 304 + PerformanceReportingManager = 305 + UnityConnectSettings = 310 + AvatarMask = 319 + PlayableDirector = 320 + VideoPlayer = 328 + VideoClip = 329 + ParticleSystemForceField = 330 + SpriteMask = 331 + WorldAnchor = 362 + OcclusionCullingData = 363 + SmallestEditorClassID = 1000 + PrefabInstance = 1001 + EditorExtensionImpl = 1002 + AssetImporter = 1003 + AssetDatabaseV1 = 1004 + Mesh3DSImporter = 1005 + TextureImporter = 1006 + ShaderImporter = 1007 + ComputeShaderImporter = 1008 + AudioImporter = 1020 + HierarchyState = 1026 + GUIDSerializer = 1027 + AssetMetaData = 1028 + DefaultAsset = 1029 + DefaultImporter = 1030 + TextScriptImporter = 1031 + SceneAsset = 1032 + NativeFormatImporter = 1034 + MonoImporter = 1035 + AssetServerCache = 1037 + LibraryAssetImporter = 1038 + ModelImporter = 1040 + FBXImporter = 1041 + TrueTypeFontImporter = 1042 + MovieImporter = 1044 + EditorBuildSettings = 1045 + DDSImporter = 1046 + InspectorExpandedState = 1048 + AnnotationManager = 1049 + PluginImporter = 1050 + EditorUserBuildSettings = 1051 + PVRImporter = 1052 + ASTCImporter = 1053 + KTXImporter = 1054 + IHVImageFormatImporter = 1055 + AnimatorStateTransition = 1101 + AnimatorState = 1102 + HumanTemplate = 1105 + AnimatorStateMachine = 1107 + PreviewAnimationClip = 1108 + AnimatorTransition = 1109 + SpeedTreeImporter = 1110 + AnimatorTransitionBase = 1111 + SubstanceImporter = 1112 + LightmapParameters = 1113 + LightingDataAsset = 1120 + GISRaster = 1121 + GISRasterImporter = 1122 + CadImporter = 1123 + SketchUpImporter = 1124 + BuildReport = 1125 + PackedAssets = 1126 + VideoClipImporter = 1127 + ActivationLogComponent = 2000 + int = 100000 + bool = 100001 + float = 100002 + MonoObject = 100003 + Collision = 100004 + Vector3f = 100005 + RootMotionData = 100006 + Collision2D = 100007 + AudioMixerLiveUpdateFloat = 100008 + AudioMixerLiveUpdateBool = 100009 + Polygon2D = 100010 + void = 100011 + TilemapCollider2D = 19719996 + AssetImporterLog = 41386430 + VFXRenderer = 73398921 + SerializableManagedRefTestClass = 76251197 + Grid = 156049354 + ScenesUsingAssets = 156483287 + ArticulationBody = 171741748 + Preset = 181963792 + EmptyObject = 277625683 + IConstraint = 285090594 + TestObjectWithSpecialLayoutOne = 293259124 + AssemblyDefinitionReferenceImporter = 294290339 + SiblingDerived = 334799969 + TestObjectWithSerializedMapStringNonAlignedStruct = 342846651 + SubDerived = 367388927 + AssetImportInProgressProxy = 369655926 + PluginBuildInfo = 382020655 + EditorProjectAccess = 426301858 + PrefabImporter = 468431735 + TestObjectWithSerializedArray = 478637458 + TestObjectWithSerializedAnimationCurve = 478637459 + TilemapRenderer = 483693784 + ScriptableCamera = 488575907 + SpriteAtlasAsset = 612988286 + SpriteAtlasDatabase = 638013454 + AudioBuildInfo = 641289076 + CachedSpriteAtlasRuntimeData = 644342135 + RendererFake = 646504946 + AssemblyDefinitionReferenceAsset = 662584278 + BuiltAssetBundleInfoSet = 668709126 + SpriteAtlas = 687078895 + RayTracingShaderImporter = 747330370 + RayTracingShader = 825902497 + LightingSettings = 850595691 + PlatformModuleSetup = 877146078 + VersionControlSettings = 890905787 + AimConstraint = 895512359 + VFXManager = 937362698 + VisualEffectSubgraph = 994735392 + VisualEffectSubgraphOperator = 994735403 + VisualEffectSubgraphBlock = 994735404 + LocalizationImporter = 1027052791 + Derived = 1091556383 + PropertyModificationsTargetTestObject = 1111377672 + ReferencesArtifactGenerator = 1114811875 + AssemblyDefinitionAsset = 1152215463 + SceneVisibilityState = 1154873562 + LookAtConstraint = 1183024399 + SpriteAtlasImporter = 1210832254 + MultiArtifactTestImporter = 1223240404 + GameObjectRecorder = 1268269756 + LightingDataAssetParent = 1325145578 + PresetManager = 1386491679 + TestObjectWithSpecialLayoutTwo = 1392443030 + StreamingManager = 1403656975 + LowerResBlitTexture = 1480428607 + StreamingController = 1542919678 RenderPassAttachment = 1571458007 + TestObjectVectorPairStringBool = 1628831178 + GridLayout = 1742807556 + AssemblyDefinitionImporter = 1766753193 + ParentConstraint = 1773428102 + FakeComponent = 1803986026 + PositionConstraint = 1818360608 + RotationConstraint = 1818360609 + ScaleConstraint = 1818360610 + Tilemap = 1839735485 + PackageManifest = 1896753125 + PackageManifestImporter = 1896753126 + TerrainLayer = 1953259897 + SpriteShapeRenderer = 1971053207 + NativeObjectType = 1977754360 + TestObjectWithSerializedMapStringBool = 1981279845 + SerializableManagedHost = 1995898324 + VisualEffectAsset = 2058629509 + VisualEffectImporter = 2058629510 + VisualEffectResource = 2058629511 + VisualEffectObject = 2059678085 + VisualEffect = 2083052967 + LocalizationAsset = 2083778819 + ScriptedImporter = 2089858483 def __str__(self): return self.name + def __format__(self, fmt): + return self.name + def __eq__(self, value): if isinstance(value, str): return self.name == value - else: - return self.value == value + elif isinstance(value, ClassIDType): + return self._value_ == value._value_ + elif isinstance(value, int): + if self.has_value(value): + return self._value_ == value + elif self._value_ == -1 and not self.has_value(value): + return True + return False + return self._value_ == value @classmethod - def _missing_(cls, value): + def has_value(cls, value): + return value in cls._value2member_map_ + + +def makeClassID(_cid): + if isinstance(_cid, ClassIDType): + return _cid + elif _cid == -1: return ClassIDType.UnknownType + elif ClassIDType.has_value(_cid): + return ClassIDType(_cid) + return _cid \ No newline at end of file diff --git a/UnityPy/CommonString.py b/UnityPy/enums/CommonString.py similarity index 91% rename from UnityPy/CommonString.py rename to UnityPy/enums/CommonString.py index 788dcd60..c91d2ca2 100644 --- a/UnityPy/CommonString.py +++ b/UnityPy/enums/CommonString.py @@ -1,4 +1,4 @@ -COMMON_STRING = { +CommonString = { 0: "AABB", 5: "AnimationClip", 19: "AnimationCurve", @@ -32,25 +32,25 @@ 256: "MdFour", 263: "MonoBehaviour", 277: "MonoScript", - 288: "byte_size", + 288: "m_ByteSize", 299: "m_Curve", 307: "m_EditorClassIdentifier", 331: "m_EditorHideFlags", 349: "m_Enabled", 359: "m_ExtensionPtr", 374: "m_GameObject", - 387: "index", - 395: "is_array", + 387: "m_Index", + 395: "m_IsArray", 405: "m_IsStatic", - 416: "meta_flag", - 427: "name", - 434: "_object_hide_flags", + 416: "m_MetaFlag", + 427: "m_Name", + 434: "m_ObjectHideFlags", 452: "m_PrefabInternal", 469: "m_PrefabParentObject", 490: "m_Script", 499: "m_StaticEditorFlags", - 519: "type", - 526: "version", + 519: "m_Type", + 526: "m_Version", 536: "Object", 543: "pair", 548: "PPtr", @@ -106,5 +106,5 @@ 1093: "m_CorrespondingSourceObject", 1121: "m_PrefabInstance", 1138: "m_PrefabAsset", - 1152: "FileSize", + 1152: "FileSize" } diff --git a/UnityPy/enums/FileType.py b/UnityPy/enums/FileType.py index b5ae77af..adfb2c96 100644 --- a/UnityPy/enums/FileType.py +++ b/UnityPy/enums/FileType.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import IntEnum -class FileType(Enum): +class FileType(IntEnum): AssetsFile = 0 BundleFile = 1 WebFile = 2 diff --git a/UnityPy/enums/GfxPrimitiveType.py b/UnityPy/enums/GfxPrimitiveType.py new file mode 100644 index 00000000..f6d4cc96 --- /dev/null +++ b/UnityPy/enums/GfxPrimitiveType.py @@ -0,0 +1,10 @@ +from enum import IntEnum + + +class GfxPrimitiveType(IntEnum): + kPrimitiveTriangles = 0 + kPrimitiveTriangleStrip = 1 + kPrimitiveQuads = 2 + kPrimitiveLines = 3 + kPrimitiveLineStrip = 4 + kPrimitivePoints = 5 \ No newline at end of file diff --git a/UnityPy/enums/PassType.py b/UnityPy/enums/PassType.py new file mode 100644 index 00000000..1b2d1c2e --- /dev/null +++ b/UnityPy/enums/PassType.py @@ -0,0 +1,7 @@ +from enum import IntEnum + + +class PassType(IntEnum): + kPassTypeNormal = 0 + kPassTypeUse = 1 + kPassTypeGrab = 2 diff --git a/UnityPy/enums/SerializedPropertyType.py b/UnityPy/enums/SerializedPropertyType.py new file mode 100644 index 00000000..de7325b8 --- /dev/null +++ b/UnityPy/enums/SerializedPropertyType.py @@ -0,0 +1,9 @@ +from enum import IntEnum + + +class SerializedPropertyType(IntEnum): + kColor = 0 + kVector = 1 + kFloat = 2 + kRange = 3 + kTexture = 4 diff --git a/UnityPy/enums/ShaderCompilerPlatform.py b/UnityPy/enums/ShaderCompilerPlatform.py new file mode 100644 index 00000000..f1d07567 --- /dev/null +++ b/UnityPy/enums/ShaderCompilerPlatform.py @@ -0,0 +1,26 @@ +from enum import IntEnum + + +class ShaderCompilerPlatform(IntEnum): + kShaderCompPlatformNone = -1 + kShaderCompPlatformGL = 0 + kShaderCompPlatformD3D9 = 1 + kShaderCompPlatformXbox360 = 2 + kShaderCompPlatformPS3 = 3 + kShaderCompPlatformD3D11 = 4 + kShaderCompPlatformGLES20 = 5 + kShaderCompPlatformNaCl = 6 + kShaderCompPlatformFlash = 7 + kShaderCompPlatformD3D11_9x = 8 + kShaderCompPlatformGLES3Plus = 9 + kShaderCompPlatformPSP2 = 10 + kShaderCompPlatformPS4 = 11 + kShaderCompPlatformXboxOne = 12 + kShaderCompPlatformPSM = 13 + kShaderCompPlatformMetal = 14 + kShaderCompPlatformOpenGLCore = 15 + kShaderCompPlatformN3DS = 16 + kShaderCompPlatformWiiU = 17 + kShaderCompPlatformVulkan = 18 + kShaderCompPlatformSwitch = 19 + kShaderCompPlatformXboxOneD3D12 = 20 diff --git a/UnityPy/enums/ShaderGpuProgramType.py b/UnityPy/enums/ShaderGpuProgramType.py new file mode 100644 index 00000000..170bf307 --- /dev/null +++ b/UnityPy/enums/ShaderGpuProgramType.py @@ -0,0 +1,36 @@ +from enum import IntEnum + + +class ShaderGpuProgramType(IntEnum): + kShaderGpuProgramUnknown = 0 + kShaderGpuProgramGLLegacy = 1 + kShaderGpuProgramGLES31AEP = 2 + kShaderGpuProgramGLES31 = 3 + kShaderGpuProgramGLES3 = 4 + kShaderGpuProgramGLES = 5 + kShaderGpuProgramGLCore32 = 6 + kShaderGpuProgramGLCore41 = 7 + kShaderGpuProgramGLCore43 = 8 + kShaderGpuProgramDX9VertexSM20 = 9 + kShaderGpuProgramDX9VertexSM30 = 10 + kShaderGpuProgramDX9PixelSM20 = 11 + kShaderGpuProgramDX9PixelSM30 = 12 + kShaderGpuProgramDX10Level9Vertex = 13 + kShaderGpuProgramDX10Level9Pixel = 14 + kShaderGpuProgramDX11VertexSM40 = 15 + kShaderGpuProgramDX11VertexSM50 = 16 + kShaderGpuProgramDX11PixelSM40 = 17 + kShaderGpuProgramDX11PixelSM50 = 18 + kShaderGpuProgramDX11GeometrySM40 = 19 + kShaderGpuProgramDX11GeometrySM50 = 20 + kShaderGpuProgramDX11HullSM50 = 21 + kShaderGpuProgramDX11DomainSM50 = 22 + kShaderGpuProgramMetalVS = 23 + kShaderGpuProgramMetalFS = 24 + kShaderGpuProgramSPIRV = 25 + kShaderGpuProgramConsoleVS = 26 + kShaderGpuProgramConsoleFS = 27 + kShaderGpuProgramConsoleHS = 28 + kShaderGpuProgramConsoleDS = 29 + kShaderGpuProgramConsoleGS = 30 + kShaderGpuProgramRayTracing = 31 diff --git a/UnityPy/enums/SpriteMeshType.py b/UnityPy/enums/SpriteMeshType.py index 6c272061..75c282c4 100644 --- a/UnityPy/enums/SpriteMeshType.py +++ b/UnityPy/enums/SpriteMeshType.py @@ -1,6 +1,6 @@ -from enum import Enum +from enum import IntEnum -class SpriteMeshType(Enum): +class SpriteMeshType(IntEnum): kSpriteMeshTypeFullRect = 0 kSpriteMeshTypeTight = 1 diff --git a/UnityPy/enums/TextureDimension.py b/UnityPy/enums/TextureDimension.py new file mode 100644 index 00000000..044e2626 --- /dev/null +++ b/UnityPy/enums/TextureDimension.py @@ -0,0 +1,13 @@ +from enum import IntEnum + + +class TextureDimension(IntEnum): + kTexDimUnknown = -1 + kTexDimNone = 0 + kTexDimAny = 1 + kTexDim2D = 2 + kTexDim3D = 3 + kTexDimCUBE = 4 + kTexDim2DArray = 5 + kTexDimCubeArray = 6 + kTexDimForce32Bit = 2147483647 diff --git a/UnityPy/enums/TextureFormat.py b/UnityPy/enums/TextureFormat.py index 308856b0..1e3085ae 100644 --- a/UnityPy/enums/TextureFormat.py +++ b/UnityPy/enums/TextureFormat.py @@ -2,66 +2,66 @@ class TextureFormat(IntEnum): - Alpha8 = (1,) - ARGB4444 = (2,) - RGB24 = (3,) - RGBA32 = (4,) - ARGB32 = (5,) - RGB565 = (7,) - R16 = (9,) - DXT1 = (10,) - DXT5 = (12,) - RGBA4444 = (13,) - BGRA32 = (14,) - RHalf = (15,) - RGHalf = (16,) - RGBAHalf = (17,) - RFloat = (18,) - RGFloat = (19,) - RGBAFloat = (20,) - YUY2 = (21,) - RGB9e5Float = (22,) - BC4 = (26,) - BC5 = (27,) - BC6H = (24,) - BC7 = (25,) - DXT1Crunched = (28,) - DXT5Crunched = (29,) - PVRTC_RGB2 = (30,) - PVRTC_RGBA2 = (31,) - PVRTC_RGB4 = (32,) - PVRTC_RGBA4 = (33,) - ETC_RGB4 = (34,) - ATC_RGB4 = (35,) - ATC_RGBA8 = (36,) - EAC_R = (41,) - EAC_R_SIGNED = (42,) - EAC_RG = (43,) - EAC_RG_SIGNED = (44,) - ETC2_RGB = (45,) - ETC2_RGBA1 = (46,) - ETC2_RGBA8 = (47,) - ASTC_RGB_4x4 = (48,) - ASTC_RGB_5x5 = (49,) - ASTC_RGB_6x6 = (50,) - ASTC_RGB_8x8 = (51,) - ASTC_RGB_10x10 = (52,) - ASTC_RGB_12x12 = (53,) - ASTC_RGBA_4x4 = (54,) - ASTC_RGBA_5x5 = (55,) - ASTC_RGBA_6x6 = (56,) - ASTC_RGBA_8x8 = (57,) - ASTC_RGBA_10x10 = (58,) - ASTC_RGBA_12x12 = (59,) - ETC_RGB4_3DS = (60,) - ETC_RGBA8_3DS = (61,) - RG16 = (62,) - R8 = (63,) - ETC_RGB4Crunched = (64,) - ETC2_RGBA8Crunched = (65,) - ASTC_HDR_4x4 = (66,) - ASTC_HDR_5x5 = (67,) - ASTC_HDR_6x6 = (68,) - ASTC_HDR_8x8 = (69,) - ASTC_HDR_10x10 = (70,) - ASTC_HDR_12x12 = (71,) + Alpha8 = 1 + ARGB4444 = 2 + RGB24 = 3 + RGBA32 = 4 + ARGB32 = 5 + RGB565 = 7 + R16 = 9 + DXT1 = 10 + DXT5 = 12 + RGBA4444 = 13 + BGRA32 = 14 + RHalf = 15 + RGHalf = 16 + RGBAHalf = 17 + RFloat = 18 + RGFloat = 19 + RGBAFloat = 20 + YUY2 = 21 + RGB9e5Float = 22 + BC4 = 26 + BC5 = 27 + BC6H = 24 + BC7 = 25 + DXT1Crunched = 28 + DXT5Crunched = 29 + PVRTC_RGB2 = 30 + PVRTC_RGBA2 = 31 + PVRTC_RGB4 = 32 + PVRTC_RGBA4 = 33 + ETC_RGB4 = 34 + ATC_RGB4 = 35 + ATC_RGBA8 = 36 + EAC_R = 41 + EAC_R_SIGNED = 42 + EAC_RG = 43 + EAC_RG_SIGNED = 44 + ETC2_RGB = 45 + ETC2_RGBA1 = 46 + ETC2_RGBA8 = 47 + ASTC_RGB_4x4 = 48 + ASTC_RGB_5x5 = 49 + ASTC_RGB_6x6 = 50 + ASTC_RGB_8x8 = 51 + ASTC_RGB_10x10 = 52 + ASTC_RGB_12x12 = 53 + ASTC_RGBA_4x4 = 54 + ASTC_RGBA_5x5 = 55 + ASTC_RGBA_6x6 = 56 + ASTC_RGBA_8x8 = 57 + ASTC_RGBA_10x10 = 58 + ASTC_RGBA_12x12 = 59 + ETC_RGB4_3DS = 60 + ETC_RGBA8_3DS = 61 + RG16 = 62 + R8 = 63 + ETC_RGB4Crunched = 64 + ETC2_RGBA8Crunched = 65 + ASTC_HDR_4x4 = 66 + ASTC_HDR_5x5 = 67 + ASTC_HDR_6x6 = 68 + ASTC_HDR_8x8 = 69 + ASTC_HDR_10x10 = 70 + ASTC_HDR_12x12 = 71 diff --git a/UnityPy/enums/__init__.py b/UnityPy/enums/__init__.py index 9e78b43a..48b74dbe 100644 --- a/UnityPy/enums/__init__.py +++ b/UnityPy/enums/__init__.py @@ -1,6 +1,13 @@ from .Audio import AudioType, AudioCompressionFormat, AUDIO_TYPE_EXTEMSION -from .BuildTarget import BuildTarget -from .ClassIDType import ClassIDType +from .BuildTarget import BuildTarget, makeBuildTarget +from .ClassIDType import ClassIDType, makeClassID from .FileType import FileType from .TextureFormat import TextureFormat from .SpriteMeshType import SpriteMeshType +from .GfxPrimitiveType import GfxPrimitiveType +from .CommonString import CommonString +from .ShaderCompilerPlatform import ShaderCompilerPlatform +from .ShaderGpuProgramType import ShaderGpuProgramType +from .SerializedPropertyType import SerializedPropertyType +from .TextureDimension import TextureDimension +from .PassType import PassType diff --git a/UnityPy/environment.py b/UnityPy/environment.py index 49b87b29..adc0d753 100644 --- a/UnityPy/environment.py +++ b/UnityPy/environment.py @@ -1,25 +1,32 @@ import io import os +from collections import Counter +from collections.abc import Callable from zipfile import ZipFile from . import files +from .files import File from .enums import FileType from .helpers import ImportHelper +from .streams import EndianBinaryReader +from .files import SerializedFile +IGNORE_DIR_COUNT = 0 +DEFAULT_TYPES = ['MonoBehaviour', 'Texture2D', 'TextAsset', 'PlayerSettings'] + +def default_progress(skip_progress): + return skip_progress class Environment: files: dict - resources: dict - container: dict - assets: dict path: str def __init__(self, *args): self.files = {} - self.container = {} - self.assets = {} - self.resources = {} self.path = "." + self.out_path = os.path.join(os.getcwd(), "output") + self.ignore_dir_lvls = IGNORE_DIR_COUNT + self.progress_function = default_progress if args: for arg in args: @@ -29,23 +36,23 @@ def __init__(self, *args): self.load_zip_file(arg) else: self.path = os.path.dirname(arg) - self.load_file(arg) + self.load_files([arg]) elif os.path.isdir(arg): self.path = arg self.load_folder(arg) else: self.path = None - self.load_file(data=arg) + self.files[str(len(self.files)) + ] = self.load_file(stream=arg) if len(self.files) == 1: self.file = list(self.files.values())[0] def load_files(self, files: list): """Loads all files (list) into the AssetsManager and merges .split files for common usage.""" - path = os.path.dirname(files[0]) - ImportHelper.merge_split_assets(path) - to_read_file = ImportHelper.processing_split_files(files) - self.load(to_read_file) + # ImportHelper.merge_split_assets(path) + # to_read_file = ImportHelper.processing_split_files(files) + self.load(files) def load_folder(self, path: str): """Loads all files in the given path and its subdirs into the AssetsManager.""" @@ -56,47 +63,142 @@ def load_folder(self, path: str): def load(self, files: list): """Loads all files into the AssetsManager.""" - # for f in files: - # self.import_files[os.path.basename(f)] = f - - # self.Progress.reset() - # use a for loop because list size can change - # for i, f in enumerate(self.import_files.values()): for f in files: - self.load_file(f) - # self.Progress.report(i + 1, len(self.import_files)) + self.files[f[len(self.path):].lstrip( + "/\\")] = self.load_file(open(f, "rb"), self) - def load_file(self, full_name: str = "", data=None): - typ, reader = ImportHelper.check_file_type(data if data else full_name) - if not full_name: - full_name = str(data[:256]) + def load_file(self, stream, parent=None): + if not parent: + parent = self + typ, reader = ImportHelper.check_file_type(stream) + #try: if typ == FileType.AssetsFile: - self.files[full_name] = files.SerializedFile(reader, self) - self.assets[full_name] = self.files[full_name] + return files.SerializedFile(reader, parent, name=stream.name) elif typ == FileType.BundleFile: - self.files[full_name] = files.BundleFile(reader, self) + return files.BundleFile(reader, parent, name=stream.name) elif typ == FileType.WebFile: - self.files[full_name] = files.WebFile(reader, self) + return files.WebFile(reader, parent, name=stream.name) elif typ == FileType.ZIP: - self.load_zip_file(reader.stream) + self.load_zip_file(stream) elif typ == FileType.ResourceFile: - self.resources[os.path.basename(full_name)] = reader + return EndianBinaryReader(stream) + #except: + # just to be sure + # cuz the SerializedFile detection isn't perfect + + # return EndianBinaryReader(stream) def load_zip_file(self, value): buffer = None if isinstance(value, str) and os.path.exists(value): buffer = open(value, "rb") elif isinstance(value, (bytes, bytearray)): - buffer = ZipFile(io.BytesIO(value)) + buffer = io.BytesIO(value) elif isinstance(value, (io.BufferedReader, io.BufferedIOBase)): buffer = value z = ZipFile(buffer) - + splits = {} for path in z.namelist(): - data = z.open(path).read() - if data: - self.load_file(path, data) + + if path[-7:-1] == ".split": + name = path[:-7] + if name not in splits: + splits[name] = [] + splits[name].append(path) + continue + + stream = z.open(path) + if stream._orig_file_size == 0: + continue + *path, name = path.split("/") + cur = self + for d in path: + if d not in cur.files: + cur.files[d] = files.File(self) + cur = cur.files[d] + cur.files[name] = self.load_file(stream, cur) + + # merge splits + for path, items in splits.items(): + # merge data + data = io.BytesIO() + for item in items: + with z.open(item) as f: + data.write(f.read()) + *path, name = path.split("/") + cur = self + for d in path: + if d not in cur.files: + cur.files[d] = files.File(self) + cur = cur.files[d] + cur.files[name] = self.load_file(data, cur) + + z.close() + + def save(self, pack="none"): + """ Saves all changed assets. + Mark assets as changed using `.mark_changed()`. + pack = "none" (default) or "lz4" + """ + for f in self.files: + if self.files[f].is_changed: + with open(os.path.join(self.out_path, os.path.basename(f)), 'wb') as out: + out.write(self.files[f].save(packer=pack)) + + def process(self, obj_modify: Callable, types: list=DEFAULT_TYPES, **kwargs): + """Accept modification function `obj_modify => obj_modify(obj, asset, local_path) to modify `types` """ + is_bundle = len(self.assets) > 1 + for f in list(self.files): + for asset in list(self.assets): + # filter objects and put Texture2Ds at the end of the list + objs = sorted((obj for obj in asset.get_objects( + ) if obj.type.name in types), key=lambda x: 1 if x.type == "Texture2D" else 0) + cobjs = sorted(((key, obj) for key, obj in asset.container.items( + ) if obj.type.name in types), key=lambda x: 1 if x[1].type == "Texture2D" else 0) + # check which mode we will have to use + num_cont = sum(cobjs) + num_objs = len(objs) + + if is_bundle: + asset_name = os.path.basename(asset.name) + else: + asset_name = os.path.basename(f) + + # check if container contains all important assets, if yes, just ignore the container + if num_objs <= num_cont * 2: + if num_cont > 1: + all_cobjs = self.progress_function(cobjs) + if is_bundle: + all_cobjs.postfix = asset_name + else: + all_cobjs = cobjs + for asset_path, obj in cobjs: + fp = os.path.join(self.out_path, *asset_path.split('/') + [self.ignore_dir_lvls:]) + if obj.type in types: + obj_modify(obj, asset_name, local_path=local_path, **kwargs) + # otherwise use the container to generate a path for the normal objects + else: + extracted = [] + if num_objs > 1: + all_objs = self.progress_function(objs) + if is_bundle: + all_objs.postfix = asset_name + else: + all_objs = objs + # find the most common path + occurence_count = Counter(os.path.splitext(asset_path)[ + 0] for asset_path in asset.container.keys()) + local_path = '' + if len(occurence_count) > 0: + local_path = os.path.join( + self.out_path, *occurence_count.most_common(1)[0][0].split('/')[self.ignore_dir_lvls:]) + + for obj in all_objs: + if obj.path_id not in extracted: + extracted.extend(obj_modify(obj, asset_name, local_path=local_path, **kwargs)) + @property def objects(self): @@ -115,3 +217,62 @@ def search(item): return ret return search(self) + + def filtered_objects(self, filter_types=None): + # iterate over assets + for asset in self.assets: + # filter objects and put Texture2Ds at the end of the list + objs = sorted((obj for obj in asset.get_filtered_objects( + )), key=lambda x: 1 if x.type == "Texture2D" else 0) + cobjs = sorted(((key, obj) for key, obj in asset.container.items( + )), key=lambda x: 1 if x[1].type == "Texture2D" else 0) + # check which mode we will have to use + num_cont = sum(cobjs) + num_objs = len(objs) + + # check if container contains all important assets, if yes, just ignore the container + if num_objs <= num_cont * 2: + for asset_path, obj in cobjs: + fp = os.path.join(DST, *asset_path.split('/') + [self.ignore_dir_lvls:]) + yield obj, fp, asset + + # otherwise use the container to generate a path for the normal objects + else: + extracted = [] + # find the most common path + occurence_count = Counter(os.path.splitext(asset_path)[ + 0] for asset_path in asset.container.keys()) + local_path = os.path.join( + DST, *occurence_count.most_common(1)[0][0].split('/')[self.ignore_dir_lvls:]) + + for obj in objs: + if obj.path_id not in extracted: + yield obj, fp, asset + + @property + def container(self): + return { + path: obj + for f in self.files.values() + if isinstance(f, File) + for path, obj in f.container.items() + } + + @property + def assets(self) -> list: + """ + Lists all assets / SerializedFiles within this environment. + """ + def gen_all_asset_files(file, ret = []): + for f in getattr(file, "files", {}).values(): + if isinstance(f, SerializedFile): + ret.append(f) + else: + gen_all_asset_files(f, ret) + return ret + return gen_all_asset_files(self) + + def get(self, key, default=None): + return getattr(self, key, default) + diff --git a/UnityPy/exceptions.py b/UnityPy/exceptions.py new file mode 100644 index 00000000..1eda4850 --- /dev/null +++ b/UnityPy/exceptions.py @@ -0,0 +1,5 @@ +class TypeTreeError(Exception): + def __init__(self, message, nodes): + # Call the base class constructor with the parameters it needs + super().__init__(message) + self.nodes = nodes \ No newline at end of file diff --git a/UnityPy/export/AudioClipConverter.py b/UnityPy/export/AudioClipConverter.py index 54f2b55c..506d4a50 100644 --- a/UnityPy/export/AudioClipConverter.py +++ b/UnityPy/export/AudioClipConverter.py @@ -1,21 +1,71 @@ -try: - from fsb5 import FSB5 -except ImportError: - print("Couldn't import fsb5.\nfsb5 is required to process convert audio clips.") +from ctypes import * +from enum import Enum +import os +import platform +from UnityPy.streams import EndianBinaryWriter +_dll = None -def extract_audioclip_samples(audio) -> dict: - """extracts all the sample data from an AudioClip - Copied from unitypack - https://github.com/HearthSim/UnityPack/blob/d9ce99fac3c917fa44b0042c9114b7cd03aa9884/unitypack/utils.py#L14 +def load_fmod_library(): + global _dll + if _dll is not None: + return + + ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + + # determine system - Windows, Darwin, Linux, Android + system = platform.system() + if system == "Linux" and "ANDROID_BOOTLOGO" in os.environ: + system = "Android" + # determine architecture + machine = platform.machine() + arch = platform.architecture()[0] - :param audio: AudioClip - :type audio: AudioClip - :return: {filename : sample(bytes)} - :rtype: dict - """ + if system in ["Windows", "Darwin"]: + if arch == "32bit": + arch = "x86" + elif arch == "64bit": + arch = "x64" + elif system == "Linux": + # Raspberry Pi and Linux on arm projects + if "arm" in machine: + if arch == "32bit": + arch = "armhf" if machine.endswith("l") else "arm" + elif arch == "64bit": + # Raise an exception for now; Once it gets supported by FMOD we can just modify the code here + _dll = False + raise NotImplementedError("ARM64 not supported by FMOD.\nUse a 32bit python version.") + elif arch == "32bit": + arch = "x86" + elif arch == "64bit": + arch = "x86_64" + else: + _dll = False + raise NotImplementedError("Couldn't find a correct FMOD library for your system ({system} - {arch}).") + try: + # build path and load library + LIB_PATH = os.path.join(ROOT, "lib", "FMOD", system, arch) + if system == 'Windows': + _dll = WinDLL(os.path.join(LIB_PATH, "fmod.dll")) + elif system in ["Linux", "Android"]: + _dll = CDLL(os.path.join(LIB_PATH, "libfmod.so")) + elif system == "Darwin": + _dll = CDLL(os.path.join(LIB_PATH, "libfmod.dylib")) + except Exception as e: + raise ImportError( + f"Failed to import the fmod library - Exception: {e}.\ + If you want to export AudioClips, you have to set UnityPy.export.AudioClipConverter._dll yourself." + ) + +def extract_audioclip_samples(audio) -> dict: + """extracts all the samples from an AudioClip + :param audio: AudioClip + :type audio: AudioClip + :return: {filename : sample(bytes)} + :rtype: dict + """ if not audio.m_AudioData: # eg. StreamedResource not available return {} @@ -25,22 +75,253 @@ def extract_audioclip_samples(audio) -> dict: return {'%s.ogg' % audio.name: audio.m_AudioData} elif magic == b'RIFF': return {'%s.wav' % audio.name: audio.m_AudioData} - return _extract_fsb(audio) + return dump_samples(audio) -def _extract_fsb(audio) -> dict: - ret = {} - af = FSB5(audio.m_AudioData) - for i, sample in enumerate(af.samples): +def dump_samples(clip): + if _dll is None: + load_fmod_library() + if not _dll: + return {} + # init system + # system = pyfmodex.System() + sys_ptr = c_void_p() + ckresult(_dll.FMOD_System_Create(byref(sys_ptr))) + # system.init(1, INIT_FLAGS.NORMAL, None) + ckresult(_dll.FMOD_System_Init(sys_ptr, clip.m_Channels, None, None)) + + # get sound + exinfo = byref(CREATESOUNDEXINFO(length=clip.m_Size)) + # sound = system.create_sound(bytes(clip.m_AudioData),mode=MODE.OPENMEMORY,exinfo=exinfo) + snd_ptr = c_void_p() + ckresult(_dll.FMOD_System_CreateSound(sys_ptr, bytes( + clip.m_AudioData), 0x00000800, exinfo, byref(snd_ptr))) + sound = Sound(snd_ptr) + # iterate over subsounds + samples = {} + for i in range(sound.num_subsounds): if i > 0: - filename = "%s-%i.%s" % (audio.name, i, af.get_sample_extension()) + filename = "%s-%i.wav" % (clip.name, i) else: - filename = "%s.%s" % (audio.name, af.get_sample_extension()) - try: - sample = af.rebuild_sample(sample) - except ValueError as e: - print("WARNING: Could not extract %r (%s)" % (audio, e)) - continue - ret[filename] = sample - - return ret + filename = "%s.wav" % clip.name + subsound = sound.get_subsound(i) + samples[filename] = subsound_to_wav(subsound) + subsound.release() + + sound.release() + # system.release() + ckresult(_dll.FMOD_System_Release(sys_ptr)) + return samples + + +def subsound_to_wav(subsound): + # get sound settings + length = subsound.get_length(0x00000004) # TIMEUNIT.PCMBYTES + channels = subsound.format.channels + bits = subsound.format.bits + sample_rate = int(subsound.default_frequency) + + # write to buffer + w = EndianBinaryWriter(endian="<") + # riff chucnk + w.write(b"RIFF") + w.write_int(length + 36) # sizeof(FmtChunk) + sizeof(RiffChunk) + length + w.write(b"WAVE") + # fmt chunck + w.write(b"fmt ") + w.write_int(16) # sizeof(FmtChunk) - sizeof(RiffChunk) + w.write_short(1) + w.write_short(channels) + w.write_int(sample_rate) + w.write_int(sample_rate * channels * bits // 8) + w.write_short(channels * bits // 8) + w.write_short(bits) + # data chunck + w.write(b"data") + w.write_int(length) + # data + lock = subsound.lock(0, length) + for ptr, length in lock: + ptr_data = string_at(ptr, length.value) + w.write(ptr_data) + subsound.unlock(*lock) + return w.bytes + + +# following code is copied from +# https://github.com/tyrylu/pyfmodex +# pyfmodex can't be used by itself +# because it would require adding the libs to the path first + +class CREATESOUNDEXINFO(Structure): + _fields_ = [("cbsize", c_int), ("length", c_uint)] + + def __init__(self, *args, **kwargs): + Structure.__init__(self, *args, **kwargs) + self.cbsize = sizeof(self) + + +class so: + def __init__(self, **kwargs): + self.__dict__.update(**kwargs) + + +class Sound(object): + def __init__(self, ptr): + """Constructor. + :param ptr: The pointer representing this object. + """ + self._ptr = ptr + + def _call_fmod(self, funcname, *args): + result = getattr(_dll, funcname)(self._ptr, *args) + ckresult(result) + + @property + def num_subsounds(self): + num = c_int() + self._call_fmod("FMOD_Sound_GetNumSubSounds", byref(num)) + return num.value + + def get_subsound(self, index): + sh_ptr = c_void_p() + self._call_fmod("FMOD_Sound_GetSubSound", index, byref(sh_ptr)) + return Sound(sh_ptr) + + def get_length(self, ltype): + len = c_uint() + self._call_fmod("FMOD_Sound_GetLength", byref(len), int(ltype)) + return len.value + + @property + def format(self): + type = c_int() + format = c_int() + channels = c_int() + bits = c_int() + self._call_fmod("FMOD_Sound_GetFormat", byref(type), + byref(format), byref(channels), byref(bits)) + return so(type=type.value, format=format.value, channels=channels.value, bits=bits.value) + + @property + def default_frequency(self): + freq = c_float() + pri = c_int() + self._call_fmod("FMOD_Sound_GetDefaults", byref(freq), byref(pri)) + return freq.value + + def lock(self, offset, length): + ptr1 = c_void_p() + len1 = c_uint() + ptr2 = c_void_p() + len2 = c_uint() + ckresult(_dll.FMOD_Sound_Lock(self._ptr, offset, length, + byref(ptr1), byref(ptr2), byref(len1), byref(len2))) + return (ptr1, len1), (ptr2, len2) + + def release(self): + self._call_fmod("FMOD_Sound_Release") + + def unlock(self, i1, i2): + """I1 and I2 are tuples of form (ptr, len).""" + ckresult(_dll.FMOD_Sound_Unlock(self._ptr, i1[0], i2[0], i1[1], i2[1])) + + +def ckresult(result): + result = RESULT(result) + if result is not RESULT.OK: + raise FmodError(result) + + +class FmodError(Exception): + def __init__(self, result): + self.result = result + self.message = result.name.replace("_", " ") + + def __str__(self): + return self.message + + +class RESULT(Enum): + OK = 0 + BADCOMMAND = 1 + CHANNEL_ALLOC = 2 + CHANNEL_STOLEN = 3 + DMA = 4 + DSP_CONNECTION = 5 + DSP_DONTPROCESS = 6 + DSP_FORMAT = 7 + DSP_INUSE = 8 + DSP_NOTFOUND = 9 + DSP_RESERVED = 10 + DSP_SILENCE = 11 + DSP_TYPE = 12 + FILE_BAD = 13 + FILE_COULDNOTSEEK = 14 + FILE_DISKEJECTED = 15 + FILE_EOF = 16 + FILE_ENDOFDATA = 17 + FILE_NOTFOUND = 18 + FORMAT = 19 + HEADER_MISMATCH = 20 + HTTP = 21 + HTTP_ACCESS = 22 + HTTP_PROXY_AUTH = 23 + HTTP_SERVER_ERROR = 24 + HTTP_TIMEOUT = 25 + INITIALIZATION = 26 + INITIALIZED = 27 + INTERNAL = 28 + INVALID_FLOAT = 29 + INVALID_HANDLE = 30 + INVALID_PARAM = 31 + INVALID_POSITION = 32 + INVALID_SPEAKER = 33 + INVALID_SYNCPOINT = 34 + INVALID_THREAD = 35 + INVALID_VECTOR = 36 + MAXAUDIBLE = 37 + MEMORY = 38 + MEMORY_CANTPOINT = 39 + NEEDS3D = 40 + NEEDSHARDWARE = 41 + NET_CONNECT = 42 + NET_SOCKET_ERROR = 43 + NET_URL = 44 + NET_WOULD_BLOCK = 45 + NOTREADY = 46 + OUTPUT_ALLOCATED = 47 + OUTPUT_CREATEBUFFER = 48 + OUTPUT_DRIVERCALL = 49 + OUTPUT_FORMAT = 50 + OUTPUT_INIT = 51 + OUTPUT_NODRIVERS = 52 + PLUGIN = 53 + PLUGIN_MISSING = 54 + PLUGIN_RESOURCE = 55 + PLUGIN_VERSION = 56 + RECORD = 57 + REVERB_CHANNELGROUP = 58 + REVERB_INSTANCE = 59 + SUBSOUNDS = 60 + SUBSOUND_ALLOCATED = 61 + SUBSOUND_CANTMOVE = 62 + TAGNOTFOUND = 63 + TOOMANYCHANNELS = 64 + TRUNCATED = 65 + UNIMPLEMENTED = 66 + UNINITIALIZED = 67 + UNSUPPORTED = 68 + VERSION = 69 + EVENT_ALREADY_LOADED = 70 + EVENT_LIVEUPDATE_BUSY = 71 + EVENT_LIVEUPDATE_MISMATCH = 72 + EVENT_LIVEUPDATE_TIMEOUT = 73 + EVENT_NOTFOUND = 74 + STUDIO_UNINITIALIZED = 75 + STUDIO_NOT_LOADED = 76 + INVALID_STRING = 77 + ALREADY_LOCKED = 78 + NOT_LOCKED = 79 + RECORD_DISCONNECTED = 80 + TOOMANYSAMPLES = 81 diff --git a/UnityPy/export/MeshExporter.py b/UnityPy/export/MeshExporter.py new file mode 100644 index 00000000..0e0d81e3 --- /dev/null +++ b/UnityPy/export/MeshExporter.py @@ -0,0 +1,59 @@ +def export_mesh(m_Mesh): + if m_Mesh.m_VertexCount <= 0: + return False + + sb = [f"g {m_Mesh.name}\r\n"] + + # region Vertices + if not m_Mesh.m_Vertices: + return False + + c = 3 + if len(m_Mesh.m_Vertices) == m_Mesh.m_VertexCount * 4: + c = 4 + + for v in range(int(m_Mesh.m_VertexCount)): + sb.append("v {0} {1} {2}\r\n".format( + -m_Mesh.m_Vertices[v * c], m_Mesh.m_Vertices[v * c + 1], m_Mesh.m_Vertices[v * c + 2])) + # endregion + + # region UV + if m_Mesh.m_UV0: + if len(m_Mesh.m_UV0) == m_Mesh.m_VertexCount * 2: + c = 2 + elif len(m_Mesh.m_UV0) == m_Mesh.m_VertexCount * 3: + c = 3 + + for v in range(int(m_Mesh.m_VertexCount)): + sb.append("vt {0} {1}\r\n".format( + m_Mesh.m_UV0[v * c], m_Mesh.m_UV0[v * c + 1])) + # endregion + + # region Normals + if m_Mesh.m_Normals: + if len(m_Mesh.m_Normals) == m_Mesh.m_VertexCount * 3: + c = 3 + elif len(m_Mesh.m_Normals) == m_Mesh.m_VertexCount * 4: + c = 4 + + for v in range(int(m_Mesh.m_VertexCount)): + sb.append("vn {0} {1} {2}\r\n".format( + -m_Mesh.m_Normals[v * c], m_Mesh.m_Normals[v * c + 1], m_Mesh.m_Normals[v * c + 2])) + # endregion + + # region Face + sum = 0 + for i in range(len(m_Mesh.m_SubMeshes)): + sb.append(f"g {m_Mesh.name}_{i}\r\n") + indexCount = m_Mesh.m_SubMeshes[i].indexCount + end = sum + indexCount // 3 + for f in range(sum, end): + sb.append("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\r\n".format( + m_Mesh.m_Indices[f * 3 + 2] + 1, + m_Mesh.m_Indices[f * 3 + 1] + 1, + m_Mesh.m_Indices[f * 3] + 1) + ) + sum = end + # endregion + sb = "".join(sb).replace("NaN", "0").replace("e", "E") + return sb diff --git a/UnityPy/export/ShaderConverter.py b/UnityPy/export/ShaderConverter.py new file mode 100644 index 00000000..76ab0917 --- /dev/null +++ b/UnityPy/export/ShaderConverter.py @@ -0,0 +1,584 @@ +import traceback +from ..streams import EndianBinaryReader +from ..helpers import CompressionHelper +import re +from itertools import groupby +from ..enums import ShaderCompilerPlatform, ShaderGpuProgramType, SerializedPropertyType +from ..enums import TextureDimension, PassType + +HEADER = ''' +////////////////////////////////////////// +// +// NOTE: This is *not* a valid shader file +// +/////////////////////////////////////////// +'''[1:] + + +def export_shader(m_Shader): + if hasattr(m_Shader, "m_SubProgramBlob"): # 5.3 - 5.4 + decompressedBytes = CompressionHelper.decompress_lz4( + m_Shader.m_SubProgramBlob, m_Shader.decompressedSize) + + blobReader = EndianBinaryReader(decompressedBytes) + program = ShaderProgram(blobReader, m_Shader.version) + + return HEADER + program.Export(bytes(m_Shader.m_Script).decode("utf8")) + + if hasattr(m_Shader, "compressedBlob"): # 5.5 and up + return HEADER + ConvertSerializedShader(m_Shader) + + return HEADER + bytes(m_Shader.m_Script).decode("utf8") + +def ConvertSerializedShader(m_Shader): + shaderPrograms = [] + + platformNumber = len(m_Shader.platforms) + for i in range(platformNumber): + compressedSize = m_Shader.compressedLengths[i] + decompressedSize = m_Shader.decompressedLengths[i] + + compressedBytes = m_Shader.compressedBlob[int(m_Shader.offsets[i]):int(m_Shader.offsets[i]) + compressedSize] + decompressedBytes = CompressionHelper.decompress_lz4(compressedBytes, decompressedSize) + + shaderPrograms.append(ShaderProgram(EndianBinaryReader(decompressedBytes, endian="<"), m_Shader.version)) + + return ConvertSerializedShaderParsedForm(m_Shader.m_ParsedForm, m_Shader.platforms, shaderPrograms) + +def ConvertSerializedShaderParsedForm(m_ParsedForm, platforms, shaderPrograms): + sb = [] + + sb.append("Shader \"{0}\" {{\n".format(m_ParsedForm.m_Name)) + + sb.append(ConvertSerializedProperties(m_ParsedForm.m_PropInfo)) + + for m_SubShader in m_ParsedForm.m_SubShaders: + sb.append(ConvertSerializedSubShader(m_SubShader, platforms, shaderPrograms)) + + if m_ParsedForm.m_FallbackName: + sb.append("Fall back \"{0}\"\n".format(m_ParsedForm.m_FallbackName)) + + if m_ParsedForm.m_CustomEditorName: + sb.append("CustomEditor \"{0}\"\n".format(m_ParsedForm.m_CustomEditorName)) + + sb.append("}") + return "".join(sb) + +def ConvertSerializedSubShader(m_SubShader, platforms, shaderPrograms): + sb = [] + + sb.append("SubShader {\n") + if m_SubShader.m_LOD != 0: + sb.append(" LOD {0}\n".format(m_SubShader.m_LOD)) + + sb.append(ConvertSerializedTagMap(m_SubShader.m_Tags, 1)) + + for m_Passe in m_SubShader.m_Passes: + sb.append(ConvertSerializedPass(m_Passe, platforms, shaderPrograms)) + + sb.append("}\n") + return "".join(sb) + +def ConvertSerializedPass(m_Passe, platforms, shaderPrograms): + sb = [] + if m_Passe.m_Type == PassType.kPassTypeNormal: + sb.append(" Pass ") + elif m_Passe.m_Type == PassType.kPassTypeUse: + sb.append(" UsePass ") + elif m_Passe.m_Type == PassType.kPassTypeGrab: + sb.append(" GrabPass ") + + if m_Passe.m_Type == PassType.kPassTypeUse: + sb.append("\"{0}\"\n".format(m_Passe.m_UseName)) + else: + sb.append("{\n") + + if m_Passe.m_Type == PassType.kPassTypeGrab: + if m_Passe.m_TextureName: + sb.append(" \"{0}\"\n".format(m_Passe.m_TextureName)) + + else: + sb.append(ConvertSerializedShaderState(m_Passe.m_State)) + + if len(m_Passe.progVertex.m_SubPrograms) > 0: + sb.append("Program \"vp\" {\n") + sb.append(ConvertSerializedSubPrograms(m_Passe.progVertex.m_SubPrograms, platforms, shaderPrograms)) + sb.append("}\n") + + if len(m_Passe.progFragment.m_SubPrograms) > 0: + sb.append("Program \"fp\" {\n") + sb.append(ConvertSerializedSubPrograms(m_Passe.progFragment.m_SubPrograms, platforms, shaderPrograms)) + sb.append("}\n") + + if len(m_Passe.progGeometry.m_SubPrograms) > 0: + sb.append("Program \"gp\" {\n") + sb.append(ConvertSerializedSubPrograms(m_Passe.progGeometry.m_SubPrograms, platforms, shaderPrograms)) + sb.append("}\n") + + if len(m_Passe.progHull.m_SubPrograms) > 0: + sb.append("Program \"hp\" {\n") + sb.append(ConvertSerializedSubPrograms(m_Passe.progHull.m_SubPrograms, platforms, shaderPrograms)) + sb.append("}\n") + + if len(m_Passe.progDomain.m_SubPrograms) > 0: + sb.append("Program \"dp\" {\n") + sb.append(ConvertSerializedSubPrograms(m_Passe.progDomain.m_SubPrograms, platforms, shaderPrograms)) + sb.append("}\n") + + sb.append("}\n") + + return "".join(sb) + +def ConvertSerializedSubPrograms(m_SubPrograms, platforms, shaderPrograms): + sb = [] + + indexFunc = lambda x: x.m_BlobIndex + typeFunc = lambda x: x.m_GpuProgramType + + groups = groupby(sorted(m_SubPrograms, key=indexFunc), indexFunc) + + for _, _group in groups: + group = list(_group) + programs = groupby(sorted(group, key=typeFunc), typeFunc) + + for programKey, _programList in programs: + subPrograms = list(_programList) + isTier = len(subPrograms) > 1 + for i in range(len(platforms)): + platform = platforms[i] + + if CheckGpuProgramUsable(platform, programKey): + for subProgram in subPrograms: + sb.append("SubProgram \"{0} ".format(GetPlatformString(platform))) + + if isTier: + sb.append("hw_tier{0:02} ".format(subProgram.m_ShaderHardwareTier)) + + sb.append("\" {\n") + sb.append(shaderPrograms[i].m_SubPrograms[subProgram.m_BlobIndex].Export()) + + sb.append("\n}\n") + + break + + return "".join(sb) + + +def ConvertSerializedShaderState(m_State): + sb = [] + if m_State.m_Name: + sb.append(" Name \"{0}\"\n".format(m_State.m_Name)) + + if m_State.m_LOD != 0: + sb.append(" LOD {0}\n".format(m_State.m_LOD)) + + sb.append(ConvertSerializedTagMap(m_State.m_Tags, 2)) + + sb.append(ConvertSerializedShaderRTBlendState(m_State.rtBlend)) + + if m_State.alphaToMask.val > 0: + sb.append(" AlphaToMask On\n") + + if m_State.zClip and m_State.zClip.val != 1: + sb.append(" ZClip Off\n") + + if m_State.zTest.val != 4: + sb.append(" ZTest ") + if m_State.zTest.val == 0: # kFuncDisabled + sb.append("Off") + elif m_State.zTest.val == 1: # kFuncNever + sb.append("Never") + elif m_State.zTest.val == 2: # kFuncLess + sb.append("Less") + elif m_State.zTest.val == 3: # kFuncEqual + sb.append("Equal") + elif m_State.zTest.val == 5: # kFuncGreater + sb.append("Greater") + elif m_State.zTest.val == 6: # kFuncNotEqual + sb.append("NotEqual") + elif m_State.zTest.val == 7: # kFuncGEqual + sb.append("GEqual") + elif m_State.zTest.val == 8: # kFuncAlways + sb.append("Always") + + sb.append("\n") + + if m_State.zWrite.val != 1: # ZWrite On + sb.append(" ZWrite Off\n") + + if m_State.culling.val != 2: # Cull Back + sb.append(" Cull ") + if m_State.culling.val == 0: # kCullOff + sb.append("Off") + elif m_State.culling.val == 1: # kCullFront + sb.append("Front") + + sb.append("\n") + + if m_State.offsetFactor.val != 0 or m_State.offsetUnits.val != 0: + sb.append(" Offset {0}, {1}\n".format(m_State.offsetFactor.val, m_State.offsetUnits.val)) + + # TODO Stencil + + # TODO Fog + + if m_State.lighting: + sb.append(" Lighting {0}\n".format(m_State.lighting and "On" or "Off")) + + sb.append(" GpuProgramID {0}\n".format(m_State.gpuProgramID)) + return "".join(sb) + + +def ConvertSerializedShaderRTBlendState(rbBlend): + # TODO Blend + sb = [] + return "".join(sb) + +def ConvertSerializedTagMap(m_Tags, intent: int): + sb = [] + if len(m_Tags.tags) > 0: + sb.append(" "*intent) + sb.append("Tags { ") + for key, value in m_Tags.tags.items(): + sb.append("\"{0}\" = \"{1}\" ".format(key, value)) + sb.append("}\n") + + return "".join(sb) + +def ConvertSerializedProperties(m_PropInfo): + sb = [] + + sb.append("Properties {\n") + for m_Prop in m_PropInfo.m_Props: + sb.append(ConvertSerializedProperty(m_Prop)) + + sb.append("}\n") + return "".join(sb) + +def ConvertSerializedProperty(m_Prop): + sb = [] + for m_Attribute in m_Prop.m_Attributes: + sb.append("[{0}] ".format(m_Attribute)) + + sb.append("{0} (\"{1}\", ".format(m_Prop.m_Name, m_Prop.m_Description)) + + if m_Prop.m_Type == SerializedPropertyType.kColor: + sb.append("Color") + elif m_Prop.m_Type == SerializedPropertyType.kVector: + sb.append("Vector") + elif m_Prop.m_Type == SerializedPropertyType.kFloat: + sb.append("Float") + elif m_Prop.m_Type == SerializedPropertyType.kRange: + sb.append("Range({0:g}, {1:g})".format(m_Prop.m_DefValue[1], m_Prop.m_DefValue[2])) + elif m_Prop.m_Type == SerializedPropertyType.kTexture: + if m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDimAny: + sb.append("any") + elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDim2D: + sb.append("2D") + elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDim3D: + sb.append("3D") + elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDimCUBE: + sb.append("Cube") + elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDim2DArray: + sb.append("2DArray") + elif m_Prop.m_DefTexture.m_TexDim == TextureDimension.kTexDimCubeArray: + sb.append("CubeArray") + + sb.append(") = ") + + if m_Prop.m_Type in [ + SerializedPropertyType.kColor, + SerializedPropertyType.kVector + ]: + sb.append("({0:g},{1:g},{2:g},{3:g})".format(m_Prop.m_DefValue[0], m_Prop.m_DefValue[1], m_Prop.m_DefValue[2],m_Prop.m_DefValue[3])) + elif m_Prop.m_Type in [ + SerializedPropertyType.kFloat, + SerializedPropertyType.kRange + ]: + sb.append(m_Prop.m_DefValue[0]) + elif m_Prop.m_Type == SerializedPropertyType.kTexture: + sb.append("\"{0}\" {{ }}".format(m_Prop.m_DefTexture.m_DefaultName)) + else: + raise ValueError(m_Prop.m_Type) + + sb.append("\n") + return "".join(map(str,sb)) + +def CheckGpuProgramUsable(platform, programType): + if platform == ShaderCompilerPlatform.kShaderCompPlatformGL: + return programType == ShaderGpuProgramType.kShaderGpuProgramGLLegacy + elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D9: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM20 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM30 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM20 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM30 ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformXbox360: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM40 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM50 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM40 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM50 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM40 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM50 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX11HullSM50 + or programType == ShaderGpuProgramType.kShaderGpuProgramDX11DomainSM50 ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES20: + return programType == ShaderGpuProgramType.kShaderGpuProgramGLES + elif platform == ShaderCompilerPlatform.kShaderCompPlatformNaCl: + raise NotImplementedError + elif platform == ShaderCompilerPlatform.kShaderCompPlatformFlash: + raise NotImplementedError + elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11_9x: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramDX10Level9Vertex + or programType == ShaderGpuProgramType.kShaderGpuProgramDX10Level9Pixel ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES3Plus: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramGLES31AEP + or programType == ShaderGpuProgramType.kShaderGpuProgramGLES31 + or programType == ShaderGpuProgramType.kShaderGpuProgramGLES3 ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSP2: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformPS4: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOne: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSM: + raise NotImplementedError + elif platform == ShaderCompilerPlatform.kShaderCompPlatformMetal: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramMetalVS + or programType == ShaderGpuProgramType.kShaderGpuProgramMetalVS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformOpenGLCore: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramGLCore32 + or programType == ShaderGpuProgramType.kShaderGpuProgramGLCore41 + or programType == ShaderGpuProgramType.kShaderGpuProgramGLCore43 ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformN3DS: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformWiiU: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformVulkan: + return programType == ShaderGpuProgramType.kShaderGpuProgramSPIRV + elif platform == ShaderCompilerPlatform.kShaderCompPlatformSwitch: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOneD3D12: + return ( programType == ShaderGpuProgramType.kShaderGpuProgramConsoleVS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleFS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleHS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleDS + or programType == ShaderGpuProgramType.kShaderGpuProgramConsoleGS ) + else: + raise NotImplementedError + + +def GetPlatformString(platform): + if platform == ShaderCompilerPlatform.kShaderCompPlatformGL: + return "openGL" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D9: + return "d3d9" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformXbox360: + return "xbox360" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformPS3: + return "ps3" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11: + return "d3d11" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES20: + return "gles" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformNaCl: + return "glesdesktop" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformFlash: + return "flash" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformD3D11_9x: + return "d3d11_9x" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformGLES3Plus: + return "gles3" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSP2: + return "psp2" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformPS4: + return "ps4" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOne: + return "xboxone" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformPSM: + return "psm" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformMetal: + return "metal" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformOpenGLCore: + return "glcore" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformN3DS: + return "n3ds" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformWiiU: + return "wiiu" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformVulkan: + return "vulkan" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformSwitch: + return "switch" + elif platform == ShaderCompilerPlatform.kShaderCompPlatformXboxOneD3D12: + return "xboxone+_d3d12" + else: + return "unknown" + + +class ShaderProgram: + def __init__(self, reader: EndianBinaryReader, version): + subProgramCapacity = reader.read_int() + self.m_SubPrograms = [] + + if version >=(2019, 3): # 2019.3 and up + entrySize = 12 + else: + entrySize = 8 + + for i in range(subProgramCapacity): + reader.Position = 4 + i * entrySize + offset = reader.read_int() + reader.Position = offset + self.m_SubPrograms.append(ShaderSubProgram(reader)) + + def Export(self, shader): + shader = re.sub(r"GpuProgramIndex (.+)", + lambda math: self.m_SubPrograms[int(math.group(1))].Export(), + shader) + + return shader + + +class ShaderSubProgram: + def __init__(self, reader: EndianBinaryReader): + #LoadGpuProgramFromData + #201509030 - Unity 5.3 + #201510240 - Unity 5.4 + #201608170 - Unity 5.5 + #201609010 - Unity 5.6, 2017.1 & 2017.2 + #201708220 - Unity 2017.3, Unity 2017.4 & Unity 2018.1 + #201802150 - Unity 2018.2 & Unity 2018.3 + #201806140 - Unity 2019.1~2020.1 + + self.m_Version = reader.read_int() + self.m_ProgramType = ShaderGpuProgramType(reader.read_int()) + reader.Position += 12 + + if self.m_Version >= 201608170: + reader.Position += 4 + + m_KeywordSize = reader.read_int() + self.m_Keywords = [] + + for i in range(m_KeywordSize): + self.m_Keywords.append(reader.read_aligned_string()) + + if self.m_Version >= 201806140: + m_LocalKeywordsSize = reader.read_int() + self.m_LocalKeywords = [] + + for i in range(m_LocalKeywordsSize): + self.m_LocalKeywords.append(reader.read_aligned_string()) + + self.m_ProgramCode = reader.read_byte_array() + reader.align_stream() + + def Export(self): + sb = [] + + if len(self.m_Keywords) > 0: + sb.append("Keywords { ") + for keyword in self.m_Keywords: + sb.append("\"{0}\" ".format(keyword)) + + sb.append("}\n") + + if hasattr(self, 'm_LocalKeywords') and len(self.m_LocalKeywords) > 0: + sb.append("Local Keywords { ") + for keyword in self.m_LocalKeywords: + sb.append("\"{0}\" ".format(keyword)) + + sb.append("}\n") + + sb.append("\"") + + if len(self.m_ProgramCode) > 0: + if self.m_ProgramType in [ + ShaderGpuProgramType.kShaderGpuProgramGLLegacy, + ShaderGpuProgramType.kShaderGpuProgramGLES31AEP, + ShaderGpuProgramType.kShaderGpuProgramGLES31, + ShaderGpuProgramType.kShaderGpuProgramGLES3, + ShaderGpuProgramType.kShaderGpuProgramGLES, + ShaderGpuProgramType.kShaderGpuProgramGLCore32, + ShaderGpuProgramType.kShaderGpuProgramGLCore41, + ShaderGpuProgramType.kShaderGpuProgramGLCore43 + ]: + sb.append(bytes(self.m_ProgramCode).decode("utf8")) + elif self.m_ProgramType in [ + ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM20, + ShaderGpuProgramType.kShaderGpuProgramDX9VertexSM30, + ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM20, + ShaderGpuProgramType.kShaderGpuProgramDX9PixelSM30 + ]: + sb.append("// shader disassembly not supported on DXBC") + elif self.m_ProgramType in [ + ShaderGpuProgramType.kShaderGpuProgramDX10Level9Vertex, + ShaderGpuProgramType.kShaderGpuProgramDX10Level9Pixel, + ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM40, + ShaderGpuProgramType.kShaderGpuProgramDX11VertexSM50, + ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM40, + ShaderGpuProgramType.kShaderGpuProgramDX11PixelSM50, + ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM40, + ShaderGpuProgramType.kShaderGpuProgramDX11GeometrySM50, + ShaderGpuProgramType.kShaderGpuProgramDX11HullSM50, + ShaderGpuProgramType.kShaderGpuProgramDX11DomainSM50 + ]: + sb.append("// shader disassembly not supported on DXBC") + elif self.m_ProgramType in [ + ShaderGpuProgramType.kShaderGpuProgramMetalVS, + ShaderGpuProgramType.kShaderGpuProgramMetalFS + ]: + reader = EndianBinaryReader(self.m_ProgramCode) + fourCC = reader.read_u_int() + if fourCC == 0xf00dcafe: + offset = reader.read_int() + reader.Position = offset + + entryName = reader.read_string_to_null() + buff = reader.read_bytes(int(reader.Length - reader.Position)) + sb.append(bytes(buff).decode("utf8")) + elif self.m_ProgramType == ShaderGpuProgramType.kShaderGpuProgramSPIRV: + # TODO SpirVShaderConverter + pass + elif self.m_ProgramType in [ + ShaderGpuProgramType.kShaderGpuProgramConsoleVS, + ShaderGpuProgramType.kShaderGpuProgramConsoleFS, + ShaderGpuProgramType.kShaderGpuProgramConsoleHS, + ShaderGpuProgramType.kShaderGpuProgramConsoleDS, + ShaderGpuProgramType.kShaderGpuProgramConsoleGS + ]: + sb.append(bytes(self.m_ProgramCode).decode("utf8")) + else: + sb.append("//shader disassembly not supported on {0}".format(self.m_ProgramType)) + + sb.append('"') + return "".join(sb) diff --git a/UnityPy/export/SpriteHelper.py b/UnityPy/export/SpriteHelper.py index 6fcd1069..51dbd673 100644 --- a/UnityPy/export/SpriteHelper.py +++ b/UnityPy/export/SpriteHelper.py @@ -52,14 +52,13 @@ def get_image_from_sprite(m_Sprite) -> Image: atlas = None if atlas: - sprite_atlas_data = atlas.render_data_map[m_Sprite.m_RenderDataKey] + sprite_atlas_data = atlas.m_RenderDataMap[m_Sprite.m_RenderDataKey] else: sprite_atlas_data = m_Sprite.m_RD m_Texture2D = sprite_atlas_data.texture alpha_texture = sprite_atlas_data.alphaTexture texture_rect = sprite_atlas_data.textureRect - texture_rect_offset = sprite_atlas_data.textureRectOffset settings_raw = sprite_atlas_data.settingsRaw original_image = get_image(m_Sprite, m_Texture2D, alpha_texture) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index bed3ac6f..49558f61 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -1,27 +1,28 @@ -from sys import platform - -import texture2ddecoder +import texture2ddecoder from PIL import Image from copy import copy -from io import BytesIO, BufferedIOBase, RawIOBase, IOBase +from io import BytesIO import struct from ..enums import TextureFormat, BuildTarget TF = TextureFormat -PREFER_BASISU = False - -def prepare_raw_image(img, flip=True): - _image = None - if (isinstance(img, str) or isinstance(img, BufferedIOBase) or - isinstance(img, RawIOBase) or isinstance(img, IOBase)): - _image = Image.open(img) - elif isinstance(img, Image.Image): - _image = img - if _image: - if flip: - _image = _image.transpose(Image.FLIP_TOP_BOTTOM) - return _image.getdata() - return None + + +def image_to_texture2d(img: Image, flip: bool = True): + # tex for eventually later usage of compressions + + tex_format = None + if flip: + img = img.transpose(Image.FLIP_TOP_BOTTOM) + + if img.mode == "RGBA": + tex_format = TextureFormat.RGBA32 + elif img.mode == "RGB": + tex_format = TextureFormat.RGB24 + elif img.mode == "A": + tex_format = TextureFormat.Alpha8 + + return img.tobytes(), tex_format def get_image_from_texture2d(texture_2d, flip=True) -> Image: @@ -37,8 +38,9 @@ def get_image_from_texture2d(texture_2d, flip=True) -> Image: image_data = copy(bytes(texture_2d.image_data)) if not image_data: return Image.new("RGB", (0, 0)) - - texture_format = texture_2d.m_TextureFormat if isinstance(texture_2d, TextureFormat) else TextureFormat(texture_2d.m_TextureFormat) + + texture_format = texture_2d.m_TextureFormat if isinstance(texture_2d, TextureFormat) else TextureFormat( + texture_2d.m_TextureFormat) selection = CONV_TABLE[texture_format] if len(selection) == 0: @@ -51,9 +53,9 @@ def get_image_from_texture2d(texture_2d, flip=True) -> Image: if "Crunched" in texture_format.name: version = texture_2d.version - if (version[0] > 2017 or (version[0] == 2017 and version[1] >= 3) #2017.3 and up - or texture_format == TextureFormat.ETC_RGB4Crunched - or texture_format == TextureFormat.ETC2_RGBA8Crunched): + if (version[0] > 2017 or (version[0] == 2017 and version[1] >= 3) # 2017.3 and up + or texture_format == TextureFormat.ETC_RGB4Crunched + or texture_format == TextureFormat.ETC2_RGBA8Crunched): image_data = texture2ddecoder.unpack_unity_crunch(image_data) else: image_data = texture2ddecoder.unpack_crunch(image_data) @@ -79,15 +81,15 @@ def swap_bytes_for_xbox(image_data: bytes, build_target: BuildTarget) -> bytes: :rtype: bytes """ if ( - build_target == BuildTarget.XBOX360 + build_target == BuildTarget.XBOX360 ): # swap bytes for Xbox confirmed,PS3 not encountered for i in range(0, len(image_data), 2): - image_data[i : i + 2] = image_data[i : i + 2][::-1] + image_data[i: i + 2] = image_data[i: i + 2][::-1] return image_data def pillow( - image_data: bytes, width: int, height: int, mode: str, codec: str, args, swap=False + image_data: bytes, width: int, height: int, mode: str, codec: str, args, swap: tuple = None ) -> Image: img = ( Image.frombytes(mode, (width, height), image_data, codec, args) @@ -133,6 +135,7 @@ def etc(image_data: bytes, width: int, height: int, fmt: list): raise NotImplementedError("unknown etc mode") return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") + def eac(image_data: bytes, width: int, height: int, fmt: list): if fmt == "EAC_R": image_data = texture2ddecoder.decode_eacr(image_data, width, height) @@ -144,24 +147,18 @@ def eac(image_data: bytes, width: int, height: int, fmt: list): image_data = texture2ddecoder.decode_eacrg_signed(image_data, width, height) return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") + def half( - image_data: bytes, width: int, height: int, mode: str, codec: str, args, swap=False + image_data: bytes, width: int, height: int, mode: str, codec: str, args, swap: tuple = None ) -> Image: # convert half-float to int8 stream = BytesIO(image_data) image_data = bytes( - int(struct.unpackb("e", stream.read(2))*256) - for i in range(width*height*len(codec)) + int(struct.unpack("e", stream.read(2))[0] * 256) + for _ in range(width * height * len(codec)) ) - img = ( - Image.frombytes(mode, (width, height), image_data, codec, args) - if width - else Image.new(mode, (width, height)) - ) - if swap: - channels = img.split() - img = Image.merge(mode, [channels[x] for x in swap]) - return img + return pillow(image_data, width, height, mode, codec, args, swap) + CONV_TABLE = { # FORMAT FUNC #ARGS..... @@ -193,10 +190,10 @@ def half( ( TF.BC7, pillow, "RGBA", "bcn", 7 ), ( TF.DXT1Crunched, pillow, "RGBA", "bcn", 1 ), ( TF.DXT5Crunched, pillow, "RGBA", "bcn", 3 ), -( TF.PVRTC_RGB2, pvrtc, False ), +( TF.PVRTC_RGB2, pvrtc, True ), ( TF.PVRTC_RGBA2, pvrtc, True ), ( TF.PVRTC_RGB4, pvrtc, False ), -( TF.PVRTC_RGBA4, pvrtc, True ), +( TF.PVRTC_RGBA4, pvrtc, False ), ( TF.ETC_RGB4, etc, (1,) ), ( TF.ATC_RGB4, atc, False ), ( TF.ATC_RGBA8, atc, True ), diff --git a/UnityPy/export/__init__.py b/UnityPy/export/__init__.py index 8d64f5db..0759e2fb 100644 --- a/UnityPy/export/__init__.py +++ b/UnityPy/export/__init__.py @@ -1 +1 @@ -from . import SpriteHelper, Texture2DConverter, AudioClipConverter +from . import SpriteHelper, Texture2DConverter, AudioClipConverter, MeshExporter diff --git a/UnityPy/files/BundleFile.py b/UnityPy/files/BundleFile.py index e00fe77c..e3aa932f 100644 --- a/UnityPy/files/BundleFile.py +++ b/UnityPy/files/BundleFile.py @@ -1,28 +1,26 @@ -from .File import File -from .SerializedFile import SerializedFile -from ..enums import FileType -from ..helpers import CompressionHelper, ImportHelper +from . import File +from ..helpers import CompressionHelper from ..streams import EndianBinaryReader, EndianBinaryWriter from collections import namedtuple -DirectoryInfo = namedtuple("DirectoryInfo", "path offset size") -DirectoryInfoFS = namedtuple("DirectoryInfoFS", "offset size flags path") BlockInfo = namedtuple("BlockInfo", "uncompressedSize compressedSize flags") +DirectoryInfoFS = namedtuple("DirectoryInfoFS", "offset size flags path") -class BundleFile(File): +class BundleFile(File.File): format: int is_changed: bool signature: str version_engine: str version_player: str - def __init__(self, reader: EndianBinaryReader, environment=None): - self.is_changed = False - self.environment = environment - self.files = {} + def __init__(self, reader: EndianBinaryReader, parent: File, name: str=None): + super().__init__(parent=parent, name=name) signature = self.signature = reader.read_string_to_null() + self.version = reader.read_u_int() + self.version_player = reader.read_string_to_null() + self.version_engine = reader.read_string_to_null() if signature == "UnityArchive": raise NotImplemented("BundleFile - UnityArchive") @@ -30,35 +28,14 @@ def __init__(self, reader: EndianBinaryReader, environment=None): m_DirectoryInfo, blocksReader = self.read_web_raw(reader) elif signature == "UnityFS": m_DirectoryInfo, blocksReader = self.read_fs(reader) + else: + raise NotImplemented(f"Unknown Bundle signature: {signature}") self.read_files(blocksReader, m_DirectoryInfo) - def read_files(self, blocksStream: EndianBinaryReader, m_DirectoryInfo): - for node in m_DirectoryInfo: - blocksStream.Position = node.offset - path = node.path - f = EndianBinaryReader(blocksStream.read(node.size), offset=(blocksStream.BaseOffset + node.offset)) - f._flag = getattr(node, "flags", None) # required for save - # check for serialized files - if path.endswith((".resS", ".resource", ".config", ".xml", ".dat")): - self.environment.resources[path] = f - else: - typ, _ = ImportHelper.check_file_type(f) - if typ == FileType.AssetsFile: - f.Position = 0 - f = SerializedFile(f, self.environment, self) - self.environment.assets[path] = f - else: - self.environment.resources[path] = f - - self.files[path] = f - def read_web_raw(self, reader: EndianBinaryReader): # def read_header_and_blocks_info(self, reader:EndianBinaryReader): - isCompressed = self.signature == "UnityWeb" - version = self.version = reader.read_u_int() - self.version_player = reader.read_string_to_null() - self.version_engine = reader.read_string_to_null() + version = self.version if version >= 4: _hash = reader.read_bytes(16) crc = reader.read_u_int() @@ -71,7 +48,6 @@ def read_web_raw(self, reader: EndianBinaryReader): compressedSize = reader.read_u_int() uncompressedSize = reader.read_u_int() - flags = True if version >= 2: completeFileSize = reader.read_u_int() @@ -81,45 +57,44 @@ def read_web_raw(self, reader: EndianBinaryReader): reader.Position = headerSize - uncompressedBytes = reader.read_bytes(compressedSize) - if flags: - uncompressedBytes = CompressionHelper.decompress_lzma(uncompressedBytes) + uncompressedBytes = CompressionHelper.decompress_lzma( + reader.read_bytes(compressedSize) + ) blocksReader = EndianBinaryReader(uncompressedBytes, offset=headerSize) nodesCount = blocksReader.read_int() m_DirectoryInfo = [ - DirectoryInfo( + File.DirectoryInfo( blocksReader.read_string_to_null(), # path blocksReader.read_u_int(), # offset blocksReader.read_u_int(), # size ) - for i in range(nodesCount) + for _ in range(nodesCount) ] return m_DirectoryInfo, blocksReader def read_fs(self, reader: EndianBinaryReader): - self.version = reader.read_u_int() - self.version_player = reader.read_string_to_null() - self.version_engine = reader.read_string_to_null() + version = self.version size = reader.read_long() # header compressedSize = reader.read_u_int() uncompressedSize = reader.read_u_int() - flags = reader.read_u_int() + self._data_flags = reader.read_u_int() + + if self.version >= 7: + reader.align_stream(16) _position = reader.Position - if flags & 0x80 != 0: # kArchiveBlocksInfoAtTheEnd + if self._data_flags & 0x80 != 0: # kArchiveBlocksInfoAtTheEnd reader.Position = reader.Length - compressedSize blocksInfoBytes = reader.read_bytes(compressedSize) reader.Position = _position else: # 0x40 kArchiveBlocksAndDirectoryInfoCombined - if self.version >= 7: - reader.allign_stream(16) blocksInfoBytes = reader.read_bytes(compressedSize) - switch = flags & 0x3F + switch = self._data_flags & 0x3F if switch == 1: # LZMA blocksInfoBytes = CompressionHelper.decompress_lzma(blocksInfoBytes) @@ -140,7 +115,7 @@ def read_fs(self, reader: EndianBinaryReader): blocksInfoReader.read_u_int(), # compressedSize blocksInfoReader.read_u_short(), # flags ) - for i in range(blocksInfoCount) + for _ in range(blocksInfoCount) ] nodesCount = blocksInfoReader.read_int() @@ -151,7 +126,7 @@ def read_fs(self, reader: EndianBinaryReader): blocksInfoReader.read_u_int(), # flags blocksInfoReader.read_string_to_null(), # path ) - for i in range(nodesCount) + for _ in range(nodesCount) ] # def read_blocks(reader : EndianBinaryReader, blocksStream): @@ -170,6 +145,9 @@ def decompress_block(blockInfo): else: # no compression return reader.read_bytes(blockInfo.uncompressedSize) + if m_BlocksInfo: + self._block_info_flags = m_BlocksInfo[0].flags + blocksReader = EndianBinaryReader( b"".join(decompress_block(blockInfo) for blockInfo in m_BlocksInfo), offset=(blocksInfoReader.real_offset()) @@ -177,12 +155,24 @@ def decompress_block(blockInfo): return m_DirectoryInfo, blocksReader - def save(self, packer="none"): + + def save(self, packer=None): + """ + Rewrites the BundleFile and returns it as bytes object. + + packer: + can be either one of the following strings + or tuple consisting of (block_info_flag, data_flag) + allowed strings: + none - no compression, default, safest bet + lz4 - lz4 compression + original - uses the original flags + """ # file_header - # signature (string_to_null) - # format (int) - # version_player (string_to_null) - # version_engine (string_to_null) + # signature (string_to_null) + # format (int) + # version_player (string_to_null) + # version_engine (string_to_null) writer = EndianBinaryWriter() writer.write_string_to_null(self.signature) @@ -197,35 +187,39 @@ def save(self, packer="none"): # self.save_web_raw(writer) elif self.signature == "UnityFS": if not packer or packer == "none": - self.save_fs(writer) + self.save_fs(writer, 64, 64) + elif packer == "original": + self.save_fs(writer, block_info_flag=self._block_info_flags, data_flag=self._data_flags) elif packer == "lz4": self.save_fs(writer, block_info_flag=194, data_flag=2) + elif isinstance(packer, tuple): + self.save_fs(writer, *packer) else: raise NotImplemented("UnityFS - Packer:", packer) return writer.bytes - def save_fs(self, writer: EndianBinaryWriter, padding=False, block_info_flag=64, data_flag=64): + def save_fs(self, writer: EndianBinaryWriter, block_info_flag : int, data_flag : int): # header # compressed blockinfo (block details & directionary) # compressed assets # 0b1000000 / 0b11000000 | 64 / 192 - uncompressed # 0b11000010 | 194 - lz4 - #block_info_flag + # block_info_flag # 0 / 0b1000000 | 0 / 64 - uncompressed # 0b1 | 1 - lzma # 0b10 | 2 - lz4 # 0b11 | 3 - lz4hc [not implemented] # 0b100 | 4 - lzham [not implemented] - #data_flag + # data_flag # header: - # bundle_size (long) - # compressed_size (int) - # uncompressed_size (int) - # flag (int) - # ?padding? (bool) + # bundle_size (long) + # compressed_size (int) + # uncompressed_size (int) + # flag (int) + # ?padding? (bool) # This will be written at the end, # because the size can only be calculated after the data compression, @@ -235,7 +229,7 @@ def save_fs(self, writer: EndianBinaryWriter, padding=False, block_info_flag=64, # *read compressed_size -> uncompressed_size # 0x10 offset # *read blocks infos of the data stream - # count (int) + # count (int) # ( # uncompressed_size(uint) # compressed_size (uint) @@ -244,22 +238,23 @@ def save_fs(self, writer: EndianBinaryWriter, padding=False, block_info_flag=64, # *decompression via info.flag & 0x3F # *afterwards the file positions - # file_count (int) + # file_count (int) # ( - # offset (long) - # size (long) - # flag (int) - # name (string_to_null) + # offset (long) + # size (long) + # flag (int) + # name (string_to_null) # ) # file list & file data + # prep nodes and build up block data data_writer = EndianBinaryWriter() files = [ ( name, - 0, #f.flag, + f.flags, data_writer.write_bytes( - f.bytes if isinstance(f, EndianBinaryReader) else f.save() + f.bytes if isinstance(f, (EndianBinaryReader, EndianBinaryWriter)) else f.save() ), ) for name, f in self.files.items() @@ -281,6 +276,7 @@ def save_fs(self, writer: EndianBinaryWriter, padding=False, block_info_flag=64, compressed_data_size = len(file_data) # write the block_info + # uncompressedDataHash block_writer = EndianBinaryWriter(b"\x00" * 0x10) # data block info # block count @@ -290,7 +286,7 @@ def save_fs(self, writer: EndianBinaryWriter, padding=False, block_info_flag=64, # compressed size block_writer.write_u_int(compressed_data_size) # flag - block_writer.write_short(data_flag) + block_writer.write_u_short(data_flag) # file block info # file count @@ -303,7 +299,7 @@ def save_fs(self, writer: EndianBinaryWriter, padding=False, block_info_flag=64, block_writer.write_long(f_len) offset += f_len # flag - block_writer.write_int(f_flag) + block_writer.write_u_int(f_flag) # name block_writer.write_string_to_null(f_name) @@ -324,21 +320,31 @@ def save_fs(self, writer: EndianBinaryWriter, padding=False, block_info_flag=64, compressed_block_data_size = len(block_data) # write the header info + ## file size writer.write_long( writer.Length + 8 + 4 + 4 + 4 - + (1 if padding else 0) + + (8 if self.version >= 7 else 0) + compressed_block_data_size + compressed_data_size ) - writer.write_int(compressed_block_data_size) - writer.write_int(uncompressed_block_data_size) - writer.write_int(block_info_flag) - if padding: - writer.write_boolean(padding) + # compressed blockInfoBytes size + writer.write_u_int(compressed_block_data_size) + # uncompressed size + writer.write_u_int(uncompressed_block_data_size) + # compression flag + writer.write_u_int(block_info_flag) + + if self.version >= 7: + # UnityFS\x00 - 8 + # size 8 + # comp sizes 4+4 + # flag 4 + # sum : 28 -> +8 alignment + writer.align_stream(16) if (block_info_flag & 0x80) != 0: # at end of file writer.write(file_data) diff --git a/UnityPy/files/File.py b/UnityPy/files/File.py index 2ce383cb..1f5d69b5 100644 --- a/UnityPy/files/File.py +++ b/UnityPy/files/File.py @@ -1,11 +1,127 @@ +from ..enums import FileType +from ..helpers import ImportHelper +from ..streams import EndianBinaryReader, EndianBinaryWriter + +from collections import namedtuple +from os.path import basename + +DirectoryInfo = namedtuple("DirectoryInfo", "path offset size") + + class File(object): name: str files: dict + cab_file: str is_changed: bool signature: str packer: str + # parent: File + def __init__(self, parent=None, name=None): + self.files = dict() + self.is_changed = False + self.cab_file = "CAB-UnityPy_Mod.resS" + self.parent = parent + self.name = basename(name) if isinstance(name, str) else '' + + def get_assets(self): + if isinstance(self, SerializedFile.SerializedFile): + return self + + for f in self.files.values(): + if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): + for asset in f.get_assets(): + yield asset + elif isinstance(f, SerializedFile.SerializedFile): + yield f + + def get_filtered_objects(self, obj_types=[]): + if len(obj_types) == 0: + return self.get_objects() + for f in self.files.values(): + if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): + for obj in f.objects: + if obj.type in obj_types: + yield obj + elif isinstance(f, SerializedFile.SerializedFile): + for obj in f.objects.values(): + if obj.type in obj_types: + yield obj + + def get_objects(self): + for f in self.files.values(): + if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): + for obj in f.objects: + yield obj + elif isinstance(f, SerializedFile.SerializedFile): + for obj in f.objects.values(): + yield obj + elif isinstance(f, ObjectReader.ObjectReader): + yield f + + def read_files(self, reader: EndianBinaryReader, files: list): + # read file data and convert it + for node in files: + reader.Position = node.offset + name = node.path + f = EndianBinaryReader(reader.read(node.size), offset=( + reader.BaseOffset + node.offset)) + # f._flag = getattr(node, "flags", None) # required for save + typ, _ = ImportHelper.check_file_type(f) + if typ == FileType.BundleFile: + f = BundleFile.BundleFile(f, self, name=name) + elif typ == FileType.WebFile: + f = WebFile.WebFile(f, self, name=name) + elif typ == FileType.AssetsFile: + # pre-check if resource file + if not name.endswith((".resS", ".resource", ".config", ".xml", ".dat")): + # try to load the file as serialized file + try: + f = SerializedFile.SerializedFile(f, self, name=name) + except ValueError: + pass + # required for BundleFiles + f.flags = getattr(node, "flags", 0) + self.files[name] = f + + def get_writeable_cab(self, name: str = None): + """ + Creates a new cab file in the bundle that contains the given data. + This is usefull for asset types that use resource files. + """ + + if not name: + name = self.cab_file + + if not name: + return None + + if name in self.files: + if isinstance(self.files[name], EndianBinaryWriter): + return self.files[name] + else: + raise ValueError( + "This cab already exists and isn't an EndianBinaryWriter") + + writer = EndianBinaryWriter() + writer.flags = 4 + writer.name = name + self.files[name] = writer + return writer + + @property + def container(self): + return { + path: obj + for f in self.files.values() + if isinstance(f, File) + for path, obj in f.container.items() + } + + def get(self, key, default=None): + return getattr(self, key, default) + def keys(self): return self.files.keys() @@ -22,4 +138,11 @@ def __repr__(self): return f"<{self.__class__.__name__}>" def mark_changed(self): - self.is_changed = True \ No newline at end of file + if isinstance(self.parent, File): + self.parent.mark_changed() + self.is_changed = True + + +# recursive import requires the import down here +from . import BundleFile, SerializedFile, WebFile, ObjectReader + diff --git a/UnityPy/files/ObjectReader.py b/UnityPy/files/ObjectReader.py new file mode 100644 index 00000000..db8ac8ab --- /dev/null +++ b/UnityPy/files/ObjectReader.py @@ -0,0 +1,237 @@ +from ..enums import ClassIDType, makeClassID +from . import SerializedFile +from .. import classes +from ..streams import EndianBinaryReader, EndianBinaryWriter +from ..helpers import TypeTreeHelper + + +class ObjectReader: + byte_start: int + byte_size: int + type_id: int + class_id: int + type: ClassIDType + path_id: int + # serialized_type: SerializedType + _read_until: int + + # saves where the parser stopped + # in case that not all data is read + # and the obj.data is changed, the unknown data can be added again + + def __init__(self, assets_file, reader: EndianBinaryReader): + self.assets_file = assets_file + self.reader = reader + self.data = b"" + self.version = assets_file.version + self.version2 = assets_file.header.version + self.platform = assets_file.target_platform + self.build_type = assets_file.build_type + + header = assets_file.header + types = assets_file.types + + if assets_file.big_id_enabled: + self.path_id = reader.read_long() + elif header.version < 14: + self.path_id = reader.read_int() + else: + reader.align_stream() + self.path_id = reader.read_long() + + if header.version >= 22: + self.byte_start_offset = (self.reader.real_offset(), 8) + self.byte_start = reader.read_long() + else: + self.byte_start_offset = (self.reader.real_offset(), 4) + self.byte_start = reader.read_u_int() + + self.byte_start += header.data_offset + self.byte_header_offset = header.data_offset + self.byte_base_offset = self.reader.BaseOffset + + self.byte_size_offset = (self.reader.real_offset(), 4) + self.byte_size = reader.read_u_int() + + self.type_id = reader.read_int() + if header.version < 16: + self.class_id = reader.read_u_short() + if types: + self.serialized_type = ( + x for x in types if x.class_id == self.type_id).__next__() + else: + self.serialized_type = SerializedFile.SerializedType( + reader, self.assets_file) + else: + typ = types[self.type_id] + self.serialized_type = typ + self.class_id = typ.class_id + + self.type = makeClassID(self.class_id) + + if header.version < 11: + self.is_destroyed = reader.read_u_short() + + if 11 <= header.version < 17: + script_type_index = reader.read_short() + if self.serialized_type: + self.serialized_type.script_type_index = script_type_index + + if header.version == 15 or header.version == 16: + self.stripped = reader.read_byte() + + def write(self, header, writer: EndianBinaryWriter, data_writer: EndianBinaryWriter): + if self.assets_file.big_id_enabled: + writer.write_long(self.path_id) + elif header.version < 14: + writer.write_int(self.path_id) + else: + writer.align_stream() + writer.write_long(self.path_id) + + if self.data: + data = self.data + # in some cases the parser doesn't read all of the object data + # games might still require the missing data + # so following code appends the missing data back to edited objects + if self.type != ClassIDType.MonoBehaviour: + end_pos = self.byte_start + self.byte_size + if self._read_until != end_pos: + self.reader.Position = self._read_until + data += self.reader.read_bytes(end_pos - self._read_until) + else: + self.reset() + data = self.reader.read(self.byte_size) + + if header.version >= 22: + writer.write_long(data_writer.Position) + else: + writer.write_u_int(data_writer.Position) + + writer.write_u_int(len(data)) + data_writer.write(data) + + writer.write_int(self.type_id) + + if header.version < 16: + writer.write_u_short(self.class_id) + + if header.version < 11: + writer.write_u_short(self.is_destroyed) + + if 11 <= header.version < 17: + writer.write_short(self.serialized_type.script_type_index) + + if header.version == 15 or header.version == 16: + writer.write_byte(self.stripped) + + def set_raw_data(self, data): + self.data = data + self.assets_file.mark_changed() + + @property + def container(self): + return ( + self.assets_file._container[self.path_id] + if self.path_id in self.assets_file._container + else None + ) + + @property + def Position(self): + return self.reader.Position + + @Position.setter + def Position(self, pos): + self.reader.Position = pos + + def reset(self): + self.reader.Position = self.byte_start + + def read(self): + try: + obj = getattr(classes, self.type.name, classes.Object)(self) + except Exception as e: + raise e + # TODO: only specific exceptions here? + except: + # HACK: in case the parsing via the class fails this solution + # uses the type tree to set the variables and then changes the class + obj = classes.Object(self) + obj.__class__ = getattr(classes, self.type.name, classes.Object) + for key, val in obj.__dict__.items(): + if " " in key: + obj.__dict__[key.replace(" ", "_")] = val + delattr(obj, key) + self._read_until = self.reader.Position + return obj + + def get(self, key, default=None): + return getattr(self, key, default) + + def __getattr__(self, item: str): + if hasattr(self.reader, item): + return getattr(self.reader, item) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.type.name) + + ################################################### + # + # Typetree Stuff + # + ################################################### + + def dump_typetree(self, nodes: list = None) -> str: + self.reset() + sb = [] + if nodes: + TypeTreeHelper.read_typetree_str(sb, nodes, self) + elif getattr(self.serialized_type, "nodes", None): + TypeTreeHelper.read_typetree_str( + sb, self.serialized_type.nodes, self) + return "".join(sb) + return "" + + def dump_typetree_structure(self) -> str: + if getattr(self.serialized_type, "nodes", None): + return TypeTreeHelper.dump_typetree(self.serialized_type.nodes) + return "" + + def read_typetree(self, nodes: list = None) -> dict: + self.reset() + tree = dict() + if nodes: + tree = TypeTreeHelper.read_typetree(nodes, self) + elif getattr(self.serialized_type, "nodes", None): + if self.serialized_type.nodes: + tree = TypeTreeHelper.read_typetree( + self.serialized_type.nodes, self) + return tree + + def save_typetree(self, tree: dict, nodes: list = None, writer: EndianBinaryWriter = None): + if not writer: + writer = EndianBinaryWriter(endian=self.reader.endian) + if nodes: + TypeTreeHelper.write_typetree(tree, nodes, writer) + elif self.serialized_type.nodes: + TypeTreeHelper.write_typetree( + tree, self.serialized_type.nodes, writer) + else: + raise ValueError("There are no TypeTree nodes for this object.") + data = writer.bytes + self.set_raw_data(data) + return data + + def get_raw_data(self) -> bytes: + pos = self.Position + self.reset() + ret = self.reader.read_bytes(self.byte_size) + self.Position = pos + return ret + + def set_raw_data(self, data): + self.data = data + if self.assets_file: + self.assets_file.mark_changed() + diff --git a/UnityPy/files/SerializedFile.py b/UnityPy/files/SerializedFile.py index dbf61ec0..b7c1c967 100644 --- a/UnityPy/files/SerializedFile.py +++ b/UnityPy/files/SerializedFile.py @@ -2,10 +2,8 @@ import re import sys -from .File import File -from .. import classes -from ..CommonString import COMMON_STRING -from ..enums import BuildTarget, ClassIDType +from . import File, ObjectReader +from ..enums import BuildTarget, makeBuildTarget, ClassIDType, CommonString from ..streams import EndianBinaryReader, EndianBinaryWriter RECURSION_LIMIT = sys.getrecursionlimit() @@ -57,6 +55,9 @@ class FileIdentifier: # external def name(self): return os.path.basename(self.path) + def __repr__(self): + return f"<{self.__class__.__name__}({self.path})>" + def __init__(self, header: SerializedFileHeader, reader: EndianBinaryReader): if header.version >= 6: self.temp_empty = reader.read_string_to_null() @@ -74,7 +75,7 @@ def write(self, header: SerializedFileHeader, writer: EndianBinaryWriter): writer.write_string_to_null(self.path) -class TypeTreeNode: +class TypeTreeNode(dict): type: str name: str byte_size: int @@ -86,6 +87,9 @@ class TypeTreeNode: type_str_offset: int name_str_offset: int + def __repr__(self): + return f"" + class BuildType: build_type: str @@ -103,19 +107,73 @@ def is_path(self): class SerializedType: - class_id: ClassIDType + class_id: int is_stripped_type: bool script_type_index = -1 nodes: list = [] # TypeTreeNode script_id: bytes # Hash128 old_type_hash: bytes # Hash128} + def __init__(self, reader, serialized_file): + version = serialized_file.header.version + self.class_id = reader.read_int() + + if version >= 16: + self.is_stripped_type = reader.read_boolean() + + if version >= 17: + self.script_type_index = reader.read_short() + + if version >= 13: + if (version < 16 and self.class_id < 0) or ( + version >= 16 and self.class_id == 114 + ): + self.script_id = reader.read_bytes(16) # Hash128 + self.old_type_hash = reader.read_bytes(16) # Hash128 + + if serialized_file._enable_type_tree: + type_tree = [] + if version >= 12 or version == 10: + self.string_data = serialized_file.read_type_tree_blob( + type_tree) + else: + serialized_file.read_type_tree(type_tree) + + if version >= 21: + self.type_dependencies = reader.read_int_array() + + self.nodes = type_tree -class SerializedFile(File): + def write(self, serialized_file, writer): + version = serialized_file.header.version + writer.write_int(self.class_id) + + if version >= 16: + writer.write_boolean(self.is_stripped_type) + + if version >= 17: + writer.write_short(self.script_type_index) + + if version >= 13: + if (version < 16 and self.class_id < 0) or ( + version >= 16 and self.class_id == 114 + ): + writer.write_bytes(self.script_id) # Hash128 + writer.write_bytes(self.old_type_hash) # Hash128 + + if serialized_file._enable_type_tree: + if version >= 12 or version == 10: + serialized_file.save_type_tree5( + self.nodes, writer, self.string_data) + else: + serialized_file.save_type_tree(self.nodes, writer) + + +class SerializedFile(File.File): reader: EndianBinaryReader is_changed: bool unity_version: str - version: list + version: tuple build_type: BuildType target_platform: BuildTarget types: list @@ -123,18 +181,22 @@ class SerializedFile(File): externals: list _container: dict objects: dict - container: dict + container_: dict _cache: dict header: SerializedFileHeader @property def files(self): - return self.objects + if self.objects: + return self.objects + return dict() + + @files.setter + def files(self, value): + self.objects = value - def __init__(self, reader: EndianBinaryReader, environment=None, parent=None): - self.is_changed = False - self.environment = environment - self.parent = parent + def __init__(self, reader: EndianBinaryReader, parent=None, name=None): + super().__init__(parent=parent, name=name) self.reader = reader self.unity_version = "2.5.0f5" @@ -148,7 +210,7 @@ def __init__(self, reader: EndianBinaryReader, environment=None, parent=None): self._container = {} self.objects = {} - self.container = {} + self.container_ = {} # used to speed up mass asset extraction # some assets refer to each other, so by keeping the result # of specific assets cached the extraction can be speed up by a lot. @@ -163,16 +225,15 @@ def __init__(self, reader: EndianBinaryReader, environment=None, parent=None): if header.version >= 9: header.endian = ">" if reader.read_boolean() else "<" header.reserved = reader.read_bytes(3) + if header.version >= 22: + header.metadata_size = reader.read_u_int() + header.file_size = reader.read_long() + header.data_offset = reader.read_long() + self.unknown = reader.read_long() # unknown else: reader.Position = header.file_size - header.metadata_size header.endian = ">" if reader.read_boolean() else "<" - if header.version >= 22: - header.metadata_size = reader.read_u_int() - header.file_size = reader.read_long() - header.data_offset = reader.read_long() - self.unknown = reader.read_long() # unknown - reader.endian = header.endian if header.version >= 7: @@ -181,14 +242,14 @@ def __init__(self, reader: EndianBinaryReader, environment=None, parent=None): if header.version >= 8: self._m_target_platform = reader.read_int() - self.target_platform = BuildTarget(self._m_target_platform) + self.target_platform = makeBuildTarget(self._m_target_platform) if header.version >= 13: self._enable_type_tree = reader.read_boolean() # ReadTypes type_count = reader.read_int() - self.types = [self.read_serialized_type() for _ in range(type_count)] + self.types = [SerializedType(reader, self) for _ in range(type_count)] self.big_id_enabled = 0 if 7 <= header.version < 14: @@ -198,7 +259,7 @@ def __init__(self, reader: EndianBinaryReader, environment=None, parent=None): object_count = reader.read_int() self.objects = {} for _ in range(object_count): - obj = ObjectReader(self, reader) + obj = ObjectReader.ObjectReader(self, reader) self.objects[obj.path_id] = obj # Read Scripts @@ -218,7 +279,7 @@ def __init__(self, reader: EndianBinaryReader, environment=None, parent=None): if header.version >= 20: ref_type_count = reader.read_int() self.ref_types = [ - self.read_serialized_type() for _ in range(ref_type_count) + SerializedType(reader, self) for _ in range(ref_type_count) ] if header.version >= 5: @@ -228,13 +289,17 @@ def __init__(self, reader: EndianBinaryReader, environment=None, parent=None): for obj in self.objects.values(): if obj.type == ClassIDType.AssetBundle: data = obj.read() - for container, asset_info in data.container.items(): + for container, asset_info in data.m_Container.items(): asset = asset_info.asset - self.container[container] = asset + self.container_[container] = asset if hasattr(asset, "path_id"): self._container[asset.path_id] = container - if environment is not None: - environment.container = {**environment.container, **self.container} + # if environment is not None: + # environment.container = {**environment.container, **self.container} + + @property + def container(self): + return self.container_ def set_version(self, string_version): self.unity_version = string_version @@ -243,66 +308,37 @@ def set_version(self, string_version): version_split = re.split(r"\D", string_version) self.version = tuple(int(x) for x in version_split) - def mark_changed(self): - self.is_changed = True - if self.parent: - self.parent.mark_changed() - - def read_serialized_type(self): - type_ = SerializedType() - type_.class_id = self.reader.read_int() - - if self.header.version >= 16: - type_.is_stripped_type = self.reader.read_boolean() - - if self.header.version >= 17: - type_.script_type_index = self.reader.read_short() - - if self.header.version >= 13: - if (self.header.version < 16 and type_.class_id < 0) or ( - self.header.version >= 16 and type_.class_id == 114 - ): - type_.script_id = self.reader.read_bytes(16) # Hash128 - type_.old_type_hash = self.reader.read_bytes(16) # Hash128 - - if self._enable_type_tree: - type_tree = [] - if self.header.version >= 12 or self.header.version == 10: - type_.string_data = self.read_type_tree_blob(type_tree) + def read_type_tree(self, type_tree): + level_stack = [[0,1]] + while level_stack: + level, count = level_stack[-1] + if count == 1: + level_stack.pop() else: - self.read_type_tree(type_tree) - - if self.header.version >= 21: - type_.type_dependencies = self.reader.read_int_array() - - type_.nodes = type_tree - - return type_ - - def read_type_tree(self, type_tree, level=0): - if level == RECURSION_LIMIT - 1: - raise RecursionError + level_stack[-1][1] -= 1 + + type_tree_node = TypeTreeNode() + type_tree.append(type_tree_node) + type_tree_node.level = level + type_tree_node.type = self.reader.read_string_to_null() + type_tree_node.name = self.reader.read_string_to_null() + type_tree_node.byte_size = self.reader.read_int() + if self.header.version == 2: + type_tree_node.variable_count = self.reader.read_int() - type_tree_node = TypeTreeNode() - type_tree.append(type_tree_node) - type_tree_node.level = level - type_tree_node.type = self.reader.read_string_to_null() - type_tree_node.name = self.reader.read_string_to_null() - type_tree_node.byte_size = self.reader.read_int() - if self.header.version == 2: - type_tree_node.variable_count = self.reader.read_int() + if self.header.version != 3: + type_tree_node.index = self.reader.read_int() - if self.header.version != 3: - type_tree_node.index = self.reader.read_int() + type_tree_node.is_array = self.reader.read_int() + type_tree_node.version = self.reader.read_int() + if self.header.version != 3: + type_tree_node.meta_flag = self.reader.read_int() - type_tree_node.is_array = self.reader.read_int() - type_tree_node.version = self.reader.read_int() - if self.header.version != 3: - type_tree_node.meta_flag = self.reader.read_int() + children_count = self.reader.read_int() + if children_count: + level_stack.append([level+1, children_count]) + return type_tree - children_count = self.reader.read_int() - for i in range(children_count): - self.read_type_tree(type_tree, level + 1) def read_type_tree_blob(self, type_tree): reader = self.reader @@ -332,134 +368,141 @@ def read_type_tree_blob(self, type_tree): return string_buffer_reader.bytes - def save(self, packer: str = "none") -> bytes: - # Structure: - # 1. header - # file header - # types - # objects - # scripts - # externals - # 2. small 0es offset - align stream - # 3. objects data - - # adjust metadata_size, data_offst and file_size - header = self.header - types = self.types - objects = self.objects - script_types = self.script_types - externals = self.externals - - # the data-offset is required for the header and is kinda hard to calculate, - # so we split the asset building up into multiple parts - fileheader_writer = EndianBinaryWriter() - header_writer = EndianBinaryWriter(endian=header.endian) - data_writer = EndianBinaryWriter(endian=header.endian) + def get_writeable_cab(self, name: str = None): + """ + Creates a new cab file in the bundle that contains the given data. + This is usefull for asset types that use resource files. + """ + if not isinstance(self.parent, (File.BundleFile.BundleFile, File.WebFile.WebFile)): + return None + cab = self.parent.get_writeable_cab(name) + + cab.path = f"archive:/{name}" + if not any(cab.path == x.path for x in self.externals): + # register as external + class FileIdentifierFake: + pass + file_identifier = FileIdentifierFake() + file_identifier.__class__ = FileIdentifier + file_identifier.temp_empty = "" + import uuid + file_identifier.guid = uuid.uuid1().urn[-16:].encode("ascii") + file_identifier.path = cab.path + file_identifier.type = 0 + self.externals.append( + file_identifier + ) - # 1. building the header without file header + return cab - # reader.Position = header.file_size - header.metadata_size - # header.endian = '>' if reader.read_boolean() else '<' - if header.version < 9: - header_writer.write_boolean(header.endian == ">") + def save(self, packer: str=None) -> bytes: + # 1. header -> has to be delayed until the very end + # 2. data -> types, objects, scripts, ... + # so write the data first + header = self.header + meta_writer = EndianBinaryWriter(endian=header.endian) + data_writer = EndianBinaryWriter(endian=header.endian) if header.version >= 7: - header_writer.write_string_to_null(self.unity_version) + meta_writer.write_string_to_null(self.unity_version) if header.version >= 8: - header_writer.write_int(self._m_target_platform) + meta_writer.write_int(self._m_target_platform) if header.version >= 13: - header_writer.write_boolean(self._enable_type_tree) + meta_writer.write_boolean(self._enable_type_tree) - # types - header_writer.write_int(len(types)) - for typ in types: - self.save_serialized_type(typ, header, header_writer) + # ReadTypes + meta_writer.write_int(len(self.types)) + for typ in self.types: + typ.write(self, meta_writer) if 7 <= header.version < 14: - header_writer.write_int(self.big_id_enabled) + meta_writer.write_int(self.big_id_enabled) - # objects - header_writer.write_int(len(objects)) - for i, obj in enumerate(objects.values()): - obj.write(header, header_writer, data_writer) - if i < len(objects) - 1: - data_writer.align_stream(8) + # ReadObjects + meta_writer.write_int(len(self.objects)) + for obj in self.objects.values(): + obj.write(header, meta_writer, data_writer) + data_writer.align_stream(8) - # scripts + # Read Scripts if header.version >= 11: - header_writer.write_int(len(script_types)) - for script_type in script_types: - script_type.write(header, header_writer) + meta_writer.write_int(len(self.script_types)) + for script_type in self.script_types: + script_type.write(header, meta_writer) - # externals - header_writer.write_int(len(externals)) - for external in externals: - external.write(header, header_writer) - - header_writer.write_string_to_null(self.userInformation) - if header.version >= 21: - header_writer.write_int(self.unknown) + # Read Externals + meta_writer.write_int(len(self.externals)) + for external in self.externals: + external.write(header, meta_writer) - if header.version >= 9: - # file header - fileheader_size = 4 * 4 + 1 + 3 # following + endian + reserved - metadata_size = header_writer.Length - # metadata size - fileheader_writer.write_u_int(header_writer.Length) - # align between metadata and data - mod = (fileheader_size + metadata_size) % 16 - align_length = 16 - mod if mod else 0 - - # file size - fileheader_writer.write_u_int( - header_writer.Length - + data_writer.Length - + fileheader_size - + align_length - ) - # version - fileheader_writer.write_u_int(header.version) - # data offset - fileheader_writer.write_u_int( - header_writer.Length + fileheader_size + align_length - ) - # endian - fileheader_writer.write_boolean(header.endian == ">") - fileheader_writer.write_bytes(header.reserved) + if header.version >= 20: + meta_writer.write_int(len(self.ref_types)) + for ref_type in self.ref_types: + ref_type.write(self, meta_writer) - # self.reader.bytes[fileheader_size:fileheader_size+header_writer.Length] == header_writer.bytes + if header.version >= 5: + meta_writer.write_string_to_null(self.userInformation) - return ( - fileheader_writer.bytes - + header_writer.bytes - + b"\x00" * align_length - + data_writer.bytes - ) + # prepare header + writer = EndianBinaryWriter() + header_size = 16 # 4*4 + metadata_size = meta_writer.Length + data_size = data_writer.Length + if header.version >= 9: + # 1 bool + 3 reserved + extra header 4 + 3*8 + header_size += 4 if header.version < 22 else 4 + 28 + data_offset = header_size + metadata_size + # align data_offset + data_offset += (16 - data_offset % 16) % 16 + file_size = data_offset + data_size + if header.version < 22: + writer.write_u_int(metadata_size) + writer.write_u_int(file_size) + writer.write_u_int(header.version) + # reader.Position = header.file_size - header.metadata_size + # so data follows right after this header -> after 32 + writer.write_u_int(data_offset) + writer.write_boolean(">" == header.endian) + writer.write_bytes(header.reserved) + else: + # old header + writer.write_u_int(0) + writer.write_u_int(0) + writer.write_u_int(header.version) + writer.write_u_int(0) + writer.write_boolean(">" == header.endian) + writer.write_bytes(header.reserved) + writer.write_u_int(metadata_size) + writer.write_long(file_size) + writer.write_long(data_offset) + writer.write_long(self.unknown) + + writer.write_bytes(meta_writer.bytes) + writer.align_stream(16) + writer.write_bytes(data_writer.bytes) else: - # file header - fileheader_size = 4 * 4 - metadata_size = header_writer.Length - # metadata size - fileheader_writer.write_u_int(metadata_size) - # file size - fileheader_writer.write_u_int( - metadata_size + data_writer.Length + fileheader_size - ) - # version - fileheader_writer.write_u_int(header.version) - # data offset - unity seems to align the stream .... but it's apparently not necessary - fileheader_writer.write_u_int(fileheader_size) - - return fileheader_writer.bytes + data_writer.bytes + header_writer.bytes + metadata_size += 1 # endian boolean + file_size = header_size + metadata_size + data_size + writer.write_u_int(metadata_size) + writer.write_u_int(file_size) + writer.write_u_int(header.version) + # reader.Position = header.file_size - header.metadata_size + # so data follows right after this header -> after 32 + writer.write_u_int(32) + writer.write_bytes(data_writer.bytes) + writer.write_boolean(">" == header.endian) + writer.write_bytes(meta_writer.bytes) + + return writer.bytes def save_serialized_type( - self, - typ: SerializedType, - header: SerializedFileHeader, - writer: EndianBinaryWriter, + self, + typ: SerializedType, + header: SerializedFileHeader, + writer: EndianBinaryWriter, ): writer.write_int(typ.class_id) @@ -471,7 +514,7 @@ def save_serialized_type( if header.version >= 13: if (header.version < 16 and typ.class_id < 0) or ( - header.version >= 16 and typ.class_id == 114 + header.version >= 16 and typ.class_id == 114 ): writer.write_bytes(typ.script_id) # Hash128 writer.write_bytes(typ.old_type_hash) # Hash128 @@ -514,25 +557,6 @@ def save_type_tree5(self, nodes: list, writer: EndianBinaryWriter, str_data=b"") # string buffer string_buffer = EndianBinaryWriter() - """ - saved_strings = {} - def save_string(value: str) -> int: - import re - if value in saved_strings: - return saved_strings[value] - - for key, val in COMMON_STRING.items(): - if val == value: - return key | 0x80000000 - if re.match(r"\d+", value): - return int(value) - else: - # normal string - offset = string_buffer.Position - saved_strings[value] = offset - string_buffer.write_string_to_null(value) - return offset - """ string_buffer.write(str_data) strings_values = [ (node.type_str_offset, node.name_str_offset) for node in nodes @@ -551,22 +575,25 @@ def save_string(value: str) -> int: writer.write_byte(node.level) # is array writer.write_boolean(node.is_array) - + # type str offfset writer.write_u_int(strings_values[i][0]) + # name str offset writer.write_u_int(strings_values[i][1]) - + # byte size writer.write_int(node.byte_size) + # index writer.write_int(node.index) + # meta flag writer.write_int(node.meta_flag) - - if self.header.version > 17: - writer.write(node.extra) + # ref hash + if self.header.version > 19: + writer.write_u_long(node.ref_type_hash) # string buffer writer.write(string_buffer.bytes) if self.header.version >= 21: - self.reader.write_bytes(b"\x00" * 4) + writer.write_bytes(b"\x00" * 4) def read_string(string_buffer_reader: EndianBinaryReader, value: int) -> str: @@ -576,144 +603,8 @@ def read_string(string_buffer_reader: EndianBinaryReader, value: int) -> str: return string_buffer_reader.read_string_to_null() offset = value & 0x7FFFFFFF - if offset in COMMON_STRING: - return COMMON_STRING[offset] + if offset in CommonString: + return CommonString[offset] return str(offset) - -class ObjectReader: - byte_start: int - byte_size: int - type_id: int - class_id: ClassIDType - path_id: int - serialized_type: SerializedType - - def __init__(self, assets_file, reader): - self.assets_file = assets_file - self.reader = reader - self.data = b"" - self.version = assets_file.version - self.version2 = assets_file.header.version - self.platform = assets_file.target_platform - self.build_type = assets_file.build_type - - header = assets_file.header - types = assets_file.types - - if assets_file.big_id_enabled: - self.path_id = reader.read_long() - elif header.version < 14: - self.path_id = reader.read_int() - else: - reader.align_stream() - self.path_id = reader.read_long() - - if header.version >= 22: - self.byte_start_offset = (self.reader.real_offset(), 8) - self.byte_start = reader.read_long() - else: - self.byte_start_offset = (self.reader.real_offset(), 4) - self.byte_start = reader.read_u_int() - - self.byte_start += header.data_offset - self.byte_header_offset = header.data_offset - self.byte_base_offset = self.reader.BaseOffset - - self.byte_size_offset = (self.reader.real_offset(), 4) - self.byte_size = reader.read_u_int() - - self.type_id = reader.read_int() - if header.version < 16: - self.class_id = reader.read_u_short() - if types: - self.serialized_type = ( - x for x in types if x.class_id == self.type_id).next() - else: - self.serialized_type = SerializedType() - else: - typ = types[self.type_id] - self.serialized_type = typ - self.class_id = typ.class_id - - self.type = ClassIDType(self.class_id) - - if header.version < 11: - self.is_destroyed = reader.read_u_short() - - if 11 <= header.version < 17: - script_type_index = reader.read_short() - if self.serialized_type: - self.serialized_type.script_type_index = script_type_index - - if header.version == 15 or header.version == 16: - self.stripped = reader.read_byte() - - def write(self, header, writer, data_writer): - # validated - if header.version < 14: - writer.write_int(self.path_id) - else: - writer.align_stream() - writer.write_long(self.path_id) - - if self.data: - data = self.data - else: - self.reset() - data = self.reader.read(self.byte_size) - - writer.write_u_int(data_writer.Position) - writer.write_u_int(len(data)) - - data_writer.write(data) - - writer.write_int(self.type_id) - - if header.version < 16: - # WARNING - CLASSIDTYPE MIGHT CHANGE THE NUMBER IF IT'S UNKNOWN - writer.write_u_short(self.class_id) - writer.write_u_short(self.is_destroyed) - - if header.version == 15 or header.version == 16: - writer.write_byte(self.stripped) - - def set_raw_data(self, data): - self.data = data - self.assets_file.mark_changed() - - @property - def container(self): - return ( - self.assets_file._container[self.path_id] - if self.path_id in self.assets_file._container - else None - ) - - def reset(self): - self.reader.Position = self.byte_start - - def read(self): - try: - return getattr(classes, self.type.name, classes.Object)(self) - except Exception as e: - raise e - # TODO: only specific exceptions here? - except: - # HACK: in case the parsing via the class fails this solution - # uses the type tree to set the variables and then changes the class - obj = classes.Object(self) - obj.__class__ = getattr(classes, self.type.name, classes.Object) - for key, val in obj.__dict__.items(): - if " " in key: - obj.__dict__[key.replace(" ", "_")] = val - delattr(obj, key) - return obj - - def __getattr__(self, item: str): - if hasattr(self.reader, item): - return getattr(self.reader, item) - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.type.name) diff --git a/UnityPy/files/WebFile.py b/UnityPy/files/WebFile.py index 791ef0e1..d0d48386 100644 --- a/UnityPy/files/WebFile.py +++ b/UnityPy/files/WebFile.py @@ -1,28 +1,24 @@ -from .BundleFile import BundleFile -from .File import File -from .SerializedFile import SerializedFile -from ..enums import FileType -from ..helpers import CompressionHelper, ImportHelper +from . import File +from ..helpers import CompressionHelper from ..streams import EndianBinaryReader, EndianBinaryWriter -class WebFile(File): +class WebFile(File.File): """A package which can hold other WebFiles, Bundles and SerialiedFiles. It may be compressed via gzip or brotli. files -- list of all files in the WebFile """ - - def __init__(self, reader: EndianBinaryReader, environment=None): + + def __init__(self, reader: EndianBinaryReader, parent: File, name=None): """Constructor Method """ - self.files = {} - self.is_changed = False - + super().__init__(parent=parent, name=name) + # check compression magic = reader.read_bytes(2) reader.Position = 0 - + if magic == CompressionHelper.GZIP_MAGIC: self.packer = "gzip" data = CompressionHelper.decompress_gzip(reader.bytes) @@ -38,73 +34,49 @@ def __init__(self, reader: EndianBinaryReader, environment=None): else: self.packer = "none" reader.endian = "<" - + # signature check signature = reader.read_string_to_null() if signature != "UnityWebData1.0": return self.signature = signature - + # read header -> contains file headers head_length = reader.read_int() - + files = [] while reader.Position < head_length: offset = reader.read_int() length = reader.read_int() path_length = reader.read_int() name = bytes(reader.read_bytes(path_length)).decode("utf-8") - files.append((name, offset, length)) - - # read file data and convert it - for name, offset, length in files: - reader.Position = offset - data = reader.read(length) - typ, item = ImportHelper.check_file_type(data) - - if typ == FileType.BundleFile: - item = BundleFile(item, environment) - elif typ == FileType.WebFile: - item = WebFile(item, environment) - - if typ == FileType.AssetsFile: - # pre-check if resource file - if name.endswith((".resS", ".resource", ".config", ".xml", ".dat")): - environment.resources[name] = item - else: - # try to load the file as serialized file - try: - item = SerializedFile(item, environment) - environment.assets[name] = item - except ValueError: - environment.resources[name] = item - - self.files[name] = item - item.parent = self - + files.append(File.DirectoryInfo(name, offset, length)) + + self.read_files(reader, files) + def save( - self, - files: dict = None, - packer: str = "none", - signature: str = "UnityWebData1.0", + self, + files: dict = None, + packer: str = "none", + signature: str = "UnityWebData1.0", ) -> bytes: # solve defaults if not files: files = self.files if not packer: packer = self.packer - + # get raw data files = { name: f.bytes if isinstance(f, EndianBinaryReader) else f.save() for name, f in files.items() } - + # create writer writer = EndianBinaryWriter(endian="<") # signature writer.write_string_to_null(signature) - + # data offset offset = sum( [ @@ -116,9 +88,9 @@ def save( 4, # offset int ] ) - + writer.write_int(offset) - + # 1. file headers for name, data in files.items(): # offset @@ -131,14 +103,15 @@ def save( enc_path = name.encode("utf-8") writer.write_int(len(enc_path)) writer.write(enc_path) - + # 2. file data for data in files.values(): writer.write(data) - + if packer == "gzip": return CompressionHelper.compress_gzip(writer.bytes) elif packer == "brotli": return CompressionHelper.compress_brotli(writer.bytes) else: return writer.bytes + diff --git a/UnityPy/files/__init__.py b/UnityPy/files/__init__.py index 2db8747a..bdaecc90 100644 --- a/UnityPy/files/__init__.py +++ b/UnityPy/files/__init__.py @@ -1,3 +1,5 @@ +from .File import File, DirectoryInfo from .SerializedFile import SerializedFile from .BundleFile import BundleFile from .WebFile import WebFile +from .ObjectReader import ObjectReader diff --git a/UnityPy/helpers/CompressionHelper.py b/UnityPy/helpers/CompressionHelper.py index 234bef86..f6233b5a 100644 --- a/UnityPy/helpers/CompressionHelper.py +++ b/UnityPy/helpers/CompressionHelper.py @@ -13,12 +13,12 @@ def decompress_lzma(data: bytes) -> bytes: """decompresses lzma-compressed data - :param data: compressed data - :type data: bytes - :raises _lzma.LZMAError: Compressed data ended before the end-of-stream marker was reached - :return: uncompressed data - :rtype: bytes - """ + :param data: compressed data + :type data: bytes + :raises _lzma.LZMAError: Compressed data ended before the end-of-stream marker was reached + :return: uncompressed data + :rtype: bytes + """ props, dict_size = struct.unpack(" bytes: def compress_lzma(data: bytes) -> bytes: """compresses data via lzma (unity specific) - The current static settings may not be the best solution, - but they are the most commonly used values and should therefore be enough for the time being. - - :param data: uncompressed data - :type data: bytes - :return: compressed data - :rtype: bytes - """ + The current static settings may not be the best solution, + but they are the most commonly used values and should therefore be enough for the time being. + + :param data: uncompressed data + :type data: bytes + :return: compressed data + :rtype: bytes + """ ec = lzma.LZMACompressor( format=lzma.FORMAT_RAW, filters=[ - {"id": lzma.FILTER_LZMA1, "dict_size": 524288, "lc": 3, "lp": 0, "pb": 2,} + {"id": lzma.FILTER_LZMA1, "dict_size": 524288, "lc": 3, "lp": 0, "pb": 2, } ], ) ec.compress(data) @@ -63,25 +63,25 @@ def compress_lzma(data: bytes) -> bytes: def decompress_lz4(data: bytes, uncompressed_size: int) -> bytes: # LZ4M/LZ4HC """decompresses lz4-compressed data - :param data: compressed data - :type data: bytes - :param uncompressed_size: size of the uncompressed data - :type uncompressed_size: int - :raises _block.LZ4BlockError: Decompression failed: corrupt input or insufficient space in destination buffer. - :return: uncompressed data - :rtype: bytes - """ + :param data: compressed data + :type data: bytes + :param uncompressed_size: size of the uncompressed data + :type uncompressed_size: int + :raises _block.LZ4BlockError: Decompression failed: corrupt input or insufficient space in destination buffer. + :return: uncompressed data + :rtype: bytes + """ return lz4.block.decompress(data, uncompressed_size) def compress_lz4(data: bytes) -> bytes: # LZ4M/LZ4HC """compresses data via lz4.block - :param data: uncompressed data - :type data: bytes - :return: compressed data - :rtype: bytes - """ + :param data: uncompressed data + :type data: bytes + :return: compressed data + :rtype: bytes + """ return lz4.block.compress( data, mode="high_compression", compression=9, store_size=False ) @@ -91,23 +91,23 @@ def compress_lz4(data: bytes) -> bytes: # LZ4M/LZ4HC def decompress_brotli(data: bytes) -> bytes: """decompresses brotli-compressed data - :param data: compressed data - :type data: bytes - :raises brotli.error: BrotliDecompress failed - :return: uncompressed data - :rtype: bytes - """ + :param data: compressed data + :type data: bytes + :raises brotli.error: BrotliDecompress failed + :return: uncompressed data + :rtype: bytes + """ return brotli.decompress(data) def compress_brotli(data: bytes) -> bytes: """compresses data via brotli - :param data: uncompressed data - :type data: bytes - :return: compressed data - :rtype: bytes - """ + :param data: uncompressed data + :type data: bytes + :return: compressed data + :rtype: bytes + """ return brotli.compress(data) @@ -115,23 +115,23 @@ def compress_brotli(data: bytes) -> bytes: def decompress_gzip(data: bytes) -> bytes: """decompresses gzip-compressed data - :param data: compressed data - :type data: bytes - :raises OSError: Not a gzipped file - :return: uncompressed data - :rtype: bytes - """ + :param data: compressed data + :type data: bytes + :raises OSError: Not a gzipped file + :return: uncompressed data + :rtype: bytes + """ return gzip.decompress(data) def compress_gzip(data: bytes) -> bytes: """compresses data via gzip - The current static settings may not be the best solution, - but they are the most commonly used values and should therefore be enough for the time being. - - :param data: uncompressed data - :type data: bytes - :return: compressed data - :rtype: bytes - """ + The current static settings may not be the best solution, + but they are the most commonly used values and should therefore be enough for the time being. + + :param data: uncompressed data + :type data: bytes + :return: compressed data + :rtype: bytes + """ return gzip.compress(data) diff --git a/UnityPy/helpers/ImportHelper.py b/UnityPy/helpers/ImportHelper.py index 1f382b82..4d058987 100644 --- a/UnityPy/helpers/ImportHelper.py +++ b/UnityPy/helpers/ImportHelper.py @@ -1,5 +1,4 @@ import os -import re from .CompressionHelper import BROTLI_MAGIC, GZIP_MAGIC from ..enums import FileType @@ -45,12 +44,12 @@ def merge_split_assets(path: str, all_directories=False): split_files = [ os.path.join(path, fp) for fp in os.listdir(path) if fp[-7:] == ".split0" ] - + for split_file in split_files: dest_file = file_name_without_extension(split_file) dest_path = os.path.dirname(split_file) - dest_full = os.path.join(dest_file, dest_path) - + dest_full = os.path.join(dest_path, dest_file) + if not os.path.exists(dest_full): with open(dest_full, "wb") as f: i = 0 @@ -64,7 +63,7 @@ def merge_split_assets(path: str, all_directories=False): def processing_split_files(select_file: list) -> list: split_files = [fp for fp in select_file if ".split" in fp] select_file = [f for f in select_file if f not in split_files] - + split_files = set([file_name_without_extension(fp) for fp in split_files]) for splitFile in split_files: if os.path.isfile: @@ -82,10 +81,10 @@ def check_file_type(input_) -> (FileType, EndianBinaryReader): reader = EndianBinaryReader(input_) except: return None, None - + if reader.Length < 20: return FileType.ResourceFile, reader - + signature = reader.read_string_to_null(20) reader.Position = 0 if signature in [ @@ -97,63 +96,54 @@ def check_file_type(input_) -> (FileType, EndianBinaryReader): return FileType.BundleFile, reader elif signature == "UnityWebData1.0": return FileType.WebFile, reader - elif signature == "PK\x03\x04\x14": + elif signature == "PK\x03\x04": return FileType.ZIP, reader else: if reader.Length < 128: return FileType.ResourceFile, reader - - magic = reader.read_bytes(2) + + magic = bytes(reader.read_bytes(2)) reader.Position = 0 if GZIP_MAGIC == magic: return FileType.WebFile, reader reader.Position = 0x20 - magic = reader.read_bytes(6) + magic = bytes(reader.read_bytes(6)) reader.Position = 0 if BROTLI_MAGIC == magic: return FileType.WebFile, reader - + # check if AssetsFile old_endian = reader.endian - assets_file = True # read as if assetsfile and check version + # ReadHeader reader.Position = 0 metadata_size = reader.read_u_int() file_size = reader.read_u_int() version = reader.read_u_int() data_offset = reader.read_u_int() - - if ( - version < 0 - or version > 100 - or any( - [ - x < 0 or x > reader.Length - for x in [metadata_size, version, data_offset] - ] - ) - or file_size < metadata_size - ): - return FileType.ResourceFile, reader - - if version >= 9: + + if version >= 22: endian = ">" if reader.read_boolean() else "<" reserved = reader.read_bytes(3) - else: - reader.Position = file_size - metadata_size - endian = ">" if reader.read_boolean() else "<" - - reader.endian = endian - - if version >= 7: - unity_version = reader.read_string_to_null() - if len([x for x in re.split(r"\D", unity_version) if x != ""]) < 2: - assets_file = False - - # check end + metadata_size = reader.read_u_int() + file_size = reader.read_long() + data_offset = reader.read_long() + unknown = reader.read_long() # unknown + + # reset reader.endian = old_endian reader.Position = 0 - if assets_file: - return FileType.AssetsFile, reader - else: + # check info + if any(( + version < 0, + version > 100, + *[ + x < 0 or x > reader.Length + for x in [file_size, metadata_size, version, data_offset] + ], + file_size < metadata_size, + file_size < data_offset + )): return FileType.ResourceFile, reader + else: + return FileType.AssetsFile, reader diff --git a/UnityPy/helpers/ResourceReader.py b/UnityPy/helpers/ResourceReader.py index d1b2acb3..8077402a 100644 --- a/UnityPy/helpers/ResourceReader.py +++ b/UnityPy/helpers/ResourceReader.py @@ -1,66 +1,76 @@ -import os - -from .ImportHelper import find_all_files +import os, glob +from ..streams import EndianBinaryReader +from ..files import File def get_resource_data(*args): """ Input: Option 1: - path - file path - assets_file - SerializedFile - offset - - size - + 0 - path - file path + 1 - assets_file - SerializedFile + 2 - offset - + 3 - size - Option 2: - reader - EndianBinaryReader - offset - - size - + 0 - reader - EndianBinaryReader + 1 - offset - + 2 - size - + + -> -2 = offset, -1 = size """ if len(args) == 4: - need_search = True - path = args[0] - assets_file = args[1] + reader = search_resource(res_path=args[0], assets_file=args[1]) elif len(args) == 3: - need_search = False reader = args[0] - offset = args[-2] - size = args[-1] + else: + raise TypeError(f"3 or 4 arguments required, but only {len(args)} given") - if need_search: - resource_file_name = path + reader.Position = args[-2] + return reader.read_bytes(args[-1]) - reader = assets_file.environment.resources.get(resource_file_name) - if not reader: - reader = assets_file.environment.resources.get( - os.path.basename(resource_file_name) - ) - if not reader: - reader = assets_file.environment.resources.get( - os.path.basename(resource_file_name.replace('.assets.resS', '.resource')) - ) - if reader: - reader.Position = offset - return reader.read_bytes(size) +def search_resource(res_path, assets_file): + # try to find the resource in the Unity packages + base_name = os.path.basename(res_path) + if os.path.splitext(base_name)[1] == ".resource": + base_name2 = base_name.replace('.resource', '.assets.resS') + else: + base_name2 = base_name.replace('.assets.resS', '.resource') - if not assets_file.environment.path: - return - current_directory = os.path.dirname(assets_file.environment.path) - resource_file_path = os.path.join(current_directory, resource_file_name) - if not os.path.isfile(resource_file_path): - find_files = find_all_files(current_directory, resource_file_name) - if find_files: - resource_file_path = find_files[0] + for p in [res_path, base_name, base_name2]: + reader = assets_file.parent.files.get(p) + if reader: + if isinstance(reader, File.File): + # in case the import helper accidentally detected a resource file as something else + reader = reader.reader + return reader - if os.path.isfile(resource_file_path): - with open(resource_file_path, "rb") as f: - f.seek(offset) - return f.read(size) - else: + # try to find it in the dir environment + c = assets_file + path = getattr(assets_file, "path", None) + while not path: + c = getattr(c,"parent",None) + if c == None: raise FileNotFoundError( - f"Can't find the resource file {resource_file_name}" + f"Can't find the resource file {res_path}" ) + path = getattr(c, "path", None) + current_directory = path + resource_file_path = os.path.join(current_directory, *res_path.split("/")) + if not os.path.isfile(resource_file_path): + resource_file_path = search_resource_file(current_directory, base_name) + if not os.path.isfile(resource_file_path): + resource_file_path = search_resource_file(current_directory, base_name.replace('.assets.resS', '.resource')) + if os.path.isfile(resource_file_path): + return EndianBinaryReader(open(resource_file_path, "rb")) else: - reader.Position = offset - return reader.read_bytes(size) + raise FileNotFoundError( + f"Can't find the resource file {res_path}" + ) + + +def search_resource_file(path, name): + #print("real file", path, name) + files = glob.glob(os.path.join(path, "**", name), recursive=True) + return files[0] if len(files) else "" diff --git a/UnityPy/helpers/TypeTreeHelper.py b/UnityPy/helpers/TypeTreeHelper.py index 7ae750e0..82636f1f 100644 --- a/UnityPy/helpers/TypeTreeHelper.py +++ b/UnityPy/helpers/TypeTreeHelper.py @@ -1,388 +1,437 @@ -from ..streams import EndianBinaryReader - - -class RefInt: - v: int - - def __init__(self, value): - self._value = value - - def __add__(self, other): - return self._value + other - - def __sub__(self, other): - self._value -= other - return self._value - - def __int__(self): - return self._value - - def __getattr__(self, item): - return self._value - - def __getitem__(self, item): - return self._value - - def __setattr__(self, key, value): - self.__dict__["_value"] = value - - def __setitem__(self, key, value): - self._value = value - - def __mod__(self, other): - return self._value % other - - def __ge__(self, other): - return self._value >= other - - def __gt__(self, other): - return self._value > other - - def __le__(self, other): - return self._value <= other - - def __lt__(self, other): - return self._value < other - - def __eq__(self, other): - return self._value == other - - -def get_members(members: list, level: int, index: int) -> list: - member2 = [members[0]] - for i in range(index + 1, len(members)): - member = members[i] - if member.level <= level: - return member2 - member2.append(member) - return member2 - - -class TypeTreeHelper: - def __init__(self, reader: EndianBinaryReader): - self.reader = reader - self.READ = { - "SInt8": self.reader.read_byte, - "UInt8": self.reader.read_u_byte, - "short": self.reader.read_short, - "SInt16": self.reader.read_short, - "unsigned short": self.reader.read_u_short, - "UInt16": self.reader.read_u_short, - "int": self.reader.read_int, - "SInt32": self.reader.read_int, - "unsigned int": self.reader.read_u_int, - "UInt32": self.reader.read_u_int, - "Type*": self.reader.read_u_int, - "long long": self.reader.read_long, - "SInt64": self.reader.read_long, - "unsigned long long": self.reader.read_u_long, - "UInt64": self.reader.read_u_long, - "float": self.reader.read_float, - "double": self.reader.read_double, - "bool": self.reader.read_boolean, - } - - self.READ2 = { - "string": self.read_string, - "map": self.read_map, - "TypelessData": self.read_typeless_data, - } - - def read_u_type(self, members: list) -> dict: - i = RefInt(0) - obj = {} - while i.v < len(members): - member = members[i.v] - obj[member.name] = self.read_value(members, i) - i.v += 1 - return obj - - def read_string(self, i, align, *args): - value = self.reader.read_aligned_string() - i.v += 3 - return value, align - - def read_map(self, i, align, members, level): - if (members[i + 1].meta_flag & 0x4000) != 0: +from typing import Dict +from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter +from ctypes import c_uint32 +import base64 +import tabulate +from ..exceptions import TypeTreeError as TypeTreeError + +''' +Example Nodes: +[{ + "level": 0, + "type": "MonoBehaviour", + "name": "Base", + "meta_flag": 0 +}, +{ + "level": 1, + "type": "int", + "name": "m_SomeNode", + "meta_flag": 0 +}] + +Example TypeTree: +{ + "m_SomeNode": 1 +} +''' + +def get_nodes(nodes: list, index: int) -> list: + '''Copies all nodes above the level of the node at the set index. + + Parameters + ---------- + nodes : list + nodes/nodes of the typetree + index : int + index of the node + + Returns + ------- + list + A list of nodes + ''' + nodes2 = [nodes[index]] + level = nodes[index].level + for node in nodes[index+1:]: + if (node.level <= level): + return nodes2 + nodes2.append(node) + return nodes2 + + +def read_typetree(nodes: list, reader: EndianBinaryReader) -> dict: + '''Reads the typetree of the object contained in the reader via the node list. + + Parameters + ---------- + nodes : list + List of nodes/nodes + reader : EndianBinaryReader + Reader of the object to be parsed + + Returns + ------- + dict + The parsed typtree + ''' + # reader.reset() + if isinstance(nodes, list) and isinstance(nodes[0], dict): + nodes = node_dict_to_class(nodes) + else: + raise ValueError("nodes must be a list() of dict()-type elements") + + obj = {} + i = c_uint32(1) + while i.value < len(nodes): + node = nodes[i.value] + obj[node.name] = read_value(nodes, reader, i) + i.value += 1 + + readed = reader.Position - reader.byte_start + if readed != reader.byte_size: + raise TypeTreeError( + f"Error while read type, read {readed} bytes but expected {reader.byte_size} bytes", nodes) + + return obj + + +def read_value(nodes: list, reader: EndianBinaryReader, i: c_uint32): + node = nodes[i.value] + typ = node.type + align = (node.meta_flag & 0x4000) != 0 + + if typ == "SInt8": + value = reader.read_byte() + elif typ in ["UInt8", "char"]: + value = reader.read_u_byte() + elif typ in ["short", "SInt16"]: + value = reader.read_short() + elif typ in ["UInt16", "unsigned short"]: + value = reader.read_u_short() + elif typ in ["int", "SInt32"]: + value = reader.read_int() + elif typ in ["UInt32", "unsigned int", "Type*"]: + value = reader.read_u_int() + elif typ in ["long long", "SInt64"]: + value = reader.read_long() + elif typ in ["UInt64", "unsigned long long", "FileSize"]: + value = reader.read_u_long() + elif typ == "float": + value = reader.read_float() + elif typ == "double": + value = reader.read_double() + elif typ == "bool": + value = reader.read_boolean() + elif typ == "string": + value = reader.read_aligned_string() + i.value += 3 # Array, Size, Data(typ) + elif typ == "map": # map == MultiDict + if ((nodes[i.value + 1].meta_flag & 0x4000) != 0): align = True - size = self.reader.read_int() - map_ = get_members(members, level, i)[4:] - i.v += len(map_) + 3 - first = get_members(map_, map_[0].level, 0) - second = map_[len(first) :] - value = {} + map_ = get_nodes(nodes, i.value) + i.value += len(map_) - 1 + first = get_nodes(map_, 4) + second = get_nodes(map_, 4 + len(first)) + size = reader.read_int() + value = [None]*size for j in range(size): - tmp1 = RefInt(0) - tmp2 = RefInt(0) - v1 = self.read_value( - first, tmp1 - ) # python reads the value first and then the key, so it has to be this way - if isinstance(v1, dict): - value[first] = tmp1 - value["values"] = self.read_value(second, tmp2) - else: - value[v1] = self.read_value(second, tmp2) - return value, align - - def read_typeless_data(self, i, align, *args): - size = self.reader.read_int() - value = self.reader.read_bytes(size) - i.v += 2 - return value, align - - def read_value(self, members: list, i) -> object: - if type(i) != RefInt: - i = RefInt(i) - member = members[i.v] - level = member.level - var_type_str = member.type - align = (member.meta_flag & 0x4000) != 0 - - value = self.READ.get(var_type_str) - if value: - value = value() - else: - value = self.READ2.get(var_type_str) - if value: - value, align = value(i, align, members, level) - else: - if i != len(members) and members[i.v + 1].type == "Array": - if (members[i.v + 1].meta_flag & 0x4000) != 0: - align = True - size = self.reader.read_int() - vector = get_members(members, level, i)[3:] - i.v += len(vector) + 2 - value = [self.read_value(vector, 0) for j in range(size)] - else: - eclass = get_members(members, level, i) - eclass.pop(0) - i.v += len(eclass) - j = RefInt(0) - value = {} - while j < len(eclass): - classmember = eclass[j.v] - name = classmember.name - value[name] = self.read_value(eclass, j) - j.v += 1 - if align: - self.reader.align_stream() - return value - - def read_type_string(self, sb: list, members: list): - i = RefInt(0) - while i < len(members): - self.read_string_value(sb, members, i) - i.v += 1 - return sb - - def read_string_value(self, sb: list, members, i: RefInt): - if type(i) != RefInt: - i = RefInt(i) - member = members[i.v] - level = member.level - var_type_str = member.type - var_name_str = member.name - append = True - align = (member.meta_flag & 0x4000) != 0 - value = self.READ.get(var_type_str) - if value: - value = value() - - elif var_type_str == "string": - append = False - string = self.reader.read_aligned_string() - sb.append( - '{0}{1} {2} = "{3}"\r\n'.format( - "\t" * level, var_type_str, var_name_str, string - ) - ) - i.v += 3 - - elif var_type_str == "vector": - if (members[i + 1].meta_flag & 0x4000) != 0: + key = read_value(first, reader, c_uint32(0)) + value[j] = (key, read_value(second, reader, c_uint32(0))) + elif typ == "TypelessData": + size = reader.read_int() + value = reader.read_bytes(size) + i.value += 2 # Size == int, Data(typ) == char/uint8 + else: + # Vector + if i.value < len(nodes) - 1 and nodes[i.value + 1].type == "Array": + if (nodes[i.value + 1].meta_flag & 0x4000) != 0: align = True - append = False - sb.append("{0}{1} {2}\r\n".format("\t" * level, var_type_str, var_name_str)) - sb.append("{0}{1} {2}\r\n".format("\t" * (level + 1), "Array", "Array")) - size = self.reader.read_int() - sb.append( - "{0}{1} {2} = {3}\r\n".format("\t" * (level + 1), "int", "size", size) - ) - vector = get_members(members, level, i.v)[3:] - i.v += len(vector) + 2 - for j in range(size): - sb.append("{0}[{1}]\r\n".format("\t" * (level + 2), j)) - tmp = RefInt(0) - self.read_string_value(sb, vector, tmp) - - elif var_type_str == "map": - if (members[i + 1].meta_flag & 0x4000) != 0: + vector = get_nodes(nodes, i.value) + i.value += len(vector) - 1 + size = reader.read_int() + value = [ + read_value(vector, reader, c_uint32(3)) + for _ in range(size) + ] + else: # Class + clz = get_nodes(nodes, i.value) + i.value += len(clz) - 1 + value = {} + j = c_uint32(1) + while j.value < len(clz): + clz_node = clz[j.value] + value[clz_node.name] = read_value(clz, reader, j) + j.value += 1 + + if align: + reader.align_stream() + return value + + +def read_typetree_str(sb: list, nodes: list, reader: EndianBinaryReader) -> list: + '''Reads the typetree of the object contained in the reader via the node list and dumps it as string. + + Parameters + ---------- + sb : list + StringBuilder - a list used to build the string dump, should be empty + nodes : list + List of nodes/nodes + reader : EndianBinaryReader + Reader of the object to be parsed + + Returns + ------- + list + The sb given as input + ''' + # reader.reset() + if isinstance(nodes, list) and isinstance(nodes[0], dict): + nodes = node_dict_to_class(nodes) + else: + raise ValueError("nodes must be a list() of dict()-type elements") + + i = c_uint32(0) + while i.value < len(nodes): + read_value_str(sb, nodes, reader, i) + i.value += 1 + + readed = reader.Position - reader.byte_start + if readed != reader.byte_size: + raise TypeTreeError( + f"Error while read type, read {readed} bytes but expected {reader.byte_size} bytes", nodes) + + return sb + + +def read_value_str(sb: list, nodes: list, reader: EndianBinaryReader, i: c_uint32) -> list: + node = nodes[i.value] + typ = node.type + align = (node.meta_flag & 0x4000) != 0 + append = True + + if typ == "SInt8": + value = reader.read_byte() + elif typ in ["UInt8", "char"]: + value = reader.read_u_byte() + elif typ in ["short", "SInt16"]: + value = reader.read_short() + elif typ in ["UInt16", "unsigned short"]: + value = reader.read_u_short() + elif typ in ["int", "SInt32"]: + value = reader.read_int() + elif typ in ["UInt32", "unsigned int", "Type*"]: + value = reader.read_u_int() + elif typ in ["long long", "SInt64"]: + value = reader.read_long() + elif typ in ["UInt64", "unsigned long long", "FileSize"]: + value = reader.read_u_long() + elif typ == "float": + value = reader.read_float() + elif typ == "double": + value = reader.read_double() + elif typ == "bool": + value = reader.read_boolean() + elif typ == "string": + value = reader.read_aligned_string() + i.value += 3 # Array, Size, Data(typ) + append = False + sb.append('{0}{1} {2} = "{3}"\r\n'.format( + "\t" * node.level, node.type, node.name, value + )) + elif typ == "map": + if ((nodes[i.value + 1].meta_flag & 0x4000) != 0): + align = True + map_ = get_nodes(nodes, i.value) + i.value += len(map_) - 1 + first = get_nodes(map_, 4) + second = get_nodes(map_, 4 + len(first)) + size = reader.read_int() + append = False + sb.append("{0}{1} {2}\r\n".format( + "\t" * node.level, node.type, node.name)) + sb.append("{0}{1} {2}\r\n".format( + "\t" * (node.level + 1), "Array", "Array")) + sb.append("{0}{1} {2} = {3}\r\n".format( + "\t" * (node.level + 1), "int", "size", size)) + for j in range(size): + sb.append("{0}[{1}]\r\n".format("\t" * (node.level + 2), j)) + sb.append("{0}{1} {2}\r\n".format( + "\t" * (node.level + 2), "pair", "data")) + read_value_str(sb, first, reader, c_uint32(0)) + read_value_str(sb, second, reader, c_uint32(0)) + elif typ == "TypelessData": + size = reader.read_int() + value = reader.read_bytes(size) + i.value += 2 # Size == int, Data(typ) == char/uint8 + append = False + sb.append("{0}{1} {2}\r\n".format( + "\t" * node.level, node.type, node.name)) + sb.append("{0}{1} {2} = {3}\r\n".format( + "\t" * node.level, "int", "size", size)) + # sb.append("{0}{1} {2} = {3}\r\n".format( + # "\t" * node.level, "UInt8", "data", base64.b64encode(value))) + else: + # Vector + if i.value < len(nodes) - 1 and nodes[i.value + 1].type == "Array": + if (nodes[i.value + 1].meta_flag & 0x4000) != 0: align = True + vector = get_nodes(nodes, i.value) + i.value += len(vector) - 1 + size = reader.read_int() append = False - sb.append("{0}{1} {2}\r\n".format("\t" * level, var_type_str, var_name_str)) - sb.append("{0}{1} {2}\r\n".format("\t" * (level + 1), "Array", "Array")) - size = self.reader.read_int() - sb.append( - "{0}{1} {2} = {3}\r\n".format("\t" * (level + 1), "int", "size", size) - ) - map_ = get_members(members, level, i.v)[4:] - i.v += len(map_) + 3 - first = get_members(map_, map_[0].level, 0) - second = map_[len(first) :] + sb.append("{0}{1} {2}\r\n".format( + "\t" * node.level, node.type, node.name)) + sb.append("{0}{1} {2}\r\n".format( + "\t" * (node.level + 1), "Array", "Array")) + sb.append("{0}{1} {2} = {3}\r\n".format( + "\t" * (node.level + 1), "int", "size", size)) for j in range(size): - sb.append("{0}[{1}]\r\n".format("\t" * (level + 2), j)) - sb.append("{0}{1} {2}\r\n".format("\t" * (level + 2), "pair", "data")) - tmp1 = RefInt(0) - tmp2 = RefInt(0) - self.read_string_value(sb, first, tmp1) - self.read_string_value(sb, second, tmp2) - - elif var_type_str == "TypelessData": + sb.append("{0}[{1}]\r\n".format("\t" * (node.level + 2), j)) + read_value_str(sb, vector, reader, c_uint32(3)) + + else: # Class + clz = get_nodes(nodes, i.value) + i.value += len(clz) - 1 + j = c_uint32(1) append = False - size = self.reader.read_int() - value = self.reader.read_bytes(size) - i.v += 2 - sb.append("{0}{1} {2}\r\n".format("\t" * level, var_type_str, var_name_str)) - sb.append("{0}{1} {2} = {3}\r\n".format("\t" * level, "int", "size", size)) - - else: - if i != len(members) and members[i + 1].type == "Array": - if (members[i + 1].meta_flag & 0x4000) != 0: - align = True - append = False - sb.append( - "{0}{1} {2}\r\n".format("\t" * level, var_type_str, var_name_str) - ) - sb.append("{0}{1} {2}\r\n".format("\t" * (level + 1), "Array", "Array")) - size = self.reader.read_int() - sb.append( - "{0}{1} {2} = {3}\r\n".format( - "\t" * (level + 1), "int", "size", size - ) - ) - vector = get_members(members, level, i.v) - i.v += len(vector) - 1 - vector = vector[3:] # vector.RemoveRange(0, 3) - for j in range(size): - sb.append("{0}[{1}]\r\n".format("\t" * (level + 2), j)) - tmp = RefInt(0) - self.read_string_value(sb, vector, tmp) - else: - append = False - sb.append( - "{0}{1} {2}\r\n".format("\t" * level, var_type_str, var_name_str) - ) - eclass = get_members(members, level, i.v) - eclass.pop(0) # .RemoveAt(0) - i.v += len(eclass) - j = RefInt(0) - while j < len(eclass): - self.read_string_value(sb, eclass, j) - j.v += 1 - - if append: - sb.append( - "{0}{1} {2} = {3}\r\n".format( - "\t" * level, var_type_str, var_name_str, value - ) - ) - if align: - self.reader.align_stream() - - return sb - - -""" - def write_u_type(self, obj: dict, members: list) -> bytes: - stream = io.BytesIO() - write = EndianBinaryReader(stream) - for i, member in enumerate(members): - var_name_str = member.name - write_value(obj[var_name_str], members, write, RefInt(i)) - pos = stream.tell() - stream.seek(0) - ret = stream.read() - stream.seek(pos) - return ret - - def write_value(value: dict, members: list, write: EndianBinaryReader, i): - member = members[i.v] - level = member.level - var_type_str = member.type - align = (member.meta_flag & 0x4000) != 0 - if var_type_str == "SInt8": - write.write_s_byte(value) - elif var_type_str == "UInt8": - write.write_byte(value) - elif var_type_str in ["short", "SInt16"]: - write.write_short(value) - elif var_type_str in ["UInt16", "unsigned short"]: - write.write_u_short(value) - elif var_type_str in ["int", "SInt32"]: - write.write_int(value) - elif var_type_str in ["UInt32", "unsigned int", "Type*"]: - write.write_u_int(value) - elif var_type_str in ["long long", "SInt64"]: - write.write_long(value) - elif var_type_str in ["UInt64", "unsigned long long"]: - write.write_u_long(value) - elif var_type_str == "float": - write.write_float(value) - elif var_type_str == "double": - write.write_double(value) - elif var_type_str == "bool": - write.write_bool(value) - elif var_type_str == "string": - write.write_aligned_string(value) - i.v += 3 - elif var_type_str == "map": - if (members[i + 1].meta_flag & 0x4000) != 0: - align = True - dic = list(value.items()) - size = len(dic) - write.Write(size) - map_ = get_members(members, level, i) - i.v += len(map_) - 1 - map_ = map_[4:] # map.RemoveRange(0, 4) - first = get_members(map_, map_[0].level, 0) - map_ = map_[len(first):] # .RemoveRange(0, len(first)) - second = map_ - for j in range(size): - tmp1 = RefInt(0) - tmp2 = RefInt(0) - write_value(dic[j][0], first, write, tmp1) # key - write_value(dic[j][1], second, write, tmp2) # value - elif var_type_str == "TypelessData": - byts = bytes(value) - size = len(byts) - write.Write(size) - write.Write(bytes) - i.v += 2 - else: - if i != len(members) and members[i + 1].type == "Array": # Array - if (members[i.v + 1].meta_flag & 0x4000) != 0: - align = True - list_ = value - size = len(list_) - write.Write(size) - vector = get_members(members, level, i) - i.v += len(vector) - 1 - vector = vector[3:] # vector.RemoveRange(0, 3) - for j in range(size): - tmp = RefInt(0) - write_value(list_[j], vector, write, tmp) - else: # Class - eclass = get_members(members, level, i) - eclass.pop(0) # .RemoveAt(0) - i.v += len(eclass) - obj = value - j = RefInt(0) - while j < len(eclass): - classmember = eclass[j.v] - name = classmember.name - write_value(obj[name], eclass, write, j) - if align: - write.align_stream(4) - - -""" + sb.append("{0}{1} {2}\r\n".format( + "\t" * node.level, node.type, node.name)) + while j.value < len(clz): + read_value_str(sb, clz, reader, j) + j.value += 1 + + if append: + sb.append("{0}{1} {2} = {3}\r\n".format( + "\t" * node.level, node.type, node.name, value + )) + + if align: + reader.align_stream() + return sb + + +def dump_typetree(nodes) -> str: + '''Dumps the structure of the given nodes. + + Parameters + ---------- + nodes : list + List of nodes/nodes + + Returns + ------- + str + The dumped structure + ''' + field_names = ["level", "type", "name", "meta_flag"] + rows = [ + [ + getattr(x, key) for key in field_names + ] + for x in nodes + ] + return tabulate.tabulate(rows, headers=field_names) + + +def write_typetree(obj: dict, nodes: list, writer: EndianBinaryWriter = None) -> EndianBinaryWriter: + '''Writes the data of the object via the given typetree of the object into the writer. + + Parameters + ---------- + obj : dict + Object to be saved + nodes : list + List of nodes/nodes + writer : EndianBinaryWriter + Writer of the object to be saved + + Returns + ------- + EndianBinaryWriter + The writer that was used to save the data of the given object. + ''' + if not writer: + writer = EndianBinaryWriter() + + if isinstance(nodes, list) and isinstance(nodes[0], dict): + nodes = node_dict_to_class(nodes) + else: + raise ValueError("nodes must be a list() of dict()-type elements") + + i = c_uint32(1) + while i.value < len(nodes): + value = obj[nodes[i.value].name] + write_value(value, nodes, writer, i) + i.value += 1 + return writer + + +def write_value(value, nodes: list, writer: EndianBinaryWriter, i: c_uint32): + node = nodes[i.value] + typ = node.type + align = (node.meta_flag & 0x4000) != 0 + + if typ == "SInt8": + writer.write_byte(value) + elif typ in ["UInt8", "char"]: + writer.write_u_byte(value) + elif typ in ["short", "SInt16"]: + writer.write_short(value) + elif typ in ["UInt16", "unsigned short"]: + writer.write_u_short(value) + elif typ in ["int", "SInt32"]: + writer.write_int(value) + elif typ in ["UInt32", "unsigned int", "Type*"]: + writer.write_u_int(value) + elif typ in ["long long", "SInt64"]: + writer.write_long(value) + elif typ in ["UInt64", "unsigned long long", "FileSize"]: + writer.write_u_long(value) + elif typ == "float": + writer.write_float(value) + elif typ == "double": + writer.write_double(value) + elif typ == "bool": + writer.write_boolean(value) + elif typ == "string": + writer.write_aligned_string(value) + i.value += 3 # Array, Size, Data(typ) + elif typ == "map": + if ((nodes[i.value + 1].meta_flag & 0x4000) != 0): + align = True + map_ = get_nodes(nodes, i.value) + i.value += len(map_) - 1 + first = get_nodes(map_, 4) + second = get_nodes(map_, 4 + len(first)) + # size + writer.write_int(len(value)) + # data + for key, val in value: + write_value(key, first, writer, c_uint32(0)) + write_value(val, second, writer, c_uint32(0)) + + elif typ == "TypelessData": + writer.write_int(len(value)) + writer.write_bytes(value) + i.value += 2 # Size == int, Data(typ) == char/uint8 + else: + # Vector + if i.value < len(nodes) - 1 and nodes[i.value + 1].type == "Array": + if (nodes[i.value + 1].meta_flag & 0x4000) != 0: + align = True + vector = get_nodes(nodes, i.value) + i.value += len(vector) - 1 + writer.write_int(len(value)) + for val in value: + write_value(val, vector, writer, c_uint32(3)) + else: # Class + clz = get_nodes(nodes, i.value) + i.value += len(clz) - 1 + j = c_uint32(1) + while j.value < len(clz): + val = value[clz[j.value].name] + write_value(val, clz, writer, j) + j.value += 1 + + if align: + writer.align_stream() + +class FakeNode: + def __init__(self, data) -> None: + self.__dict__ = data + +def node_dict_to_class(nodes: list): + return [FakeNode(node) for node in nodes] + diff --git a/UnityPy/helpers/__init__.py b/UnityPy/helpers/__init__.py index 23b7fa46..c67ce73f 100644 --- a/UnityPy/helpers/__init__.py +++ b/UnityPy/helpers/__init__.py @@ -1,2 +1 @@ -from . import CompressionHelper, ImportHelper -from .TypeTreeHelper import TypeTreeHelper +from . import CompressionHelper, ImportHelper, TypeTreeHelper diff --git a/UnityPy/math/Color.py b/UnityPy/math/Color.py index f4a4dfa4..ff008fda 100644 --- a/UnityPy/math/Color.py +++ b/UnityPy/math/Color.py @@ -13,9 +13,6 @@ def __init__(self, r: float, g: float, b: float, a: float): self.B = b self.A = a - def GetHashCode(self): - return Vector4(self).GetHashCode() - def __eq__(self, other): if isinstance(other, Color): return self.__dict__ == other.__dict__ diff --git a/UnityPy/math/Half.py b/UnityPy/math/Half.py index 8702e178..6d56e1b9 100644 --- a/UnityPy/math/Half.py +++ b/UnityPy/math/Half.py @@ -1,10 +1,5 @@ import struct - -from UnityPy import math - -# import binascii - -# import numpy +import math MaxValue = 65504.0 MinValue = -65504.0 @@ -12,13 +7,13 @@ def ToHalf(*args) -> float: """ - Converts the input into a half-float. - Inputs: - unsigned integer - or - buffer (bytes, buffer) - offset - """ + Converts the input into a half-float. + Inputs: + unsigned integer + or + buffer (bytes, buffer) + offset + """ # int input -> pack as UInt16 if len(args) == 1: data = struct.pack("H", args[0]) @@ -26,6 +21,8 @@ def ToHalf(*args) -> float: # buffer input elif len(args) == 2: val = struct.unpack_from("e", args[0], args[1])[0] + else: + raise ValueError("Invalid amount of arguments") if math.isnan(val): # print('Nan') @@ -45,61 +42,61 @@ def ToHalf(*args) -> float: # PositiveInfinity = ToHalf(0x7c00) # class Float16Compressor: -# def __init__(self): -# self.temp = 0 - -# def compress(self, float32): -# F16_EXPONENT_BITS = 0x1F -# F16_EXPONENT_SHIFT = 10 -# F16_EXPONENT_BIAS = 15 -# F16_MANTISSA_BITS = 0x3ff -# F16_MANTISSA_SHIFT = (23 - F16_EXPONENT_SHIFT) -# F16_MAX_EXPONENT = (F16_EXPONENT_BITS << F16_EXPONENT_SHIFT) - -# a = struct.pack('>f', float32) -# b = binascii.hexlify(a) - -# f32 = int(b, 16) -# f16 = 0 -# sign = (f32 >> 16) & 0x8000 -# exponent = ((f32 >> 23) & 0xff) - 127 -# mantissa = f32 & 0x007fffff - -# if exponent == 128: -# f16 = sign | F16_MAX_EXPONENT -# if mantissa: -# f16 |= (mantissa & F16_MANTISSA_BITS) -# elif exponent > 15: -# f16 = sign | F16_MAX_EXPONENT -# elif exponent > -15: -# exponent += F16_EXPONENT_BIAS -# mantissa >>= F16_MANTISSA_SHIFT -# f16 = sign | exponent << F16_EXPONENT_SHIFT | mantissa -# else: -# f16 = sign -# return f16 - -# def decompress(self, float16): -# s = int((float16 >> 15) & 0x00000001) # sign -# e = int((float16 >> 10) & 0x0000001f) # exponent -# f = int(float16 & 0x000003ff) # fraction - -# if e == 0: -# if f == 0: -# return int(s << 31) -# else: -# while not (f & 0x00000400): -# f = f << 1 -# e -= 1 -# e += 1 -# f &= ~0x00000400 -# # print(s,e,f) -# elif e == 31: -# if f == 0: -# return int((s << 31) | 0x7f800000) -# else: -# return int((s << 31) | 0x7f800000 | (f << 13)) - -# e = e + (127 - 15) -# f = f << 13 -# return int((s << 31) | (e << 23) | f) +# def __init__(self): +# self.temp = 0 + +# def compress(self, float32): +# F16_EXPONENT_BITS = 0x1F +# F16_EXPONENT_SHIFT = 10 +# F16_EXPONENT_BIAS = 15 +# F16_MANTISSA_BITS = 0x3ff +# F16_MANTISSA_SHIFT = (23 - F16_EXPONENT_SHIFT) +# F16_MAX_EXPONENT = (F16_EXPONENT_BITS << F16_EXPONENT_SHIFT) + +# a = struct.pack('>f', float32) +# b = binascii.hexlify(a) + +# f32 = int(b, 16) +# f16 = 0 +# sign = (f32 >> 16) & 0x8000 +# exponent = ((f32 >> 23) & 0xff) - 127 +# mantissa = f32 & 0x007fffff + +# if exponent == 128: +# f16 = sign | F16_MAX_EXPONENT +# if mantissa: +# f16 |= (mantissa & F16_MANTISSA_BITS) +# elif exponent > 15: +# f16 = sign | F16_MAX_EXPONENT +# elif exponent > -15: +# exponent += F16_EXPONENT_BIAS +# mantissa >>= F16_MANTISSA_SHIFT +# f16 = sign | exponent << F16_EXPONENT_SHIFT | mantissa +# else: +# f16 = sign +# return f16 + +# def decompress(self, float16): +# s = int((float16 >> 15) & 0x00000001) # sign +# e = int((float16 >> 10) & 0x0000001f) # exponent +# f = int(float16 & 0x000003ff) # fraction + +# if e == 0: +# if f == 0: +# return int(s << 31) +# else: +# while not (f & 0x00000400): +# f = f << 1 +# e -= 1 +# e += 1 +# f &= ~0x00000400 +# # print(s,e,f) +# elif e == 31: +# if f == 0: +# return int((s << 31) | 0x7f800000) +# else: +# return int((s << 31) | 0x7f800000 | (f << 13)) + +# e = e + (127 - 15) +# f = f << 13 +# return int((s << 31) | (e << 23) | f) diff --git a/UnityPy/math/Quaternion.py b/UnityPy/math/Quaternion.py index ceed30df..e315785f 100644 --- a/UnityPy/math/Quaternion.py +++ b/UnityPy/math/Quaternion.py @@ -3,102 +3,48 @@ class Quaternion: Y: float Z: float W: float - - def __init__(self, x: float, y: float, z: float, w: float): + + def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 0.0): + self._data = [0.0] * 4 self.X = x self.Y = y self.Z = z self.W = w - - -""" -using System; -using System.Runtime.InteropServices; - -namespace AssetStudio -{ - [StructLayout(LayoutKind.Sequential, Pack = 4)] - public struct Quaternion : IEquatable - { - public float X; - public float Y; - public float Z; - public float W; - - public Quaternion(float x, float y, float z, float w) - { - X = x; - Y = y; - Z = z; - W = w; - } - - public float this[int index] - { - get - { - switch (index) - { - case 0: return X; - case 1: return Y; - case 2: return Z; - case 3: return W; - default: throw new ArgumentOutOfRangeException(nameof(index), "Invalid Quaternion index!"); - } - } - - set - { - switch (index) - { - case 0: X = value; break; - case 1: Y = value; break; - case 2: Z = value; break; - case 3: W = value; break; - default: throw new ArgumentOutOfRangeException(nameof(index), "Invalid Quaternion index!"); - } - } - } - - public override int GetHashCode() - { - return X.GetHashCode() ^ (Y.GetHashCode() << 2) ^ (Z.GetHashCode() >> 2) ^ (W.GetHashCode() >> 1); - } - - public override bool Equals(object other) - { - if (!(other is Quaternion)) - return false; - return Equals((Quaternion)other); - } - - public bool Equals(Quaternion other) - { - return X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z) && W.Equals(other.W); - } - - public static float Dot(Quaternion a, Quaternion b) - { - return a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W; - } - - private static bool IsEqualUsingDot(float dot) - { - return dot > 1.0f - kEpsilon; - } - - public static bool operator ==(Quaternion lhs, Quaternion rhs) - { - return IsEqualUsingDot(Dot(lhs, rhs)); - } - - public static bool operator !=(Quaternion lhs, Quaternion rhs) - { - return !(lhs == rhs); - } - - private const float kEpsilon = 0.000001F; - } -} - -""" + + @property + def X(self) -> float: + return self._data[0] + + @X.setter + def X(self, value: float): + self._data[0] = value + + @property + def Y(self) -> float: + return self._data[1] + + @Y.setter + def Y(self, value: float): + self._data[1] = value + + @property + def Z(self) -> float: + return self._data[2] + + @Z.setter + def Z(self, value: float): + self._data[2] = value + + @property + def W(self) -> float: + return self._data[3] + + @W.setter + def W(self, value: float): + self._data[3] = value + + def __getitem__(self, value): + return self._data[value] + + def __setitem__(self, index, value): + self._data[index] = value diff --git a/UnityPy/math/Rectangle.py b/UnityPy/math/Rectangle.py index 3bab0dc8..d32ca639 100644 --- a/UnityPy/math/Rectangle.py +++ b/UnityPy/math/Rectangle.py @@ -12,9 +12,9 @@ def __init__(self, *args, **kwargs): elif kwargs: self.__dict__.update(kwargs) - def round(rect): + def round(self): return Rectangle( - round(rect.x), round(rect.y), round(rect.width), round(rect.height) + round(self.x), round(self.y), round(self.width), round(self.height) ) @property @@ -35,8 +35,8 @@ def bottom(self): @property def size(self): - return (self.width, self.height) + return self.width, self.height @property def location(self): - return (self.x, self.y) + return self.x, self.y diff --git a/UnityPy/streams/EndianBinaryReader.py b/UnityPy/streams/EndianBinaryReader.py index adc597f5..98125815 100644 --- a/UnityPy/streams/EndianBinaryReader.py +++ b/UnityPy/streams/EndianBinaryReader.py @@ -15,13 +15,13 @@ def __new__(cls, item, endian=">", offset=0): obj = super(EndianBinaryReader, cls).__new__( EndianBinaryReader_Memoryview) else: - obj = obj = super(EndianBinaryReader, cls).__new__( + obj = super(EndianBinaryReader, cls).__new__( EndianBinaryReader_Streamable ) obj.__init__(item, endian) return obj - def __init__(self, endian=">", offset=0): + def __init__(self, item, endian=">", offset=0): self.endian = endian self.BaseOffset = offset self.Position = 0 @@ -71,7 +71,7 @@ def read_double(self) -> float: def read_boolean(self) -> bool: return bool(struct.unpack(self.endian + "?", self.read(1))[0]) - def read_string(self, size=None, encoding="utf-8") -> str: + def read_string(self, size=None, encoding="utf8") -> str: if size is None: ret = self.read_string_to_null() else: @@ -89,22 +89,19 @@ def read_string_to_null(self, max_length=32767) -> str: c = self.read(1) if not c: raise ValueError("Unterminated string: %r" % ret) - return b"".join(ret).decode("utf8", "replace") + return b"".join(ret).decode("utf8", "surrogateescape") def read_aligned_string(self): length = self.read_int() if 0 < length <= self.Length - self.Position: - string_data = self.read_bytes(length) - result = string_data.decode("utf8", "backslashreplace") + string_data = bytes(self.read_bytes(length)) + result = string_data.decode("utf8", "surrogateescape") self.align_stream() return result return "" def align_stream(self, alignment=4): - pos = self.Position - mod = pos % alignment - if mod != 0: - self.Position += alignment - mod + self.Position += (alignment - self.Position % alignment) % alignment def read_quaternion(self): return Quaternion( @@ -132,11 +129,14 @@ def read_color4(self): self.read_float(), self.read_float(), self.read_float(), self.read_float() ) + def read_byte_array(self): + return self.read(self.read_int()) + def read_matrix(self): return Matrix4x4(self.read_float_array(16)) def read_array(self, command, length: int): - return [command() for i in range(length)] + return [command() for _ in range(length)] def read_boolean_array(self): return self.read_array(self.read_boolean, self.read_int()) @@ -176,12 +176,16 @@ def real_offset(self): """ return self.BaseOffset + self.Position + def read_the_rest(self, obj_start, obj_size): + """ Returns the rest of the current reader bytes.""" + return self.read_bytes(obj_size - (self.Position - obj_start)) + class EndianBinaryReader_Memoryview(EndianBinaryReader): view: memoryview def __init__(self, view, endian=">", offset=0): - super().__init__(endian=endian, offset=offset) + super().__init__(view, endian=endian, offset=offset) self.view = memoryview(view) self.Length = len(view) @@ -193,6 +197,8 @@ def dispose(self): self.view.release() def read(self, length: int): + if not length: + return b"" ret = self.view[self.Position: self.Position + length] self.Position += length return ret @@ -201,7 +207,7 @@ def read_aligned_string(self): length = self.read_int() if 0 < length <= self.Length - self.Position: string_data = self.read_bytes(length) - result = string_data.tobytes().decode("utf8", "backslashreplace") + result = bytes(string_data).decode("utf8", "surrogateescape") self.align_stream() return result return "" @@ -213,7 +219,7 @@ class EndianBinaryReader_Streamable(EndianBinaryReader): def __init__(self, stream, endian=">", offset=0): self.stream = stream self.Length = self.stream.seek(0, 2) - offset - super().__init__(endian=endian, offset=offset) + super().__init__(stream, endian=endian, offset=offset) def get_position(self): return self.stream.tell() @@ -227,7 +233,7 @@ def set_position(self, value): def bytes(self): last_pos = self.Position self.Position = 0 - ret = self.read() + ret = self.read(self.Length) self.Position = last_pos return ret @@ -235,10 +241,8 @@ def dispose(self): self.stream.close() pass - def read(self, *args): - _data = self.stream.read(*args) - # TODO: the switch in a debug module - if False and len(args) > 0 and len(_data) != args[0]: - raise EOFError("got only %d bytes out of %d requested" % - (len(_data), args[0])) - return _data + def read(self, length: int): + if not length: + return b"" + return self.stream.read(length) + diff --git a/UnityPy/streams/EndianBinaryWriter.py b/UnityPy/streams/EndianBinaryWriter.py index b8b6048c..c8b921c5 100644 --- a/UnityPy/streams/EndianBinaryWriter.py +++ b/UnityPy/streams/EndianBinaryWriter.py @@ -79,32 +79,20 @@ def write_double(self, value: float): def write_boolean(self, value: bool): self.write(struct.pack(self.endian + "?", value)) - """ - def write_string(self, size=None, encoding="utf-8") -> str: - if size is None: - ret = self.write_string_to_null() - else: - ret = struct.unpack(f"{self.endian}{size}is", self.read(size))[0] - try: - return ret.decode(encoding) - except UnicodeDecodeError: - return ret - """ - def write_string_to_null(self, value: str): - self.write(value.encode("utf8")) + self.write(value.encode("utf8", "surrogateescape")) self.write(b"\0") def write_aligned_string(self, value: str): - self.write_int(len(value)) - self.write(value.encode("utf8", "backslashreplace")) + bstring = value.encode("utf8", "surrogateescape") + self.write_int(len(bstring)) + self.write(bstring) self.align_stream(4) def align_stream(self, alignment=4): pos = self.stream.tell() - mod = pos % alignment - if mod != 0: - self.write(b"\0" * (alignment - mod)) + align = (alignment - pos % alignment) % alignment + self.write(b"\0" * align) def write_quaternion(self, value: Quaternion): self.write_float(value.X) @@ -149,6 +137,10 @@ def write_array(self, command, value: list, write_length: bool = True): for val in value: command(val) + def write_byte_array(self, value: bytes): + self.write_int(len(value)) + self.write(value) + def write_boolean_array(self, value: list): self.write_array(self.write_boolean, value) diff --git a/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json new file mode 100644 index 00000000..b1b654e8 --- /dev/null +++ b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json @@ -0,0 +1,72 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v3.1/win-x64", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v3.1": {}, + ".NETCoreApp,Version=v3.1/win-x64": { + "TypeTreeGenerator/1.0.0": { + "dependencies": { + "ILRepack": "2.0.18", + "ILRepack.Lib.MSBuild.Task": "2.0.18.2", + "Mono.Cecil": "0.11.3" + }, + "runtime": { + "TypeTreeGenerator.dll": {} + } + }, + "ILRepack/2.0.18": {}, + "ILRepack.Lib.MSBuild.Task/2.0.18.2": {}, + "Mono.Cecil/0.11.3": { + "runtime": { + "lib/netstandard2.0/Mono.Cecil.Mdb.dll": { + "assemblyVersion": "0.11.3.0", + "fileVersion": "0.11.3.0" + }, + "lib/netstandard2.0/Mono.Cecil.Pdb.dll": { + "assemblyVersion": "0.11.3.0", + "fileVersion": "0.11.3.0" + }, + "lib/netstandard2.0/Mono.Cecil.Rocks.dll": { + "assemblyVersion": "0.11.3.0", + "fileVersion": "0.11.3.0" + }, + "lib/netstandard2.0/Mono.Cecil.dll": { + "assemblyVersion": "0.11.3.0", + "fileVersion": "0.11.3.0" + } + } + } + } + }, + "libraries": { + "TypeTreeGenerator/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "ILRepack/2.0.18": { + "type": "package", + "serviceable": true, + "sha512": "sha512-sR5Aj3JLDbA8JwESfWYfigSz5k1oNSHPN434W1LX8sboWJYEOSQP/KkvZGmJKPgajzSUKkl2jDar3LPZiMFU4Q==", + "path": "ilrepack/2.0.18", + "hashPath": "ilrepack.2.0.18.nupkg.sha512" + }, + "ILRepack.Lib.MSBuild.Task/2.0.18.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-lreK19MMDA/ekeqjLY4w171cG7+pHwjeTMHMNq6tghuhTOIOfHT+b/LTMj5jpfbJoR7J6AMD3yOYZoyY/2wtJA==", + "path": "ilrepack.lib.msbuild.task/2.0.18.2", + "hashPath": "ilrepack.lib.msbuild.task.2.0.18.2.nupkg.sha512" + }, + "Mono.Cecil/0.11.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-DNYE+io5XfEE8+E+5padThTPHJARJHbz1mhbhMPNrrWGKVKKqj/KEeLvbawAmbIcT73NuxLV7itHZaYCZcVWGg==", + "path": "mono.cecil/0.11.3", + "hashPath": "mono.cecil.0.11.3.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.dll b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.dll new file mode 100644 index 00000000..f9df5439 Binary files /dev/null and b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.dll differ diff --git a/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb new file mode 100644 index 00000000..527ffa1a Binary files /dev/null and b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb differ diff --git a/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json new file mode 100644 index 00000000..bc456d78 --- /dev/null +++ b/examples/MonoBehavioursUsingAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json @@ -0,0 +1,9 @@ +{ + "runtimeOptions": { + "tfm": "netcoreapp3.1", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "3.1.0" + } + } +} \ No newline at end of file diff --git a/examples/MonoBehavioursUsingAssembly/dump_assets.py b/examples/MonoBehavioursUsingAssembly/dump_assets.py new file mode 100644 index 00000000..8404657f --- /dev/null +++ b/examples/MonoBehavioursUsingAssembly/dump_assets.py @@ -0,0 +1,137 @@ +import os, sys, json +from glob import glob +import UnityPy +from collections import Counter +import zipfile +from tqdm import tqdm + +TYPES = ['TextAsset', 'MonoBehaviour', 'Texture2D', 'Shader'] + +ROOT = os.path.abspath(os.getcwd()) # base directory +DST = os.path.join(ROOT, "output") # destination folder +ASSETS = glob(os.path.join(ROOT,"*.assets")) + glob(os.path.join(ROOT,"data.unity3d")) # sources +ASSEMBLY_TREES = dict() +if os.path.isfile("assembly_typetrees.json"): + with open("assembly_typetrees.json", "r", encoding="utf-8-sig") as f: + ASSEMBLY_TREES = json.loads(f.read()) + +def main(): + os.makedirs(DST, exist_ok=True) + for file_name in ASSETS: + extension = os.path.splitext(file_name)[1] + src = os.path.realpath(os.path.join(ROOT, file_name)) + + am = None + if extension == ".zip": + archive = zipfile.ZipFile(src, 'r') + for zf in archive.namelist(): + am = UnityPy.load(archive.open(zf)) + print("Parsing file:", zf) + else: + am = UnityPy.load(src) + print("Parsing file:", src) + if am is None: + continue + #am.out_path = DST + am.ignore_dir_lvls = 2 + am.progress_function = tqdm + am.process(export_obj, TYPES) + #am.save() + +class FakeNode: + """A fake/minimal Node class for use in UnityPy.""" + + def __init__(self, **kwargs): + self.__dict__.update(**kwargs) + +def make_path(*args): + fp = os.path.join(*args) + os.makedirs(os.path.dirname(fp), exist_ok=True) + return fp + +def export_obj(obj, asset: str, local_path: str) -> list: + objfmt = str(obj.type) + + data = obj.read() + name = data.name if (data.name is not None and data.name != '') else "unnamed asset" + fname, extension = os.path.splitext(name) + objname = "%s-%s-%d" % (fname, asset, obj.path_id) + + if objfmt == "TextAsset": + if data.script: + fp = f"{make_path(DST, local_path, os.path.split(fname)[0], objname)}.txt" + if not os.path.isfile(fp): + with open(fp, "wb") as f: + f.write(data.script) + + elif objfmt == "Texture2D": + fp = f"{make_path(DST, local_path, fname)}.png" + if not os.path.isfile(fp): + try: + data.image.save(fp) + except Exception as e: + if data.m_TextureFormat.name is not None: + objfmt = data.m_TextureFormat.name + print(repr(e), "in file:", objname, "object type:", objfmt) + return [] + + elif objfmt == "Sprite": + fp = f"{make_path(DST, local_path, fname)}.png" + if not os.path.isfile(fp): + data.image.save(fp) + + elif objfmt == "MonoBehaviour": + is_raw = True + script = None + if obj.serialized_type.nodes: + try: + tree = obj.read_typetree() + is_raw = False + except Exception as e: + #print("Error", str(e), "in", objname) + pass + if not data.m_Script: + # RIP, no referenced script, can only dump raw + pass + else: + script = data.m_Script.read() + if not is_raw or not script or ( + ASSEMBLY_TREES and script.m_ClassName not in ASSEMBLY_TREES): + # TypeTree already found + # or + # class not found in known ASSEMBLY_TREES, + # so we have to add the classes from some other dlls + pass + elif ASSEMBLY_TREES: + nodes = ASSEMBLY_TREES[script.m_ClassName] + try: + tree = obj.read_typetree(nodes) + is_raw = False + except Exception as e: + #print("Error", str(e), "in", objname) + pass + + if is_raw: + extension = "dat" + export = data.get_raw_data() + else: + extension = "json" + export = json.dumps(tree, indent=4, ensure_ascii=False).encode("utf-8-sig") + + if script: + fp = f"{make_path(DST, local_path, script.m_Namespace, script.m_ClassName, objname)}.{extension}" + else: + fp = f"{make_path(DST, local_path, objname)}.{extension}" + if not os.path.isfile(fp): + with open(fp, "wb") as f: + f.write(export) + + #else: + # fp = "%s-%s-%d" % (asset, obj.path_id, obj.type) + + #print("writing", obj.type, "to", fp, "format", objfmt) + return [obj.path_id] + + +if __name__ == '__main__': + main() diff --git a/examples/MonoBehavioursUsingAssembly/dump_dll_typetrees.py b/examples/MonoBehavioursUsingAssembly/dump_dll_typetrees.py new file mode 100644 index 00000000..01158efc --- /dev/null +++ b/examples/MonoBehavioursUsingAssembly/dump_dll_typetrees.py @@ -0,0 +1,130 @@ +# py3 +# requirements: +# pythonnet 3+ +# pip install git+https://github.com/pythonnet/pythonnet/ +# TypeTreeGenerator +# https://github.com/K0lb3/TypeTreeGenerator +# requires .NET 5.0 SDK +# https://dotnet.microsoft.com/download/dotnet/5.0 +# +# pythonnet 2 and TypeTreeGenerator created with net4.8 works on Windows, +# so it can do without pythonnet_init, +# all other systems need pythonnet 3 and either .net 5 or .net core 3 and pythonnet_init + +import os +import UnityPy +from typing import Dict +import json +import glob + +ROOT = os.path.dirname(os.path.realpath(__file__)) +TYPETREE_GENERATOR_PATH = os.path.join(ROOT, "TypeTreeGenerator") +CLR_CONFIG = os.path.join(TYPETREE_GENERATOR_PATH, "TypeTreeGenerator.runtimeconfig.json") + +def main(): + # Dump the trees for all classes in the assembly + dll_folder = os.path.join(ROOT, "Managed") + tree_path = os.path.join(ROOT, "assembly_typetrees.json") + trees = dump_assembly_trees(dll_folder, tree_path) + # by dumping it as json, it can be redistributed, + # so that other people don't have to setup pythonnet3 + # People who don't like to share their decrypted dlls could also share the relevant structures this way. + +def dump_assembly_trees(dll_folder: str, out_path: str): + # Don't regenerate the file if it exists already + if os.path.isfile(out_path): + with open(out_path, "r", encoding="utf-8-sig") as f: + return json.load(f) + + # Init pythonnet, so that it uses the correct .net for the generator + pythonnet_init() + # create generator + g = create_generator(dll_folder) + + # Generate a typetree for all existing classes in the Assembly-CSharp + # While this could also be done dynamically for each required class, + # it's faster and easier overall to just fetch all at once + trees = {} + for dll in glob.glob(os.path.join(dll_folder, "*.dll"), recursive=True): + dll = os.path.basename(dll) + print("Generating TypeTree for", dll) + tree = generate_tree(g, dll, "", "") + if tree: + trees.update(tree) + + if out_path and len(trees) > 0: + with open(out_path, "w", encoding="utf-8-sig") as f: + json.dump(trees, f, ensure_ascii=False, indent=4) + return trees + + +def pythonnet_init(): + """Correctly sets-up pythonnet for the typetree generator""" + + # Prepare correct runtime + from clr_loader import get_coreclr + from pythonnet import set_runtime + + rt = get_coreclr(CLR_CONFIG) + set_runtime(rt) + + +def create_generator(dll_folder: str): + """Loads TypeTreeGenerator library and returns an instance of the Generator class.""" + + # Temporarily add the typetree generator dir to paths, + # so that pythonnet can find its files + import sys + sys.path.append(TYPETREE_GENERATOR_PATH) + + import clr + clr.AddReference("TypeTreeGenerator") + + # Import Generator class from the loaded library + from Generator import Generator + + # Create an instance of the Generator class + g = Generator() + # load the dll folder into the generator + g.loadFolder(dll_folder) + return g + + +def generate_tree( + g: "Generator", + assembly: str, + class_name: str, + namespace: str, + unity_version=[2018, 4, 3, 1], +) -> Dict[str, Dict]: + """Generates the typetree structure / nodes for the specified class.""" + # C# System + from System import Array + + unity_version_cs = Array[int](unity_version) + + # Fetch all type definitions + def_iter = g.getTypeDefs(assembly, class_name, namespace) + + # Create the nodes + trees = {} + for d in def_iter: + try: + nodes = g.convertToTypeTreeNodes(d, unity_version_cs) + except Exception as e: + # print(d.Name, e) + continue + trees[d.Name] = [ + { + "level" : node.m_Level, + "type" : node.m_Type, + "name" : node.m_Name, + "meta_flag" : node.m_MetaFlag, + } + for node in nodes + ] + return trees + + +if __name__ == "__main__": + main() diff --git a/examples/MonoBehavioursUsingAssembly/requirements.txt b/examples/MonoBehavioursUsingAssembly/requirements.txt new file mode 100644 index 00000000..e824936f --- /dev/null +++ b/examples/MonoBehavioursUsingAssembly/requirements.txt @@ -0,0 +1,4 @@ +UnityPy +clr_loader +tqdm +git+https://github.com/pythonnet/pythonnet.git \ No newline at end of file diff --git a/examples/MonoBehavioursUsingAssembly/undump_assets.py b/examples/MonoBehavioursUsingAssembly/undump_assets.py new file mode 100644 index 00000000..a34a4fca --- /dev/null +++ b/examples/MonoBehavioursUsingAssembly/undump_assets.py @@ -0,0 +1,99 @@ +import os, sys, struct, json +from glob import glob +from functools import partial +import UnityPy +from UnityPy.math import Vector2, Rectangle +from PIL import Image +from tqdm import tqdm + +ROOT = os.path.abspath(os.getcwd()) # base directory +TYPES = ["MonoBehaviour", "Texture2D", "TextAsset", "Sprite", "Shader"] +#DST = os.path.join(ROOT, "output") # destination folder + +ASSETS = os.path.join(ROOT, "original", "data.unity3d") # source folder or file +OUT_PATH = "translation_out" +IN_TEXTS = "translation_out\\assets\\*.txt" +IN_IMAGES = "images\\**\\*.png" +IN_SPRITES = "images\\**\\*.png" +IN_MONO_BEHAVIOURS = "translation_out\\assets\\**\\*.dat" +IN_JSON_MONO_BEHAVIOURS = "translation_out\\json\\**\\*.json" + +ASSEMBLY_TREES = dict() +if os.path.isfile("assembly_typetrees.json"): + with open("assembly_typetrees.json", "r", encoding="utf-8-sig") as f: + ASSEMBLY_TREES = json.loads(f.read()) + +def base_name(path): + return os.path.splitext(os.path.basename(path))[0] + +def main(): + texts = glob(IN_TEXTS) + images = glob(IN_IMAGES, recursive=True) + sprites = glob(IN_SPRITES) + mbehavs = glob(IN_MONO_BEHAVIOURS, recursive=True) + glob(IN_JSON_MONO_BEHAVIOURS, recursive=True) + + def obj_modify(obj, asset, **kwargs): + objfmt = obj.type + data = obj.read() + name = f"{asset.name}-{obj.path_id}." + if objfmt == "Sprite": + fname = next((path for path in sprites if data.name == base_name(path)), None) + if not fname: return [] + with open(fname, "rb") as img: + _img = Image.open(img) + if _img.height != int(data.m_Rect.height) or _img.width != int(data.m_Rect.width): + return [obj.path_id] + if data.name in ["demo_1_sprite_object_mod"]: + data.m_RD.settingsRaw = 2 + data.m_RD.textureRect = data.m_Rect + data.m_RD.textureRectOffset = Vector2(0, 0) + data.m_RD.uvTransform.X = 0 + data.m_RD.uvTransform.Y = 0 + data.m_RD.uvTransform.Z = 0 + data.m_RD.uvTransform.W = 0 + data.save() + else: + with open(fname + ".bin", "rb") as dat: + obj.set_raw_data(dat.read()) + if objfmt == "Texture2D": + fname = next((path for path in images if data.name == base_name(path)), None) + if not fname: return [] + with open(fname, "rb") as img: + _img = Image.open(img) + if _img.height != data.m_Height or _img.width != data.m_Width: + # it's not the same image even if names are the same + return [obj.path_id] + data.image = _img + data.save() + if objfmt == "TextAsset": + fname = next((path for path in texts if name in path), None) + if not fname: return [] + with open(fname, "r", encoding="utf-8") as txt: + data.text = txt.read() + data.save() + elif objfmt == "MonoBehaviour" or objfmt == "Shader": + fname = next((path for path in mbehavs if name in path), None) + if not fname: return [] + if ".json" == os.path.splitext(fname)[1]: + script = data.m_Script.read() + nodes = ASSEMBLY_TREES[script.m_ClassName] + with open(fname, "r", encoding="utf-8-sig") as dat: + obj.save_typetree(json.load(dat), nodes) + else: + with open(fname, "rb") as dat: + obj.set_raw_data(dat.read()) + return [obj.path_id] + + for file_name in glob(ASSETS): + print(f"Processing {file_name}...") + extension = os.path.splitext(file_name)[1] + am = UnityPy.load(os.path.realpath(os.path.join(ROOT, file_name))) + am.out_path = OUT_PATH + am.progress_function = tqdm + am.process(partial(obj_modify, files=mbehavs), TYPES) + print(f"Writing results to {am.out_path}{os.path.basename(file_name)}...") + am.save(pack= "lz4") #"none") # + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/setup.py b/setup.py index e0b423bc..d2ef43a5 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ setuptools.setup( name="UnityPy", packages=setuptools.find_packages(), + include_package_data = True, version=version, author="K0lb3", description="A pythonic port of AssetStudio by Perfare", @@ -14,23 +15,30 @@ long_description_content_type="text/markdown", url="https://github.com/K0lb3/UnityPy", download_url="https://github.com/K0lb3/UnityPy/tarball/master", - keywords=['Unity', 'unitypack', 'UnityPy', "unpack"], + keywords=['python', 'unpacker', 'game tools', 'unity', 'unity-asset', 'python3', 'data-minig', 'unitypack', 'assetstudio', 'unity-asset-extractor'], classifiers=[ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Games/Entertainment", "Topic :: Multimedia :: Graphics", ], install_requires=[ - "lz4", - "brotli", + # block compression/decompression + "lz4", # BundleFile block compression + "brotli", # WebFile compression + # Texture & Sprite handling "Pillow", - "fsb5", - "texture2ddecoder" + "texture2ddecoder", # texture decompression + # raw typetree dumping + "tabulate" ] ) diff --git a/tests/samples/char_118_yuki.ab b/tests/samples/char_118_yuki.ab new file mode 100644 index 00000000..dda9c41a Binary files /dev/null and b/tests/samples/char_118_yuki.ab differ diff --git a/tests/samples/xinzexi_2_n_tex b/tests/samples/xinzexi_2_n_tex new file mode 100644 index 00000000..e80f3c81 Binary files /dev/null and b/tests/samples/xinzexi_2_n_tex differ diff --git a/tests/samples/xinzexi_2_n_tex_mesh b/tests/samples/xinzexi_2_n_tex_mesh new file mode 100644 index 00000000..8dc16ec8 --- /dev/null +++ b/tests/samples/xinzexi_2_n_tex_mesh @@ -0,0 +1,1812 @@ +g xinzExi_2_n-mEsh +v -1152.0 671.0 0.0 +v -1152.0 1343.0 0.0 +v -1440.0 1343.0 0.0 +v -1440.0 671.0 0.0 +v -1152.0 31.0 0.0 +v -1152.0 671.0 0.0 +v -1440.0 671.0 0.0 +v -1440.0 31.0 0.0 +v -832.0 607.0 0.0 +v -832.0 927.0 0.0 +v -1152.0 927.0 0.0 +v -1152.0 607.0 0.0 +v -512.0 607.0 0.0 +v -512.0 927.0 0.0 +v -832.0 927.0 0.0 +v -832.0 607.0 0.0 +v -64.0 319.0 0.0 +v -64.0 607.0 0.0 +v -384.0 607.0 0.0 +v -384.0 319.0 0.0 +v -384.0 319.0 0.0 +v -384.0 607.0 0.0 +v -672.0 607.0 0.0 +v -672.0 319.0 0.0 +v -960.0 927.0 0.0 +v -960.0 1215.0 0.0 +v -1152.0 1215.0 0.0 +v -1152.0 927.0 0.0 +v -960.0 1215.0 0.0 +v -960.0 1503.0 0.0 +v -1152.0 1503.0 0.0 +v -1152.0 1215.0 0.0 +v -1440.0 735.0 0.0 +v -1440.0 991.0 0.0 +v -1632.0 991.0 0.0 +v -1632.0 735.0 0.0 +v -1440.0 991.0 0.0 +v -1440.0 1247.0 0.0 +v -1632.0 1247.0 0.0 +v -1632.0 991.0 0.0 +v -704.0 223.0 0.0 +v -704.0 479.0 0.0 +v -832.0 479.0 0.0 +v -832.0 223.0 0.0 +v -1440.0 1247.0 0.0 +v -1440.0 1407.0 0.0 +v -1632.0 1407.0 0.0 +v -1632.0 1247.0 0.0 +v -1152.0 1343.0 0.0 +v -1152.0 1503.0 0.0 +v -1312.0 1503.0 0.0 +v -1312.0 1343.0 0.0 +v -1632.0 1663.0 0.0 +v -1632.0 2047.0 0.0 +v -1696.0 2047.0 0.0 +v -1696.0 1663.0 0.0 +v -1632.0 1311.0 0.0 +v -1632.0 1663.0 0.0 +v -1696.0 1663.0 0.0 +v -1696.0 1311.0 0.0 +v -992.0 1503.0 0.0 +v -992.0 1599.0 0.0 +v -1216.0 1599.0 0.0 +v -1216.0 1503.0 0.0 +v -1440.0 223.0 0.0 +v -1440.0 447.0 0.0 +v -1536.0 447.0 0.0 +v -1536.0 223.0 0.0 +v -800.0 479.0 0.0 +v -800.0 607.0 0.0 +v -960.0 607.0 0.0 +v -960.0 479.0 0.0 +v -800.0 927.0 0.0 +v -800.0 1055.0 0.0 +v -960.0 1055.0 0.0 +v -960.0 927.0 0.0 +v -1024.0 95.0 0.0 +v -1024.0 255.0 0.0 +v -1152.0 255.0 0.0 +v -1152.0 95.0 0.0 +v -1440.0 31.0 0.0 +v -1440.0 223.0 0.0 +v -1536.0 223.0 0.0 +v -1536.0 31.0 0.0 +v -192.0 191.0 0.0 +v -192.0 255.0 0.0 +v -448.0 255.0 0.0 +v -448.0 191.0 0.0 +v -448.0 191.0 0.0 +v -448.0 255.0 0.0 +v -704.0 255.0 0.0 +v -704.0 191.0 0.0 +v -832.0 1311.0 0.0 +v -832.0 1439.0 0.0 +v -928.0 1439.0 0.0 +v -928.0 1311.0 0.0 +v -800.0 1599.0 0.0 +v -800.0 1727.0 0.0 +v -896.0 1727.0 0.0 +v -896.0 1599.0 0.0 +v -1632.0 1055.0 0.0 +v -1632.0 1183.0 0.0 +v -1728.0 1183.0 0.0 +v -1728.0 1055.0 0.0 +v -256.0 607.0 0.0 +v -256.0 703.0 0.0 +v -384.0 703.0 0.0 +v -384.0 607.0 0.0 +v -1632.0 927.0 0.0 +v -1632.0 1055.0 0.0 +v -1728.0 1055.0 0.0 +v -1728.0 927.0 0.0 +v -384.0 607.0 0.0 +v -384.0 703.0 0.0 +v -512.0 703.0 0.0 +v -512.0 607.0 0.0 +v -1600.0 31.0 0.0 +v -1600.0 159.0 0.0 +v -1696.0 159.0 0.0 +v -1696.0 31.0 0.0 +v -1600.0 159.0 0.0 +v -1600.0 287.0 0.0 +v -1696.0 287.0 0.0 +v -1696.0 159.0 0.0 +v -1888.0 735.0 0.0 +v -1888.0 895.0 0.0 +v -1937.0 895.0 0.0 +v -1937.0 735.0 0.0 +v -1760.0 959.0 0.0 +v -1760.0 1055.0 0.0 +v -1856.0 1055.0 0.0 +v -1856.0 959.0 0.0 +v -896.0 1535.0 0.0 +v -896.0 1631.0 0.0 +v -992.0 1631.0 0.0 +v -992.0 1535.0 0.0 +v -128.0 287.0 0.0 +v -128.0 319.0 0.0 +v -416.0 319.0 0.0 +v -416.0 287.0 0.0 +v -416.0 287.0 0.0 +v -416.0 319.0 0.0 +v -704.0 319.0 0.0 +v -704.0 287.0 0.0 +v -1888.0 607.0 0.0 +v -1888.0 735.0 0.0 +v -1937.0 735.0 0.0 +v -1937.0 607.0 0.0 +v -1408.0 1503.0 0.0 +v -1408.0 1567.0 0.0 +v -1536.0 1567.0 0.0 +v -1536.0 1503.0 0.0 +v -1344.0 1407.0 0.0 +v -1344.0 1535.0 0.0 +v -1408.0 1535.0 0.0 +v -1408.0 1407.0 0.0 +v -0.0 415.0 0.0 +v -0.0 543.0 0.0 +v -64.0 543.0 0.0 +v -64.0 415.0 0.0 +v -1088.0 479.0 0.0 +v -1088.0 607.0 0.0 +v -1152.0 607.0 0.0 +v -1152.0 479.0 0.0 +v -1536.0 31.0 0.0 +v -1536.0 223.0 0.0 +v -1568.0 223.0 0.0 +v -1568.0 31.0 0.0 +v -1536.0 1503.0 0.0 +v -1536.0 1567.0 0.0 +v -1632.0 1567.0 0.0 +v -1632.0 1503.0 0.0 +v -1536.0 223.0 0.0 +v -1536.0 415.0 0.0 +v -1568.0 415.0 0.0 +v -1568.0 223.0 0.0 +v -1696.0 159.0 0.0 +v -1696.0 255.0 0.0 +v -1760.0 255.0 0.0 +v -1760.0 159.0 0.0 +v -768.0 1343.0 0.0 +v -768.0 1407.0 0.0 +v -832.0 1407.0 0.0 +v -832.0 1343.0 0.0 +v -448.0 831.0 0.0 +v -448.0 895.0 0.0 +v -512.0 895.0 0.0 +v -512.0 831.0 0.0 +v -1280.0 0.0 0.0 +v -1280.0 31.0 0.0 +v -1408.0 31.0 0.0 +v -1408.0 0.0 0.0 +v -1440.0 1439.0 0.0 +v -1440.0 1503.0 0.0 +v -1504.0 1503.0 0.0 +v -1504.0 1439.0 0.0 +v -1472.0 671.0 0.0 +v -1472.0 735.0 0.0 +v -1536.0 735.0 0.0 +v -1536.0 671.0 0.0 +v -1792.0 479.0 0.0 +v -1792.0 543.0 0.0 +v -1856.0 543.0 0.0 +v -1856.0 479.0 0.0 +v -320.0 703.0 0.0 +v -320.0 767.0 0.0 +v -384.0 767.0 0.0 +v -384.0 703.0 0.0 +v -448.0 703.0 0.0 +v -448.0 767.0 0.0 +v -512.0 767.0 0.0 +v -512.0 703.0 0.0 +v -1568.0 319.0 0.0 +v -1568.0 447.0 0.0 +v -1600.0 447.0 0.0 +v -1600.0 319.0 0.0 +v -1856.0 863.0 0.0 +v -1856.0 991.0 0.0 +v -1888.0 991.0 0.0 +v -1888.0 863.0 0.0 +v -1696.0 95.0 0.0 +v -1696.0 159.0 0.0 +v -1760.0 159.0 0.0 +v -1760.0 95.0 0.0 +v -1568.0 191.0 0.0 +v -1568.0 319.0 0.0 +v -1600.0 319.0 0.0 +v -1600.0 191.0 0.0 +v -160.0 255.0 0.0 +v -160.0 287.0 0.0 +v -288.0 287.0 0.0 +v -288.0 255.0 0.0 +v -832.0 351.0 0.0 +v -832.0 479.0 0.0 +v -864.0 479.0 0.0 +v -864.0 351.0 0.0 +v -288.0 255.0 0.0 +v -288.0 287.0 0.0 +v -416.0 287.0 0.0 +v -416.0 255.0 0.0 +v -672.0 383.0 0.0 +v -672.0 479.0 0.0 +v -704.0 479.0 0.0 +v -704.0 383.0 0.0 +v -1440.0 639.0 0.0 +v -1440.0 735.0 0.0 +v -1472.0 735.0 0.0 +v -1472.0 639.0 0.0 +v -928.0 1119.0 0.0 +v -928.0 1215.0 0.0 +v -960.0 1215.0 0.0 +v -960.0 1119.0 0.0 +v -1856.0 767.0 0.0 +v -1856.0 863.0 0.0 +v -1888.0 863.0 0.0 +v -1888.0 767.0 0.0 +v -1568.0 63.0 0.0 +v -1568.0 159.0 0.0 +v -1600.0 159.0 0.0 +v -1600.0 63.0 0.0 +v -832.0 255.0 0.0 +v -832.0 351.0 0.0 +v -864.0 351.0 0.0 +v -864.0 255.0 0.0 +v -1696.0 415.0 0.0 +v -1696.0 447.0 0.0 +v -1792.0 447.0 0.0 +v -1792.0 415.0 0.0 +v -512.0 255.0 0.0 +v -512.0 287.0 0.0 +v -608.0 287.0 0.0 +v -608.0 255.0 0.0 +v -704.0 927.0 0.0 +v -704.0 959.0 0.0 +v -800.0 959.0 0.0 +v -800.0 927.0 0.0 +v -1600.0 415.0 0.0 +v -1600.0 447.0 0.0 +v -1696.0 447.0 0.0 +v -1696.0 415.0 0.0 +v -608.0 927.0 0.0 +v -608.0 959.0 0.0 +v -704.0 959.0 0.0 +v -704.0 927.0 0.0 +v -1408.0 0.0 0.0 +v -1408.0 31.0 0.0 +v -1504.0 31.0 0.0 +v -1504.0 0.0 0.0 +v -1696.0 1791.0 0.0 +v -1696.0 1887.0 0.0 +v -1728.0 1887.0 0.0 +v -1728.0 1791.0 0.0 +v -1408.0 1407.0 0.0 +v -1408.0 1503.0 0.0 +v -1440.0 1503.0 0.0 +v -1440.0 1407.0 0.0 +v -608.0 255.0 0.0 +v -608.0 287.0 0.0 +v -704.0 287.0 0.0 +v -704.0 255.0 0.0 +v -1696.0 1887.0 0.0 +v -1696.0 1983.0 0.0 +v -1728.0 1983.0 0.0 +v -1728.0 1887.0 0.0 +v -1600.0 1599.0 0.0 +v -1600.0 1663.0 0.0 +v -1632.0 1663.0 0.0 +v -1632.0 1599.0 0.0 +v -768.0 511.0 0.0 +v -768.0 575.0 0.0 +v -800.0 575.0 0.0 +v -800.0 511.0 0.0 +v -1504.0 1471.0 0.0 +v -1504.0 1503.0 0.0 +v -1568.0 1503.0 0.0 +v -1568.0 1471.0 0.0 +v -1216.0 1567.0 0.0 +v -1216.0 1599.0 0.0 +v -1280.0 1599.0 0.0 +v -1280.0 1567.0 0.0 +v -832.0 1567.0 0.0 +v -832.0 1599.0 0.0 +v -896.0 1599.0 0.0 +v -896.0 1567.0 0.0 +v -672.0 543.0 0.0 +v -672.0 607.0 0.0 +v -704.0 607.0 0.0 +v -704.0 543.0 0.0 +v -1440.0 479.0 0.0 +v -1440.0 543.0 0.0 +v -1472.0 543.0 0.0 +v -1472.0 479.0 0.0 +v -416.0 799.0 0.0 +v -416.0 863.0 0.0 +v -448.0 863.0 0.0 +v -448.0 799.0 0.0 +v -384.0 767.0 0.0 +v -384.0 831.0 0.0 +v -416.0 831.0 0.0 +v -416.0 767.0 0.0 +v -1632.0 383.0 0.0 +v -1632.0 415.0 0.0 +v -1696.0 415.0 0.0 +v -1696.0 383.0 0.0 +v -768.0 1663.0 0.0 +v -768.0 1727.0 0.0 +v -800.0 1727.0 0.0 +v -800.0 1663.0 0.0 +v -896.0 1631.0 0.0 +v -896.0 1695.0 0.0 +v -928.0 1695.0 0.0 +v -928.0 1631.0 0.0 +v -1760.0 223.0 0.0 +v -1760.0 287.0 0.0 +v -1792.0 287.0 0.0 +v -1792.0 223.0 0.0 +v -1728.0 31.0 0.0 +v -1728.0 95.0 0.0 +v -1760.0 95.0 0.0 +v -1760.0 31.0 0.0 +v -1120.0 255.0 0.0 +v -1120.0 319.0 0.0 +v -1152.0 319.0 0.0 +v -1152.0 255.0 0.0 +v -1792.0 223.0 0.0 +v -1792.0 287.0 0.0 +v -1824.0 287.0 0.0 +v -1824.0 223.0 0.0 +v -864.0 415.0 0.0 +v -864.0 479.0 0.0 +v -896.0 479.0 0.0 +v -896.0 415.0 0.0 +v -32.0 351.0 0.0 +v -32.0 415.0 0.0 +v -64.0 415.0 0.0 +v -64.0 351.0 0.0 +v -1088.0 63.0 0.0 +v -1088.0 95.0 0.0 +v -1152.0 95.0 0.0 +v -1152.0 63.0 0.0 +v -1600.0 0.0 0.0 +v -1600.0 31.0 0.0 +v -1664.0 31.0 0.0 +v -1664.0 0.0 0.0 +v -960.0 191.0 0.0 +v -960.0 255.0 0.0 +v -992.0 255.0 0.0 +v -992.0 191.0 0.0 +v -480.0 159.0 0.0 +v -480.0 191.0 0.0 +v -544.0 191.0 0.0 +v -544.0 159.0 0.0 +v -1888.0 543.0 0.0 +v -1888.0 607.0 0.0 +v -1920.0 607.0 0.0 +v -1920.0 543.0 0.0 +v -512.0 1087.0 0.0 +v -512.0 1151.0 0.0 +v -544.0 1151.0 0.0 +v -544.0 1087.0 0.0 +v -896.0 1055.0 0.0 +v -896.0 1119.0 0.0 +v -928.0 1119.0 0.0 +v -928.0 1055.0 0.0 +v -1600.0 1439.0 0.0 +v -1600.0 1503.0 0.0 +v -1632.0 1503.0 0.0 +v -1632.0 1439.0 0.0 +v -1632.0 1247.0 0.0 +v -1632.0 1311.0 0.0 +v -1664.0 1311.0 0.0 +v -1664.0 1247.0 0.0 +v -1888.0 895.0 0.0 +v -1888.0 959.0 0.0 +v -1920.0 959.0 0.0 +v -1920.0 895.0 0.0 +v -960.0 543.0 0.0 +v -960.0 607.0 0.0 +v -992.0 607.0 0.0 +v -992.0 543.0 0.0 +v -1760.0 1055.0 0.0 +v -1760.0 1119.0 0.0 +v -1792.0 1119.0 0.0 +v -1792.0 1055.0 0.0 +v -768.0 959.0 0.0 +v -768.0 1023.0 0.0 +v -800.0 1023.0 0.0 +v -800.0 959.0 0.0 +v -1824.0 895.0 0.0 +v -1824.0 959.0 0.0 +v -1856.0 959.0 0.0 +v -1856.0 895.0 0.0 +v -928.0 1343.0 0.0 +v -928.0 1407.0 0.0 +v -960.0 1407.0 0.0 +v -960.0 1343.0 0.0 +v -1504.0 1407.0 0.0 +v -1504.0 1439.0 0.0 +v -1568.0 1439.0 0.0 +v -1568.0 1407.0 0.0 +v -1312.0 1375.0 0.0 +v -1312.0 1439.0 0.0 +v -1344.0 1439.0 0.0 +v -1344.0 1375.0 0.0 +v -928.0 1407.0 0.0 +v -928.0 1471.0 0.0 +v -960.0 1471.0 0.0 +v -960.0 1407.0 0.0 +v -1728.0 1087.0 0.0 +v -1728.0 1151.0 0.0 +v -1760.0 1151.0 0.0 +v -1760.0 1087.0 0.0 +v -192.0 607.0 0.0 +v -192.0 639.0 0.0 +v -256.0 639.0 0.0 +v -256.0 607.0 0.0 +v -1856.0 575.0 0.0 +v -1856.0 639.0 0.0 +v -1888.0 639.0 0.0 +v -1888.0 575.0 0.0 +v -1728.0 1023.0 0.0 +v -1728.0 1087.0 0.0 +v -1760.0 1087.0 0.0 +v -1760.0 1023.0 0.0 +v -128.0 607.0 0.0 +v -128.0 639.0 0.0 +v -192.0 639.0 0.0 +v -192.0 607.0 0.0 +v -1312.0 1439.0 0.0 +v -1312.0 1503.0 0.0 +v -1344.0 1503.0 0.0 +v -1344.0 1439.0 0.0 +v -1440.0 575.0 0.0 +v -1440.0 639.0 0.0 +v -1472.0 639.0 0.0 +v -1472.0 575.0 0.0 +v -672.0 319.0 0.0 +v -672.0 383.0 0.0 +v -704.0 383.0 0.0 +v -704.0 319.0 0.0 +v -1408.0 1343.0 0.0 +v -1408.0 1407.0 0.0 +v -1440.0 1407.0 0.0 +v -1440.0 1343.0 0.0 +v -928.0 1055.0 0.0 +v -928.0 1119.0 0.0 +v -960.0 1119.0 0.0 +v -960.0 1055.0 0.0 +v -1568.0 0.0 0.0 +v -1568.0 63.0 0.0 +v -1600.0 63.0 0.0 +v -1600.0 0.0 0.0 +v -1504.0 1567.0 0.0 +v -1504.0 1599.0 0.0 +v -1568.0 1599.0 0.0 +v -1568.0 1567.0 0.0 +v -1568.0 1567.0 0.0 +v -1568.0 1599.0 0.0 +v -1632.0 1599.0 0.0 +v -1632.0 1567.0 0.0 +v -1696.0 1631.0 0.0 +v -1696.0 1695.0 0.0 +v -1728.0 1695.0 0.0 +v -1728.0 1631.0 0.0 +v -1696.0 1567.0 0.0 +v -1696.0 1631.0 0.0 +v -1728.0 1631.0 0.0 +v -1728.0 1567.0 0.0 +v -1856.0 511.0 0.0 +v -1856.0 575.0 0.0 +v -1888.0 575.0 0.0 +v -1888.0 511.0 0.0 +v -1600.0 287.0 0.0 +v -1600.0 351.0 0.0 +v -1632.0 351.0 0.0 +v -1632.0 287.0 0.0 +v -1568.0 1407.0 0.0 +v -1568.0 1439.0 0.0 +v -1632.0 1439.0 0.0 +v -1632.0 1407.0 0.0 +v -1600.0 351.0 0.0 +v -1600.0 415.0 0.0 +v -1632.0 415.0 0.0 +v -1632.0 351.0 0.0 +v -1152.0 1599.0 0.0 +v -1152.0 1631.0 0.0 +v -1216.0 1631.0 0.0 +v -1216.0 1599.0 0.0 +v -992.0 159.0 0.0 +v -992.0 223.0 0.0 +v -1024.0 223.0 0.0 +v -1024.0 159.0 0.0 +v -992.0 223.0 0.0 +v -992.0 287.0 0.0 +v -1024.0 287.0 0.0 +v -1024.0 223.0 0.0 +v -1760.0 447.0 0.0 +v -1760.0 479.0 0.0 +v -1824.0 479.0 0.0 +v -1824.0 447.0 0.0 +v -1120.0 351.0 0.0 +v -1120.0 415.0 0.0 +v -1152.0 415.0 0.0 +v -1152.0 351.0 0.0 +v -1120.0 415.0 0.0 +v -1120.0 479.0 0.0 +v -1152.0 479.0 0.0 +v -1152.0 415.0 0.0 +v -1696.0 447.0 0.0 +v -1696.0 479.0 0.0 +v -1760.0 479.0 0.0 +v -1760.0 447.0 0.0 +v -448.0 799.0 0.0 +v -448.0 831.0 0.0 +v -480.0 831.0 0.0 +v -480.0 799.0 0.0 +v -288.0 703.0 0.0 +v -288.0 735.0 0.0 +v -320.0 735.0 0.0 +v -320.0 703.0 0.0 +v -736.0 959.0 0.0 +v -736.0 991.0 0.0 +v -768.0 991.0 0.0 +v -768.0 959.0 0.0 +v -1792.0 927.0 0.0 +v -1792.0 959.0 0.0 +v -1824.0 959.0 0.0 +v -1824.0 927.0 0.0 +v -1536.0 703.0 0.0 +v -1536.0 735.0 0.0 +v -1568.0 735.0 0.0 +v -1568.0 703.0 0.0 +v -704.0 575.0 0.0 +v -704.0 607.0 0.0 +v -736.0 607.0 0.0 +v -736.0 575.0 0.0 +v -416.0 703.0 0.0 +v -416.0 735.0 0.0 +v -448.0 735.0 0.0 +v -448.0 703.0 0.0 +v -352.0 767.0 0.0 +v -352.0 799.0 0.0 +v -384.0 799.0 0.0 +v -384.0 767.0 0.0 +v -224.0 639.0 0.0 +v -224.0 671.0 0.0 +v -256.0 671.0 0.0 +v -256.0 639.0 0.0 +v -480.0 767.0 0.0 +v -480.0 799.0 0.0 +v -512.0 799.0 0.0 +v -512.0 767.0 0.0 +v -32.0 543.0 0.0 +v -32.0 575.0 0.0 +v -64.0 575.0 0.0 +v -64.0 543.0 0.0 +v -896.0 351.0 0.0 +v -896.0 383.0 0.0 +v -928.0 383.0 0.0 +v -928.0 351.0 0.0 +v -1472.0 447.0 0.0 +v -1472.0 479.0 0.0 +v -1504.0 479.0 0.0 +v -1504.0 447.0 0.0 +v -1632.0 287.0 0.0 +v -1632.0 319.0 0.0 +v -1664.0 319.0 0.0 +v -1664.0 287.0 0.0 +v -1760.0 0.0 0.0 +v -1760.0 31.0 0.0 +v -1792.0 31.0 0.0 +v -1792.0 0.0 0.0 +v -1024.0 255.0 0.0 +v -1024.0 287.0 0.0 +v -1056.0 287.0 0.0 +v -1056.0 255.0 0.0 +v -1824.0 543.0 0.0 +v -1824.0 575.0 0.0 +v -1856.0 575.0 0.0 +v -1856.0 543.0 0.0 +v -1824.0 255.0 0.0 +v -1824.0 287.0 0.0 +v -1856.0 287.0 0.0 +v -1856.0 255.0 0.0 +v -736.0 479.0 0.0 +v -736.0 511.0 0.0 +v -768.0 511.0 0.0 +v -768.0 479.0 0.0 +v -896.0 447.0 0.0 +v -896.0 479.0 0.0 +v -928.0 479.0 0.0 +v -928.0 447.0 0.0 +v -1760.0 479.0 0.0 +v -1760.0 511.0 0.0 +v -1792.0 511.0 0.0 +v -1792.0 479.0 0.0 +v -1792.0 1055.0 0.0 +v -1792.0 1087.0 0.0 +v -1824.0 1087.0 0.0 +v -1824.0 1055.0 0.0 +v -1440.0 447.0 0.0 +v -1440.0 479.0 0.0 +v -1472.0 479.0 0.0 +v -1472.0 447.0 0.0 +v -768.0 479.0 0.0 +v -768.0 511.0 0.0 +v -800.0 511.0 0.0 +v -800.0 479.0 0.0 +v -1696.0 383.0 0.0 +v -1696.0 415.0 0.0 +v -1728.0 415.0 0.0 +v -1728.0 383.0 0.0 +v -1728.0 0.0 0.0 +v -1728.0 31.0 0.0 +v -1760.0 31.0 0.0 +v -1760.0 0.0 0.0 +v -1760.0 191.0 0.0 +v -1760.0 223.0 0.0 +v -1792.0 223.0 0.0 +v -1792.0 191.0 0.0 +v -1824.0 863.0 0.0 +v -1824.0 895.0 0.0 +v -1856.0 895.0 0.0 +v -1856.0 863.0 0.0 +v -1216.0 1599.0 0.0 +v -1216.0 1631.0 0.0 +v -1248.0 1631.0 0.0 +v -1248.0 1599.0 0.0 +v -416.0 767.0 0.0 +v -416.0 799.0 0.0 +v -448.0 799.0 0.0 +v -448.0 767.0 0.0 +v -672.0 511.0 0.0 +v -672.0 543.0 0.0 +v -704.0 543.0 0.0 +v -704.0 511.0 0.0 +v -384.0 735.0 0.0 +v -384.0 767.0 0.0 +v -416.0 767.0 0.0 +v -416.0 735.0 0.0 +v -928.0 1631.0 0.0 +v -928.0 1663.0 0.0 +v -960.0 1663.0 0.0 +v -960.0 1631.0 0.0 +v -800.0 1311.0 0.0 +v -800.0 1343.0 0.0 +v -832.0 1343.0 0.0 +v -832.0 1311.0 0.0 +v -1376.0 1343.0 0.0 +v -1376.0 1375.0 0.0 +v -1408.0 1375.0 0.0 +v -1408.0 1343.0 0.0 +v -1632.0 1183.0 0.0 +v -1632.0 1215.0 0.0 +v -1664.0 1215.0 0.0 +v -1664.0 1183.0 0.0 +v -864.0 1055.0 0.0 +v -864.0 1087.0 0.0 +v -896.0 1087.0 0.0 +v -896.0 1055.0 0.0 +v -640.0 1087.0 0.0 +v -640.0 1119.0 0.0 +v -672.0 1119.0 0.0 +v -672.0 1087.0 0.0 +v -1248.0 1535.0 0.0 +v -1248.0 1567.0 0.0 +v -1280.0 1567.0 0.0 +v -1280.0 1535.0 0.0 +v -992.0 1599.0 0.0 +v -992.0 1631.0 0.0 +v -1024.0 1631.0 0.0 +v -1024.0 1599.0 0.0 +v -1216.0 1503.0 0.0 +v -1216.0 1535.0 0.0 +v -1248.0 1535.0 0.0 +v -1248.0 1503.0 0.0 +v -1344.0 1375.0 0.0 +v -1344.0 1407.0 0.0 +v -1376.0 1407.0 0.0 +v -1376.0 1375.0 0.0 +v -896.0 1439.0 0.0 +v -896.0 1471.0 0.0 +v -928.0 1471.0 0.0 +v -928.0 1439.0 0.0 +vt 0.00048828125 0.0010246038436889648 +vt 0.00048828125 0.6895492076873779 +vt 0.14111328125 0.6895492076873779 +vt 0.14111328125 0.0010246038436889648 +vt 0.14208984375 0.0010246038436889648 +vt 0.14208984375 0.6567623019218445 +vt 0.28271484375 0.6567623019218445 +vt 0.28271484375 0.0010246038436889648 +vt 0.28369140625 0.0010246038436889648 +vt 0.28369140625 0.32889342308044434 +vt 0.43994140625 0.32889342308044434 +vt 0.43994140625 0.0010246038436889648 +vt 0.44091796875 0.0010246038436889648 +vt 0.44091796875 0.32889342308044434 +vt 0.59716796875 0.32889342308044434 +vt 0.59716796875 0.0010246038436889648 +vt 0.59814453125 0.0010246038436889648 +vt 0.59814453125 0.29610657691955566 +vt 0.75439453125 0.29610657691955566 +vt 0.75439453125 0.0010246038436889648 +vt 0.75537109375 0.0010246038436889648 +vt 0.75537109375 0.29610657691955566 +vt 0.89599609375 0.29610657691955566 +vt 0.89599609375 0.0010246038436889648 +vt 0.89697265625 0.0010246038436889648 +vt 0.89697265625 0.29610657691955566 +vt 0.99072265625 0.29610657691955566 +vt 0.99072265625 0.0010246038436889648 +vt 0.59814453125 0.2981557250022888 +vt 0.59814453125 0.5932376980781555 +vt 0.69189453125 0.5932376980781555 +vt 0.69189453125 0.2981557250022888 +vt 0.69287109375 0.2981557250022888 +vt 0.69287109375 0.5604507923126221 +vt 0.78662109375 0.5604507923126221 +vt 0.78662109375 0.2981557250022888 +vt 0.78759765625 0.2981557250022888 +vt 0.78759765625 0.5604507923126221 +vt 0.88134765625 0.5604507923126221 +vt 0.88134765625 0.2981557250022888 +vt 0.88232421875 0.2981557250022888 +vt 0.88232421875 0.5604507923126221 +vt 0.94482421875 0.5604507923126221 +vt 0.94482421875 0.2981557250022888 +vt 0.28369140625 0.33094263076782227 +vt 0.28369140625 0.49487704038619995 +vt 0.37744140625 0.49487704038619995 +vt 0.37744140625 0.33094263076782227 +vt 0.37841796875 0.33094263076782227 +vt 0.37841796875 0.49487704038619995 +vt 0.45654296875 0.49487704038619995 +vt 0.45654296875 0.33094263076782227 +vt 0.94580078125 0.2981557250022888 +vt 0.94580078125 0.6915983557701111 +vt 0.97705078125 0.6915983557701111 +vt 0.97705078125 0.2981557250022888 +vt 0.45751953125 0.33094263076782227 +vt 0.45751953125 0.6915983557701111 +vt 0.48876953125 0.6915983557701111 +vt 0.48876953125 0.33094263076782227 +vt 0.28369140625 0.4969262480735779 +vt 0.28369140625 0.5952868461608887 +vt 0.39306640625 0.5952868461608887 +vt 0.39306640625 0.4969262480735779 +vt 0.48974609375 0.33094263076782227 +vt 0.48974609375 0.5604507923126221 +vt 0.53662109375 0.5604507923126221 +vt 0.53662109375 0.33094263076782227 +vt 0.48974609375 0.5625 +vt 0.48974609375 0.6936475038528442 +vt 0.56787109375 0.6936475038528442 +vt 0.56787109375 0.5625 +vt 0.69287109375 0.5625 +vt 0.69287109375 0.6936475038528442 +vt 0.77099609375 0.6936475038528442 +vt 0.77099609375 0.5625 +vt 0.39404296875 0.4969262480735779 +vt 0.39404296875 0.6608606576919556 +vt 0.45654296875 0.6608606576919556 +vt 0.45654296875 0.4969262480735779 +vt 0.77197265625 0.5625 +vt 0.77197265625 0.7592213153839111 +vt 0.81884765625 0.7592213153839111 +vt 0.81884765625 0.5625 +vt 0.81982421875 0.5625 +vt 0.81982421875 0.6280738115310669 +vt 0.94482421875 0.6280738115310669 +vt 0.94482421875 0.5625 +vt 0.81982421875 0.6301229596138 +vt 0.81982421875 0.6956967115402222 +vt 0.94482421875 0.6956967115402222 +vt 0.94482421875 0.6301229596138 +vt 0.59814453125 0.5952868461608887 +vt 0.59814453125 0.7264344692230225 +vt 0.64501953125 0.7264344692230225 +vt 0.64501953125 0.5952868461608887 +vt 0.28369140625 0.5973360538482666 +vt 0.28369140625 0.7284836173057556 +vt 0.33056640625 0.7284836173057556 +vt 0.33056640625 0.5973360538482666 +vt 0.33154296875 0.5973360538482666 +vt 0.33154296875 0.7284836173057556 +vt 0.37841796875 0.7284836173057556 +vt 0.37841796875 0.5973360538482666 +vt 0.14208984375 0.6588114500045776 +vt 0.14208984375 0.7571721076965332 +vt 0.20458984375 0.7571721076965332 +vt 0.20458984375 0.6588114500045776 +vt 0.20556640625 0.6588114500045776 +vt 0.20556640625 0.7899590134620667 +vt 0.25244140625 0.7899590134620667 +vt 0.25244140625 0.6588114500045776 +vt 0.39404296875 0.6629098653793335 +vt 0.39404296875 0.7612705230712891 +vt 0.45654296875 0.7612705230712891 +vt 0.45654296875 0.6629098653793335 +vt 0.00048828125 0.6915983557701111 +vt 0.00048828125 0.8227459192276001 +vt 0.04736328125 0.8227459192276001 +vt 0.04736328125 0.6915983557701111 +vt 0.04833984375 0.6915983557701111 +vt 0.04833984375 0.8227459192276001 +vt 0.09521484375 0.8227459192276001 +vt 0.09521484375 0.6915983557701111 +vt 0.56884765625 0.33094263076782227 +vt 0.56884765625 0.49487704038619995 +vt 0.59326171875 0.49487704038619995 +vt 0.59326171875 0.33094263076782227 +vt 0.94580078125 0.6936475038528442 +vt 0.94580078125 0.7920081615447998 +vt 0.99267578125 0.7920081615447998 +vt 0.99267578125 0.6936475038528442 +vt 0.48974609375 0.6956967115402222 +vt 0.48974609375 0.7940573692321777 +vt 0.53662109375 0.7940573692321777 +vt 0.53662109375 0.6956967115402222 +vt 0.59814453125 0.7284836173057556 +vt 0.59814453125 0.7612705230712891 +vt 0.73876953125 0.7612705230712891 +vt 0.73876953125 0.7284836173057556 +vt 0.77197265625 0.7612705230712891 +vt 0.77197265625 0.7940573692321777 +vt 0.91259765625 0.7940573692321777 +vt 0.91259765625 0.7612705230712891 +vt 0.56884765625 0.4969262480735779 +vt 0.56884765625 0.6280738115310669 +vt 0.59326171875 0.6280738115310669 +vt 0.59326171875 0.4969262480735779 +vt 0.28369140625 0.7305327653884888 +vt 0.28369140625 0.7961065769195557 +vt 0.34619140625 0.7961065769195557 +vt 0.34619140625 0.7305327653884888 +vt 0.09619140625 0.6915983557701111 +vt 0.09619140625 0.8227459192276001 +vt 0.12744140625 0.8227459192276001 +vt 0.12744140625 0.6915983557701111 +vt 0.45751953125 0.6936475038528442 +vt 0.45751953125 0.8247950673103333 +vt 0.48876953125 0.8247950673103333 +vt 0.48876953125 0.6936475038528442 +vt 0.53759765625 0.6956967115402222 +vt 0.53759765625 0.8268442749977112 +vt 0.56884765625 0.8268442749977112 +vt 0.56884765625 0.6956967115402222 +vt 0.56982421875 0.6301229596138 +vt 0.56982421875 0.8268442749977112 +vt 0.58544921875 0.8268442749977112 +vt 0.58544921875 0.6301229596138 +vt 0.14208984375 0.7592213153839111 +vt 0.14208984375 0.8247950673103333 +vt 0.18896484375 0.8247950673103333 +vt 0.18896484375 0.7592213153839111 +vt 0.25341796875 0.6588114500045776 +vt 0.25341796875 0.8555327653884888 +vt 0.26904296875 0.8555327653884888 +vt 0.26904296875 0.6588114500045776 +vt 0.73974609375 0.6956967115402222 +vt 0.73974609375 0.7940573692321777 +vt 0.77099609375 0.7940573692321777 +vt 0.77099609375 0.6956967115402222 +vt 0.91357421875 0.6977459192276001 +vt 0.91357421875 0.7633196711540222 +vt 0.94482421875 0.7633196711540222 +vt 0.94482421875 0.6977459192276001 +vt 0.34716796875 0.7305327653884888 +vt 0.34716796875 0.7961065769195557 +vt 0.37841796875 0.7961065769195557 +vt 0.37841796875 0.7305327653884888 +vt 0.39404296875 0.7633196711540222 +vt 0.39404296875 0.7961065769195557 +vt 0.45654296875 0.7961065769195557 +vt 0.45654296875 0.7633196711540222 +vt 0.59814453125 0.7633196711540222 +vt 0.59814453125 0.8288934230804443 +vt 0.62939453125 0.8288934230804443 +vt 0.62939453125 0.7633196711540222 +vt 0.63037109375 0.7633196711540222 +vt 0.63037109375 0.8288934230804443 +vt 0.66162109375 0.8288934230804443 +vt 0.66162109375 0.7633196711540222 +vt 0.66259765625 0.7633196711540222 +vt 0.66259765625 0.8288934230804443 +vt 0.69384765625 0.8288934230804443 +vt 0.69384765625 0.7633196711540222 +vt 0.69482421875 0.7633196711540222 +vt 0.69482421875 0.8288934230804443 +vt 0.72607421875 0.8288934230804443 +vt 0.72607421875 0.7633196711540222 +vt 0.91357421875 0.7653688192367554 +vt 0.91357421875 0.8309426307678223 +vt 0.94482421875 0.8309426307678223 +vt 0.94482421875 0.7653688192367554 +vt 0.20556640625 0.7920081615447998 +vt 0.20556640625 0.9231557250022888 +vt 0.22119140625 0.9231557250022888 +vt 0.22119140625 0.7920081615447998 +vt 0.22216796875 0.7920081615447998 +vt 0.22216796875 0.9231557250022888 +vt 0.23779296875 0.9231557250022888 +vt 0.23779296875 0.7920081615447998 +vt 0.94580078125 0.7940573692321777 +vt 0.94580078125 0.8596311807632446 +vt 0.97705078125 0.8596311807632446 +vt 0.97705078125 0.7940573692321777 +vt 0.97802734375 0.7940573692321777 +vt 0.97802734375 0.9252049326896667 +vt 0.99365234375 0.9252049326896667 +vt 0.99365234375 0.7940573692321777 +vt 0.73974609375 0.7961065769195557 +vt 0.73974609375 0.8288934230804443 +vt 0.80224609375 0.8288934230804443 +vt 0.80224609375 0.7961065769195557 +vt 0.48974609375 0.7961065769195557 +vt 0.48974609375 0.9272540807723999 +vt 0.50537109375 0.9272540807723999 +vt 0.50537109375 0.7961065769195557 +vt 0.80322265625 0.7961065769195557 +vt 0.80322265625 0.8288934230804443 +vt 0.86572265625 0.8288934230804443 +vt 0.86572265625 0.7961065769195557 +vt 0.50634765625 0.7961065769195557 +vt 0.50634765625 0.8944672346115112 +vt 0.52197265625 0.8944672346115112 +vt 0.52197265625 0.7961065769195557 +vt 0.86669921875 0.7961065769195557 +vt 0.86669921875 0.8944672346115112 +vt 0.88232421875 0.8944672346115112 +vt 0.88232421875 0.7961065769195557 +vt 0.88330078125 0.7961065769195557 +vt 0.88330078125 0.8944672346115112 +vt 0.89892578125 0.8944672346115112 +vt 0.89892578125 0.7961065769195557 +vt 0.28369140625 0.7981557250022888 +vt 0.28369140625 0.8965163826942444 +vt 0.29931640625 0.8965163826942444 +vt 0.29931640625 0.7981557250022888 +vt 0.30029296875 0.7981557250022888 +vt 0.30029296875 0.8965163826942444 +vt 0.31591796875 0.8965163826942444 +vt 0.31591796875 0.7981557250022888 +vt 0.31689453125 0.7981557250022888 +vt 0.31689453125 0.8965163826942444 +vt 0.33251953125 0.8965163826942444 +vt 0.33251953125 0.7981557250022888 +vt 0.33349609375 0.7981557250022888 +vt 0.33349609375 0.8309426307678223 +vt 0.38037109375 0.8309426307678223 +vt 0.38037109375 0.7981557250022888 +vt 0.39404296875 0.7981557250022888 +vt 0.39404296875 0.8309426307678223 +vt 0.44091796875 0.8309426307678223 +vt 0.44091796875 0.7981557250022888 +vt 0.00048828125 0.8247950673103333 +vt 0.00048828125 0.8575819730758667 +vt 0.04736328125 0.8575819730758667 +vt 0.04736328125 0.8247950673103333 +vt 0.04833984375 0.8247950673103333 +vt 0.04833984375 0.8575819730758667 +vt 0.09521484375 0.8575819730758667 +vt 0.09521484375 0.8247950673103333 +vt 0.14208984375 0.8268442749977112 +vt 0.14208984375 0.8596311807632446 +vt 0.18896484375 0.8596311807632446 +vt 0.18896484375 0.8268442749977112 +vt 0.53759765625 0.8288934230804443 +vt 0.53759765625 0.8616803288459778 +vt 0.58447265625 0.8616803288459778 +vt 0.58447265625 0.8288934230804443 +vt 0.09619140625 0.8247950673103333 +vt 0.09619140625 0.9231557250022888 +vt 0.11181640625 0.9231557250022888 +vt 0.11181640625 0.8247950673103333 +vt 0.11279296875 0.8247950673103333 +vt 0.11279296875 0.9231557250022888 +vt 0.12841796875 0.9231557250022888 +vt 0.12841796875 0.8247950673103333 +vt 0.59814453125 0.8309426307678223 +vt 0.59814453125 0.8637294769287109 +vt 0.64501953125 0.8637294769287109 +vt 0.64501953125 0.8309426307678223 +vt 0.45751953125 0.8268442749977112 +vt 0.45751953125 0.9252049326896667 +vt 0.47314453125 0.9252049326896667 +vt 0.47314453125 0.8268442749977112 +vt 0.64599609375 0.8309426307678223 +vt 0.64599609375 0.8965163826942444 +vt 0.66162109375 0.8965163826942444 +vt 0.66162109375 0.8309426307678223 +vt 0.66259765625 0.8309426307678223 +vt 0.66259765625 0.8965163826942444 +vt 0.67822265625 0.8965163826942444 +vt 0.67822265625 0.8309426307678223 +vt 0.67919921875 0.8309426307678223 +vt 0.67919921875 0.8637294769287109 +vt 0.71044921875 0.8637294769287109 +vt 0.71044921875 0.8309426307678223 +vt 0.71142578125 0.8309426307678223 +vt 0.71142578125 0.8637294769287109 +vt 0.74267578125 0.8637294769287109 +vt 0.74267578125 0.8309426307678223 +vt 0.74365234375 0.8309426307678223 +vt 0.74365234375 0.8637294769287109 +vt 0.77490234375 0.8637294769287109 +vt 0.77490234375 0.8309426307678223 +vt 0.77587890625 0.8309426307678223 +vt 0.77587890625 0.8965163826942444 +vt 0.79150390625 0.8965163826942444 +vt 0.79150390625 0.8309426307678223 +vt 0.79248046875 0.8309426307678223 +vt 0.79248046875 0.8965163826942444 +vt 0.80810546875 0.8965163826942444 +vt 0.80810546875 0.8309426307678223 +vt 0.80908203125 0.8309426307678223 +vt 0.80908203125 0.8965163826942444 +vt 0.82470703125 0.8965163826942444 +vt 0.82470703125 0.8309426307678223 +vt 0.82568359375 0.8309426307678223 +vt 0.82568359375 0.8965163826942444 +vt 0.84130859375 0.8965163826942444 +vt 0.84130859375 0.8309426307678223 +vt 0.33349609375 0.8329918384552002 +vt 0.33349609375 0.8657786846160889 +vt 0.36474609375 0.8657786846160889 +vt 0.36474609375 0.8329918384552002 +vt 0.84228515625 0.8309426307678223 +vt 0.84228515625 0.8965163826942444 +vt 0.85791015625 0.8965163826942444 +vt 0.85791015625 0.8309426307678223 +vt 0.36572265625 0.8329918384552002 +vt 0.36572265625 0.8985655903816223 +vt 0.38134765625 0.8985655903816223 +vt 0.38134765625 0.8329918384552002 +vt 0.39404296875 0.8329918384552002 +vt 0.39404296875 0.8985655903816223 +vt 0.40966796875 0.8985655903816223 +vt 0.40966796875 0.8329918384552002 +vt 0.41064453125 0.8329918384552002 +vt 0.41064453125 0.8985655903816223 +vt 0.42626953125 0.8985655903816223 +vt 0.42626953125 0.8329918384552002 +vt 0.42724609375 0.8329918384552002 +vt 0.42724609375 0.8985655903816223 +vt 0.44287109375 0.8985655903816223 +vt 0.44287109375 0.8329918384552002 +vt 0.91357421875 0.8329918384552002 +vt 0.91357421875 0.8985655903816223 +vt 0.92919921875 0.8985655903816223 +vt 0.92919921875 0.8329918384552002 +vt 0.25341796875 0.8575819730758667 +vt 0.25341796875 0.9231557250022888 +vt 0.26904296875 0.9231557250022888 +vt 0.26904296875 0.8575819730758667 +vt 0.00048828125 0.8596311807632446 +vt 0.00048828125 0.9252049326896667 +vt 0.01611328125 0.9252049326896667 +vt 0.01611328125 0.8596311807632446 +vt 0.01708984375 0.8596311807632446 +vt 0.01708984375 0.8924180269241333 +vt 0.04833984375 0.8924180269241333 +vt 0.04833984375 0.8596311807632446 +vt 0.04931640625 0.8596311807632446 +vt 0.04931640625 0.8924180269241333 +vt 0.08056640625 0.8924180269241333 +vt 0.08056640625 0.8596311807632446 +vt 0.14208984375 0.8616803288459778 +vt 0.14208984375 0.9272540807723999 +vt 0.15771484375 0.9272540807723999 +vt 0.15771484375 0.8616803288459778 +vt 0.15869140625 0.8616803288459778 +vt 0.15869140625 0.8944672346115112 +vt 0.18994140625 0.8944672346115112 +vt 0.18994140625 0.8616803288459778 +vt 0.94580078125 0.8616803288459778 +vt 0.94580078125 0.9272540807723999 +vt 0.96142578125 0.9272540807723999 +vt 0.96142578125 0.8616803288459778 +vt 0.53759765625 0.8637294769287109 +vt 0.53759765625 0.9293032884597778 +vt 0.55322265625 0.9293032884597778 +vt 0.55322265625 0.8637294769287109 +vt 0.55419921875 0.8637294769287109 +vt 0.55419921875 0.9293032884597778 +vt 0.56982421875 0.9293032884597778 +vt 0.56982421875 0.8637294769287109 +vt 0.57080078125 0.8637294769287109 +vt 0.57080078125 0.9293032884597778 +vt 0.58642578125 0.9293032884597778 +vt 0.58642578125 0.8637294769287109 +vt 0.59814453125 0.8657786846160889 +vt 0.59814453125 0.931352436542511 +vt 0.61376953125 0.931352436542511 +vt 0.61376953125 0.8657786846160889 +vt 0.61474609375 0.8657786846160889 +vt 0.61474609375 0.931352436542511 +vt 0.63037109375 0.931352436542511 +vt 0.63037109375 0.8657786846160889 +vt 0.67919921875 0.8657786846160889 +vt 0.67919921875 0.931352436542511 +vt 0.69482421875 0.931352436542511 +vt 0.69482421875 0.8657786846160889 +vt 0.69580078125 0.8657786846160889 +vt 0.69580078125 0.931352436542511 +vt 0.71142578125 0.931352436542511 +vt 0.71142578125 0.8657786846160889 +vt 0.71240234375 0.8657786846160889 +vt 0.71240234375 0.931352436542511 +vt 0.72802734375 0.931352436542511 +vt 0.72802734375 0.8657786846160889 +vt 0.72900390625 0.8657786846160889 +vt 0.72900390625 0.931352436542511 +vt 0.74462890625 0.931352436542511 +vt 0.74462890625 0.8657786846160889 +vt 0.74560546875 0.8657786846160889 +vt 0.74560546875 0.931352436542511 +vt 0.76123046875 0.931352436542511 +vt 0.76123046875 0.8657786846160889 +vt 0.33349609375 0.8678278923034668 +vt 0.33349609375 0.9006147384643555 +vt 0.36474609375 0.9006147384643555 +vt 0.36474609375 0.8678278923034668 +vt 0.01708984375 0.8944672346115112 +vt 0.01708984375 0.9600409865379333 +vt 0.03271484375 0.9600409865379333 +vt 0.03271484375 0.8944672346115112 +vt 0.03369140625 0.8944672346115112 +vt 0.03369140625 0.9600409865379333 +vt 0.04931640625 0.9600409865379333 +vt 0.04931640625 0.8944672346115112 +vt 0.05029296875 0.8944672346115112 +vt 0.05029296875 0.9600409865379333 +vt 0.06591796875 0.9600409865379333 +vt 0.06591796875 0.8944672346115112 +vt 0.15869140625 0.8965163826942444 +vt 0.15869140625 0.9293032884597778 +vt 0.18994140625 0.9293032884597778 +vt 0.18994140625 0.8965163826942444 +vt 0.06689453125 0.8944672346115112 +vt 0.06689453125 0.9600409865379333 +vt 0.08251953125 0.9600409865379333 +vt 0.08251953125 0.8944672346115112 +vt 0.50634765625 0.8965163826942444 +vt 0.50634765625 0.9620901346206665 +vt 0.52197265625 0.9620901346206665 +vt 0.52197265625 0.8965163826942444 +vt 0.86669921875 0.8965163826942444 +vt 0.86669921875 0.9293032884597778 +vt 0.89794921875 0.9293032884597778 +vt 0.89794921875 0.8965163826942444 +vt 0.28369140625 0.8985655903816223 +vt 0.28369140625 0.9641393423080444 +vt 0.29931640625 0.9641393423080444 +vt 0.29931640625 0.8985655903816223 +vt 0.30029296875 0.8985655903816223 +vt 0.30029296875 0.9641393423080444 +vt 0.31591796875 0.9641393423080444 +vt 0.31591796875 0.8985655903816223 +vt 0.31689453125 0.8985655903816223 +vt 0.31689453125 0.9641393423080444 +vt 0.33251953125 0.9641393423080444 +vt 0.33251953125 0.8985655903816223 +vt 0.64599609375 0.8985655903816223 +vt 0.64599609375 0.9641393423080444 +vt 0.66162109375 0.9641393423080444 +vt 0.66162109375 0.8985655903816223 +vt 0.66259765625 0.8985655903816223 +vt 0.66259765625 0.9641393423080444 +vt 0.67822265625 0.9641393423080444 +vt 0.67822265625 0.8985655903816223 +vt 0.77587890625 0.8985655903816223 +vt 0.77587890625 0.9641393423080444 +vt 0.79150390625 0.9641393423080444 +vt 0.79150390625 0.8985655903816223 +vt 0.79248046875 0.8985655903816223 +vt 0.79248046875 0.931352436542511 +vt 0.82373046875 0.931352436542511 +vt 0.82373046875 0.8985655903816223 +vt 0.82470703125 0.8985655903816223 +vt 0.82470703125 0.931352436542511 +vt 0.85595703125 0.931352436542511 +vt 0.85595703125 0.8985655903816223 +vt 0.36572265625 0.9006147384643555 +vt 0.36572265625 0.9661885499954224 +vt 0.38134765625 0.9661885499954224 +vt 0.38134765625 0.9006147384643555 +vt 0.39404296875 0.9006147384643555 +vt 0.39404296875 0.9661885499954224 +vt 0.40966796875 0.9661885499954224 +vt 0.40966796875 0.9006147384643555 +vt 0.41064453125 0.9006147384643555 +vt 0.41064453125 0.9661885499954224 +vt 0.42626953125 0.9661885499954224 +vt 0.42626953125 0.9006147384643555 +vt 0.42724609375 0.9006147384643555 +vt 0.42724609375 0.9661885499954224 +vt 0.44287109375 0.9661885499954224 +vt 0.44287109375 0.9006147384643555 +vt 0.91357421875 0.9006147384643555 +vt 0.91357421875 0.9334016442298889 +vt 0.94482421875 0.9334016442298889 +vt 0.94482421875 0.9006147384643555 +vt 0.33349609375 0.9026639461517334 +vt 0.33349609375 0.9682376980781555 +vt 0.34912109375 0.9682376980781555 +vt 0.34912109375 0.9026639461517334 +vt 0.09619140625 0.9252049326896667 +vt 0.09619140625 0.9579917788505554 +vt 0.12744140625 0.9579917788505554 +vt 0.12744140625 0.9252049326896667 +vt 0.20556640625 0.9252049326896667 +vt 0.20556640625 0.9907786846160889 +vt 0.22119140625 0.9907786846160889 +vt 0.22119140625 0.9252049326896667 +vt 0.22216796875 0.9252049326896667 +vt 0.22216796875 0.9907786846160889 +vt 0.23779296875 0.9907786846160889 +vt 0.23779296875 0.9252049326896667 +vt 0.45751953125 0.9272540807723999 +vt 0.45751953125 0.9600409865379333 +vt 0.48876953125 0.9600409865379333 +vt 0.48876953125 0.9272540807723999 +vt 0.25341796875 0.9252049326896667 +vt 0.25341796875 0.9907786846160889 +vt 0.26904296875 0.9907786846160889 +vt 0.26904296875 0.9252049326896667 +vt 0.00048828125 0.9272540807723999 +vt 0.00048828125 0.9928278923034668 +vt 0.01611328125 0.9928278923034668 +vt 0.01611328125 0.9272540807723999 +vt 0.94580078125 0.9293032884597778 +vt 0.94580078125 0.9620901346206665 +vt 0.97705078125 0.9620901346206665 +vt 0.97705078125 0.9293032884597778 +vt 0.97802734375 0.9272540807723999 +vt 0.97802734375 0.9600409865379333 +vt 0.99365234375 0.9600409865379333 +vt 0.99365234375 0.9272540807723999 +vt 0.14208984375 0.9293032884597778 +vt 0.14208984375 0.9620901346206665 +vt 0.15771484375 0.9620901346206665 +vt 0.15771484375 0.9293032884597778 +vt 0.48974609375 0.9293032884597778 +vt 0.48974609375 0.9620901346206665 +vt 0.50537109375 0.9620901346206665 +vt 0.50537109375 0.9293032884597778 +vt 0.15869140625 0.931352436542511 +vt 0.15869140625 0.9641393423080444 +vt 0.17431640625 0.9641393423080444 +vt 0.17431640625 0.931352436542511 +vt 0.17529296875 0.931352436542511 +vt 0.17529296875 0.9641393423080444 +vt 0.19091796875 0.9641393423080444 +vt 0.19091796875 0.931352436542511 +vt 0.53759765625 0.931352436542511 +vt 0.53759765625 0.9641393423080444 +vt 0.55322265625 0.9641393423080444 +vt 0.55322265625 0.931352436542511 +vt 0.55419921875 0.931352436542511 +vt 0.55419921875 0.9641393423080444 +vt 0.56982421875 0.9641393423080444 +vt 0.56982421875 0.931352436542511 +vt 0.57080078125 0.931352436542511 +vt 0.57080078125 0.9641393423080444 +vt 0.58642578125 0.9641393423080444 +vt 0.58642578125 0.931352436542511 +vt 0.86669921875 0.931352436542511 +vt 0.86669921875 0.9641393423080444 +vt 0.88232421875 0.9641393423080444 +vt 0.88232421875 0.931352436542511 +vt 0.88330078125 0.931352436542511 +vt 0.88330078125 0.9641393423080444 +vt 0.89892578125 0.9641393423080444 +vt 0.89892578125 0.931352436542511 +vt 0.59814453125 0.9334016442298889 +vt 0.59814453125 0.9661885499954224 +vt 0.61376953125 0.9661885499954224 +vt 0.61376953125 0.9334016442298889 +vt 0.61474609375 0.9334016442298889 +vt 0.61474609375 0.9661885499954224 +vt 0.63037109375 0.9661885499954224 +vt 0.63037109375 0.9334016442298889 +vt 0.67919921875 0.9334016442298889 +vt 0.67919921875 0.9661885499954224 +vt 0.69482421875 0.9661885499954224 +vt 0.69482421875 0.9334016442298889 +vt 0.69580078125 0.9334016442298889 +vt 0.69580078125 0.9661885499954224 +vt 0.71142578125 0.9661885499954224 +vt 0.71142578125 0.9334016442298889 +vt 0.71240234375 0.9334016442298889 +vt 0.71240234375 0.9661885499954224 +vt 0.72802734375 0.9661885499954224 +vt 0.72802734375 0.9334016442298889 +vt 0.72900390625 0.9334016442298889 +vt 0.72900390625 0.9661885499954224 +vt 0.74462890625 0.9661885499954224 +vt 0.74462890625 0.9334016442298889 +vt 0.74560546875 0.9334016442298889 +vt 0.74560546875 0.9661885499954224 +vt 0.76123046875 0.9661885499954224 +vt 0.76123046875 0.9334016442298889 +vt 0.79248046875 0.9334016442298889 +vt 0.79248046875 0.9661885499954224 +vt 0.80810546875 0.9661885499954224 +vt 0.80810546875 0.9334016442298889 +vt 0.80908203125 0.9334016442298889 +vt 0.80908203125 0.9661885499954224 +vt 0.82470703125 0.9661885499954224 +vt 0.82470703125 0.9334016442298889 +vt 0.82568359375 0.9334016442298889 +vt 0.82568359375 0.9661885499954224 +vt 0.84130859375 0.9661885499954224 +vt 0.84130859375 0.9334016442298889 +vt 0.84228515625 0.9334016442298889 +vt 0.84228515625 0.9661885499954224 +vt 0.85791015625 0.9661885499954224 +vt 0.85791015625 0.9334016442298889 +vt 0.91357421875 0.9354507923126221 +vt 0.91357421875 0.9682376980781555 +vt 0.92919921875 0.9682376980781555 +vt 0.92919921875 0.9354507923126221 +vt 0.09619140625 0.9600409865379333 +vt 0.09619140625 0.9928278923034668 +vt 0.11181640625 0.9928278923034668 +vt 0.11181640625 0.9600409865379333 +vt 0.11279296875 0.9600409865379333 +vt 0.11279296875 0.9928278923034668 +vt 0.12841796875 0.9928278923034668 +vt 0.12841796875 0.9600409865379333 +vt 0.01708984375 0.9620901346206665 +vt 0.01708984375 0.9948770403862 +vt 0.03271484375 0.9948770403862 +vt 0.03271484375 0.9620901346206665 +vt 0.03369140625 0.9620901346206665 +vt 0.03369140625 0.9948770403862 +vt 0.04931640625 0.9948770403862 +vt 0.04931640625 0.9620901346206665 +vt 0.05029296875 0.9620901346206665 +vt 0.05029296875 0.9948770403862 +vt 0.06591796875 0.9948770403862 +vt 0.06591796875 0.9620901346206665 +vt 0.06689453125 0.9620901346206665 +vt 0.06689453125 0.9948770403862 +vt 0.08251953125 0.9948770403862 +vt 0.08251953125 0.9620901346206665 +vt 0.45751953125 0.9620901346206665 +vt 0.45751953125 0.9948770403862 +vt 0.47314453125 0.9948770403862 +vt 0.47314453125 0.9620901346206665 +vt 0.97802734375 0.9620901346206665 +vt 0.97802734375 0.9948770403862 +vt 0.99365234375 0.9948770403862 +vt 0.99365234375 0.9620901346206665 +vt 0.14208984375 0.9641393423080444 +vt 0.14208984375 0.9969262480735779 +vt 0.15771484375 0.9969262480735779 +vt 0.15771484375 0.9641393423080444 +vt 0.48974609375 0.9641393423080444 +vt 0.48974609375 0.9969262480735779 +vt 0.50537109375 0.9969262480735779 +vt 0.50537109375 0.9641393423080444 +vt 0.50634765625 0.9641393423080444 +vt 0.50634765625 0.9969262480735779 +vt 0.52197265625 0.9969262480735779 +vt 0.52197265625 0.9641393423080444 +vt 0.94580078125 0.9641393423080444 +vt 0.94580078125 0.9969262480735779 +vt 0.96142578125 0.9969262480735779 +vt 0.96142578125 0.9641393423080444 +vt 0.15869140625 0.9661885499954224 +vt 0.15869140625 0.998975396156311 +vt 0.17431640625 0.998975396156311 +vt 0.17431640625 0.9661885499954224 +vt 0.17529296875 0.9661885499954224 +vt 0.17529296875 0.998975396156311 +vt 0.19091796875 0.998975396156311 +vt 0.19091796875 0.9661885499954224 +vt 0.28369140625 0.9661885499954224 +vt 0.28369140625 0.998975396156311 +vt 0.29931640625 0.998975396156311 +vt 0.29931640625 0.9661885499954224 +vt 0.30029296875 0.9661885499954224 +vt 0.30029296875 0.998975396156311 +vt 0.31591796875 0.998975396156311 +vt 0.31591796875 0.9661885499954224 +vt 0.31689453125 0.9661885499954224 +vt 0.31689453125 0.998975396156311 +vt 0.33251953125 0.998975396156311 +vt 0.33251953125 0.9661885499954224 +vt 0.53759765625 0.9661885499954224 +vt 0.53759765625 0.998975396156311 +vt 0.55322265625 0.998975396156311 +vt 0.55322265625 0.9661885499954224 +vt 0.55419921875 0.9661885499954224 +vt 0.55419921875 0.998975396156311 +vt 0.56982421875 0.998975396156311 +vt 0.56982421875 0.9661885499954224 +vt 0.57080078125 0.9661885499954224 +vt 0.57080078125 0.998975396156311 +vt 0.58642578125 0.998975396156311 +vt 0.58642578125 0.9661885499954224 +vt 0.64599609375 0.9661885499954224 +vt 0.64599609375 0.998975396156311 +vt 0.66162109375 0.998975396156311 +vt 0.66162109375 0.9661885499954224 +g xinzExi_2_n-mEsh_0 +f 3/3/3 2/2/2 1/1/1 +f 1/1/1 4/4/4 3/3/3 +f 7/7/7 6/6/6 5/5/5 +f 5/5/5 8/8/8 7/7/7 +f 11/11/11 10/10/10 9/9/9 +f 9/9/9 12/12/12 11/11/11 +f 15/15/15 14/14/14 13/13/13 +f 13/13/13 16/16/16 15/15/15 +f 19/19/19 18/18/18 17/17/17 +f 17/17/17 20/20/20 19/19/19 +f 23/23/23 22/22/22 21/21/21 +f 21/21/21 24/24/24 23/23/23 +f 27/27/27 26/26/26 25/25/25 +f 25/25/25 28/28/28 27/27/27 +f 31/31/31 30/30/30 29/29/29 +f 29/29/29 32/32/32 31/31/31 +f 35/35/35 34/34/34 33/33/33 +f 33/33/33 36/36/36 35/35/35 +f 39/39/39 38/38/38 37/37/37 +f 37/37/37 40/40/40 39/39/39 +f 43/43/43 42/42/42 41/41/41 +f 41/41/41 44/44/44 43/43/43 +f 47/47/47 46/46/46 45/45/45 +f 45/45/45 48/48/48 47/47/47 +f 51/51/51 50/50/50 49/49/49 +f 49/49/49 52/52/52 51/51/51 +f 55/55/55 54/54/54 53/53/53 +f 53/53/53 56/56/56 55/55/55 +f 59/59/59 58/58/58 57/57/57 +f 57/57/57 60/60/60 59/59/59 +f 63/63/63 62/62/62 61/61/61 +f 61/61/61 64/64/64 63/63/63 +f 67/67/67 66/66/66 65/65/65 +f 65/65/65 68/68/68 67/67/67 +f 71/71/71 70/70/70 69/69/69 +f 69/69/69 72/72/72 71/71/71 +f 75/75/75 74/74/74 73/73/73 +f 73/73/73 76/76/76 75/75/75 +f 79/79/79 78/78/78 77/77/77 +f 77/77/77 80/80/80 79/79/79 +f 83/83/83 82/82/82 81/81/81 +f 81/81/81 84/84/84 83/83/83 +f 87/87/87 86/86/86 85/85/85 +f 85/85/85 88/88/88 87/87/87 +f 91/91/91 90/90/90 89/89/89 +f 89/89/89 92/92/92 91/91/91 +f 95/95/95 94/94/94 93/93/93 +f 93/93/93 96/96/96 95/95/95 +f 99/99/99 98/98/98 97/97/97 +f 97/97/97 100/100/100 99/99/99 +f 103/103/103 102/102/102 101/101/101 +f 101/101/101 104/104/104 103/103/103 +f 107/107/107 106/106/106 105/105/105 +f 105/105/105 108/108/108 107/107/107 +f 111/111/111 110/110/110 109/109/109 +f 109/109/109 112/112/112 111/111/111 +f 115/115/115 114/114/114 113/113/113 +f 113/113/113 116/116/116 115/115/115 +f 119/119/119 118/118/118 117/117/117 +f 117/117/117 120/120/120 119/119/119 +f 123/123/123 122/122/122 121/121/121 +f 121/121/121 124/124/124 123/123/123 +f 127/127/127 126/126/126 125/125/125 +f 125/125/125 128/128/128 127/127/127 +f 131/131/131 130/130/130 129/129/129 +f 129/129/129 132/132/132 131/131/131 +f 135/135/135 134/134/134 133/133/133 +f 133/133/133 136/136/136 135/135/135 +f 139/139/139 138/138/138 137/137/137 +f 137/137/137 140/140/140 139/139/139 +f 143/143/143 142/142/142 141/141/141 +f 141/141/141 144/144/144 143/143/143 +f 147/147/147 146/146/146 145/145/145 +f 145/145/145 148/148/148 147/147/147 +f 151/151/151 150/150/150 149/149/149 +f 149/149/149 152/152/152 151/151/151 +f 155/155/155 154/154/154 153/153/153 +f 153/153/153 156/156/156 155/155/155 +f 159/159/159 158/158/158 157/157/157 +f 157/157/157 160/160/160 159/159/159 +f 163/163/163 162/162/162 161/161/161 +f 161/161/161 164/164/164 163/163/163 +f 167/167/167 166/166/166 165/165/165 +f 165/165/165 168/168/168 167/167/167 +f 171/171/171 170/170/170 169/169/169 +f 169/169/169 172/172/172 171/171/171 +f 175/175/175 174/174/174 173/173/173 +f 173/173/173 176/176/176 175/175/175 +f 179/179/179 178/178/178 177/177/177 +f 177/177/177 180/180/180 179/179/179 +f 183/183/183 182/182/182 181/181/181 +f 181/181/181 184/184/184 183/183/183 +f 187/187/187 186/186/186 185/185/185 +f 185/185/185 188/188/188 187/187/187 +f 191/191/191 190/190/190 189/189/189 +f 189/189/189 192/192/192 191/191/191 +f 195/195/195 194/194/194 193/193/193 +f 193/193/193 196/196/196 195/195/195 +f 199/199/199 198/198/198 197/197/197 +f 197/197/197 200/200/200 199/199/199 +f 203/203/203 202/202/202 201/201/201 +f 201/201/201 204/204/204 203/203/203 +f 207/207/207 206/206/206 205/205/205 +f 205/205/205 208/208/208 207/207/207 +f 211/211/211 210/210/210 209/209/209 +f 209/209/209 212/212/212 211/211/211 +f 215/215/215 214/214/214 213/213/213 +f 213/213/213 216/216/216 215/215/215 +f 219/219/219 218/218/218 217/217/217 +f 217/217/217 220/220/220 219/219/219 +f 223/223/223 222/222/222 221/221/221 +f 221/221/221 224/224/224 223/223/223 +f 227/227/227 226/226/226 225/225/225 +f 225/225/225 228/228/228 227/227/227 +f 231/231/231 230/230/230 229/229/229 +f 229/229/229 232/232/232 231/231/231 +f 235/235/235 234/234/234 233/233/233 +f 233/233/233 236/236/236 235/235/235 +f 239/239/239 238/238/238 237/237/237 +f 237/237/237 240/240/240 239/239/239 +f 243/243/243 242/242/242 241/241/241 +f 241/241/241 244/244/244 243/243/243 +f 247/247/247 246/246/246 245/245/245 +f 245/245/245 248/248/248 247/247/247 +f 251/251/251 250/250/250 249/249/249 +f 249/249/249 252/252/252 251/251/251 +f 255/255/255 254/254/254 253/253/253 +f 253/253/253 256/256/256 255/255/255 +f 259/259/259 258/258/258 257/257/257 +f 257/257/257 260/260/260 259/259/259 +f 263/263/263 262/262/262 261/261/261 +f 261/261/261 264/264/264 263/263/263 +f 267/267/267 266/266/266 265/265/265 +f 265/265/265 268/268/268 267/267/267 +f 271/271/271 270/270/270 269/269/269 +f 269/269/269 272/272/272 271/271/271 +f 275/275/275 274/274/274 273/273/273 +f 273/273/273 276/276/276 275/275/275 +f 279/279/279 278/278/278 277/277/277 +f 277/277/277 280/280/280 279/279/279 +f 283/283/283 282/282/282 281/281/281 +f 281/281/281 284/284/284 283/283/283 +f 287/287/287 286/286/286 285/285/285 +f 285/285/285 288/288/288 287/287/287 +f 291/291/291 290/290/290 289/289/289 +f 289/289/289 292/292/292 291/291/291 +f 295/295/295 294/294/294 293/293/293 +f 293/293/293 296/296/296 295/295/295 +f 299/299/299 298/298/298 297/297/297 +f 297/297/297 300/300/300 299/299/299 +f 303/303/303 302/302/302 301/301/301 +f 301/301/301 304/304/304 303/303/303 +f 307/307/307 306/306/306 305/305/305 +f 305/305/305 308/308/308 307/307/307 +f 311/311/311 310/310/310 309/309/309 +f 309/309/309 312/312/312 311/311/311 +f 315/315/315 314/314/314 313/313/313 +f 313/313/313 316/316/316 315/315/315 +f 319/319/319 318/318/318 317/317/317 +f 317/317/317 320/320/320 319/319/319 +f 323/323/323 322/322/322 321/321/321 +f 321/321/321 324/324/324 323/323/323 +f 327/327/327 326/326/326 325/325/325 +f 325/325/325 328/328/328 327/327/327 +f 331/331/331 330/330/330 329/329/329 +f 329/329/329 332/332/332 331/331/331 +f 335/335/335 334/334/334 333/333/333 +f 333/333/333 336/336/336 335/335/335 +f 339/339/339 338/338/338 337/337/337 +f 337/337/337 340/340/340 339/339/339 +f 343/343/343 342/342/342 341/341/341 +f 341/341/341 344/344/344 343/343/343 +f 347/347/347 346/346/346 345/345/345 +f 345/345/345 348/348/348 347/347/347 +f 351/351/351 350/350/350 349/349/349 +f 349/349/349 352/352/352 351/351/351 +f 355/355/355 354/354/354 353/353/353 +f 353/353/353 356/356/356 355/355/355 +f 359/359/359 358/358/358 357/357/357 +f 357/357/357 360/360/360 359/359/359 +f 363/363/363 362/362/362 361/361/361 +f 361/361/361 364/364/364 363/363/363 +f 367/367/367 366/366/366 365/365/365 +f 365/365/365 368/368/368 367/367/367 +f 371/371/371 370/370/370 369/369/369 +f 369/369/369 372/372/372 371/371/371 +f 375/375/375 374/374/374 373/373/373 +f 373/373/373 376/376/376 375/375/375 +f 379/379/379 378/378/378 377/377/377 +f 377/377/377 380/380/380 379/379/379 +f 383/383/383 382/382/382 381/381/381 +f 381/381/381 384/384/384 383/383/383 +f 387/387/387 386/386/386 385/385/385 +f 385/385/385 388/388/388 387/387/387 +f 391/391/391 390/390/390 389/389/389 +f 389/389/389 392/392/392 391/391/391 +f 395/395/395 394/394/394 393/393/393 +f 393/393/393 396/396/396 395/395/395 +f 399/399/399 398/398/398 397/397/397 +f 397/397/397 400/400/400 399/399/399 +f 403/403/403 402/402/402 401/401/401 +f 401/401/401 404/404/404 403/403/403 +f 407/407/407 406/406/406 405/405/405 +f 405/405/405 408/408/408 407/407/407 +f 411/411/411 410/410/410 409/409/409 +f 409/409/409 412/412/412 411/411/411 +f 415/415/415 414/414/414 413/413/413 +f 413/413/413 416/416/416 415/415/415 +f 419/419/419 418/418/418 417/417/417 +f 417/417/417 420/420/420 419/419/419 +f 423/423/423 422/422/422 421/421/421 +f 421/421/421 424/424/424 423/423/423 +f 427/427/427 426/426/426 425/425/425 +f 425/425/425 428/428/428 427/427/427 +f 431/431/431 430/430/430 429/429/429 +f 429/429/429 432/432/432 431/431/431 +f 435/435/435 434/434/434 433/433/433 +f 433/433/433 436/436/436 435/435/435 +f 439/439/439 438/438/438 437/437/437 +f 437/437/437 440/440/440 439/439/439 +f 443/443/443 442/442/442 441/441/441 +f 441/441/441 444/444/444 443/443/443 +f 447/447/447 446/446/446 445/445/445 +f 445/445/445 448/448/448 447/447/447 +f 451/451/451 450/450/450 449/449/449 +f 449/449/449 452/452/452 451/451/451 +f 455/455/455 454/454/454 453/453/453 +f 453/453/453 456/456/456 455/455/455 +f 459/459/459 458/458/458 457/457/457 +f 457/457/457 460/460/460 459/459/459 +f 463/463/463 462/462/462 461/461/461 +f 461/461/461 464/464/464 463/463/463 +f 467/467/467 466/466/466 465/465/465 +f 465/465/465 468/468/468 467/467/467 +f 471/471/471 470/470/470 469/469/469 +f 469/469/469 472/472/472 471/471/471 +f 475/475/475 474/474/474 473/473/473 +f 473/473/473 476/476/476 475/475/475 +f 479/479/479 478/478/478 477/477/477 +f 477/477/477 480/480/480 479/479/479 +f 483/483/483 482/482/482 481/481/481 +f 481/481/481 484/484/484 483/483/483 +f 487/487/487 486/486/486 485/485/485 +f 485/485/485 488/488/488 487/487/487 +f 491/491/491 490/490/490 489/489/489 +f 489/489/489 492/492/492 491/491/491 +f 495/495/495 494/494/494 493/493/493 +f 493/493/493 496/496/496 495/495/495 +f 499/499/499 498/498/498 497/497/497 +f 497/497/497 500/500/500 499/499/499 +f 503/503/503 502/502/502 501/501/501 +f 501/501/501 504/504/504 503/503/503 +f 507/507/507 506/506/506 505/505/505 +f 505/505/505 508/508/508 507/507/507 +f 511/511/511 510/510/510 509/509/509 +f 509/509/509 512/512/512 511/511/511 +f 515/515/515 514/514/514 513/513/513 +f 513/513/513 516/516/516 515/515/515 +f 519/519/519 518/518/518 517/517/517 +f 517/517/517 520/520/520 519/519/519 +f 523/523/523 522/522/522 521/521/521 +f 521/521/521 524/524/524 523/523/523 +f 527/527/527 526/526/526 525/525/525 +f 525/525/525 528/528/528 527/527/527 +f 531/531/531 530/530/530 529/529/529 +f 529/529/529 532/532/532 531/531/531 +f 535/535/535 534/534/534 533/533/533 +f 533/533/533 536/536/536 535/535/535 +f 539/539/539 538/538/538 537/537/537 +f 537/537/537 540/540/540 539/539/539 +f 543/543/543 542/542/542 541/541/541 +f 541/541/541 544/544/544 543/543/543 +f 547/547/547 546/546/546 545/545/545 +f 545/545/545 548/548/548 547/547/547 +f 551/551/551 550/550/550 549/549/549 +f 549/549/549 552/552/552 551/551/551 +f 555/555/555 554/554/554 553/553/553 +f 553/553/553 556/556/556 555/555/555 +f 559/559/559 558/558/558 557/557/557 +f 557/557/557 560/560/560 559/559/559 +f 563/563/563 562/562/562 561/561/561 +f 561/561/561 564/564/564 563/563/563 +f 567/567/567 566/566/566 565/565/565 +f 565/565/565 568/568/568 567/567/567 +f 571/571/571 570/570/570 569/569/569 +f 569/569/569 572/572/572 571/571/571 +f 575/575/575 574/574/574 573/573/573 +f 573/573/573 576/576/576 575/575/575 +f 579/579/579 578/578/578 577/577/577 +f 577/577/577 580/580/580 579/579/579 +f 583/583/583 582/582/582 581/581/581 +f 581/581/581 584/584/584 583/583/583 +f 587/587/587 586/586/586 585/585/585 +f 585/585/585 588/588/588 587/587/587 +f 591/591/591 590/590/590 589/589/589 +f 589/589/589 592/592/592 591/591/591 +f 595/595/595 594/594/594 593/593/593 +f 593/593/593 596/596/596 595/595/595 +f 599/599/599 598/598/598 597/597/597 +f 597/597/597 600/600/600 599/599/599 +f 603/603/603 602/602/602 601/601/601 +f 601/601/601 604/604/604 603/603/603 +f 607/607/607 606/606/606 605/605/605 +f 605/605/605 608/608/608 607/607/607 +f 611/611/611 610/610/610 609/609/609 +f 609/609/609 612/612/612 611/611/611 +f 615/615/615 614/614/614 613/613/613 +f 613/613/613 616/616/616 615/615/615 +f 619/619/619 618/618/618 617/617/617 +f 617/617/617 620/620/620 619/619/619 +f 623/623/623 622/622/622 621/621/621 +f 621/621/621 624/624/624 623/623/623 +f 627/627/627 626/626/626 625/625/625 +f 625/625/625 628/628/628 627/627/627 +f 631/631/631 630/630/630 629/629/629 +f 629/629/629 632/632/632 631/631/631 +f 635/635/635 634/634/634 633/633/633 +f 633/633/633 636/636/636 635/635/635 +f 639/639/639 638/638/638 637/637/637 +f 637/637/637 640/640/640 639/639/639 +f 643/643/643 642/642/642 641/641/641 +f 641/641/641 644/644/644 643/643/643 +f 647/647/647 646/646/646 645/645/645 +f 645/645/645 648/648/648 647/647/647 +f 651/651/651 650/650/650 649/649/649 +f 649/649/649 652/652/652 651/651/651 +f 655/655/655 654/654/654 653/653/653 +f 653/653/653 656/656/656 655/655/655 +f 659/659/659 658/658/658 657/657/657 +f 657/657/657 660/660/660 659/659/659 +f 663/663/663 662/662/662 661/661/661 +f 661/661/661 664/664/664 663/663/663 +f 667/667/667 666/666/666 665/665/665 +f 665/665/665 668/668/668 667/667/667 +f 671/671/671 670/670/670 669/669/669 +f 669/669/669 672/672/672 671/671/671 +f 675/675/675 674/674/674 673/673/673 +f 673/673/673 676/676/676 675/675/675 +f 679/679/679 678/678/678 677/677/677 +f 677/677/677 680/680/680 679/679/679 +f 683/683/683 682/682/682 681/681/681 +f 681/681/681 684/684/684 683/683/683 +f 687/687/687 686/686/686 685/685/685 +f 685/685/685 688/688/688 687/687/687 +f 691/691/691 690/690/690 689/689/689 +f 689/689/689 692/692/692 691/691/691 +f 695/695/695 694/694/694 693/693/693 +f 693/693/693 696/696/696 695/695/695 +f 699/699/699 698/698/698 697/697/697 +f 697/697/697 700/700/700 699/699/699 +f 703/703/703 702/702/702 701/701/701 +f 701/701/701 704/704/704 703/703/703 +f 707/707/707 706/706/706 705/705/705 +f 705/705/705 708/708/708 707/707/707 +f 711/711/711 710/710/710 709/709/709 +f 709/709/709 712/712/712 711/711/711 +f 715/715/715 714/714/714 713/713/713 +f 713/713/713 716/716/716 715/715/715 +f 719/719/719 718/718/718 717/717/717 +f 717/717/717 720/720/720 719/719/719 +f 723/723/723 722/722/722 721/721/721 +f 721/721/721 724/724/724 723/723/723 diff --git a/tests/test_main.py b/tests/test_main.py index a134b699..1c73fb29 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,38 +1,71 @@ import os +import UnityPy +from PIL import Image SAMPLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples") -def test_import(): - import UnityPy -def test_read(): - import UnityPy +def test_read_single(): for f in os.listdir(SAMPLES): - am = UnityPy.AssetsManager(os.path.join(SAMPLES, f)) - for asset in am.assets.values(): - for obj in asset.objects.values(): - obj.read() - + env = UnityPy.load(os.path.join(SAMPLES, f)) + for obj in env.objects: + obj.read() + + +def test_read_batch(): + env = UnityPy.load(SAMPLES) + for obj in env.objects: + obj.read() + + def test_texture2d(): import UnityPy + for f in os.listdir(SAMPLES): - am = UnityPy.AssetsManager(os.path.join(SAMPLES, f)) - for asset in am.assets.values(): - for obj in asset.objects.values(): - if obj.type == "Texture2D": - obj.read().image.save("test.png") + env = UnityPy.load(os.path.join(SAMPLES, f)) + for obj in env.objects: + if obj.type == "Texture2D": + data = obj.read() + data.image.save("test.png") + data.image = data.image.transpose(Image.ROTATE_90) + data.save() + def test_sprite(): import UnityPy + for f in os.listdir(SAMPLES): - am = UnityPy.AssetsManager(os.path.join(SAMPLES, f)) - for asset in am.assets.values(): - for obj in asset.objects.values(): - if obj.type == "Sprite": - obj.read().image.save("test.png") + env = UnityPy.load(os.path.join(SAMPLES, f)) + for obj in env.objects: + if obj.type == "Sprite": + obj.read().image.save("test.png") + + +def test_audioclip(): + import UnityPy + + env = UnityPy.load(os.path.join(SAMPLES, "char_118_yuki.ab")) + for obj in env.objects: + if obj.type == "AudioClip": + clip = obj.read() + assert len(clip.samples) == 1 + + +def test_mesh(): + env = UnityPy.load(os.path.join(SAMPLES, "xinzexi_2_n_tex")) + with open(os.path.join(SAMPLES, 'xinzexi_2_n_tex_mesh'), 'rb') as f: + wanted = f.read().replace(b'\r', b'') + for obj in env.objects: + if obj.type == "Mesh": + mesh = obj.read() + data = mesh.export() + if isinstance(data, str): + data = data.encode('utf8').replace(b'\r', b'') + assert data == wanted + if __name__ == "__main__": for x in list(locals()): if str(x)[:4] == "test": locals()[x]() - input("All Tests Passed") \ No newline at end of file + input("All Tests Passed")