diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index bec4e38f..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: K0lb3 diff --git a/.github/ISSUE_TEMPLATE/bug---error-report.md b/.github/ISSUE_TEMPLATE/bug---error-report.md deleted file mode 100644 index dd64a8b9..00000000 --- a/.github/ISSUE_TEMPLATE/bug---error-report.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Bug & Error report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Code** -A snippet of the code section that cases the bug or error. - -**Error** -The error message that is produced by python. - -**Bug** -A description of what you expect to happen/see and what actually happens. - -**To Reproduce** -- a copy of the file that causes the problem -- following data: - - Python version - - UnityPy version diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 88ad43f5..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,67 +0,0 @@ -# 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 deleted file mode 100644 index 30d4f016..00000000 --- a/.github/workflows/python-package.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Test and Publish - -on: [push, pull_request, workflow_dispatch] - -jobs: - test: - strategy: - fail-fast: false - matrix: - os: ["ubuntu-latest", "macOS-latest", "windows-latest"] - - runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.7" - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip pytest - - - name: Install UnityPy - run: | - pip install . - - - name: Run tests - run: | - pytest -v -s - - release: - needs: [test] - strategy: - fail-fast: false - matrix: - os: ["ubuntu-20.04", "windows-2019", "macos-11"] - cp: ["cp37", "cp38", "cp39", "cp310", "cp311"] - - timeout-minutes: 30 - - runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} - ${{ matrix.cp }} - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip wheel setuptools - python -m pip install --upgrade twine build - - - name: Set up QEMU - if: runner.os == 'Linux' - uses: docker/setup-qemu-action@v2 - with: - platforms: all - - - name: Build wheels - uses: pypa/cibuildwheel@v2.11.2 - env: - CIBW_ARCHS_LINUX: auto aarch64 - CIBW_ARCHS_MACOS: x86_64 arm64 universal2 - CIBW_BUILD: | - ${{ matrix.cp }}-manylinux_x86_64 - ${{ matrix.cp }}-manylinux_i686 - ${{ matrix.cp }}-manylinux_aarch64 - ${{ matrix.cp }}-win_amd64 - ${{ matrix.cp }}-win32 - ${{ matrix.cp }}-macosx_x86_64 - ${{ matrix.cp }}-macosx_arm64 - ${{ matrix.cp }}-macosx_universal2 - # Disabled repair wheel since the fmob lib is not compatible - CIBW_REPAIR_WHEEL_COMMAND: "" - CIBW_REPAIR_WHEEL_COMMAND_LINUX: 'mv {wheel} {dest_dir}/"$(basename {wheel} | sed "s/-linux_/-manylinux_2_17_/")"' - CIBW_TEST_REQUIRES: pytest - CIBW_TEST_COMMAND: pytest -v -s {package}/tests - CIBW_TEST_SKIP: "*-macosx_arm64 *-macosx_universal2:arm64" - - - name: Publish to PyPI - if: success() && github.event_name == 'push' && env.TWINE_PASSWORD != '' && github.ref == 'refs/heads/master' - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: | - twine upload ./wheelhouse/*.whl --skip-existing - python -m build --sdist - twine upload ./dist/*.tar.gz --skip-existing diff --git a/README.md b/README.md index 18149b6b..d2391816 100644 --- a/README.md +++ b/README.md @@ -1,394 +1,27 @@ # UnityPy -[![Discord server invite](https://discordapp.com/api/guilds/603359898507673630/embed.png)](https://discord.gg/C6txv7M) -[![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/github/license/K0lb3/UnityPy)](https://github.com/K0lb3/UnityPy/blob/master/LICENSE) -![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. +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 (and all other types that you have the typetree of) - -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). - -If you're using UnityPy a commercial project, -a donation to a charitable cause or a sponsorship of this project is expected. - -**As UnityPy is still in active development breaking changes can happen.** -Those changes are usually limited to minor versions (x.y) and not to patch versions (x.y.z). -So in case that you don't want to actively maintain your project, -make sure to make a note of the used UnityPy version in your README or add a check in your code. -e.g. - -```python -if UnityPy.__version__ != '1.9.6': - raise ImportError("Invalid UnityPy version detected. Please use version 1.9.6") -``` - -1. [Installation](#installation) -2. [Example](#example) -3. [Important Classes](#important-classes) -4. [Important Object Types](#important-object-types) -5. [Custom Fileystem](#custom-filesystem) -6. [Credits](#credits) + - Texture2D + - Sprite(indirectly via linked Texture2D) + - TextAsset + - MonoBehaviour ## Installation -**Python 3.7.0 or higher is required** +**Python 3.10.0 or higher is required** -via pypi +Download/clone the git and use: ```cmd -pip install UnityPy -``` - -from source - -```cmd -git clone https://github.com/K0lb3/UnityPy.git -cd UnityPy -python -m pip install . -``` - -### Notes - -#### Windows - -Visual C++ Redistributable is required for the brotli dependency. - -### Crash without warning/error - -The C-implementation of the typetree reader can directly crash python. -In case this happens, the usage of the C-typetree reader can be disabled by adding these two lines to your main file. - -```python -from UnityPy.helpers import TypeTreeHelper -TypeTreeHelper.read_typetree_c = False -``` - -## Example - -The following is a simple example. - -```python -import os -import UnityPy - -def unpack_all_assets(source_folder : str, destination_folder : str): - # iterate over all files in source folder - for root, dirs, files in os.walk(source_folder): - for file_name in files: - # generate file_path - file_path = os.path.join(root, file_name) - # load that file via UnityPy.load - env = UnityPy.load(file_path) - - # iterate over internal objects - for obj in env.objects: - # process specific object types - if obj.type.name in ["Texture2D", "Sprite"]: - # parse the object data - data = obj.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.name 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 with slightly advanced python skills should look at [UnityPy/tools/extractor.py](UnityPy/tools/extractor.py) for a more advanced example. -It can also be used as a general template or as an importable tool. - -### Setting the decryption key for Unity CN's AssetBundle encryption - -The chinese version of Unity has its own inbuild option to encrypt AssetBundles/BundleFiles. As it's a feature of Unity itself, and not a game specific protection, it is included in UnityPy as well. -To enable encryption simply use `UnityPy.set_assetbundle_decrypt_key(key)`, with key being the value that the game that loads the budles passes to `AssetBundle.SetAssetBundleDecryptKey`. - -## Important Classes - -### [Environment](UnityPy/environment.py) - -Environment loads and parses the given files. -It can be initialized via: - -- a file path - apk files can be loaded as well -- a folder path - loads all files in that folder (bad idea for folders with a lot of files) -- a stream - e.g., io.BytesIO, file stream,... -- a bytes object - will be loaded into a stream - -UnityPy can detect if the file is a WebFile, BundleFile, Asset, or APK. - -The unpacked assets will be loaded into `.files`, a dict consisting of `asset-name : asset`. - -All objects of the loaded assets can be easily accessed via `.objects`, -which itself is a simple recursive iterator. - -```python -import io -import UnityPy - -# all of the following would work -src = "file_path" -src = b"bytes" -src = io.BytesIO(b"Streamable") - -env = UnityPy.load(src) - -for obj in env.objects: - ... - -# saving an edited file - # apply modifications to the objects - # don't forget to use data.save() - ... -with open(dst, "wb") as f: - f.write(env.file.save()) -``` - -### [Asset](UnityPy/files/SerializedFile.py) - -Assets are a container that contains multiple objects. -One of these objects can be an AssetBundle, which contains a file path for some of the objects in the same asset. - -All objects can be found in the `.objects` dict - `{ID : object}`. - -The objects with a file path can be found in the `.container` dict - `{path : object}`. - -### [Object](UnityPy/files/ObjectReader.py) - -Objects contain the _actual_ files, e.g., textures, text files, meshes, settings, ... - -To acquire the actual data of an object it has to be read first. This happens via the `.read()` function. This isn't done automatically to save time because only a small part of the objects are of interest. Serialized objects can be set with raw data using `.set_raw_data(data)` or modified with `.save()` function, if supported. - -## Important Object Types - -All object types can be found in [UnityPy/classes](UnityPy/classes/). - -### [Texture2D](UnityPy/classes/Texture2D.py) - -- `.name` -- `.image` converts the texture into a `PIL.Image` -- `.m_Width` - texture width (int) -- `.m_Height` - texture height (int) - -**Export** - -```python -from PIL import Image -for obj in env.objects: - if obj.type.name == "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() +pip install . ``` -### [Sprite](UnityPy/classes/Sprite.py) - -Sprites are part of a texture and can have a separate alpha-image as well. -Unlike most other extractors (including AssetStudio), UnityPy merges those two images by itself. - -- `.name` -- `.image` - converts the merged texture part into a `PIL.Image` -- `.m_Width` - sprite width (int) -- `.m_Height` - sprite height (int) - -**Export** - -```python -for obj in env.objects: - if obj.type.name == "Sprite": - data = obj.read() - data.image.save(path) -``` - -### [TextAsset](UnityPy/classes/TextAsset.py) - -TextAssets are usually normal text files. - -- `.name` -- `.script` - binary data (bytes) -- `.text` - script decoded via UTF8 (str) - -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.name == "TextAsset": - # export asset - data = obj.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 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 exist, 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` -- `.raw_data` - data after the basic initialisation - -**Export** - -```python -import json - -for obj in env.objects: - if obj.type.name == "MonoBehaviour": - # export - if obj.serialized_type.nodes: - # save decoded data - tree = obj.read_typetree() - fp = os.path.join(extract_dir, f"{tree['m_Name']}.json") - with open(fp, "wt", encoding = "utf8") as f: - json.dump(tree, f, ensure_ascii = False, indent = 4) - 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: - data = obj.read() - with open(os.path.join(replace_dir, data.name)) as f: - data.save(raw_data = f.read()) -``` - -### [AudioClip](UnityPy/classes/AudioClip.py) - -- `.samples` - `{sample-name : sample-data}` - -The samples are converted into the .wav format. -The sample data is a .wav file in bytes. - -```python -clip : AudioClip -for name, data in clip.samples.items(): - with open(name, "wb") as f: - f.write(data) -``` - -### [Font](UnityPy/classes/Font.py) - -```python -if obj.type.name == "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) -``` - -### [Mesh](UnityPy/classes/Mesh.py) - -- `.export()` - mesh exported as .obj (str) - -The mesh will be converted to the Wavefront .obj file format. - -```python -mesh : Mesh -with open(f"{mesh.name}.obj", "wt", newline = "") as f: - # newline = "" is important - f.write(mesh.export()) -``` - -### Renderer, MeshRenderer, SkinnedMeshRenderer - -ALPHA-VERSION - -- `.export(export_dir)` - exports the associated mesh, materials, and textures into the given directory - -The mesh and materials will be in the Wavefront formats. - -```python -mesh_renderer : Renderer -export_dir: str - -if mesh_renderer.m_GameObject: - # get the name of the model - game_object = mesh_renderer.m_GameObject.read() - export_dir = os.path.join(export_dir, game_object.name) -mesh_renderer.export(export_dir) -``` - -## Custom-Filesystem - -UnityPy uses [fsspec](https://github.com/fsspec/filesystem_spec) under the hood to manage all filesystem interactions. -This allows using various different types of filesystems without having to change UnityPy's code. -It also means that you can use your own custom filesystem to e.g. handle indirection via catalog files, load assets on demand from a server, or decrypt files. - -Following methods of the filesystem have to be implemented for using it in UnityPy. - -- sep (not a function, just the seperator as character) -- isfile(self, path: str) -> bool -- isdir(self, path: str) -> bool -- exists(self, path: str, \*\*kwargs) -> bool -- walk(self, path: str, \*\*kwargs) -> Iterable[List[str], List[str], List[str]] -- open(self, path: str, mode: str = "rb", \*\*kwargs) -> file ("rb" mode required, "wt" required for ModelExporter) -- makedirs(self, path: str, exist_ok: bool = False) -> bool - -## Credits +### Usage -First of all, -thanks a lot to all contributors of UnityPy and all of its users. +Take a look at [/examples/dump_assets.py](/examples/dump_assets.py) for an example of usage. -Also, -many thanks to: +This fork can also dump scene trees of GameObjects and their descendants showing their names and IDs in the file. -- [Perfare](https://github.com/Perfare) for creating and maintaining and every contributor of [AssetStudio](https://github.com/Perfare/AssetStudio) -- [ds5678](https://github.com/ds5678) for the [TypeTreeDumps](https://github.com/AssetRipper/TypeTreeDumps) and the [custom minimal Tpk format](https://github.com/AssetRipper/Tpk) -- [Razmoth](https://github.com/Razmoth) for figuring out and sharing Unity CN's AssetBundle decryption ([src](https://github.com/Razmoth/PGRStudio)). diff --git a/UnityPy/__init__.py b/UnityPy/__init__.py index d7f71da1..661b2dcf 100644 --- a/UnityPy/__init__.py +++ b/UnityPy/__init__.py @@ -1,12 +1,13 @@ -__version__ = "1.10.1" +__version__ = "1.20.16_uuf" -from .environment import Environment -from .helpers.ArchiveStorageManager import set_assetbundle_decrypt_key - - -def load(*args, fs=None, **kwargs): - return Environment(*args, fs=fs, **kwargs) +from . import color_logging +from .environment import Environment as Environment +from .helpers.ArchiveStorageManager import ( + set_assetbundle_decrypt_key as set_assetbundle_decrypt_key, +) +def load(*args, **kwargs): + return Environment(*args, fs=None, **kwargs) # backward compatibility AssetsManager = Environment diff --git a/UnityPy/classes/AnimationClip.py b/UnityPy/classes/AnimationClip.py index 9176ab1d..0eb9d20e 100644 --- a/UnityPy/classes/AnimationClip.py +++ b/UnityPy/classes/AnimationClip.py @@ -519,6 +519,8 @@ def __init__(self, reader): self.typeID = ClassIDType(reader.read_u_short()) self.customType = reader.read_byte() self.isPPtrCurve = reader.read_byte() + if version >= (2022, 1): # 2022.1 and up + self.isIntCurve = reader.read_byte() reader.align_stream() @@ -552,6 +554,21 @@ def FindBinding(self, index): return None +class AnimationEvent: + def __init__(self, reader): + version = reader.version + self.time = reader.read_float() + self.functionName = reader.read_aligned_string() + self.data = reader.read_aligned_string() + self.objectReferenceParameter = PPtr(reader) + self.floatParameter = reader.read_float() + + if version > (3, ): + self.intParameter = reader.read_int() + + self.messageOptions = reader.read_int() + + class AnimationType(IntEnum): kLegacy = (1,) kGeneric = (2,) @@ -609,11 +626,12 @@ def __init__(self, reader): if version >= (4, 3): # 4.3 and up self.m_ClipBindingConstant = AnimationClipBindingConstant(reader) + if version >= (2018, 3): + self.m_HasGenericRootTransform = reader.read_boolean() + self.m_HasMotionFloatCurves = reader.read_boolean() + reader.align_stream() -# m_HasGenericRootTransform 2018.3 -# m_HasMotionFloatCurves 2018.3 -# numEvents = reader.read_int() -# self.m_Events = [ -# AnimationEvent(reader) -# for _ in range(numEvents) -# ] + numEvents = self.reader.read_int() + self.m_Events = [AnimationEvent(self.reader) for _ in range(numEvents)] + if version >= (2017, ): + reader.align_stream() diff --git a/UnityPy/classes/AssetBundle.py b/UnityPy/classes/AssetBundle.py index d4fd87c8..6912d95e 100644 --- a/UnityPy/classes/AssetBundle.py +++ b/UnityPy/classes/AssetBundle.py @@ -1,5 +1,6 @@ from .NamedObject import NamedObject from .PPtr import PPtr +from ..streams import EndianBinaryWriter class AssetInfo: @@ -8,6 +9,13 @@ def __init__(self, reader): self.preload_size = reader.read_int() self.asset = PPtr(reader) + def save(self, writer): + if writer is None: + writer = EndianBinaryWriter() + writer.write_int(self.preload_index) + writer.write_int(self.preload_size) + self.asset.save(writer) + class AssetBundle(NamedObject): def __init__(self, reader): @@ -15,8 +23,55 @@ def __init__(self, reader): preload_table_size = reader.read_int() self.m_PreloadTable = [PPtr(reader) for _ in range(preload_table_size)] container_size = reader.read_int() - self.m_Container = {} - # TODO - m_Container is a multi-dict, multiple values can have the same key - for i in range(container_size): + self.m_Container = [] + # TODO: m_Container is a multi-dict, multiple values can have the same key + for _ in range(container_size): key = reader.read_aligned_string() - self.m_Container[key] = AssetInfo(reader) + self.m_Container.append((key, AssetInfo(reader))) + self.m_MainAsset = AssetInfo(reader) + self.m_RuntimeCompatibility = reader.read_u_int() + self.m_AssetBundleName = reader.read_aligned_string() + self.m_Dependencies = reader.read_string_array() + self.m_IsStreamedSceneAssetBundle = reader.read_boolean() + reader.align_stream() + if reader.version[:2] > (2017,2): + self.m_ExplicitDataLayout = reader.read_int() + if reader.version[0] >= (2017): + self.m_PathFlags = reader.read_int() + if reader.version[:2] > (2017, 2): + container_size = reader.read_int() + self.m_SceneHashes = {} + for _ in range(container_size): + key = reader.read_aligned_string() + value = reader.read_aligned_string() + self.m_SceneHashes[key] = value + pass + + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + super().save(writer) + version = self.version + writer.write_int(len(self.m_PreloadTable)) + for preload in self.m_PreloadTable: + preload.save(writer) + writer.write_int(len(self.m_Container)) + for citem in self.m_Container: + writer.write_aligned_string(citem[0]) + citem[1].save(writer) + self.m_MainAsset.save(writer) + writer.write_aligned_string(self.m_RuntimeCompatibility) + writer.write_aligned_string(self.m_AssetBundleName) + writer.write_string_array(self.m_Dependencies) + writer.write_boolean( self.m_IsStreamedSceneAssetBundle) + writer.align_stream() + if version[:2] > (2017,2): + writer.write_int(self.m_ExplicitDataLayout) + if writer.version[0] >= (2017): + writer.write_int(self.m_PathFlags) + if version[:2] > (2017, 2): + writer.write_int(len(self.m_SceneHashes)) + for hashitem in self.m_SceneHashes: + writer.write_aligned_string(hashitem[0]) + writer.write_aligned_string(hashitem[1]) diff --git a/UnityPy/classes/AssetReference.py b/UnityPy/classes/AssetReference.py new file mode 100644 index 00000000..7c814c22 --- /dev/null +++ b/UnityPy/classes/AssetReference.py @@ -0,0 +1,35 @@ +class AssetTypeReference: + def __init__(self, className="", nameSpace="", asmName=""): + self.m_ClassName = className + self.m_Namespace = nameSpace + self.m_AsmName = asmName + + def ReadMetadata(self, reader): + self.m_ClassName = reader.read_string_to_null() + self.m_Namespace = reader.read_string_to_null() + self.m_AsmName = reader.read_string_to_null() + + def ReadAsset(self, reader): + self.m_ClassName = reader.read_aligned_string() + self.m_Namespace = reader.read_aligned_string() + self.m_AsmName = reader.read_aligned_string() + + def WriteMetadata(self, writer): + writer.writer_string_to_null(self.m_ClassName) + writer.writer_string_to_null(self.m_Namespace) + writer.writer_string_to_null(self.m_AsmName) + + def WriteAsset(self, writer): + writer.write_aligned_string(self.m_ClassName) + writer.write_aligned_string(self.m_Namespace) + writer.write_aligned_string(self.m_AsmName) + + def __eq__(self, other): + if not isinstance(other, AssetTypeReference): + return False + return self.m_ClassName == other.ClassName and self.m_Namespace == other.Namespace and self.m_AsmName == other.AsmName + + def __hash__(self): + return hash((self.m_ClassName, self.m_Namespace, self.m_AsmName)) + +TERMINUS = AssetTypeReference("Terminus", "UnityEngine.DMAT", "FAKE_ASM") \ No newline at end of file diff --git a/UnityPy/classes/Behaviour.py b/UnityPy/classes/Behaviour.py index 0e0bb4ba..aa9e9cdf 100644 --- a/UnityPy/classes/Behaviour.py +++ b/UnityPy/classes/Behaviour.py @@ -6,11 +6,10 @@ def __init__(self, reader : EndianBinaryReader): super().__init__(reader=reader) 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/BuildSettings.py b/UnityPy/classes/BuildSettings.py index 41d7545b..1c298991 100644 --- a/UnityPy/classes/BuildSettings.py +++ b/UnityPy/classes/BuildSettings.py @@ -1,12 +1,74 @@ from .Object import Object +from ..streams import EndianBinaryWriter +from .PPtr import PPtr, save_ptr class BuildSettings(Object): def __init__(self, reader): super().__init__(reader=reader) - self.levels = reader.read_string_array() - self.has_render_texture = reader.read_boolean() - self.has_pro_version = reader.read_boolean() - self.has_publishing_rights = reader.read_boolean() - self.has_shadows = reader.read_boolean() - self.version = reader.read_aligned_string() + version = self.version + self.scenes = reader.read_string_array() + self.preloadedPlugins = reader.read_string_array() + self.enabledVRDevices = reader.read_string_array() + self.buildTags = reader.read_string_array() + if version < (2022, 2): + self.buildGUID = reader.read_bytes(16) + self.hasPROVersion = reader.read_boolean() + self.isNoWatermarkBuild = reader.read_boolean() + """ + self.isPrototypingBuild = reader.read_boolean() + self.isEducationalBuild = reader.read_boolean() + self.isEmbedded = reader.read_boolean() + self.isTrial = reader.read_boolean() + self.hasPublishingRights = reader.read_boolean() + self.hasShadows = reader.read_boolean() + self.hasSoftShadows = reader.read_boolean() + self.hasLocalLightShadows = reader.read_boolean() + self.hasAdvancedVersion = reader.read_boolean() + self.enableDynamicBatching = reader.read_boolean() + self.isDebugBuild = reader.read_boolean() + self.usesOnMouseEvents = reader.read_boolean() + self.hasClusterRendering = reader.read_boolean() + + self.m_Version = reader.read_aligned_string() + self.m_AuthToken = reader.read_aligned_string() + self.m_GraphicsAPIs = reader.read_int_array() + """ + self.the_rest = reader.read_the_rest() + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + version = self.version + super().save(writer, intern_call=True) + writer.write_string_array(self.scenes) + writer.write_string_array(self.preloadedPlugins) + writer.write_string_array(self.enabledVRDevices) + writer.write_string_array(self.buildTags) + if version < (2022, 2): + writer.write_bytes(self.buildGUID) + writer.write_boolean(self.hasPROVersion) + writer.write_boolean(self.isNoWatermarkBuild) + """ + writer.write_boolean(self.isPrototypingBuild) + writer.write_boolean(self.isEducationalBuild) + writer.write_boolean(self.isEmbedded) + writer.write_boolean(self.isTrial) + writer.write_boolean(self.hasPublishingRights) + writer.write_boolean(self.hasShadows) + writer.write_boolean(self.hasSoftShadows) + writer.write_boolean(self.hasLocalLightShadows) + writer.write_boolean(self.hasAdvancedVersion) + writer.write_boolean(self.enableDynamicBatching) + writer.write_boolean(self.isDebugBuild) + writer.write_boolean(self.usesOnMouseEvents) + writer.write_boolean(self.hasClusterRendering) + + writer.write_aligned_string(self.m_Version) + writer.write_aligned_string(self.m_AuthToken) + writer.write_int_array(self.m_GraphicsAPIs) + """ + + writer.write_bytes(self.the_rest) + + self.set_raw_data(writer) \ No newline at end of file diff --git a/UnityPy/classes/Component.py b/UnityPy/classes/Component.py index f36abfe8..6e3bc324 100644 --- a/UnityPy/classes/Component.py +++ b/UnityPy/classes/Component.py @@ -1,5 +1,5 @@ from .EditorExtension import EditorExtension -from .PPtr import PPtr, save_ptr +from .PPtr import PPtr from ..streams import EndianBinaryReader, EndianBinaryWriter @@ -8,9 +8,8 @@ def __init__(self, reader: EndianBinaryReader): super().__init__(reader=reader) self.m_GameObject = PPtr(reader) # GameObject - def save(self, writer: EndianBinaryWriter = None): + def save(self, writer: EndianBinaryWriter): if writer is None: writer = EndianBinaryWriter(endian=self.reader.endian) - version = self.version super().save(writer) - save_ptr(self.m_GameObject, writer) + self.m_GameObject.save(writer) diff --git a/UnityPy/classes/EditorExtension.py b/UnityPy/classes/EditorExtension.py index 4d01b55b..d9a32b16 100644 --- a/UnityPy/classes/EditorExtension.py +++ b/UnityPy/classes/EditorExtension.py @@ -1,5 +1,5 @@ from .Object import Object -from .PPtr import PPtr, save_ptr +from .PPtr import PPtr from ..enums import BuildTarget from ..streams import EndianBinaryWriter @@ -10,11 +10,13 @@ def __init__(self, reader): if self.platform == BuildTarget.NoTarget: self.m_PrefabParentObject = PPtr(reader) self.m_PrefabInternal = PPtr(reader) + pass 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.m_PrefabParentObject, writer) - save_ptr(self.m_PrefabInternal, writer) + self.m_PrefabParentObject.save(writer) + self.m_PrefabInternal.save(writer) + pass diff --git a/UnityPy/classes/Font.py b/UnityPy/classes/Font.py index 0241b478..8a6af74b 100644 --- a/UnityPy/classes/Font.py +++ b/UnityPy/classes/Font.py @@ -23,7 +23,9 @@ def __init__(self, reader): for i in range(KerningValues_size): reader.Position += 8 self.m_PixelScale = reader.read_float() + FontData_size = reader.read_int() + self.m_FontData = b"" if FontData_size > 0: self.m_FontData = reader.read_bytes(FontData_size) else: @@ -80,6 +82,8 @@ def __init__(self, reader): reader.align_stream() else: self.m_PixelScale = reader.read_float() + FontData_size = reader.read_int() + self.m_FontData = b"" if FontData_size > 0: self.m_FontData = reader.read_bytes(FontData_size) diff --git a/UnityPy/classes/GameObject.py b/UnityPy/classes/GameObject.py index 7517fc40..467de8d0 100644 --- a/UnityPy/classes/GameObject.py +++ b/UnityPy/classes/GameObject.py @@ -1,5 +1,6 @@ from .EditorExtension import EditorExtension from .PPtr import PPtr +from ..streams import EndianBinaryWriter from ..enums import ClassIDType @@ -24,12 +25,13 @@ def __init__(self, reader): self.m_SkinnedMeshRenderer = None self.m_MeshFilter = None - component_size = reader.read_int() - - self.m_Components = [None] * component_size - for i in range(component_size): - if self.version < (5, 5): - first = reader.read_int() + components_size = reader.read_int() + self.m_Components = [None] * components_size + if self.version[:2] < (5, 5): + self._firsts = [None] * components_size + for i in range(components_size): + if self.version[:2] < (5, 5): + self._firsts[i] = reader.read_int() component = PPtr(reader) self.m_Components[i] = component @@ -47,4 +49,36 @@ def __init__(self, reader): self.m_MeshFilter = component self.m_Layer = reader.read_int() - self.name = reader.read_aligned_string() + self.m_Name = reader.read_aligned_string() + if self.version > (2019, ): + self.m_Tag = reader.read_u_short() + self.m_IsActive = reader.read_boolean() + + def __key(self): + return (self.assets_file, self.m_PathID) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + if isinstance(other, GameObject): + return self.__key() == other.__key() + return NotImplemented + + def save(self, writer: EndianBinaryWriter = None, intern_call=True): + if not writer: + writer = EndianBinaryWriter(endian=self.reader.endian) + super().save(writer) + component_size = len(self.m_Components) + writer.write_int(component_size) + for i in range(component_size): + if self.version[:2] < (5, 5): + writer.write_int(self._firsts[i]) + self.m_Components[i].save(writer) + writer.write_int(self.m_Layer) + writer.write_aligned_string(self.m_Name) + if self.version > (2019, ): + writer.write_u_short(self.m_Tag) + writer.write_boolean(self.m_IsActive) + if not intern_call: + self.set_raw_data(writer) \ No newline at end of file diff --git a/UnityPy/classes/Material.py b/UnityPy/classes/Material.py index f8697512..f5a0bfec 100644 --- a/UnityPy/classes/Material.py +++ b/UnityPy/classes/Material.py @@ -1,9 +1,10 @@ from .NamedObject import NamedObject from .PPtr import PPtr - +from ..streams import EndianBinaryReader, EndianBinaryWriter +from ..exceptions import sanity_check class Material(NamedObject): - def __init__(self, reader): + def __init__(self, reader: EndianBinaryReader): super().__init__(reader=reader) version = self.version self.m_Shader = PPtr(reader) # Shader @@ -21,50 +22,132 @@ def __init__(self, reader): if version >= (5, 6): # 5.6 and up self.m_EnableInstancingVariants = reader.read_boolean() - # var m_DoubleSidedGI = a_Stream.read_boolean() //2017 and up + if version > (2017,): + self.m_DoubleSidedGI = reader.read_boolean() #2017+ reader.align_stream() if version >= (4, 3): # 4.3 and up self.m_CustomRenderQueue = reader.read_int() if version >= (5, 1): # 5.1 and up - self.stringTagMap = { - reader.read_aligned_string(): reader.read_aligned_string() - for _ in range(reader.read_int()) - } + numTags = reader.read_int() + sanity_check("numTags", numTags) + self.stringTagMap = {} + for _ in range(numTags): + key = reader.read_aligned_string() + value = reader.read_aligned_string() + self.stringTagMap[key] = value if version >= (5, 6): # 5.6 and up self.disabledShaderPasses = reader.read_string_array() self.m_SavedProperties = UnityPropertySheet(reader) + if version >= (2021, 1): # TODO: check min version + numBuildTextureStacks = reader.read_int() + sanity_check("numBuildTextureStacks", numBuildTextureStacks) + self.m_BuildTextureStacks = [BuildTextureStackReference(reader) for _ in range(numBuildTextureStacks)] + pass + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + super().save(writer) + version = self.version + + self.m_Shader.save(writer) + + if version >= (2021, 3): # 2021.3 and up + writer.write_string_array(self.m_ValidKeywords) + writer.write_string_array(self.m_InvalidKeywords) + elif version >= (5,): # 5.0 and up + writer.write_aligned_string(self.m_ShaderKeywords) + elif version >= (4, 1): # 4.x + writer.write_string_array(self.m_ShaderKeywords) + + if version >= (5,): + writer.write_u_int(self.m_LightmapFlags) + + if version >= (5, 6): # 5.6 and up + writer.write_boolean(self.m_EnableInstancingVariants) + if version > (2017,): + writer.write_boolean(self.m_DoubleSidedGI) #2017+ + writer.align_stream() + if version >= (4, 3): # 4.3 and up + writer.write_int(self.m_CustomRenderQueue) + + if version >= (5, 1): # 5.1 and up + writer.write_int(len(self.stringTagMap)) + for k, v in self.stringTagMap: + writer.write_aligned_string(k) + writer.write_aligned_string(v) + + if version >= (5, 6): # 5.6 and up + writer.write_string_array(self.disabledShaderPasses) + + self.m_SavedProperties.save(writer, version) + if version >= (2021, 1): # 5.6 and up + writer.write_int(len(self.m_BuildTextureStacks)) + for item in self.m_BuildTextureStacks: + item.save(writer) + + self.set_raw_data(writer) + +class BuildTextureStackReference: + def __init__(self, reader: EndianBinaryReader): + self.groupName = reader.read_aligned_string() + self.itemName = reader.read_aligned_string() + + def save(self, writer: EndianBinaryWriter): + writer.write_aligned_string(self.groupName) + writer.write_aligned_string(self.itemName) class UnityTexEnv: - def __init__(self, reader): + def __init__(self, reader: EndianBinaryReader): self.m_Texture = PPtr(reader) # Texture self.m_Scale = reader.read_vector2() self.m_Offset = reader.read_vector2() + def save(self, writer): + self.m_Texture.save(writer) + writer.write_vector2(self.m_Scale) + writer.write_vector2(self.m_Offset) class UnityPropertySheet: - def __init__(self, reader): + def __init__(self, reader: EndianBinaryReader): self.m_TexEnvs = { reader.read_aligned_string(): UnityTexEnv(reader) for _ in range(reader.read_int()) } - if reader.version >= (2021,): # 2021.1 and up self.m_Ints = { reader.read_aligned_string(): reader.read_int() for _ in range(reader.read_int()) } - self.m_Floats = { reader.read_aligned_string(): reader.read_float() for _ in range(reader.read_int()) } - self.m_Colors = { reader.read_aligned_string(): reader.read_color4() for _ in range(reader.read_int()) } + + def save(self, writer: EndianBinaryWriter, version: tuple): + writer.write_int(len(self.m_TexEnvs.keys())) + for k, v in self.m_TexEnvs.items(): + writer.write_aligned_string(k) + v.save(writer) + if version >= (2021,): # 2021.1 and up + writer.write_int(len(self.m_Ints.keys())) + for k, v in self.m_Ints.items(): + writer.write_aligned_string(k) + writer.write_int(v) + writer.write_int(len(self.m_Floats.keys())) + for k, v in self.m_Floats.items(): + writer.write_aligned_string(k) + writer.write_float(v) + writer.write_int(len(self.m_Colors.keys())) + for k, v in self.m_Colors.items(): + writer.write_aligned_string(k) + writer.write_color4(v) diff --git a/UnityPy/classes/Mesh.py b/UnityPy/classes/Mesh.py index 1985bdd6..7cce9f63 100644 --- a/UnityPy/classes/Mesh.py +++ b/UnityPy/classes/Mesh.py @@ -494,6 +494,10 @@ def __init__(self, reader): m_CollisionVertexCount = reader.read_int() self.m_MeshUsageFlags = reader.read_int() + + if version >= (2022, 1): # 2022.1 and up + self.m_CookingOptions = reader.read_int() + if version >= (5,): # 5.0 and up self.m_BakedConvexCollisionMesh = reader.read_bytes(reader.read_int()) reader.align_stream() @@ -697,7 +701,7 @@ def DecompressCompressedMesh(self): 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 diff --git a/UnityPy/classes/MeshRenderer.py b/UnityPy/classes/MeshRenderer.py deleted file mode 100644 index ada714ac..00000000 --- a/UnityPy/classes/MeshRenderer.py +++ /dev/null @@ -1,6 +0,0 @@ -from .Renderer import Renderer - - -class MeshRenderer(Renderer): - def __init__(self, reader): - super().__init__(reader=reader) diff --git a/UnityPy/classes/MonoBehaviour.py b/UnityPy/classes/MonoBehaviour.py index 956cc8f2..753ac939 100644 --- a/UnityPy/classes/MonoBehaviour.py +++ b/UnityPy/classes/MonoBehaviour.py @@ -1,5 +1,5 @@ from .Behaviour import Behaviour -from .PPtr import PPtr, save_ptr +from .PPtr import PPtr from ..streams import EndianBinaryReader, EndianBinaryWriter from ..exceptions import TypeTreeError as TypeTreeError @@ -7,31 +7,40 @@ class MonoBehaviour(Behaviour): def __init__(self, reader: EndianBinaryReader): super().__init__(reader=reader) self.m_Script = PPtr(reader) - self.name = reader.read_aligned_string() + self.m_Name = reader.read_aligned_string() self._raw_offset = reader.Position - if self.assets_file._enable_type_tree: + if self.assets_file.type_trees_saved and self.assets_file._use_type_trees: try: - self.read_typetree() + self.read_typetree(all_trees=self.assets_file.get_all_typetrees()) except TypeTreeError as e: - print("Failed to read TypeTree:\n", e) - self.assets_file._enable_type_tree = False + print_info(f"failed to read TypeTree for {self.m_Name} [path_id={self.m_PathID}]: {e}") + self.assets_file._use_type_trees = False + self.raw_monobehaviour = self.reader.read_the_rest() + else: + self.raw_monobehaviour = self.reader.read_the_rest() + + def __getitem__(self, item): + return getattr(self, item) def save(self, writer: EndianBinaryWriter = None, raw_data: bytes = None): + if raw_data is None: + if not self.raw_monobehaviour: + ValueError("No raw MonoBehaviour data provided") + else: + raw_data = self.raw_monobehaviour + 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) + self.m_Script.save(writer) + writer.write_aligned_string(self.m_Name) writer.write(raw_data) - - self.set_raw_data(writer.bytes) - @property - def raw_data(self) -> bytes: + self.set_raw_data(writer) + + def get_raw_monobehaviour(self) -> bytes: """ Reads the undocumentated data following the default init. This is usefull for classes that are stored via MonoBehaviours. diff --git a/UnityPy/classes/MonoScript.py b/UnityPy/classes/MonoScript.py index 6635100a..28f47808 100644 --- a/UnityPy/classes/MonoScript.py +++ b/UnityPy/classes/MonoScript.py @@ -21,3 +21,4 @@ def __init__(self, reader): self.m_AssemblyName = reader.read_aligned_string() if version < (2018, 2): # 2018.2 down self.m_IsEditorScript = reader.read_boolean() + reader.align_stream() diff --git a/UnityPy/classes/Object.py b/UnityPy/classes/Object.py index 2c8381ee..f83fef2a 100644 --- a/UnityPy/classes/Object.py +++ b/UnityPy/classes/Object.py @@ -1,20 +1,16 @@ +import types from .PPtr import PPtr -from ..enums import BuildTarget -from ..helpers import TypeTreeHelper +from ..enums import BuildTarget, ClassIDType from ..streams import EndianBinaryWriter from ..files import ObjectReader -import types -from ..exceptions import TypeTreeError as TypeTreeError - class Object(object): type_tree: dict def __init__(self, reader: ObjectReader): self.reader = reader - self.assets_file = reader.assets_file self.type = reader.type - self.path_id = reader.path_id + self.m_PathID = reader.path_id self.version = reader.version self.build_type = reader.build_type self.platform = reader.platform @@ -25,7 +21,11 @@ def __init__(self, reader: ObjectReader): if self.platform == BuildTarget.NoTarget: self._object_hide_flags = reader.read_u_int() - self.container = self.assets_file.container.path_dict.get(self.path_id) + self.container = ( + self.assets_file._container[self.m_PathID] + if self.type != ClassIDType.AssetBundle and self.m_PathID in self.assets_file._container + else None + ) self.reader.reset() if type(self) == Object: @@ -41,8 +41,8 @@ def dump_typetree(self, nodes: list = None) -> str: def dump_typetree_structure(self) -> str: return self.reader.dump_typetree_structure() - def read_typetree(self, nodes: list = None, wrap: bool = False) -> dict: - tree = self.reader.read_typetree(nodes) + def read_typetree(self, nodes: list = None, wrap: bool = False, all_trees=None) -> dict: + tree = self.reader.read_typetree(nodes, all_trees=all_trees) self.type_tree = NodeHelper(tree, self.assets_file) return self.type_tree if wrap else tree @@ -54,7 +54,7 @@ def class_to_dict(value): 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 {"m_FileID": value.m_FileID, "m_PathID": value.m_PathID} return { key: class_to_dict(val) for key, val in value.__dict__.items() @@ -70,7 +70,7 @@ def class_to_dict(value): def get_raw_data(self) -> bytes: return self.reader.get_raw_data() - def set_raw_data(self, data): + def set_raw_data(self, data: bytes): self.reader.set_raw_data(data) def save(self, writer: EndianBinaryWriter = None, intern_call=False): @@ -84,11 +84,6 @@ def save(self, writer: EndianBinaryWriter = None, intern_call=False): # so we have to use the typetree if it exists self.save_typetree() - 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, name): """ If item not found in __dict__, read type_tree and check if it is in there. @@ -107,26 +102,31 @@ def get(self, key, default=None): return getattr(self, key, default) def __repr__(self): - return f"<{self.__class__.__name__} path_id={self.path_id}>" + return f"<{self.__class__.__name__} path_id={self.m_PathID}>" def __hash__(self): - return hash(self.path_id) + if self.assets_file and hasattr(self.assets_file, "name"): + return hash((self.assets_file.name, self.m_PathID)) + return hash(self.m_PathID) def __eq__(self, other): + same_file = True + if self.assets_file and other.assets_file and hasattr(self.assets_file, "name") and hasattr(other.assets_file, "name"): + same_file = (self.assets_file.name == other.assets_file.name) if isinstance(other, Object): - return self.path_id == other.path_id + return self.m_PathID == other.m_PathID and same_file elif isinstance(other, int): - return self.path_id == other + return self.m_PathID == other and same_file return False class NodeHelper: def __init__(self, data, assets_file): if "m_PathID" in data and "m_FileID" in data: - # used to make pointers directly useable - self.path_id = data["m_PathID"] - self.file_id = data["m_FileID"] - self.index = data.get("m_Index", -2) + # used to make pointers directly usable + self.m_FileID = data["m_FileID"] + self.m_PathID = data["m_PathID"] + self.m_Index = data.get("m_Index", -2) self.assets_file = assets_file self._obj = None self.__class__ = PPtr @@ -134,13 +134,14 @@ def __init__(self, data, assets_file): self.__dict__ = { key: NodeHelper(val, assets_file) for key, val in data.items() } + pass def __new__(cls, data, assets_file): if isinstance(data, dict): return super(NodeHelper, cls).__new__(cls) - elif isinstance(data, list): + if isinstance(data, list): return [NodeHelper(x, assets_file) for x in data] - elif isinstance(data, tuple): + if isinstance(data, tuple): return tuple(NodeHelper(x, assets_file) for x in data) return data @@ -154,13 +155,12 @@ def dump(val): if isinstance(val, NodeHelper) else [dump(item) for item in val] if isinstance(val, list) - else {"m_PathID": val.path_id, "m_FileID": val.file_id} + else {"m_FileID": val.m_FileID, "m_PathID": val.m_PathID} if isinstance(val, PPtr) else [x for x in val] if isinstance(val, (bytearray, bytes)) else val ) - return {key: dump(val) for key, val in self.__dict__.items()} def items(self): @@ -172,9 +172,12 @@ def values(self): def keys(self): return self.__dict__.keys() + def save(self, *args, **kwargs): + raise NotImplementedError( + "This is a helper structure, please create its proper parser"+ + " or use Object's save_typetree() method." + ) + def __repr__(self): name = getattr(self, "m_Name", None) - if name: - return f"" - else: - return "" + return f"" if name else "" diff --git a/UnityPy/classes/PPtr.py b/UnityPy/classes/PPtr.py index 96ae6d64..29460cc5 100644 --- a/UnityPy/classes/PPtr.py +++ b/UnityPy/classes/PPtr.py @@ -1,64 +1,116 @@ -from ..files import ObjectReader -from ..streams import EndianBinaryWriter +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, cast from ..enums import ClassIDType +T = TypeVar("T") + +if TYPE_CHECKING: + from ..files import ObjectReader + from ..streams import EndianBinaryWriter + from ..files.SerializedFile import SerializedFile + +from ..config import WARNED_NOTFOUND_ONCE + +# NOTE: don't call this by itself since it may be depreciated def save_ptr(obj, writer: EndianBinaryWriter): if isinstance(obj, PPtr): - writer.write_int(obj.file_id) + writer.write_int(obj.m_FileID) else: - writer.write_int(0) # it's usually 0...... + writer.write_int(0) # it's usually 0... if obj._version < 14: - writer.write_int(obj.path_id) + writer.write_int(obj.m_PathID) else: - writer.write_long(obj.path_id) + writer.write_long(obj.m_PathID) +WARNED_NOTFOUND = [] class PPtr: + autopreload: bool = True + + @property + def file_id(self) -> int: + return self.m_FileID # backwards compatibility + + @property + def path_id(self) -> int: + return self.m_PathID # backwards compatibility + def __init__(self, reader: ObjectReader): self._version = reader.version2 - self.index = -2 - self.file_id = reader.read_int() - self.path_id = reader.read_int() if self._version < 14 else reader.read_long() + self.m_Index = -2 + self.m_FileID = reader.read_int() + self.m_PathID = reader.read_int() if self._version < 14 else reader.read_long() self.assets_file = reader.assets_file self._obj = None + def __getitem__(self, item): + if item in ["file_id", "m_FileID"]: return self.m_FileID + elif item in ["path_id", "m_PathID"]: return self.m_PathID + elif item in ["index", "m_Index"]: return self.m_Index + return getattr(self, item) + + def __repr__(self): + return "<%s %s>" % ( + self.__class__.__name__, + self._obj.__class__.__repr__(self.get_obj()) if self.get_obj() + else f"[m_FileID: {self.m_FileID}; m_PathID: {self.m_PathID}; m_Index: {self.m_Index}; asset: {self.assets_file.name}]" + ) + def save(self, writer: EndianBinaryWriter): save_ptr(self, writer) def get_obj(self): - if self._obj != None: + global WARNED_NOTFOUND + + if self._obj != None and self.m_PathID != 0: return self._obj - manager = None + if self.m_PathID == 0: + return None - if self.file_id == 0: + manager = None + if self.m_FileID == 0: manager = self.assets_file - elif self.file_id > 0 and self.file_id - 1 < len(self.assets_file.externals): - if self.index == -2: + elif self.m_FileID > 0 and self.m_FileID - 1 < len(self.assets_file.externals): + if self.m_Index == -2: environment = self.assets_file.environment external_name = self.external_name # try to find it in the already registered cabs + + if WARNED_NOTFOUND_ONCE and external_name in WARNED_NOTFOUND: + self._obj = None + return self._obj + manager = environment.get_cab(external_name) - # not found, load all dependencies and try again if not manager: self.assets_file.load_dependencies([external_name]) manager = environment.get_cab(external_name) + if not manager and self.autopreload and environment: + manager = environment.load_file(external_name) + if manager: + environment.register_cab(external_name, manager) + if manager is not None: - self._obj = manager.objects.get(self.path_id) + self._obj = manager.objects.get(self.m_PathID) else: self._obj = None if self.external_name: - print(f"Couldn't find dependency {self.external_name}") - print("You can try to load it manually to the environment in advance") - print("for Web-&BundleFiles: env.load_file(dependency)") - print( - "for SerializedFiles: env.register_cab(depdency_basename, env.load_file(dependency)" + if self.assets_file.environment.ignore_dependencies: return None + print_warning(f"Couldn't find dependency: {self.external_name}\n" + + "You can try to load it manually to the environment in advance\n" + f" for Web-&BundleFiles: env.load_file(\"{self.external_name}\")\n" + + f" for SerializedFiles: env.register_cab(\"{self.external_name}\")\n"+ + f" or env.load_file(full path to \"{self.external_name}\")") + if WARNED_NOTFOUND_ONCE and self.external_name not in WARNED_NOTFOUND: + WARNED_NOTFOUND.append(self.external_name) + elif self.m_PathID: + print_warning(f"Couldn't find referenced object with m_PathID: {self.m_PathID} " #+ + #f"and name: {getattr(self, 'name', '')}" ) - elif self.path_id: - print(f"Couldn't find referenced object with path_id {self.path_id}") return self._obj @@ -71,20 +123,92 @@ def type(self): @property def external_name(self): - if self.file_id > 0 and self.file_id - 1 < len(self.assets_file.externals): - return self.assets_file.externals[self.file_id - 1].name + if self.m_FileID > 0 and self.m_FileID - 1 < len(self.assets_file.externals): + return self.assets_file.externals[self.m_FileID - 1].name def __getattr__(self, key): obj = self.get_obj() + if obj is None: + if key == "read": + return lambda: None + raise AttributeError(f"{self} has no method called \"{key}\"") return getattr(obj, key) - def __repr__(self): - return "<%s %s>" % ( - self.__class__.__name__, - self._obj.__class__.__repr__(self.get_obj()) - if self.get_obj() - else "Not Found", - ) + #@property + #def type(self) -> ClassIDType: + #return self.deref().type + + # backwards compatibility - to be removed in UnityPy 2 + #def read(self): + #return self.deref_parse_as_object() + + # backwards compatibility - to be removed in UnityPy 2 + def read_typetree(self): + return self.deref_parse_as_dict() + + def deref(self, assetsfile: Optional[SerializedFile] = None) -> ObjectReader[T]: + assetsfile = assetsfile or self.assetsfile + if assetsfile is None: + raise ValueError("PPtr can't deref without an assetsfile!") + + if self.m_PathID == 0: + raise ValueError("PPtr can't deref with m_PathID == 0!") + + if self.m_FileID == 0: + pass + else: + # resolve file id to external name + external_id = self.m_FileID - 1 + if external_id >= len(assetsfile.externals): + raise FileNotFoundError("Failed to resolve pointer - invalid m_FileID!") + external = assetsfile.externals[external_id] + + # resolve external name to assetsfile + container = assetsfile.parent + if container is None: + # TODO - use default fs + raise FileNotFoundError( + f"PPtr points to {external.path} but no container is set!" + ) - def __bool__(self): - return True if self.get_obj() else False + external_clean_path = external.path + if external_clean_path.startswith("archive:/"): + external_clean_path = external_clean_path[9:] + if external_clean_path.startswith("assets/"): + external_clean_path = external_clean_path[7:] + external_clean_path = external_clean_path.rsplit("/")[-1].lower() + + for key, file in container.files.items(): + if key.lower() == external_clean_path: + assetsfile = file + break + else: + env = assetsfile.environment + cab = env.find_file(external_clean_path) + if cab: + assetsfile = cab + else: + raise FileNotFoundError( + f"Failed to resolve pointer - {external.path} not found!" + ) + + return cast("ObjectReader[T]", assetsfile.objects[self.m_PathID]) + + def deref_parse_as_object(self, assetsfile: Optional[SerializedFile] = None) -> T: + return self.deref(assetsfile).parse_as_object() + + def deref_parse_as_dict( + self, assetsfile: Optional[SerializedFile] = None + ) -> dict[str, Any]: + return self.deref(assetsfile).parse_as_dict() + + def __bool__(self) -> bool: + return self.m_PathID != 0 + + def __hash__(self) -> int: + return hash((self.m_FileID, self.m_PathID)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PPtr): + return False + return self.m_FileID == other.m_FileID and self.m_PathID == other.m_PathID \ No newline at end of file diff --git a/UnityPy/classes/PlayerSettings.py b/UnityPy/classes/PlayerSettings.py index e291d7ea..9066a673 100644 --- a/UnityPy/classes/PlayerSettings.py +++ b/UnityPy/classes/PlayerSettings.py @@ -1,8 +1,42 @@ from .Object import Object +from ..streams import EndianBinaryWriter +from ..files import ObjectReader +class UVs: + def __init__(self, reader: ObjectReader): + #self.serializedVersion = reader.read_int() + self.x = reader.read_float() + self.y = reader.read_float() + self.width = reader.read_float() + self.height = reader.read_float() + + def save(self, writer): + #self.serializedVersion = reader.read_int() + writer.write_float(self.x) + writer.write_float(self.y) + writer.write_float(self.width) + writer.write_float(self.height) + +class SplashScreenLogo: + def __init__(self, reader: ObjectReader): + self.PPtr = reader.read_bytes(16) + self.duration = reader.read_float() + + def save(self, writer): + writer.write_bytes(self.PPtr) + writer.write_float(self.duration) + +class AspectRatio: + def __init__(self, reader: ObjectReader): + self.PPtr = reader.read_bytes(16) + self.duration = reader.read_float() + + def save(self, writer): + writer.write_bytes(self.PPtr) + writer.write_float(self.duration) class PlayerSettings(Object): - def __init__(self, reader): + def __init__(self, reader: ObjectReader): super().__init__(reader=reader) version = self.version if version >= (5, 4): # 5.4.0 nad up @@ -27,3 +61,234 @@ def __init__(self, reader): self.accelerometerFrequency = reader.read_int() self.companyName = reader.read_aligned_string() self.productName = reader.read_aligned_string() + + self.defaultCursor = reader.read_bytes(16) + self.cursorHotspot = reader.read_int() + self.m_SplashScreenBackgroundColor = reader.read_vector4() + + self.m_ShowUnitySplashScreen = reader.read_boolean() + self.m_ShowUnitySplashLogo = reader.read_boolean() + reader.align_stream() + """ + self.m_SplashScreenOverlayOpacity = reader.read_float() + self.m_SplashScreenAnimation = reader.read_int() + self.m_SplashScreenLogoStyle = reader.read_int() + self.m_SplashScreenDrawMode = reader.read_int() + self.m_SplashScreenBackgroundAnimationZoom = reader.read_float() + self.m_SplashScreenLogoAnimationZoom = reader.read_float() + self.m_SplashScreenBackgroundLandscapeAspect = reader.read_float() + self.m_SplashScreenBackgroundPortraitAspect = reader.read_float() + self.m_SplashScreenBackgroundLandscapeUvs = UVs(reader) + self.m_SplashScreenBackgroundPortraitUvs = UVs(reader) + self.m_SplashScreenLogos = reader.read_array(lambda: SplashScreenLogo(), reader.read_int()) + self.m_SplashScreenBackgroundLandscape = reader.read_bytes(16) + self.m_SplashScreenBackgroundPortrait = reader.read_bytes(16) + self.m_VirtualRealitySplashScreen = reader.read_bytes(16) + reader.align_stream() + #self.m_HolographicTrackingLossScreen = reader.read_bytes(16) + + self.defaultScreenWidth = reader.read_int() + self.defaultScreenHeight = reader.read_int() + self.defaultScreenWidthWeb = reader.read_int() + self.defaultScreenHeightWeb = reader.read_int() + self.m_StereoRenderingPath = reader.read_int() + self.m_ActiveColorSpace = reader.read_int() + + self.m_MTRendering = reader.read_boolean() + + #self.mobileMTRenderingBaked = reader.read_boolean() + #self.playerMinOpenGLESVersion = reader.read_int() + + self.mipStripping = reader.read_boolean() + reader.align_stream() + self.numberOfMipsStripped = reader.read_int() + self.m_StackTraceTypes = reader.read_array(lambda: reader.read_int(), reader.read_int()) #reader.read_bytes(16) + + self.iosShowActivityIndicatorOnLoading = reader.read_int() + self.androidShowActivityIndicatorOnLoading = reader.read_int() + self.iosUseCustomAppBackgroundBehavior = reader.read_boolean() + self.iosAllowHTTPDownload = reader.read_boolean() + + self.allowedAutorotateToPortrait = reader.read_boolean() + self.allowedAutorotateToPortraitUpsideDown = reader.read_boolean() + self.allowedAutorotateToLandscapeRight = reader.read_boolean() + self.allowedAutorotateToLandscapeLeft = reader.read_boolean() + self.useOSAutorotation = reader.read_boolean() + self.use32BitDisplayBuffer = reader.read_boolean() + self.preserveFramebufferAlpha = reader.read_boolean() + self.disableDepthAndStencilBuffers = reader.read_boolean() + + self.androidStartInFullscreen = reader.read_boolean() + self.androidRenderOutsideSafeArea = reader.read_boolean() + self.androidUseSwappy = reader.read_boolean() + self.androidBlitType = reader.read_int() + self.androidResizableWindow = reader.read_boolean() + self.androidDefaultWindowWidth = reader.read_int() + self.androidDefaultWindowHeight = reader.read_int() + self.androidMinimumWindowWidth = reader.read_int() + self.androidMinimumWindowHeight = reader.read_int() + self.androidFullscreenMode = reader.read_boolean() + self.defaultIsNativeResolution = reader.read_boolean() + self.macRetinaSupport = reader.read_boolean() + + self.runInBackground = reader.read_boolean() + self.captureSingleScreen = reader.read_boolean() + self.muteOtherAudioSources = reader.read_boolean() + self.PrepareIOSForRecording = reader.read_boolean() + self.ForceIOSSpeakersWhenRecording = reader.read_boolean() + reader.align_stream() + self.deferSystemGesturesMode = reader.read_boolean() + self.hideHomeButton = reader.read_boolean() + + self.submitAnalytics = reader.read_boolean() + self.usePlayerLog = reader.read_boolean() + + self.bakeCollisionMeshes = reader.read_boolean() + self.forceSingleInstance = reader.read_boolean() + self.useFlipModelSwapchain = reader.read_boolean() + self.resizableWindow = reader.read_boolean() + + self.useMacAppStoreValidation = reader.read_boolean() + reader.align_stream() + self.macAppStoreCategory = reader.read_aligned_string() + self.gpuSkinning = reader.read_boolean() + + self.xboxPIXTextureCapture = reader.read_boolean() + self.xboxEnableAvatar = reader.read_boolean() + self.xboxEnableKinect = reader.read_boolean() + self.xboxEnableKinectAutoTracking = reader.read_boolean() + self.xboxEnableFitness = reader.read_boolean() + + self.visibleInBackground = reader.read_boolean() + self.allowFullscreenSwitch = reader.read_boolean() + reader.align_stream() + self.fullscreenMode = reader.read_byte() + reader.align_stream() + + """""" + self.xboxSpeechDB = reader.read_int() + self.xboxEnableHeadOrientation = reader.read_boolean() + reader.align_stream() + self.xboxEnableGuest = reader.read_boolean() + reader.align_stream() + self.xboxEnablePIXSampling = reader.read_boolean() + reader.align_stream() + self.metalFramebufferOnly = reader.read_boolean() + reader.align_stream() + + self.xboxOneResolution = reader.read_int() + self.xboxOneSResolution = reader.read_int() + self.xboxOneXResolution = reader.read_int() + self.xboxOneMonoLoggingLevel = reader.read_int() + self.xboxOneLoggingLevel = reader.read_int() + self.xboxOneDisableEsram = reader.read_boolean() + reader.align_stream() + self.xboxOneEnableTypeOptimization = reader.read_boolean() + reader.align_stream() + self.xboxOnePresentImmediateThreshold = reader.read_u_int() + + self.switchQueueCommandMemory = reader.read_int() + self.switchQueueControlMemory = reader.read_int() + self.switchQueueComputeMemory = reader.read_int() + self.switchNVNShaderPoolsGranularity = reader.read_int() + self.switchNVNDefaultPoolsGranularity = reader.read_int() + self.switchNVNOtherPoolsGranularity = reader.read_int() + self.switchNVNMaxPublicTextureIDCount = reader.read_int() + self.switchNVNMaxPublicSamplerIDCount = reader.read_int() + self.stadiaPresentMode = reader.read_int() + self.stadiaTargetFramerate = reader.read_int() + reader.align_stream() + + self.vulkanNumSwapchainBuffers = reader.read_u_int() + self.vulkanEnableSetSRGBWrite = reader.read_boolean() + self.vulkanEnablePreTransform = reader.read_boolean() + self.vulkanEnableLateAcquireNextImage = reader.read_boolean() + self.vulkanEnableCommandBufferRecycling = reader.read_boolean() + reader.align_stream() + + self.m_SupportedAspectRatios = reader.read_bytes(5) + self.bundleVersion = reader.read_aligned_string() + self.preloadedAssets = reader.read_array(lambda: reader.read_bytes(16), reader.read_int()) + reader.align_stream() + + self.metroInputSource = reader.read_bytes(16) #reader.read_boolean() + self.wsaTransparentSwapchain = reader.read_boolean() #reader.read_boolean() + reader.align_stream() + self.m_HolographicPauseOnTrackingLoss = reader.read_boolean() + self.xboxOneDisableKinectGpuReservation = reader.read_boolean() + self.xboxOneEnable7thCore = reader.read_boolean() + reader.align_stream() + #self.vrSettings + self.enable360StereoCapture = reader.read_boolean() + reader.align_stream() + self.isWsaHolographicRemotingEnabled = reader.read_boolean() + reader.align_stream() + self.enableFrameTimingStats = reader.read_boolean() + reader.align_stream() + self.useHDRDisplay = reader.read_boolean() + reader.align_stream() + self.D3DHDRBitDepth = reader.read_int() + self.m_ColorGamuts = reader.read_array(reader.read_int, reader.read_int()) + self.targetPixelDensity = reader.read_int() + self.resolutionScalingMode = reader.read_int() + self.androidSupportedAspectRatio = reader.read_int() + self.androidMaxAspectRatio = reader.read_float() + reader.align_stream() + + self.activeInputHandler = reader.read_int() + self.cloudProjectId = reader.read_aligned_string() + self.framebufferDepthMemorylessMode = reader.read_int() + self.qualitySettingsNames = reader.read_array(lambda: reader.read_bytes(16), reader.read_int()) + reader.align_stream() + + self.projectName = reader.read_aligned_string() + self.organizationId = reader.read_boolean() + self.cloudEnabled = reader.read_boolean() + self.legacyClampBlendShapeWeights = reader.read_int() + reader.align_stream() + self.playerDataPath = reader.read_aligned_string() + self.forceSRGBBlit = reader.read_boolean() + self.virtualTexturingSupportEnabled = reader.read_boolean() + self.uploadClearedTextureDataAfterCreationFromScript = reader.read_boolean() + """ + self.the_rest = reader.read_the_rest() + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + 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.defaultCursor) + writer.write_int(self.cursorHotspot) + writer.write_vector4(self.m_SplashScreenBackgroundColor) + + writer.write_boolean(self.m_ShowUnitySplashScreen) + writer.write_boolean(self.m_ShowUnitySplashLogo) + writer.align_stream() + + writer.write_bytes(self.the_rest) + self.set_raw_data(writer) + diff --git a/UnityPy/classes/RectTransform.py b/UnityPy/classes/RectTransform.py index 73514920..1a7275a6 100644 --- a/UnityPy/classes/RectTransform.py +++ b/UnityPy/classes/RectTransform.py @@ -1,6 +1,23 @@ from .Transform import Transform - +from ..streams import EndianBinaryWriter class RectTransform(Transform): def __init__(self, reader): super().__init__(reader=reader) + self.m_AnchorMin = reader.read_vector2() + self.m_AnchorMax = reader.read_vector2() + self.m_AnchoredPosition = reader.read_vector2() + self.m_SizeDelta = reader.read_vector2() + self.m_Pivot = reader.read_vector2() + + def save(self, writer = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + super().save(writer, intern_call=True) + writer.write_vector2(self.m_AnchorMin) + writer.write_vector2(self.m_AnchorMax) + writer.write_vector2(self.m_AnchoredPosition) + writer.write_vector2(self.m_SizeDelta) + writer.write_vector2(self.m_Pivot) + + self.set_raw_data(writer) diff --git a/UnityPy/classes/Renderer.py b/UnityPy/classes/Renderer.py index 4e65f415..eea62722 100644 --- a/UnityPy/classes/Renderer.py +++ b/UnityPy/classes/Renderer.py @@ -86,10 +86,9 @@ def __init__(self, reader): self.m_SortingLayer = reader.read_short() else: self.m_SortingLayerID = reader.read_u_int() - - # SInt16 m_SortingLayer 5.6 and up - self.m_SortingOrder = reader.read_short() - reader.align_stream() + if version[:2] >= (5, 6): # 5.6+ + self.m_SortingOrder = reader.read_short() + reader.align_stream() def export(self, export_dir: str) -> None: MeshRendererExporter.export_mesh_renderer(self, export_dir) diff --git a/UnityPy/classes/ResourceManager.py b/UnityPy/classes/ResourceManager.py index 2f490caf..581c2a8a 100644 --- a/UnityPy/classes/ResourceManager.py +++ b/UnityPy/classes/ResourceManager.py @@ -1,6 +1,7 @@ from .Object import Object -from .PPtr import PPtr, save_ptr +from .PPtr import PPtr from ..streams import EndianBinaryReader, EndianBinaryWriter +from .. import config class ResourceManager(Object): @@ -19,5 +20,7 @@ def save(self, writer: EndianBinaryWriter = None): writer.write_int(len(self.m_Container)) for key, val in self.m_Container.items(): writer.write_aligned_string(key) - save_ptr(val, writer) + if config.DEBUG: + assert isinstance(val, PPtr), "{val} must be an instance of PPtr class" + val.save(writer) return writer diff --git a/UnityPy/classes/Shader.py b/UnityPy/classes/Shader.py index c2be9a08..ab7a4134 100644 --- a/UnityPy/classes/Shader.py +++ b/UnityPy/classes/Shader.py @@ -1,70 +1,245 @@ 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 - +from ..exceptions import sanity_check +from ..files import ObjectReader +from ..classes import PPtr +from ..streams import EndianBinaryWriter + +class D3D_BLEND(IntEnum): + D3D_BLEND_ZERO = 1 # The blend factor is (0, 0, 0, 0). No pre-blend operation. + D3D_BLEND_ONE = 2 # The blend factor is (1, 1, 1, 1). No pre-blend operation. + D3D_BLEND_SRC_COLOR = 3 # The blend factor is (Rₛ, Gₛ, Bₛ, Aₛ), that is color data (RGB) from a pixel shader. No pre-blend operation. + D3D_BLEND_INV_SRC_COLOR = 4 # The blend factor is (1 - Rₛ, 1 - Gₛ, 1 - Bₛ, 1 - Aₛ), that is color data (RGB) from a pixel shader. The pre-blend operation inverts the data, generating 1 - RGB. + D3D_BLEND_SRC_ALPHA = 5 # The blend factor is (Aₛ, Aₛ, Aₛ, Aₛ), that is alpha data (A) from a pixel shader. No pre-blend operation. + D3D_BLEND_INV_SRC_ALPHA = 6 # The blend factor is ( 1 - Aₛ, 1 - Aₛ, 1 - Aₛ, 1 - Aₛ), that is alpha data (A) from a pixel shader. The pre-blend operation inverts the data, generating 1 - A. + D3D_BLEND_DEST_ALPHA = 7 # The blend factor is (Ad Ad Ad Ad), that is alpha data from a render target. No pre-blend operation. + D3D_BLEND_INV_DEST_ALPHA = 8 # The blend factor is (1 - Ad 1 - Ad 1 - Ad 1 - Ad), that is alpha data from a render target. The pre-blend operation inverts the data, generating 1 - A. + D3D_BLEND_DEST_COLOR = 9 # The blend factor is (Rd, Gd, Bd, Ad), that is color data from a render target. No pre-blend operation. + D3D_BLEND_INV_DEST_COLOR = 10 # The blend factor is (1 - Rd, 1 - Gd, 1 - Bd, 1 - Ad), that is color data from a render target. The pre-blend operation inverts the data, generating 1 - RGB. + D3D_BLEND_SRC_ALPHA_SAT = 11 # The blend factor is (f, f, f, 1); where f = min(Aₛ, 1 - Ad). The pre-blend operation clamps the data to 1 or less. + D3D_BLEND_BLEND_FACTOR = 14 # The blend factor is the blend factor set with OMSetBlendState. No pre-blend operation. + D3D_BLEND_INV_BLEND_FACTOR = 15 # The blend factor is the blend factor set with OMSetBlendState. The pre-blend operation inverts the blend factor, generating 1 - blend_factor. + D3D_BLEND_SRC1_COLOR = 16 # The blend factor is data sources both as color data output by a pixel shader. There is no pre-blend operation. This blend factor supports dual-source color blending. + D3D_BLEND_INV_SRC1_COLOR = 17 # The blend factor is data sources both as color data output by a pixel shader. The pre-blend operation inverts the data, generating 1 - RGB. This blend factor supports dual-source color blending. + D3D_BLEND_SRC1_ALPHA = 18 # The blend factor is data sources as alpha data output by a pixel shader. There is no pre-blend operation. This blend factor supports dual-source color blending. + D3D_BLEND_INV_SRC1_ALPHA = 19 # The blend factor is data sources as alpha data output by a pixel shader. The pre-blend operation inverts the data, generating 1 - A. This blend factor supports dual-source color blending. + D3D_BLEND_ALPHA_FACTOR = 20 # DX12 The blend factor is (A, A, A, A), where the constant, A, is taken from the blend factor set with OMSetBlendFactor. + D3D_BLEND_INV_ALPHA_FACTOR = 21 # DX12 The blend factor is (1 – A, 1 – A, 1 – A, 1 – A), where the constant, A, is taken from the blend factor set with OMSetBlendFactor. + + +class D3D_BLEND_OP(IntEnum): + D3D_BLEND_OP_ADD = 1 # Add source 1 and source 2 + D3D_BLEND_OP_SUBTRACT = 2 # Subtract source 1 from source 2 + D3D_BLEND_OP_REV_SUBTRACT = 3 # Subtract source 2 from source 1 + D3D_BLEND_OP_MIN = 4 # Find the minimum of source 1 and source 2 + D3D_BLEND_OP_MAX = 5 # Find the maximum of source 1 and source 2 + +class D3D_STENCIL_OP(IntEnum): + D3D_STENCIL_OP_KEEP = 1 # Keep the existing stencil data + D3D_STENCIL_OP_ZERO = 2 # Set the stencil data to 0 + D3D_STENCIL_OP_REPLACE = 3 # Set the stencil data to the reference value set by calling ID3D11DeviceContext::OMSetDepthStencilState. + D3D_STENCIL_OP_INCR_SAT = 4 # Increment the stencil value by 1, and clamp the result + D3D_STENCIL_OP_DECR_SAT = 5 # Decrement the stencil value by 1, and clamp the result + D3D_STENCIL_OP_INVERT = 6 # Invert the stencil data + D3D_STENCIL_OP_INCR = 7 # Increment the stencil value by 1, and wrap the result if necessary + D3D_STENCIL_OP_DECR = 8 # Decrement the stencil value by 1, and wrap the result if necessary + +class D3D_COLOR_WRITE_ENABLE(IntEnum): + D3D_COLOR_WRITE_ENABLE_RED = 1 + D3D_COLOR_WRITE_ENABLE_GREEN = 2 + D3D_COLOR_WRITE_ENABLE_BLUE = 4 + D3D_COLOR_WRITE_ENABLE_ALPHA = 8 + D3D_COLOR_WRITE_ENABLE_ALL = D3D_COLOR_WRITE_ENABLE_RED | D3D_COLOR_WRITE_ENABLE_GREEN | D3D_COLOR_WRITE_ENABLE_BLUE | D3D_COLOR_WRITE_ENABLE_ALPHA class Shader(NamedObject): def export(self): return export_shader(self) - def __init__(self, reader): + def __init__(self, reader: ObjectReader): super().__init__(reader=reader) - version = reader.version + version = self.version if version >= (5, 5): # 5.5 and up self.m_ParsedForm = SerializedShader(reader) + numPlatforms = reader.read_u_int() + sanity_check("numPlatforms", numPlatforms) self.platforms = [ - ShaderCompilerPlatform(x) for x in reader.read_u_int_array() + ShaderCompilerPlatform(reader.read_u_int()) for x in range(numPlatforms) ] - + #details_pos = reader.Position + #try: if version >= (2019, 3): # 2019.3 and up - 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] + numOffsets = reader.read_u_int() + sanity_check("numOffsets", numOffsets) + self.offsets = [reader.read_u_int_array() for _ in range(numOffsets)] + reader.align_stream() + numCompressedLengths = reader.read_u_int() + sanity_check("numPlatforms", numCompressedLengths) + self.compressedLengths = [reader.read_u_int_array() for _ in range(numCompressedLengths)] + reader.align_stream() + numdecompressedLengths = reader.read_u_int() + sanity_check("numdecompressedLengths", numdecompressedLengths) + self.decompressedLengths = [reader.read_u_int_array() for _ in range(numdecompressedLengths)] + reader.align_stream() else: self.offsets = reader.read_u_int_array() self.compressedLengths = reader.read_u_int_array() self.decompressedLengths = reader.read_u_int_array() - self.compressedBlob = reader.read_bytes(reader.read_int()) + + compressedBlobSize = reader.read_int() + sanity_check("compressedBlobSize", compressedBlobSize, reader.Length) + self.compressedBlob = reader.read_bytes(compressedBlobSize) + reader.align_stream() + + if version > (2021,3): + self.stageCounts = reader.read_u_int_array() + reader.align_stream() + numDependencies = reader.read_u_int() + sanity_check("numDependencies", numDependencies) + self.m_Dependencies = [PPtr(reader) for _ in range(numDependencies)] + reader.align_stream() + + numNonModifiableTextures = reader.read_u_int() + sanity_check("numNonModifiableTextures", numNonModifiableTextures) + self.m_NonModifiableTextures = {} + for _ in range(numNonModifiableTextures): + key = reader.read_aligned_string() + self.m_NonModifiableTextures[key] = PPtr(reader) + self.m_ShaderIsBaked = reader.read_bool() + reader.align_stream() + #except Exception as e: + #print_debug(e) + #reader.Position = details_pos + #self.the_rest = reader.read_the_rest() else: - self.m_Script = reader.read_bytes(reader.read_int()) + scriptSize = reader.read_int() + sanity_check("scriptSize", scriptSize, reader.Length) + self.m_Script = reader.read_bytes(scriptSize) reader.align_stream() self.m_PathName = reader.read_aligned_string() if version >= (5, 3): # 5.3 - 5.4 self.decompressedSize = reader.read_u_int() - self.m_SubProgramBlob = reader.read_bytes(reader.read_int()) + SubProgramBlobSize = reader.read_int() + sanity_check("SubProgramBlobSize", SubProgramBlobSize, reader.Length) + self.m_SubProgramBlob = reader.read_bytes(SubProgramBlobSize) + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + writer.version = self.reader.version + super().save(writer=writer) + version = self.version + if version >= (5, 5): # 5.5 and up + self.m_ParsedForm.save(writer) + writer.write_u_int(len(self.platforms)) + for i in range(len(self.platforms)): + writer.write_u_int(int(self.platforms[i])) + + if version >= (2019, 3): # 2019.3 and up + writer.write_u_int(len(self.offsets)) + for i in range(len(self.compressedLengths)): + writer.write_u_int_array(self.offsets[i], write_length=True) + writer.align_stream() + writer.write_u_int(len(self.compressedLengths)) + for i in range(len(self.compressedLengths)): + writer.write_u_int_array(self.compressedLengths[i], write_length=True) + writer.align_stream() + writer.write_u_int(len(self.decompressedLengths)) + for i in range(len(self.decompressedLengths)): + writer.write_u_int_array(self.decompressedLengths[i], write_length=True) + writer.align_stream() + else: + writer.write_u_int_array(self.offsets, write_length=True) + writer.write_u_int_array(self.compressedLengths, write_length=True) + writer.write_u_int_array(self.decompressedLengths, write_length=True) + + writer.write_int(len(self.compressedBlob)) + writer.write_bytes(self.compressedBlob) + writer.align_stream() + + if version >= (2021,3): + writer.write_u_int_array(self.stageCounts, write_length=True) + writer.align_stream() + + writer.write_u_int(len(self.m_Dependencies)) + for item in self.m_Dependencies: + item.save(writer) + writer.align_stream() + + writer.write_u_int(len(self.m_NonModifiableTextures.keys())) + for k, v in self.m_NonModifiableTextures.items(): + writer.write_aligned_string(k) + v.save(writer) + writer.write_bool(self.m_ShaderIsBaked) + writer.align_stream() + else: + writer.write_int(len(self.m_Script)) + writer.write_bytes(self.m_Script) + writer.align_stream() + writer.write_aligned_string(self.m_PathName) + if version >= (5, 3): # 5.3 - 5.4 + writer.write_u_int(self.decompressedSize) + writer.write_int(len(self.m_SubProgramBlob)) + writer.write_bytes(self.m_SubProgramBlob) + + self.set_raw_data(writer) + + @property + def name(self): + return self.m_ParsedForm.m_Name class StructParameter: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.m_NameIndex = reader.read_int() self.m_Index = reader.read_int() self.m_ArraySize = reader.read_int() self.m_StructSize = reader.read_int() numVectorParams = reader.read_int() - self.m_VectorParams = [VectorParameter(reader) for _ in range(numVectorParams)] - + self.m_VectorMembers = [ + VectorParameter(reader) for _ in range(numVectorParams) + ] numMatrixParams = reader.read_int() - self.m_MatrixParams = [MatrixParameter(reader) for _ in range(numMatrixParams)] + self.m_MatrixMembers = [ + MatrixParameter(reader) for _ in range(numMatrixParams) + ] + def save(self, writer: EndianBinaryWriter): + writer.write_int(self.m_NameIndex) + writer.write_int(self.m_Index) + writer.write_int(self.m_ArraySize) + writer.write_int(self.m_StructSize) + writer.write_int(len(self.m_VectorMembers)) + for item in self.m_VectorMembers: + item.save(writer) + writer.write_int(len(self.m_MatrixMembers)) + for item in self.m_MatrixMembers: + item.save(writer) class SamplerParameter: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.sampler = reader.read_u_int() self.bindPoint = reader.read_int() + def save(self, writer: EndianBinaryWriter): + writer.write_u_int(self.sampler) + writer.write_int(self.bindPoint) class SerializedTextureProperty: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.m_DefaultName = reader.read_aligned_string() self.m_TexDim = TextureDimension(reader.read_int()) + def save(self, writer: EndianBinaryWriter): + writer.write_aligned_string(self.m_DefaultName) + writer.write_int(int(self.m_TexDim)) + class SerializedProperty: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.m_Name = reader.read_aligned_string() self.m_Description = reader.read_aligned_string() self.m_Attributes = reader.read_string_array() @@ -73,46 +248,110 @@ def __init__(self, reader): self.m_DefValue = reader.read_float_array(4) self.m_DefTexture = SerializedTextureProperty(reader) + def save(self, writer: EndianBinaryWriter): + writer.write_aligned_string(self.m_Name) + writer.write_aligned_string(self.m_Description) + writer.write_string_array(self.m_Attributes) + writer.write_int(int(self.m_Type)) + writer.write_u_int(self.m_Flags) + writer.write_float_array(self.m_DefValue) + self.m_DefTexture.save(writer) + class SerializedProperties: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): numProps = reader.read_int() - self.m_Props = [SerializedProperty(reader) for _ in range(numProps)] + sanity_check("numProps", numProps) + self.m_Props = [ + SerializedProperty(reader) for _ in range(numProps) + ] + + def save(self, writer: EndianBinaryWriter): + writer.write_int(len(self.m_Props)) + for item in self.m_Props: + item.save(writer) + + +class RTBlendType(IntEnum): + NOPE = 0 + BLEND = 1 + OP = 2 + STENCIL = 3 + CMASK = 4 class SerializedShaderFloatValue: - def __init__(self, reader): + def __init__(self, reader: ObjectReader, optype=RTBlendType.NOPE): self.val = reader.read_float() self.name = reader.read_aligned_string() + self._optype = optype + + @property + def _desc(self): + if self._optype == RTBlendType.BLEND: + return D3D_BLEND(int(self.val)) + elif self._optype == RTBlendType.OP: + return D3D_BLEND_OP(int(self.val)) + elif self._optype == RTBlendType.STENCIL: + return D3D_STENCIL_OP(int(self.val)) + elif self._optype == RTBlendType.CMASK: + return D3D_COLOR_WRITE_ENABLE(int(self.val)) + return self.val + + def save(self, writer: EndianBinaryWriter): + writer.write_float(self.val) + writer.write_aligned_string(self.name) class SerializedShaderRTBlendState: - def __init__(self, reader): - self.srcBlend = SerializedShaderFloatValue(reader) - self.destBlend = SerializedShaderFloatValue(reader) - self.srcBlendAlpha = SerializedShaderFloatValue(reader) - self.destBlendAlpha = SerializedShaderFloatValue(reader) - self.blendOp = SerializedShaderFloatValue(reader) - self.blendOpAlpha = SerializedShaderFloatValue(reader) - self.colMask = SerializedShaderFloatValue(reader) + def __init__(self, reader: ObjectReader): + self.srcBlend = SerializedShaderFloatValue(reader, RTBlendType.BLEND) + self.destBlend = SerializedShaderFloatValue(reader, RTBlendType.BLEND) + self.srcBlendAlpha = SerializedShaderFloatValue(reader, RTBlendType.BLEND) + self.destBlendAlpha = SerializedShaderFloatValue(reader, RTBlendType.BLEND) + self.blendOp = SerializedShaderFloatValue(reader, RTBlendType.OP) + self.blendOpAlpha = SerializedShaderFloatValue(reader, RTBlendType.OP) + self.colMask = SerializedShaderFloatValue(reader, RTBlendType.CMASK) + + def save(self, writer: EndianBinaryWriter): + self.srcBlend.save(writer) + self.destBlend.save(writer) + self.srcBlendAlpha.save(writer) + self.destBlendAlpha.save(writer) + self.blendOp.save(writer) + self.blendOpAlpha.save(writer) + self.colMask.save(writer) class SerializedStencilOp: - def __init__(self, reader): - self.pass_ = SerializedShaderFloatValue(reader) - self.fail = SerializedShaderFloatValue(reader) - self.zFail = SerializedShaderFloatValue(reader) - self.comp = SerializedShaderFloatValue(reader) + def __init__(self, reader: ObjectReader): + self.pass_ = SerializedShaderFloatValue(reader, RTBlendType.STENCIL) + self.fail = SerializedShaderFloatValue(reader, RTBlendType.STENCIL) + self.zFail = SerializedShaderFloatValue(reader, RTBlendType.STENCIL) + self.comp = SerializedShaderFloatValue(reader, RTBlendType.STENCIL) + + def save(self, writer: EndianBinaryWriter): + self.pass_.save(writer) + self.fail.save(writer) + self.zFail.save(writer) + self.comp.save(writer) class SerializedShaderVectorValue: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.x = SerializedShaderFloatValue(reader) self.y = SerializedShaderFloatValue(reader) self.z = SerializedShaderFloatValue(reader) self.w = SerializedShaderFloatValue(reader) self.name = reader.read_aligned_string() + def save(self, writer: EndianBinaryWriter): + self.x.save(writer) + self.y.save(writer) + self.z.save(writer) + self.w.save(writer) + writer.write_aligned_string(self.name) + class FogMode(IntEnum): kFogUnknown = (-1,) @@ -123,7 +362,7 @@ class FogMode(IntEnum): class SerializedShaderState: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): version = reader.version self.m_Name = reader.read_aligned_string() @@ -157,23 +396,71 @@ def __init__(self, reader): self.lighting = reader.read_boolean() reader.align_stream() + def save(self, writer: EndianBinaryWriter): + version = writer.version + + writer.write_aligned_string(self.m_Name) + for item in self.rtBlend: + item.save(writer) + + writer.write_boolean(self.rtSeparateBlend) + writer.align_stream() + if version >= (2017, 2): # 2017.2 and up + self.zClip.save(writer) + self.zTest.save(writer) + self.zWrite.save(writer) + self.culling.save(writer) + if version >= (2020,): # 2020.1 and up + self.conservative.save(writer) + self.offsetFactor.save(writer) + self.offsetUnits.save(writer) + self.alphaToMask.save(writer) + self.stencilOp.save(writer) + self.stencilOpFront.save(writer) + self.stencilOpBack.save(writer) + self.stencilReadMask.save(writer) + self.stencilWriteMask.save(writer) + self.stencilRef.save(writer) + self.fogStart.save(writer) + self.fogEnd.save(writer) + self.fogDensity.save(writer) + self.fogColor.save(writer) + writer.write_int(int(self.fogMode)) + writer.write_int(self.gpuProgramID) + self.m_Tags.save(writer) + writer.write_int(self.m_LOD) + writer.write_boolean(self.lighting) + writer.align_stream() class ShaderBindChannel: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.source = reader.read_byte() self.target = reader.read_byte() + def save(self, writer: EndianBinaryWriter): + writer.write_byte(self.source) + writer.write_byte(self.target) class ParserBindChannels: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): numChannels = reader.read_int() - self.m_Channels = [ShaderBindChannel(reader) for _ in range(numChannels)] + sanity_check("numChannels", numChannels) + self.m_Channels = [ + ShaderBindChannel(reader) for _ in range(numChannels) + ] reader.align_stream() self.m_SourceMap = reader.read_u_int() + def save(self, writer: EndianBinaryWriter): + writer.write_int(len(self.m_Channels)) + for item in self.m_Channels: + item.save(writer) + writer.align_stream() + writer.write_u_int(self.m_SourceMap) + class VectorParameter: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.m_NameIndex = reader.read_int() self.m_Index = reader.read_int() self.m_ArraySize = reader.read_int() @@ -181,9 +468,17 @@ def __init__(self, reader): self.m_Dim = reader.read_byte() reader.align_stream() + def save(self, writer: EndianBinaryWriter): + writer.write_int(self.m_NameIndex) + writer.write_int(self.m_Index) + writer.write_int(self.m_ArraySize) + writer.write_byte(self.m_Type) + writer.write_byte(self.m_Dim) + writer.align_stream() + class MatrixParameter: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.m_NameIndex = reader.read_int() self.m_Index = reader.read_int() self.m_ArraySize = reader.read_int() @@ -191,9 +486,17 @@ def __init__(self, reader): self.m_RowCount = reader.read_byte() reader.align_stream() + def save(self, writer: EndianBinaryWriter): + writer.write_int(self.m_NameIndex) + writer.write_int(self.m_Index) + writer.write_int(self.m_ArraySize) + writer.write_byte(self.m_Type) + writer.write_byte(self.m_RowCount) + writer.align_stream() + class TextureParameter: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): version = reader.version self.m_NameIndex = reader.read_int() self.m_Index = reader.read_int() @@ -203,86 +506,187 @@ def __init__(self, reader): self.m_Dim = reader.read_byte() reader.align_stream() + def save(self, writer: EndianBinaryWriter): + version = writer.version + writer.write_int(self.m_NameIndex) + writer.write_int(self.m_Index) + writer.write_int(self.m_SamplerIndex) + if version >= (2017, 3): # 2017.3 and up + writer.write_boolean(self.m_MultiSampled) + writer.write_byte(self.m_Dim) + writer.align_stream() + class BufferBinding: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): 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() + self.m_ArraySize = reader.read_int() + + def save(self, writer: EndianBinaryWriter): + writer.write_int(self.m_NameIndex) + writer.write_int(self.m_Index) + if writer.version >= (2020,): # 2020.1 and up + writer.write_int(self.m_ArraySize) class ConstantBuffer: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): version = reader.version self.m_NameIndex = reader.read_int() numMatrixParams = reader.read_int() - self.m_MatrixParams = [MatrixParameter(reader) for _ in range(numMatrixParams)] + sanity_check("numMatrixParams", numMatrixParams) + self.m_MatrixParams = [ + MatrixParameter(reader) for _ in range(numMatrixParams) + ] numVectorParams = reader.read_int() - self.m_VectorParams = [VectorParameter(reader) for _ in range(numVectorParams)] + sanity_check("numVectorParams", numVectorParams) + self.m_VectorParams = [ + VectorParameter(reader) for _ in range(numVectorParams) + ] if version >= (2017, 3): # 2017.3 and up numStructParams = reader.read_int() + sanity_check("numStructParams", numStructParams) self.m_StructParams = [ StructParameter(reader) for _ in range(numStructParams) ] + self.m_Size = reader.read_int() - if version >= (2021, 1, 4) or (version[0] == 2020 and version >= (2020, 3, 2)): + if version >= (2020, 3, 2): self.m_IsPartialCB = reader.read_boolean() reader.align_stream() + def save(self, writer: EndianBinaryWriter): + version = writer.version + writer.write_int(self.m_NameIndex) + + writer.write_int(len(self.m_MatrixParams)) + for item in self.m_MatrixParams: + item.save(writer) + + writer.write_int(len(self.m_VectorParams)) + for item in self.m_VectorParams: + item.save(writer) + + if version >= (2017, 3): # 2017.3 and up + writer.write_int(len(self.m_StructParams)) + for item in self.m_StructParams: + item.save(writer) + + writer.write_int(self.m_Size) + + if version >= (2020, 3, 2): + writer.write_boolean(self.m_IsPartialCB) + writer.align_stream() class UAVParameter: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.m_NameIndex = reader.read_int() self.m_Index = reader.read_int() self.m_OriginalIndex = reader.read_int() + def save(self, writer: EndianBinaryWriter): + writer.write_int(self.m_NameIndex) + writer.write_int(self.m_Index) + writer.write_int(self.m_OriginalIndex) class SerializedProgramParameters: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): numVectorParams = reader.read_int() - self.m_VectorParams = [VectorParameter(reader) for _ in range(numVectorParams)] + sanity_check("numVectorParams", numVectorParams) + self.m_VectorParams = [ + VectorParameter(reader) for _ in range(numVectorParams) + ] numMatrixParams = reader.read_int() - self.m_MatrixParams = [MatrixParameter(reader) for _ in range(numMatrixParams)] + sanity_check("numMatrixParams", numMatrixParams) + self.m_MatrixParams = [ + MatrixParameter(reader) for _ in range(numMatrixParams) + ] numTextureParams = reader.read_int() + sanity_check("numTextureParams", numTextureParams) self.m_TextureParams = [ TextureParameter(reader) for _ in range(numTextureParams) ] numBufferParams = reader.read_int() - self.m_BufferParams = [BufferBinding(reader) for _ in range(numBufferParams)] + sanity_check("numBufferParams", numBufferParams) + self.m_BufferParams = [ + BufferBinding(reader) for _ in range(numBufferParams) + ] numConstantBuffers = reader.read_int() + sanity_check("numConstantBuffers", numConstantBuffers) self.m_ConstantBuffers = [ ConstantBuffer(reader) for _ in range(numConstantBuffers) ] numConstantBufferBindings = reader.read_int() + sanity_check("numConstantBufferBindings", numConstantBufferBindings) self.m_ConstantBufferBindings = [ BufferBinding(reader) for _ in range(numConstantBufferBindings) ] numUAVParams = reader.read_int() - self.m_UAVParams = [UAVParameter(reader) for _ in range(numUAVParams)] + sanity_check("numUAVParams", numUAVParams) + self.m_UAVParams = [ + UAVParameter(reader) for _ in range(numUAVParams) + ] numSamplers = reader.read_int() - self.m_Samplers = [SamplerParameter(reader) for _ in range(numSamplers)] + sanity_check("numSamplers", numSamplers) + self.m_Samplers = [ + SamplerParameter(reader) for _ in range(numSamplers) + ] + + def save(self, writer: EndianBinaryWriter): + writer.write_int(len(self.m_VectorParams)) + for item in self.m_VectorParams: + item.save(writer) + + writer.write_int(len(self.m_MatrixParams)) + for item in self.m_MatrixParams: + item.save(writer) + writer.write_int(len(self.m_TextureParams)) + for item in self.m_TextureParams: + item.save(writer) + + writer.write_int(len(self.m_BufferParams)) + for item in self.m_BufferParams: + item.save(writer) + + writer.write_int(len(self.m_ConstantBuffers)) + for item in self.m_ConstantBuffers: + item.save(writer) + + writer.write_int(len(self.m_ConstantBufferBindings)) + for item in self.m_ConstantBufferBindings: + item.save(writer) + + writer.write_int(len(self.m_UAVParams)) + for item in self.m_UAVParams: + item.save(writer) + + writer.write_int(len(self.m_Samplers)) + for item in self.m_Samplers: + item.save(writer) + pass class SerializedSubProgram: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): version = reader.version self.m_BlobIndex = reader.read_u_int() self.m_Channels = ParserBindChannels(reader) - if (2019,) <= version < (2021, 2): # 2019 ~2021.1 + if (2019, 0) <= version[:2] < (2021, 2): # 2019 ~2021.1 self.m_GlobalKeywordIndices = reader.read_u_short_array() reader.align_stream() self.m_LocalKeywordIndices = reader.read_u_short_array() @@ -342,42 +746,167 @@ def __init__(self, reader): else: self.m_ShaderRequirements = reader.read_int() + def save(self, writer: EndianBinaryWriter): + version = writer.version + + writer.write_u_int(self.m_BlobIndex) + self.m_Channels.save(writer) + + if (2019, 0) <= version[:2] < (2021, 2): # 2019 ~2021.1 + writer.write_u_short_array(self.m_GlobalKeywordIndices) + writer.align_stream() + writer.write_u_short_array(self.m_LocalKeywordIndices) + writer.align_stream() + else: + writer.write_u_short_array(self.m_KeywordIndices) + if version >= (2017,): # 2017 and up + writer.align_stream() + + writer.write_byte(self.m_ShaderHardwareTier) + writer.write_byte(int(self.m_GpuProgramType)) + writer.align_stream() + + if version >= (2021, 1, 4) or (version[0] == 2020 and version >= (2020, 3, 2)): + self.m_Parameters.save(writer) + else: + writer.write_int(len(self.m_VectorParams)) + for item in self.m_VectorParams: + item.save(writer) + + writer.write_int(len(self.m_MatrixParams)) + for item in self.m_MatrixParams: + item.save(writer) + + writer.write_int(len(self.m_TextureParams)) + for item in self.m_TextureParams: + item.save(writer) + + writer.write_int(len(self.m_BufferParams)) + for item in self.m_BufferParams: + item.save(writer) + + writer.write_int(len(self.m_ConstantBuffers)) + for item in self.m_ConstantBuffers: + item.save(writer) + + writer.write_int(len(self.m_ConstantBufferBindings)) + for item in self.m_ConstantBufferBindings: + item.save(writer) + + writer.write_int(len(self.m_UAVParams)) + for item in self.m_UAVParams: + item.save(writer) + + if version >= (2017,): # 2017 and up + writer.write_int(len(self.m_Samples)) + for item in self.m_Samples: + item.save(writer) + + if version >= (2017, 2): # 2017.2 and up + if version >= (2021,): + writer.write_long(self.m_ShaderRequirements) + else: + writer.write_int(self.m_ShaderRequirements) + +class SerializedPlayerSubProgram: + def __init__(self, reader: ObjectReader): + self.m_BlobIndex = reader.read_u_int() + self.m_KeywordIndices = reader.read_u_short_array() + reader.align_stream() + self.m_ShaderRequirements = reader.read_long() + self.m_GpuProgramType = ShaderGpuProgramType(reader.read_byte()) + reader.align_stream() + + def save(self, writer: EndianBinaryWriter): + writer.write_u_int(self.m_BlobIndex) + writer.write_u_short_array(self.m_KeywordIndices) + writer.align_stream() + writer.write_long(self.m_ShaderRequirements) + writer.write_byte(int(self.m_GpuProgramType)) + writer.align_stream() + class SerializedProgram: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): version = reader.version numSubPrograms = reader.read_int() + sanity_check("numSubPrograms", numSubPrograms) self.m_SubPrograms = [ SerializedSubProgram(reader) for _ in range(numSubPrograms) ] - if version >= (2021, 1, 4) or (version[0] == 2020 and version >= (2020, 3, 2)): + if version >= (2021, 3): + numPlayerSubPrograms = reader.read_int() + sanity_check("numPlayerSubPrograms", numPlayerSubPrograms) + self.m_PlayerSubPrograms = [] + for _ in range(numPlayerSubPrograms): + vectorSize = reader.read_int() + self.m_PlayerSubPrograms.append([SerializedPlayerSubProgram(reader) for _ in range(vectorSize)]) + + numParameterBlobIndices = reader.read_int() + sanity_check("numParameterBlobIndices", numParameterBlobIndices) + self.m_ParameterBlobIndices = [ + reader.read_u_int_array() for _ in range(numParameterBlobIndices) + ] + + if version >= (2020, 3, 2): self.m_CommonParameters = SerializedProgramParameters(reader) + if version >= (2022, 0): # TODO: not precise + self.m_SerializedKeywordStateMask = reader.read_u_short_array() + + def save(self, writer: EndianBinaryWriter): + version = writer.version + + writer.write_int(len(self.m_SubPrograms)) + for arr in self.m_SubPrograms: + writer.write_int(len(arr)) + for item in arr: + item.save(writer) + + if version >= (2021, 3): + writer.write_int(len(self.m_PlayerSubPrograms)) + for arr in self.m_PlayerSubPrograms: + writer.write_int(len(arr)) + for item in arr: + item.save(writer) + + writer.write_int(len(self.m_ParameterBlobIndices)) + for item in self.m_ParameterBlobIndices: + writer.write_u_int_array(item, write_length=True) + + if version >= (2020, 3, 2): + self.m_CommonParameters.save(writer) + + if version >= (2022, 0): # TODO: version is not precise + writer.write_u_short_array(self.m_SerializedKeywordStateMask) + class SerializedPass: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): version = reader.version if version >= (2020, 2): # 2020.2 and up numEditorDataHash = reader.read_int() - m_EditorDataHash = [ + sanity_check("numEditorDataHash", numEditorDataHash) + self.m_EditorDataHash = [ reader.read_bytes(16) # Hash128(reader) for _ in range(numEditorDataHash) ] reader.align_stream() - m_Platforms = reader.read_byte_array() + self.m_Platforms = reader.read_byte_array() reader.align_stream() - if version < (2021, 2): - m_LocalKeywordMask = reader.read_u_short_array() + if version[:2] < (2021, 2): + self.m_LocalKeywordMask = reader.read_u_short_array() reader.align_stream() - m_GlobalKeywordMask = reader.read_u_short_array() + self.m_GlobalKeywordMask = reader.read_u_short_array() reader.align_stream() - numIndices = reader.read_int() + numNameIndices = reader.read_int() + sanity_check("numNameIndices", numNameIndices) self.m_NameIndices = {} - for _ in range(numIndices): + for _ in range(numNameIndices): key = reader.read_aligned_string() self.m_NameIndices[key] = reader.read_int() self.m_Type = PassType(reader.read_int()) @@ -398,67 +927,175 @@ def __init__(self, reader): self.m_Name = reader.read_aligned_string() self.m_TextureName = reader.read_aligned_string() self.m_Tags = SerializedTagMap(reader) - if version >= (2021, 2): - m_SerializedKeywordStateMask = reader.read_u_short_array() + if version[:2] >= (2021, 2): + self.m_SerializedKeywordStateMask = reader.read_u_short_array() reader.align_stream() + pass + + def save(self, writer: EndianBinaryWriter): + version = writer.version + + if version >= (2020, 2): # 2020.2 and up + writer.write_int(len(self.m_EditorDataHash)) + for item in self.m_EditorDataHash: + writer.write_bytes(item) # Hash128(writer) + + writer.align_stream() + writer.write_byte_array(self.m_Platforms) + writer.align_stream() + if version[:2] < (2021, 2): + writer.write_u_short_array(self.m_LocalKeywordMask) + writer.align_stream() + writer.write_u_short_array(self.m_GlobalKeywordMask) + writer.align_stream() + + numNameIndices = len(self.m_NameIndices.keys()) + writer.write_int(numNameIndices) + for k, v in self.m_NameIndices.items(): + writer.write_aligned_string(k) + writer.write_int(v) + writer.write_int(int(self.m_Type)) + self.m_State.save(writer) + writer.write_u_int(self.m_ProgramMask) + self.progVertex.save(writer) + self.progFragment.save(writer) + self.progGeometry.save(writer) + self.progHull.save(writer) + self.progDomain.save(writer) + if version >= (2019, 3): # 2019.3 and up + self.progRayTracing.save(writer) + writer.write_boolean(self.m_HasInstancingVariant) + if version >= (2018,): # 2018 and up + writer.write_boolean(self.m_HasProceduralInstancingVariant) + writer.align_stream() + writer.write_aligned_string(self.m_UseName) + writer.write_aligned_string(self.m_Name) + writer.write_aligned_string(self.m_TextureName) + self.m_Tags.save(writer) + if version[:2] >= (2021, 2): + writer.write_u_short_array(self.m_SerializedKeywordStateMask) + writer.align_stream() + pass class SerializedTagMap: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): numTags = reader.read_int() + sanity_check("numTags", numTags) self.tags = {} for _ in range(numTags): key = reader.read_aligned_string() self.tags[key] = reader.read_aligned_string() + def save(self, writer: EndianBinaryWriter): + numTags = len(self.tags.keys()) + writer.write_int(numTags) + for k, v in self.tags.items(): + writer.write_aligned_string(k) + writer.write_aligned_string(v) class SerializedSubShader: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): numPasses = reader.read_int() - self.m_Passes = [SerializedPass(reader) for _ in range(numPasses)] + self.m_Passes = [ + SerializedPass(reader) for _ in range(numPasses) + ] self.m_Tags = SerializedTagMap(reader) self.m_LOD = reader.read_int() + def save(self, writer: EndianBinaryWriter): + writer.write_int(len(self.m_Passes)) + for item in self.m_Passes: + item.save(writer) + self.m_Tags.save(writer) + writer.write_int(self.m_LOD) + class SerializedShaderDependency: - def __init__(self, reader): - self.from_ = reader.read_aligned_string() - self.to = reader.read_aligned_string() + def __init__(self, reader: ObjectReader): + self.d_from = reader.read_aligned_string() + self.d_to = reader.read_aligned_string() + + def save(self, writer: EndianBinaryWriter): + writer.write_aligned_string(self.d_from) + writer.write_aligned_string(self.d_to) class SerializedCustomEditorForRenderPipeline: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): self.customEditorName = reader.read_aligned_string() self.renderPipelineType = reader.read_aligned_string() + def save(self, writer: EndianBinaryWriter): + writer.write_aligned_string(self.customEditorName) + writer.write_aligned_string(self.renderPipelineType) + class SerializedShader: - def __init__(self, reader): + def __init__(self, reader: ObjectReader): version = reader.version 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) + ] - if version >= (2021, 2): + if version[:2] >= (2021, 2): self.m_KeywordNames = reader.read_string_array() - self.m_KeywordFlags = reader.read_bytes(reader.read_int()) + numKeywordFlags = reader.read_int() + sanity_check("numKeywordFlags", numKeywordFlags) + self.m_KeywordFlags = reader.read_bytes(numKeywordFlags) reader.align_stream() self.m_Name = reader.read_aligned_string() self.m_CustomEditorName = reader.read_aligned_string() self.m_FallbackName = reader.read_aligned_string() + numDependencies = reader.read_int() self.m_Dependencies = [ SerializedShaderDependency(reader) for _ in range(numDependencies) ] - if version >= (2021,): - m_CustomEditorForRenderPipelinesSize = reader.read_int() - self.m_CustomEditorForRenderPipelines = [ - SerializedCustomEditorForRenderPipeline(reader) - for _ in range(m_CustomEditorForRenderPipelinesSize) - ] + if version[0] >= 2021: + self.m_CustomEditorForRenderPipelines = [] + numCustomEditorForRenderPipelinesSize = reader.read_int() + sanity_check("numCustomEditorForRenderPipelinesSize", numCustomEditorForRenderPipelinesSize) + for _ in range(numCustomEditorForRenderPipelinesSize): + self.m_CustomEditorForRenderPipelines.append( + SerializedCustomEditorForRenderPipeline(reader) + ) self.m_DisableNoSubshadersMessage = reader.read_boolean() reader.align_stream() + + def save(self, writer: EndianBinaryWriter): + version = writer.version + + self.m_PropInfo.save(writer) + + writer.write_int(len(self.m_SubShaders)) + for item in self.m_SubShaders: + item.save(writer) + + if version[:2] >= (2021, 2): + writer.write_string_array(self.m_KeywordNames) + writer.write_int(len(self.m_KeywordFlags)) + writer.write_bytes(self.m_KeywordFlags) + writer.align_stream() + + writer.write_aligned_string(self.m_Name) + writer.write_aligned_string(self.m_CustomEditorName) + writer.write_aligned_string(self.m_FallbackName) + + writer.write_int(len(self.m_Dependencies)) + for item in self.m_Dependencies: + item.save(writer) + + if version[0] >= 2021: + writer.write_int(len(self.m_CustomEditorForRenderPipelines)) + for item in self.m_CustomEditorForRenderPipelines: + item.save(writer) + + writer.write_boolean(self.m_DisableNoSubshadersMessage) + writer.align_stream() \ No newline at end of file diff --git a/UnityPy/classes/Sprite.py b/UnityPy/classes/Sprite.py index 3f65e84e..7cb8c605 100644 --- a/UnityPy/classes/Sprite.py +++ b/UnityPy/classes/Sprite.py @@ -50,14 +50,7 @@ def __init__(self, reader): if version >= (2018,): # 2018 and up m_BonesSize = reader.read_int() - # TODO: might occur in earlier 2020 versions - 2020.3.13 reported - if version >= ( - 2020, - 3, - ): - self.m_Bones = [SpriteBone() for _ in range(m_BonesSize)] - else: - self.m_Bones = [reader.read_vector2_array() for _ in range(m_BonesSize)] + self.m_Bones = [SpriteBone(self.reader) for _ in range(m_BonesSize)] def save(self, writer: EndianBinaryWriter = None): if writer is None: @@ -96,14 +89,10 @@ def save(self, writer: EndianBinaryWriter = None): if version >= (2018,): # 2018 and up writer.write_int(len(self.m_Bones)) - if version >= (2020, 3): - for bone in self.m_Bones: - bone.save(writer) - else: - for bone in self.m_Bones: - writer.write_vector2_array(bone) + for bone in self.m_Bones: + bone.save(writer, version) - self.set_raw_data(writer.bytes) + self.set_raw_data(writer) class SecondarySpriteTexture: @@ -241,14 +230,28 @@ class SpriteBone: def __init__(self, reader: EndianBinaryReader) -> None: self.name = reader.read_aligned_string() + + if reader.version >= (2021,): + self.guid = reader.read_aligned_string() + self.position = reader.read_vector3() - self.rotation = reader.read_vector3() + self.rotation = reader.read_quaternion() self.length = reader.read_float() self.parentId = reader.read_int() - def save(self, writer: EndianBinaryWriter): + if reader.version >= (2021,): + self.color = reader.read_color_uint() + + def save(self, writer: EndianBinaryWriter, version): writer.write_aligned_string(self.name) + + if version >= (2021,): + writer.write_aligned_string(self.guid) + writer.write_vector3(self.position) - writer.write_vector3(self.rotation) + writer.write_quaternion(self.rotation) writer.write_float(self.length) writer.write_int(self.parentId) + + if version >= (2021,): + writer.write_color_uint(self.color) diff --git a/UnityPy/classes/SpriteAtlas.py b/UnityPy/classes/SpriteAtlas.py index 8bf65c45..790e01d3 100644 --- a/UnityPy/classes/SpriteAtlas.py +++ b/UnityPy/classes/SpriteAtlas.py @@ -37,4 +37,4 @@ def __init__(self, reader): self.secondaryTextures = [ SecondarySpriteTexture(reader) for _ in range(secondaryTexturesSize) ] - reader.align_stream() + reader.align_stream() \ No newline at end of file diff --git a/UnityPy/classes/TextAsset.py b/UnityPy/classes/TextAsset.py index 8c1a0165..3e275408 100644 --- a/UnityPy/classes/TextAsset.py +++ b/UnityPy/classes/TextAsset.py @@ -6,11 +6,12 @@ class TextAsset(NamedObject): def __init__(self, reader): super().__init__(reader=reader) self.m_Script = reader.read_bytes(reader.read_int()) + reader.align_stream() @property def script(self): # required for backward compatibility - return self.m_Script + return bytes(self.m_Script) @script.setter def script(self, value): @@ -18,18 +19,19 @@ def script(self, value): @property def text(self): - return bytes(self.script).decode("utf8") + return bytes(self.m_Script).decode("utf-8") @text.setter def text(self, val): - self.script = val.encode("utf8") + self.m_Script = val.encode("utf-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.m_Script)) writer.write_bytes(self.m_Script) writer.align_stream() - self.set_raw_data(writer.bytes) + self.set_raw_data(writer) diff --git a/UnityPy/classes/Texture.py b/UnityPy/classes/Texture.py index 652ea1b6..212f964c 100644 --- a/UnityPy/classes/Texture.py +++ b/UnityPy/classes/Texture.py @@ -5,18 +5,20 @@ class Texture(NamedObject): def __init__(self, reader): super().__init__(reader=reader) - if self.version >= (2017, 3): # 2017.3 and up - self.m_ForcedFallbackFormat = reader.read_int() - self.m_DownscaleFallback = reader.read_boolean() - if self.version >= (2020,2): # 2020.2 and up + if self.version >= (2017, 3): + if self.version < (2023,2): + self.m_ForcedFallbackFormat = reader.read_int() + self.m_DownscaleFallback = reader.read_boolean() + if self.version >= (2020,2): 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.m_ForcedFallbackFormat) - writer.write_boolean(self.m_DownscaleFallback) - if self.version >= (2020,2): # 2020.2 and up + if self.version >= (2017, 3): + if self.version < (2023,2): + writer.write_int(self.m_ForcedFallbackFormat) + writer.write_boolean(self.m_DownscaleFallback) + if self.version >= (2020,2): writer.write_boolean(self.m_IsAlphaChannelOptional) writer.align_stream() diff --git a/UnityPy/classes/Texture2D.py b/UnityPy/classes/Texture2D.py index 705aeaba..26904bfa 100644 --- a/UnityPy/classes/Texture2D.py +++ b/UnityPy/classes/Texture2D.py @@ -2,127 +2,12 @@ from ..enums import TextureFormat from ..export import Texture2DConverter from ..helpers.ResourceReader import get_resource_data -from ..streams import EndianBinaryWriter +from ..streams import EndianBinaryReader, EndianBinaryWriter from PIL import Image from io import BufferedIOBase, RawIOBase, IOBase class Texture2D(Texture): - @property - def image(self): - return Texture2DConverter.get_image_from_texture2d(self) - - @image.setter - 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 - if img is None: - raise Exception("No image provided") - - 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.m_TextureFormat - ) - - # disable mipmaps as we don't store them ourselves by default - if self.version[:2] < (5, 2): # 5.2 down - self.m_MipMap = False - else: - self.m_MipCount = 1 - - self.image_data = img_data - self.m_MipCount = 1 - # width * height * channel count - self.m_CompleteImageSize = len( - img_data - ) # img.width * img.height * len(img.getbands()) - self.m_TextureFormat = tex_format - - @property - def image_data(self): - if not self._image_data and self.m_StreamData is not None: - self._image_data = get_resource_data( - self.m_StreamData.path, - self.assets_file, - self.m_StreamData.offset, - self.m_StreamData.size, - ) - 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 = "" - - @image_data.setter - def image_data(self, data: bytes): - self._image_data = data - # ignore writing to cab for now until it's more stable - # if self.version >= (5, 3) and self.m_StreamData.path: - # 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() - self.reset_streamdata() - - def set_image( - self, - img, - target_format: TextureFormat = None, - in_cab: bool = False, - mipmap_count: int = 1, - ): - if img is None: - raise Exception("No image provided") - if not target_format: - target_format = self.m_TextureFormat - - img_data, tex_format = Texture2DConverter.image_to_texture2d(img, target_format) - if mipmap_count > 1: - width = self.m_Width - height = self.m_Height - re_img = img - for i in range(mipmap_count - 1): - width //= 2 - height //= 2 - if width < 4 or height < 4: - mipmap_count = i + 1 - break - re_img = re_img.resize((width, height), Image.BICUBIC) - img_data += Texture2DConverter.image_to_texture2d( - re_img, target_format - )[0] - - if self.version[:2] < (5, 2): # 5.2 down - self.m_MipMap = mipmap_count > 1 - else: - self.m_MipCount = mipmap_count - - 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( - img_data - ) # img.width * img.height * len(img.getbands()) - self.m_TextureFormat = tex_format - def __init__(self, reader): super().__init__(reader=reader) version = self.version @@ -130,10 +15,10 @@ def __init__(self, reader): self.m_Width = reader.read_int() self.m_Height = reader.read_int() self.m_CompleteImageSize = reader.read_int() - if version >= (2020,): # 2020.1 and up + if version >= (2020, 1): # 2020.1 and up self.m_MipsStripped = reader.read_int() self.m_TextureFormat = TextureFormat(reader.read_int()) - if version[:2] < (5, 2): # 5.2 down + if version < (5, 2): # 3.4.0 - 5.1.5f1 self.m_MipMap = reader.read_boolean() else: self.m_MipCount = reader.read_int() @@ -142,8 +27,12 @@ def __init__(self, reader): self.m_IsReadable = reader.read_boolean() # 2.6 and up if version >= (2020,): # 2020.1 and up self.m_IsPreProcessed = reader.read_boolean() - if version >= (2019, 3): # 2019.3 and up + if (2019, 3) <= version[:2] < (2022, 2): # 2019.3.0f6 - 2022.2.0a18 self.m_IgnoreMasterTextureLimit = reader.read_boolean() + if version >= (2022, 2, 0): # 2022.2.0f1 and up + self.m_IgnoreMipmapLimit = reader.read_boolean() + reader.align_stream() + self.m_MipmapLimitGroupName = reader.read_aligned_string() if (3,) <= version[:2] <= (5, 4): # 3.0 - 5.4 self.m_ReadAllowed = reader.read_boolean() if version >= (2018, 2): # 2018.2 and up @@ -164,8 +53,7 @@ def __init__(self, reader): reader.align_stream() image_data_size = reader.read_int() - self._image_data = b"" - + self._image_data = b'' if image_data_size != 0: self._image_data = reader.read_bytes(image_data_size) @@ -173,8 +61,9 @@ def __init__(self, reader): if version >= (5, 3): # 5.3 and up # always read the StreamingInfo for resaving self.m_StreamData = StreamingInfo(reader, version) - # don't read the data directly, + # don't read the data right away, # as we don't want the parser break if the file is missing + pass def save(self, writer: EndianBinaryWriter = None): if writer is None: @@ -188,7 +77,7 @@ def save(self, writer: EndianBinaryWriter = None): if version >= (2020,): # 2020.1 and up writer.write_int(self.m_MipsStripped) writer.write_int(self.m_TextureFormat.value) - if version[:2] < (5, 2): # 5.2 down + if version < (5, 2): # 5.2 down writer.write_boolean(self.m_MipMap) else: writer.write_int(self.m_MipCount) @@ -197,8 +86,12 @@ def save(self, writer: EndianBinaryWriter = None): writer.write_boolean(self.m_IsReadable) # 2.6 and up if version >= (2020,): # 2020.1 and up writer.write_boolean(self.m_IsPreProcessed) - if version >= (2019, 3): # 2019.3 and up + if (2019, 3) <= version[:2] < (2022, 2): # 2019.3.0f6 - 2022.2.0a18 writer.write_boolean(self.m_IgnoreMasterTextureLimit) + if version >= (2022, 2, 0): # 2022.2.0f1 and up + writer.write_boolean(self.m_IgnoreMipmapLimit) + writer.align_stream() + writer.write_aligned_string(self.m_MipmapLimitGroupName) 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 @@ -219,22 +112,136 @@ def save(self, writer: EndianBinaryWriter = None): writer.align_stream() if version[:2] < (5, 3): - # version without m_StreamData + # version without m_StreamData support writer.write_int(len(self.image_data)) writer.write_bytes(self.image_data) else: - # decide if m_StreamData is used + # decide if m_StreamData is used, we reset it in image_data on assignment 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) - self.m_StreamData.save(writer, version) - self.set_raw_data(writer.bytes) + self.set_raw_data(writer) + + @property + def name(self): + return self.m_Name + + @property + def image(self): + return Texture2DConverter.get_image_from_texture2d(self) + + @image.setter + def image(self, img): # simple alias + self.set_image(img) + + def mipmapped_dims(self): + return () + + def check_image_valid(self, img: Image.Image): + if not isinstance(img, Image.Image): + return False + bad_width = img.size[0] != self.m_Width + bad_height = img.size[1] != self.m_Height + if bad_width or bad_height: + print_error(f"Image {self.name} must have the same dimensions as original Texture2D " + ( + f"{' mipmaps: ' + str(self.m_MipCount) + '; ' if hasattr(self, 'm_MipCount') else ''}") + ( + f"{img.size[0]} != {self.m_Width}; " if bad_width else "") + ( + f"{img.size[1]} != {self.m_Height}" if bad_height else "")) + return False + return True + + @property + def image_data(self): + if not self._image_data and self.m_StreamData is not None: + if not self.m_StreamData.size: + self._image_data = b'' + else: + self._image_data = get_resource_data( + self.m_StreamData.path, + self.assets_file, + self.m_StreamData.offset, + self.m_StreamData.size, + ) + 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 = "" + @image_data.setter + def image_data(self, data: bytes): + self._image_data = bytes(data) if data else b'' + # img.width * img.height * ( channel count = len(img.getbands()) ) + self.m_CompleteImageSize = len(self._image_data) + # prefer writing to cab if possible, but... + self.reset_streamdata() + + def set_image( + self, + img, + in_cab: bool = False, + mipmap_count: int = 1, + mipmaps: tuple = (), + ) -> bool: + """Sets Texture2D image, set its corresponding m_ attributes to + its dimensions and the desired output texture format + """ + if img is None: + print_error("No image provided") + return False + elif ( + isinstance(img, str) + or isinstance(img, BufferedIOBase) + or isinstance(img, RawIOBase) + or isinstance(img, IOBase) + ): + img = Image.open(img) + + if not self.check_image_valid(img): + return False + + if mipmap_count > 1: + if img.mode == "RGBA" or "transparency" in img.info: + self.m_TextureFormat = TextureFormat.RGBA32 + else: + self.m_TextureFormat = TextureFormat.RGB24 + + # NOTE: no need for the texture meta parameters since we have its attributes + img_data, tex_format = Texture2DConverter.image_to_texture2d(img, self.m_TextureFormat) + if mipmap_count > 1: + width = self.m_Width + height = self.m_Height + re_img = img + for i in range(mipmap_count - 1): + width //= 2 + height //= 2 + if width < 4 or height < 4: + mipmap_count = i + 1 + break + if mipmaps and i < len(mipmaps): + re_img = Image.open(mipmaps[i]) + else: + re_img = re_img.resize((width, height), Image.BILINEAR) + img_data += Texture2DConverter.image_to_texture2d( + re_img, self.m_TextureFormat + )[0] + + if self.version[:2] < (5, 2): # 5.2 down + self.m_MipMap = mipmap_count > 1 + else: + self.m_MipCount = mipmap_count + + self.image_data = img_data + self.m_TextureFormat = tex_format + return True class StreamingInfo: offset: int @@ -257,6 +264,9 @@ def save(self, writer, version): writer.write_int(self.size) writer.write_aligned_string(self.path) + def __repr__(self): + return f"StreamingInfo<{self.path}: {self.size} @{self.offset}>" + class GLTextureSettings: def __init__(self, reader, version): diff --git a/UnityPy/classes/Texture2DArray.py b/UnityPy/classes/Texture2DArray.py new file mode 100644 index 00000000..5ba6f0b2 --- /dev/null +++ b/UnityPy/classes/Texture2DArray.py @@ -0,0 +1,84 @@ +from typing import Optional, List + +from .Texture import Texture +from .Texture2D import StreamingInfo +from ..enums.GraphicsFormat import GraphicsFormat, GRAPHICS_TO_TEXTURE_MAP +from ..export import Texture2DConverter +from ..helpers.ResourceReader import get_resource_data +from PIL import Image + + +class GLTextureSettings: + m_Aniso: int + m_FilterMode: int + m_MipBias: float + m_WrapMode: Optional[int] = None + m_WrapU: Optional[int] = None + m_WrapV: Optional[int] = None + m_WrapW: Optional[int] = None + + def __init__(self, reader): + self.__dict__.update(**self.read_typetree(wrap=True).__dict__) + + +class Texture2DArray(Texture): + image_data: bytes + m_ColorSpace: int + m_DataSize: int + m_Depth: int + m_Format: GraphicsFormat + m_Height: int + m_IsReadable: bool + m_MipCount: int + m_Name: str + m_TextureSettings: GLTextureSettings + m_Width: int + m_DownscaleFallback: Optional[bool] = None + m_ForcedFallbackFormat: Optional[int] = None + m_IgnoreMipmapLimit: Optional[bool] = None + m_IsAlphaChannelOptional: Optional[bool] = None + m_MipmapLimitGroupName: Optional[str] = None + m_MipsStripped: Optional[int] = None + m_StreamData: Optional[StreamingInfo] = None + m_UsageMode: Optional[int] = None + + def __init__(self, reader): + super().__init__(reader=reader) + self.__dict__.update(**reader.read_typetree(wrap=True).__dict__) + self.m_TextureSettings.__class__ = GLTextureSettings + self.m_Format = GraphicsFormat(self.m_Format) + + @property + def image_data(self): + data = getattr(self, "image data", None) + if data is None: + data = get_resource_data( + self.m_StreamData.path, + self.assets_file, + self.m_StreamData.offset, + self.m_StreamData.size, + ) + return data + + @property + def images(self) -> List[Image.Image]: + texture_format = GRAPHICS_TO_TEXTURE_MAP.get(self.m_Format) + if not texture_format: + raise NotImplementedError( + f"GraphicsFormat {self.m_Format} not supported yet" + ) + + # calculate the number of textures in the array + texture_size = self.m_DataSize // self.m_Depth + return [ + Texture2DConverter.parse_image_data( + self.image_data[offset : offset + texture_size], + self.m_Width, + self.m_Height, + texture_format, + self.version, + 0, + None, + ) + for offset in range(0, self.m_DataSize, texture_size) + ] diff --git a/UnityPy/classes/Transform.py b/UnityPy/classes/Transform.py index df8419d2..81232069 100644 --- a/UnityPy/classes/Transform.py +++ b/UnityPy/classes/Transform.py @@ -1,5 +1,6 @@ from .Component import Component from .PPtr import PPtr +from ..streams import EndianBinaryWriter class Transform(Component): @@ -8,7 +9,27 @@ def __init__(self, reader): self.m_LocalRotation = reader.read_quaternion() self.m_LocalPosition = reader.read_vector3() self.m_LocalScale = reader.read_vector3() + if reader.version[:2] >= (2021, 3): # TODO: check if lower + reader.align_stream() - children_count = reader.read_int() - self.m_Children = [PPtr(reader) for _ in range(children_count)] + numChildren = reader.read_int() + self.m_Children = [PPtr(reader) for _ in range(numChildren)] self.m_Father = PPtr(reader) + + def save(self, writer = None, intern_call=False): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + super().save(writer) + + writer.write_quaternion(self.m_LocalRotation) + writer.write_vector3(self.m_LocalPosition) + writer.write_vector3(self.m_LocalScale) + if self.reader.version[:2] >= (2021, 3): # TODO: check if lower + writer.align_stream() + + writer.write_int(len(self.m_Children)) + [self.m_Children[i].save(writer) for i in range(len(self.m_Children))] + self.m_Father.save(writer) + + if not intern_call: + self.set_raw_data(writer) diff --git a/UnityPy/classes/UnityConnectSettings.py b/UnityPy/classes/UnityConnectSettings.py new file mode 100644 index 00000000..ecb1afd7 --- /dev/null +++ b/UnityPy/classes/UnityConnectSettings.py @@ -0,0 +1,108 @@ +from .Object import Object +from ..streams import EndianBinaryWriter +from ..files import ObjectReader + +class CrashReportingSettings(): + def __init__(self, reader: ObjectReader): + self.m_EventUrl = reader.read_aligned_string() + self.m_Enabled = reader.read_bool() + reader.align_stream() + self.m_LogBufferSize = reader.read_u_int() + reader.align_stream() + + def save(self, writer: EndianBinaryWriter): + writer.write_aligned_string(self.m_EventUrl) + writer.write_bool(self.m_Enabled) + writer.align_stream() + writer.write_u_int(self.m_LogBufferSize) + writer.align_stream() + +class UnityPurchasingSettings(): + def __init__(self, reader: ObjectReader): + self.m_Enabled = reader.read_bool() + self.m_TestMode = reader.read_bool() + reader.align_stream() + + def save(self, writer: EndianBinaryWriter): + writer.write_bool(self.m_Enabled) + writer.write_bool(self.m_TestMode) + writer.align_stream() + +class UnityAnalyticsSettings(): + def __init__(self, reader: ObjectReader): + self.m_Enabled = reader.read_bool() + self.m_TestMode = reader.read_bool() + self.m_InitializeOnStartup = reader.read_bool() + self.m_PackageRequiringCoreStatsPresent = reader.read_bool() + reader.align_stream() + + def save(self, writer: EndianBinaryWriter): + writer.write_bool(self.m_Enabled) + writer.write_bool(self.m_TestMode) + writer.write_bool(self.m_InitializeOnStartup) + writer.write_bool(self.m_PackageRequiringCoreStatsPresent) + writer.align_stream() + +class UnityAdsSettings(): + def __init__(self, reader: ObjectReader): + self.m_Enabled = reader.read_bool() + self.m_InitializeOnStartup = reader.read_bool() + self.m_TestMode = reader.read_bool() + reader.align_stream() + self.m_GameId = reader.read_aligned_string() + + def save(self, writer: EndianBinaryWriter): + writer.write_bool(self.m_Enabled) + writer.write_bool(self.m_InitializeOnStartup) + writer.write_bool(self.m_TestMode) + writer.align_stream() + writer.write_aligned_string(self.m_GameId) + +class PerformanceReportingSettings(): + def __init__(self, reader: ObjectReader): + self.m_Enabled = reader.read_bool() + reader.align_stream() + + def save(self, writer: EndianBinaryWriter): + writer.write_bool(self.m_Enabled) + writer.align_stream() + +class UnityConnectSettings(Object): + def __init__(self, reader: ObjectReader): + super().__init__(reader=reader) + version = self.version + self.m_Enabled = reader.read_bool() + self.m_TestMode = reader.read_bool() + reader.align_stream() + self.m_EventOldUrl = reader.read_aligned_string() + self.m_EventUrl = reader.read_aligned_string() + self.m_ConfigUrl = reader.read_aligned_string() + self.m_DashboardUrl = reader.read_aligned_string() + self.m_TestInitMode = reader.read_int() + reader.align_stream() + self.m_CrashReportingSettings = CrashReportingSettings(reader) + self.m_UnityPurchasingSettings = UnityPurchasingSettings(reader) + self.m_UnityAnalyticsSettings = UnityAnalyticsSettings(reader) + self.m_UnityAdsSettings = UnityAdsSettings(reader) + self.m_PerformanceReportingSettings = PerformanceReportingSettings(reader) + + def save(self, writer: EndianBinaryWriter = None): + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + super().save(writer, intern_call=True) + writer.write_bool(self.m_Enabled) + writer.write_bool(self.m_TestMode) + writer.align_stream() + writer.write_aligned_string(self.m_EventOldUrl) + writer.write_aligned_string(self.m_EventUrl) + writer.write_aligned_string(self.m_ConfigUrl) + writer.write_aligned_string(self.m_DashboardUrl) + writer.write_int(self.m_TestInitMode) + writer.align_stream() + self.m_CrashReportingSettings.save(writer) + self.m_UnityPurchasingSettings.save(writer) + self.m_UnityAnalyticsSettings.save(writer) + self.m_UnityAdsSettings.save(writer) + self.m_PerformanceReportingSettings.save(writer) + + self.set_raw_data(writer) diff --git a/UnityPy/classes/UnknownObject.py b/UnityPy/classes/UnknownObject.py new file mode 100644 index 00000000..b6a3caa4 --- /dev/null +++ b/UnityPy/classes/UnknownObject.py @@ -0,0 +1,32 @@ +from typing import Optional + +from ..helpers.TypeTreeNode import TypeTreeNode +from .Object import Object + + +class UnknownObject(Object): + """An object of unknown type that showed up during typetree parsing.""" + + __node__: Optional[TypeTreeNode] + + def __init__(self, __node__: TypeTreeNode = None, **kwargs): + self.__node__ = __node__ + self.__dict__.update(**kwargs) + + def get_type(self): + return self.__node__.m_Type if self.__node__ else None + + def __repr__(self) -> str: + def format_value(v): + vstr = repr(v) + if len(vstr) > 100: + return vstr[:97] + "..." + return vstr + + inner_str = ", ".join( + f"{k}={format_value(v)}" + for k, v in self.__dict__.items() + if k != "__node__" + ) + + return f" {inner_str}>" diff --git a/UnityPy/classes/VideoClip.py b/UnityPy/classes/VideoClip.py index 66ceb03d..8ca4a3f6 100644 --- a/UnityPy/classes/VideoClip.py +++ b/UnityPy/classes/VideoClip.py @@ -1,44 +1,95 @@ +from __future__ import annotations +from functools import partial +from typing import TYPE_CHECKING +from os.path import basename + +if TYPE_CHECKING: + from ..files import ObjectReader + from ..streams import EndianBinaryWriter from .NamedObject import NamedObject from ..helpers.ResourceReader import get_resource_data from .PPtr import PPtr +class StreamedResource: + def __init__(self, reader: "ObjectReader", version): + self.m_Source = reader.read_aligned_string() + self.m_Offset = reader.read_u_long() + self.m_Size = reader.read_u_long() + + def save(self, writer: "EndianBinaryWriter" = None, version=(2017,)): + writer.write_aligned_string(self.m_Source) + writer.write_int(self.m_Offset) + writer.write_u_long(self.m_Offset) + writer.write_u_long(self.m_Size) + + class VideoClip(NamedObject): - def __init__(self, reader): + def __init__(self, reader: "ObjectReader"): + version = self._version = reader.version super().__init__(reader=reader) 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 + if self.version[:2] >= (2017, 2): # 2017.2 and up 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() + self.m_AudioChannelCount = reader.read_array(reader.read_u_short) reader.align_stream() - self.m_AudioSampleRate = reader.read_u_int_array() - self.m_AudioLanguage = reader.read_string_array() - if self.version >= (2020,): # 2020.1 and up - m_VideoShadersSize = reader.read_int() - self.m_VideoShaders = [ - PPtr(reader) for _ in range(m_VideoShadersSize) - ] - - # 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 self.source: - self.m_VideoData = get_resource_data( - self.source, self.assets_file, self.offset, self.size) + self.m_AudioSampleRate = reader.read_array(reader.read_u_int) + #reader.align_stream() + self.m_AudioLanguage = reader.read_array(reader.read_aligned_string) + if self.version[0] >= 2020: # 2020.1 and up + self.m_VideoShaders = reader.read_array(partial(PPtr, reader)) + self.m_ExternalResources = StreamedResource(reader, version=version) + self.m_HasSplitAlpha = reader.read_bool() + if version > (2020, 1, 0, 19): + self.m_sRGB = reader.read_bool() + reader.align_stream() + + def save(self, writer: "EndianBinaryWriter" = None): + version = self._version + super().save(writer) + if writer is None: + writer = EndianBinaryWriter(endian=self.reader.endian) + writer.write_aligned_string(self.m_OriginalPath) + writer.write_u_int(self.m_ProxyWidth) + writer.write_u_int(self.m_ProxyHeight) + writer.write_u_int(self.Width) + writer.write_u_int(self.Height) + if version[:2] >= (2017, 2): + writer.write_u_int(self.m_PixelAspecRatioNum) + writer.write_u_int(self.m_PixelAspecRatioDen) + writer.write_double(self.m_FrameRate) + writer.write_u_long(self.m_FrameCount) + writer.write_int(self.m_Format) + writer.write_array(writer.write_u_short, self.m_AudioChannelCount) + writer.align_stream() + writer.write_array(writer.write_u_int, self.m_AudioSampleRate) + #writer.align_stream() + writer.write_array(writer.write_aligned_string, self.m_AudioLanguage) + if version[0] >= 2020: + (writer.write_u_int(len(self.m_VideoShaders.keys())), [item.save(writer) for item in self.m_VideoShaders]) + self.m_ExternalResources.save(writer, version=version) + writer.write_bool(self.m_HasSplitAlpha) + if version > (2020, 1, 0, 19): + writer.write_bool(self.m_sRGB) + writer.align_stream() + + @property + def name(self): + return basename(self.m_OriginalPath) + + @property + def video(self): + if self.m_ExternalResources.m_Source: + return get_resource_data( + self.m_ExternalResources.m_Source, self.assets_file, self.m_ExternalResources.m_Offset, self.m_ExternalResources.m_Size) else: - self.reader.Position = self.data_offset - self.m_VideoData = self.reader.read_bytes(self.size) + self.reader.Position = self.m_ExternalResources.data_offset + return self.reader.read_bytes(self.m_ExternalResources.m_Size) diff --git a/UnityPy/classes/__init__.py b/UnityPy/classes/__init__.py index 301aac1f..b0c3ae86 100644 --- a/UnityPy/classes/__init__.py +++ b/UnityPy/classes/__init__.py @@ -15,7 +15,7 @@ from .Material import Material from .Mesh import Mesh from .MeshFilter import MeshFilter -from .MeshRenderer import MeshRenderer +#from .MeshRenderer import MeshRenderer from .MonoBehaviour import MonoBehaviour from .MonoScript import MonoScript from .MovieTexture import MovieTexture @@ -34,5 +34,7 @@ from .TextAsset import TextAsset from .Texture import Texture from .Texture2D import Texture2D +from .Texture2DArray import Texture2DArray from .Transform import Transform +from .UnityConnectSettings import UnityConnectSettings from .VideoClip import VideoClip diff --git a/UnityPy/color_logging.py b/UnityPy/color_logging.py new file mode 100644 index 00000000..863773b8 --- /dev/null +++ b/UnityPy/color_logging.py @@ -0,0 +1,107 @@ +import builtins + +DEFAULT_FILE = ".\\unitypy_log.txt" +LOG_FORMAT = "%(asctime)s (%(name)s): %(levelname)s - %(message)s" + +NO_COLORAMA = False +try: + from colorama import Fore, Style, just_fix_windows_console + just_fix_windows_console() +except: + NO_COLORAMA = True + +from . import config + +# NOTE: we use ENABLE_LOGGING_MODULE = 0 to better control +# console output since logging only uses full lines +unitypy_logger = None +if config.ENABLE_LOGGING_MODULE: + import logging + unitypy_logger = logging.getLogger("UnityPy") + unitypy_logger.setLevel(logging.DEBUG if config.DEBUG else logging.INFO) + if config.ENABLE_LOGGING_MODULE == 2: + file_handler = logging.FileHandler(DEFAULT_FILE, 'w', encoding="utf-8") + file_handler.setFormatter(logging.Formatter(LOG_FORMAT)) + for h in unitypy_logger.handlers[:]: + unitypy_logger.removeHandler(h) + unitypy_logger.addHandler(file_handler) + else: + if not NO_COLORAMA: + class CustomFormatter(logging.Formatter): + grey = Style.DIM + yellow = Fore.YELLOW + red = Fore.RED + reset = Style.RESET_ALL + format = LOG_FORMAT + FORMATS = { + logging.DEBUG: grey + format + reset, + logging.INFO: grey + format + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: bold_red + format + reset + } + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno, self.format) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + for h in unitypy_logger.handlers[:]: + unitypy_logger.removeHandler(h) + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(CustomFormatter()) + unitypy_logger.addHandler(stream_handler) + +def print_info(text1, text2='', end='\n'): + """Prints an info text, the first parameter is a dimmed string, the second is a white prefix""" + if config.ENABLE_LOGGING_MODULE: + unitypy_logger.info(str(text2) + str(text1)) + else: + print(str(text2) + str(text1), end=end) + +def print_debug(text1, text2='', end='\n'): + """Prints an info text, the first parameter is a dimmed string, the second is a white prefix""" + if config.ENABLE_LOGGING_MODULE: + unitypy_logger.debug(str(text2) + str(text1)) + else: + if not config.DEBUG: return + if NO_COLORAMA: + print(str(text2) + str(text1), end=end) + else: + print(str(text2) + (Style.DIM + str(text1) + Style.RESET_ALL), end=end) + +def print_error(text1, text2='', end='\n'): + """Prints an error text, the first parameter is a red string, the second is a white prefix""" + if config.ENABLE_LOGGING_MODULE: + unitypy_logger.error(str(text2) + str(text1)) + else: + if NO_COLORAMA: + print(str(text2) + str(text1), end=end) + else: + print(str(text2) + (Fore.RED + str(text1) + Style.RESET_ALL), end=end) + +def print_exception(text1, text2='', end='\n'): + """Prints an error text, the first parameter is a red string, the second is a white prefix""" + if config.ENABLE_LOGGING_MODULE: + unitypy_logger.exception(str(text2) + str(text1)) + else: + if NO_COLORAMA: + print(str(text2) + str(text1), end=end) + else: + print(str(text2) + (Fore.RED + str(text1) + Style.RESET_ALL), end=end) + +def print_warning(text1, text2='', end='\n'): + """Prints a warning text, the first parameter is a yellow string, the second is a white prefix""" + if config.ENABLE_LOGGING_MODULE: + unitypy_logger.warning(str(text2) + str(text1)) + else: + if NO_COLORAMA: + print(str(text2) + str(text1), end=end) + else: + print(str(text2) + (Fore.YELLOW + str(text1) + Style.RESET_ALL), end=end) + +# HACK: This is so we needn't export them in every .py file of the module +builtins.print_info = print_info +builtins.print_debug = print_debug +builtins.print_exception = print_exception +builtins.print_error = print_error +builtins.print_warning = print_warning diff --git a/UnityPy/config.py b/UnityPy/config.py index a22a2962..4e37379c 100644 --- a/UnityPy/config.py +++ b/UnityPy/config.py @@ -1,14 +1,40 @@ -# used when no version is defined by the SerializedFile or its BundleFile -FALLBACK_UNITY_VERSION = "2.5.0f5" -# determines if the typetree structures for the Object types will be parsed -# disabling this will reduce the load time by a lot (half of the time is spend on parsing the typetrees) -# but it will also prevent saving an edited file -SERIALIZED_FILE_PARSE_TYPETREE = True +# Used when no version is defined by the SerializedFile or its BundleFile +FALLBACK_UNITY_VERSION = "2022.1.3f1" + +from os import environ + +def is_true_env(name:str, default:str='True'): + return environ.get(name, default).strip('"').lower() in ('true', '1', 't', 'yes', 'y') + +def get_env_int(name:str, default:str|int=0): + return int(environ.get(name, str(default)).strip('"')) + +DEBUG = is_true_env('DEBUG', 'True') # Toggles debugging branches (useful if memory is exhausted due to errors in typetrees) + +# Determines if the TypeTrees for the Object types will be parsed. +# Disabling this will reduce the load time by a lot (half of the time is spend +# on parsing the typetrees) but it will also prevent saving some edited files, +# unless we provide the TypeTrees by ourselves. +SERIALIZED_FILE_PARSE_TYPETREE = is_true_env('SERIALIZED_FILE_PARSE_TYPETREE') + +BIG_OBJECT_GUARD = int(3e9) # 0 or don't try to unpack bigger objects (bytes) + +ENABLE_TYPETREEHELPER_FALLBACK = is_true_env('ENABLE_TYPETREEHELPER_FALLBACK') # Fallback to TypeTreeHelper parsing in asset bundles + +EXTENDED_SEARCH = is_true_env('EXTENDED_SEARCH', 'True') # Search all cwd subdirectories for resources + +DEBUG_TYPETREES = is_true_env('DEBUG_TYPETREES', 'False') # Toggles TypeTree debugging in ObjectReader and TypeTreeHelper + +ENABLE_BINARY_BLOBS = is_true_env('ENABLE_BINARY_BLOBS', 'False') # Enables custom UnityPyBinaryBlob type in TypeTrees + +ENABLE_LOGGING_MODULE = get_env_int('ENABLE_LOGGING_MODULE', 0) # 0 = console print; 1 = console log; 2 = file log (default: .\unitypy_log.txt) + +# INFO: internal +WARNED_NOTFOUND_ONCE = True # GLOBAL WARNING SUPPRESSION FALLBACK_VERSION_WARNED = False # for FALLBACK_UNITY_VERSION - # GET FUNCTIONS def get_fallback_version(): global FALLBACK_VERSION_WARNED diff --git a/UnityPy/enums/BundleFile.py b/UnityPy/enums/BundleFile.py index fec1cf4a..ebf68bcc 100644 --- a/UnityPy/enums/BundleFile.py +++ b/UnityPy/enums/BundleFile.py @@ -2,25 +2,30 @@ class CompressionFlags(IntFlag): - NONE = 0 + NO = 0 LZMA = 1 - LZ4 = 2 - LZ4HC = 3 - LZHAM = 4 + LZ4 = 0b10 + LZ4HC = 0b11 + LZHAM = 0b100 + META_UNCOMPRESSED = 0b1000000 + META_LZ4 = 0b11000010 class ArchiveFlagsOld(IntFlag): - CompressionTypeMask = 0x3F + CompressionTypeMask = 0b111111 BlocksAndDirectoryInfoCombined = 0x40 BlocksInfoAtTheEnd = 0x80 OldWebPluginCompatibility = 0x100 + UnityCNEncryption = 0 + BlockInfoNeedPaddingAtStart = 0 UsesAssetBundleEncryption = 0x200 class ArchiveFlags(IntFlag): - CompressionTypeMask = 0x3F + CompressionTypeMask = 0b111111 BlocksAndDirectoryInfoCombined = 0x40 BlocksInfoAtTheEnd = 0x80 OldWebPluginCompatibility = 0x100 + UnityCNEncryption = 0x100 BlockInfoNeedPaddingAtStart = 0x200 UsesAssetBundleEncryption = 0x400 diff --git a/UnityPy/enums/ClassIDType.py b/UnityPy/enums/ClassIDType.py index b7c5f924..52bfe90d 100644 --- a/UnityPy/enums/ClassIDType.py +++ b/UnityPy/enums/ClassIDType.py @@ -70,14 +70,14 @@ class ClassIDType(ExtendableEnum): AnimatorController = 91 GUILayer = 92 RuntimeAnimatorController = 93 - ScriptMapper = 94 + ShaderNameRegistry = 94 Animator = 95 TrailRenderer = 96 DelayedCallManager = 98 TextMesh = 102 RenderSettings = 104 Light = 108 - CGProgram = 109 + ShaderInclude = 109 BaseAnimationTrack = 110 Animation = 111 MonoBehaviour = 114 @@ -278,9 +278,9 @@ class ClassIDType(ExtendableEnum): PackedAssets = 1126 VideoClipImporter = 1127 ActivationLogComponent = 2000 - int = 100000 - bool = 100001 - float = 100002 + INT = 100000 + BOOL = 100001 + FLOAT = 100002 MonoObject = 100003 Collision = 100004 Vector3f = 100005 @@ -291,7 +291,7 @@ class ClassIDType(ExtendableEnum): Polygon2D = 100010 void = 100011 TilemapCollider2D = 19719996 - AssetImporterLog = 41386430 + ImportLog = 41386430 VFXRenderer = 73398921 SerializableManagedRefTestClass = 76251197 Grid = 156049354 @@ -307,6 +307,7 @@ class ClassIDType(ExtendableEnum): SubDerived = 367388927 AssetImportInProgressProxy = 369655926 PluginBuildInfo = 382020655 + MemorySettings = 387306366 EditorProjectAccess = 426301858 PrefabImporter = 468431735 TestObjectWithSerializedArray = 478637458 @@ -322,15 +323,20 @@ class ClassIDType(ExtendableEnum): BuiltAssetBundleInfoSet = 668709126 SpriteAtlas = 687078895 RayTracingShaderImporter = 747330370 + PreviewImporter = 815301076 RayTracingShader = 825902497 LightingSettings = 850595691 PlatformModuleSetup = 877146078 VersionControlSettings = 890905787 + CustomCollider2D = 893571522 AimConstraint = 895512359 VFXManager = 937362698 + RoslynAnalyzerConfigAsset = 947337230 + RuleSetFileAsset = 954905827 VisualEffectSubgraph = 994735392 VisualEffectSubgraphOperator = 994735403 VisualEffectSubgraphBlock = 994735404 + Prefab = 1001480554 LocalizationImporter = 1027052791 Derived = 1091556383 PropertyModificationsTargetTestObject = 1111377672 @@ -346,19 +352,28 @@ class ClassIDType(ExtendableEnum): TestObjectWithSpecialLayoutTwo = 1392443030 StreamingManager = 1403656975 LowerResBlitTexture = 1480428607 + VideoBuildInfo = 1521398425 StreamingController = 1542919678 RenderPassAttachment = 1571458007 + ShaderContainer = 1557264870 + RoslynAdditionalFileAsset = 1597193336 TestObjectVectorPairStringBool = 1628831178 + RoslynAdditionalFileImporter = 1642787288 + SceneRoots = 1660057539 + BrokenPrefabAsset = 1731078267 + AndroidAssetPackImporter = 1736697216 GridLayout = 1742807556 AssemblyDefinitionImporter = 1766753193 ParentConstraint = 1773428102 FakeComponent = 1803986026 + RuleSetFileImporter = 1777034230 PositionConstraint = 1818360608 RotationConstraint = 1818360609 ScaleConstraint = 1818360610 Tilemap = 1839735485 PackageManifest = 1896753125 PackageManifestImporter = 1896753126 + RoslynAnalyzerConfigImporter = 1903396204 TerrainLayer = 1953259897 SpriteShapeRenderer = 1971053207 NativeObjectType = 1977754360 @@ -371,3 +386,4 @@ class ClassIDType(ExtendableEnum): VisualEffect = 2083052967 LocalizationAsset = 2083778819 ScriptedImporter = 2089858483 + ShaderIncludeImporter = 2103361453 diff --git a/UnityPy/enums/CommonString.py b/UnityPy/enums/CommonString.py index c91d2ca2..e5f8a204 100644 --- a/UnityPy/enums/CommonString.py +++ b/UnityPy/enums/CommonString.py @@ -1,4 +1,5 @@ -īģŋCommonString = { +īģŋ# format is `offset: name` separated by \0s so each string is +1 to the previous length +CommonString = { 0: "AABB", 5: "AnimationClip", 19: "AnimationCurve", @@ -106,5 +107,8 @@ 1093: "m_CorrespondingSourceObject", 1121: "m_PrefabInstance", 1138: "m_PrefabAsset", - 1152: "FileSize" + 1152: "FileSize", + 1161: "Hash128" } + +CommonStringReversed: dict[str, int] = dict(map(reversed, CommonString.items())) diff --git a/UnityPy/enums/FileType.py b/UnityPy/enums/FileType.py index adfb2c96..cec86886 100644 --- a/UnityPy/enums/FileType.py +++ b/UnityPy/enums/FileType.py @@ -7,3 +7,9 @@ class FileType(IntEnum): WebFile = 2 ResourceFile = 9 ZIP = 10 + +class FileIDType(IntEnum): + Normal = 0 + Cached = 1 + Serialized = 2 + Meta = 3 diff --git a/UnityPy/enums/GraphicsFormat.py b/UnityPy/enums/GraphicsFormat.py new file mode 100644 index 00000000..07bb568c --- /dev/null +++ b/UnityPy/enums/GraphicsFormat.py @@ -0,0 +1,277 @@ +from enum import IntEnum + +from .TextureFormat import TextureFormat + + +class GraphicsFormat(IntEnum): + NONE = 0 + R8_SRGB = 1 + R8G8_SRGB = 2 + R8G8B8_SRGB = 3 + R8G8B8A8_SRGB = 4 + + R8_UNorm = 5 + R8G8_UNorm = 6 + R8G8B8_UNorm = 7 + R8G8B8A8_UNorm = 8 + + R8_SNorm = 9 + R8G8_SNorm = 10 + R8G8B8_SNorm = 11 + R8G8B8A8_SNorm = 12 + + R8_UInt = 13 + R8G8_UInt = 14 + R8G8B8_UInt = 15 + R8G8B8A8_UInt = 16 + + R8_SInt = 17 + R8G8_SInt = 18 + R8G8B8_SInt = 19 + R8G8B8A8_SInt = 20 + + R16_UNorm = 21 + R16G16_UNorm = 22 + R16G16B16_UNorm = 23 + R16G16B16A16_UNorm = 24 + + R16_SNorm = 25 + R16G16_SNorm = 26 + R16G16B16_SNorm = 27 + R16G16B16A16_SNorm = 28 + + R16_UInt = 29 + R16G16_UInt = 30 + R16G16B16_UInt = 31 + R16G16B16A16_UInt = 32 + + R16_SInt = 33 + R16G16_SInt = 34 + R16G16B16_SInt = 35 + R16G16B16A16_SInt = 36 + + R32_UInt = 37 + R32G32_UInt = 38 + R32G32B32_UInt = 39 + R32G32B32A32_UInt = 40 + + R32_SInt = 41 + R32G32_SInt = 42 + R32G32B32_SInt = 43 + R32G32B32A32_SInt = 44 + + R16_SFloat = 45 + R16G16_SFloat = 46 + R16G16B16_SFloat = 47 + R16G16B16A16_SFloat = 48 + R32_SFloat = 49 + R32G32_SFloat = 50 + R32G32B32_SFloat = 51 + R32G32B32A32_SFloat = 52 + + B8G8R8_SRGB = 56 + B8G8R8A8_SRGB = 57 + B8G8R8_UNorm = 58 + B8G8R8A8_UNorm = 59 + B8G8R8_SNorm = 60 + B8G8R8A8_SNorm = 61 + B8G8R8_UInt = 62 + B8G8R8A8_UInt = 63 + B8G8R8_SInt = 64 + B8G8R8A8_SInt = 65 + + R4G4B4A4_UNormPack16 = 66 + B4G4R4A4_UNormPack16 = 67 + R5G6B5_UNormPack16 = 68 + B5G6R5_UNormPack16 = 69 + R5G5B5A1_UNormPack16 = 70 + B5G5R5A1_UNormPack16 = 71 + A1R5G5B5_UNormPack16 = 72 + + E5B9G9R9_UFloatPack32 = 73 + B10G11R11_UFloatPack32 = 74 + + A2B10G10R10_UNormPack32 = 75 + A2B10G10R10_UIntPack32 = 76 + A2B10G10R10_SIntPack32 = 77 + A2R10G10B10_UNormPack32 = 78 + A2R10G10B10_UIntPack32 = 79 + A2R10G10B10_SIntPack32 = 80 + A2R10G10B10_XRSRGBPack32 = 81 + A2R10G10B10_XRUNormPack32 = 82 + R10G10B10_XRSRGBPack32 = 83 + R10G10B10_XRUNormPack32 = 84 + A10R10G10B10_XRSRGBPack32 = 85 + A10R10G10B10_XRUNormPack32 = 86 + + D16_UNorm = 90 + D24_UNorm = 91 + D24_UNorm_S8_UInt = 92 + D32_SFloat = 93 + D32_SFloat_S8_UInt = 94 + S8_UInt = 95 + + RGB_DXT1_SRGB = 96 + RGBA_DXT1_SRGB = 96 + RGB_DXT1_UNorm = 97 + RGBA_DXT1_UNorm = 97 + RGBA_DXT3_SRGB = 98 + RGBA_DXT3_UNorm = 99 + RGBA_DXT5_SRGB = 100 + RGBA_DXT5_UNorm = 101 + R_BC4_UNorm = 102 + R_BC4_SNorm = 103 + RG_BC5_UNorm = 104 + RG_BC5_SNorm = 105 + RGB_BC6H_UFloat = 106 + RGB_BC6H_SFloat = 107 + RGBA_BC7_SRGB = 108 + RGBA_BC7_UNorm = 109 + + RGB_PVRTC_2Bpp_SRGB = 110 + RGB_PVRTC_2Bpp_UNorm = 111 + RGB_PVRTC_4Bpp_SRGB = 112 + RGB_PVRTC_4Bpp_UNorm = 113 + RGBA_PVRTC_2Bpp_SRGB = 114 + RGBA_PVRTC_2Bpp_UNorm = 115 + RGBA_PVRTC_4Bpp_SRGB = 116 + RGBA_PVRTC_4Bpp_UNorm = 117 + + RGB_ETC_UNorm = 118 + RGB_ETC2_SRGB = 119 + RGB_ETC2_UNorm = 120 + RGB_A1_ETC2_SRGB = 121 + RGB_A1_ETC2_UNorm = 122 + RGBA_ETC2_SRGB = 123 + RGBA_ETC2_UNorm = 124 + + R_EAC_UNorm = 125 + R_EAC_SNorm = 126 + RG_EAC_UNorm = 127 + RG_EAC_SNorm = 128 + + RGBA_ASTC4X4_SRGB = 129 + RGBA_ASTC4X4_UNorm = 130 + RGBA_ASTC5X5_SRGB = 131 + RGBA_ASTC5X5_UNorm = 132 + RGBA_ASTC6X6_SRGB = 133 + RGBA_ASTC6X6_UNorm = 134 + RGBA_ASTC8X8_SRGB = 135 + RGBA_ASTC8X8_UNorm = 136 + RGBA_ASTC10X10_SRGB = 137 + RGBA_ASTC10X10_UNorm = 138 + RGBA_ASTC12X12_SRGB = 139 + RGBA_ASTC12X12_UNorm = 140 + + YUV2 = 141 + + RGBA_ASTC4X4_UFloat = 145 + RGBA_ASTC5X5_UFloat = 146 + RGBA_ASTC6X6_UFloat = 147 + RGBA_ASTC8X8_UFloat = 148 + RGBA_ASTC10X10_UFloat = 149 + RGBA_ASTC12X12_UFloat = 150 + + D16_UNorm_S8_UInt = 151 + + +# very experimental & untested +GRAPHICS_TO_TEXTURE_MAP = { + GraphicsFormat.R8_SRGB: TextureFormat.R8, + GraphicsFormat.R8G8_SRGB: TextureFormat.RG16, + GraphicsFormat.R8G8B8_SRGB: TextureFormat.RGB24, + GraphicsFormat.R8G8B8A8_SRGB: TextureFormat.RGBA32, + GraphicsFormat.R8_UNorm: TextureFormat.R8, + GraphicsFormat.R8G8_UNorm: TextureFormat.RG16, + GraphicsFormat.R8G8B8_UNorm: TextureFormat.RGB24, + GraphicsFormat.R8G8B8A8_UNorm: TextureFormat.RGBA32, + GraphicsFormat.R8_SNorm: TextureFormat.R8_SIGNED, + GraphicsFormat.R8G8_SNorm: TextureFormat.RG16_SIGNED, + GraphicsFormat.R8G8B8_SNorm: TextureFormat.RGB24_SIGNED, + GraphicsFormat.R8G8B8A8_SNorm: TextureFormat.RGBA32_SIGNED, + GraphicsFormat.R8_UInt: TextureFormat.R16, + GraphicsFormat.R8G8_UInt: TextureFormat.RG32, + GraphicsFormat.R8G8B8_UInt: TextureFormat.RGB48, + GraphicsFormat.R8G8B8A8_UInt: TextureFormat.RGBA64, + GraphicsFormat.R8_SInt: TextureFormat.R16_SIGNED, + GraphicsFormat.R8G8_SInt: TextureFormat.RG32_SIGNED, + GraphicsFormat.R8G8B8_SInt: TextureFormat.RGB48_SIGNED, + GraphicsFormat.R8G8B8A8_SInt: TextureFormat.RGBA64_SIGNED, + GraphicsFormat.R16_UNorm: TextureFormat.R16, + GraphicsFormat.R16G16_UNorm: TextureFormat.RG32, + GraphicsFormat.R16G16B16_UNorm: TextureFormat.RGB48, + GraphicsFormat.R16G16B16A16_UNorm: TextureFormat.RGBA64, + GraphicsFormat.R16_SNorm: TextureFormat.R16_SIGNED, + GraphicsFormat.R16G16_SNorm: TextureFormat.RG32_SIGNED, + GraphicsFormat.R16G16B16_SNorm: TextureFormat.RGB48_SIGNED, + GraphicsFormat.R16G16B16A16_SNorm: TextureFormat.RGBA64_SIGNED, + GraphicsFormat.R16_UInt: TextureFormat.R16, + GraphicsFormat.R16G16_UInt: TextureFormat.RG32, + GraphicsFormat.R16G16B16_UInt: TextureFormat.RGB48, + GraphicsFormat.R16G16B16A16_UInt: TextureFormat.RGBA64, + GraphicsFormat.R16_SInt: TextureFormat.R16_SIGNED, + GraphicsFormat.R16G16_SInt: TextureFormat.RG32_SIGNED, + GraphicsFormat.R16G16B16_SInt: TextureFormat.RGB48_SIGNED, + GraphicsFormat.R16G16B16A16_SInt: TextureFormat.RGBA64_SIGNED, + GraphicsFormat.B8G8R8_SRGB: TextureFormat.BGR24, + GraphicsFormat.B8G8R8A8_SRGB: TextureFormat.BGRA32, + GraphicsFormat.B8G8R8_UNorm: TextureFormat.BGR24, + GraphicsFormat.B8G8R8A8_UNorm: TextureFormat.BGRA32, + # GraphicsFormat.B8G8R8_SNorm: TextureFormat.BGR24_SIGNED, + # GraphicsFormat.B8G8R8A8_SNorm: TextureFormat.BGRA32_SIGNED, + # GraphicsFormat.B8G8R8_UInt: TextureFormat.BGR48, + GraphicsFormat.RGB_DXT1_SRGB: TextureFormat.DXT1, + GraphicsFormat.RGBA_DXT1_SRGB: TextureFormat.DXT1, + GraphicsFormat.RGB_DXT1_UNorm: TextureFormat.DXT1, + GraphicsFormat.RGBA_DXT1_UNorm: TextureFormat.DXT1, + GraphicsFormat.RGBA_DXT3_SRGB: TextureFormat.DXT3, + GraphicsFormat.RGBA_DXT3_UNorm: TextureFormat.DXT3, + GraphicsFormat.RGBA_DXT5_SRGB: TextureFormat.DXT5, + GraphicsFormat.RGBA_DXT5_UNorm: TextureFormat.DXT5, + GraphicsFormat.R_BC4_UNorm: TextureFormat.BC4, + # GraphicsFormat.R_BC4_SNorm: TextureFormat.BC4_SIGNED, + GraphicsFormat.RG_BC5_UNorm: TextureFormat.BC5, + # GraphicsFormat.RG_BC5_SNorm: TextureFormat.BC5_SIGNED, + GraphicsFormat.RGB_BC6H_UFloat: TextureFormat.BC6H, + # GraphicsFormat.RGB_BC6H_SFloat: TextureFormat.BC6H_SIGNED, + GraphicsFormat.RGBA_BC7_SRGB: TextureFormat.BC7, + GraphicsFormat.RGBA_BC7_UNorm: TextureFormat.BC7, + GraphicsFormat.RGB_PVRTC_2Bpp_SRGB: TextureFormat.PVRTC_RGB2, + GraphicsFormat.RGB_PVRTC_2Bpp_UNorm: TextureFormat.PVRTC_RGB2, + GraphicsFormat.RGB_PVRTC_4Bpp_SRGB: TextureFormat.PVRTC_RGB4, + GraphicsFormat.RGB_PVRTC_4Bpp_UNorm: TextureFormat.PVRTC_RGB4, + GraphicsFormat.RGBA_PVRTC_2Bpp_SRGB: TextureFormat.PVRTC_RGBA2, + GraphicsFormat.RGBA_PVRTC_2Bpp_UNorm: TextureFormat.PVRTC_RGBA2, + GraphicsFormat.RGBA_PVRTC_4Bpp_SRGB: TextureFormat.PVRTC_RGBA4, + GraphicsFormat.RGBA_PVRTC_4Bpp_UNorm: TextureFormat.PVRTC_RGBA4, + GraphicsFormat.RGB_ETC_UNorm: TextureFormat.ETC_RGB4, + GraphicsFormat.RGB_ETC2_SRGB: TextureFormat.ETC2_RGB, + GraphicsFormat.RGB_ETC2_UNorm: TextureFormat.ETC2_RGB, + GraphicsFormat.RGB_A1_ETC2_SRGB: TextureFormat.ETC2_RGBA1, + GraphicsFormat.RGB_A1_ETC2_UNorm: TextureFormat.ETC2_RGBA1, + GraphicsFormat.RGBA_ETC2_SRGB: TextureFormat.ETC2_RGBA8, + GraphicsFormat.RGBA_ETC2_UNorm: TextureFormat.ETC2_RGBA8, + GraphicsFormat.R_EAC_UNorm: TextureFormat.EAC_R, + GraphicsFormat.R_EAC_SNorm: TextureFormat.EAC_R_SIGNED, + GraphicsFormat.RG_EAC_UNorm: TextureFormat.EAC_RG, + GraphicsFormat.RG_EAC_SNorm: TextureFormat.EAC_RG_SIGNED, + GraphicsFormat.RGBA_ASTC4X4_SRGB: TextureFormat.ASTC_RGBA_4x4, + GraphicsFormat.RGBA_ASTC4X4_UNorm: TextureFormat.ASTC_RGBA_4x4, + GraphicsFormat.RGBA_ASTC5X5_SRGB: TextureFormat.ASTC_RGBA_5x5, + GraphicsFormat.RGBA_ASTC5X5_UNorm: TextureFormat.ASTC_RGBA_5x5, + GraphicsFormat.RGBA_ASTC6X6_SRGB: TextureFormat.ASTC_RGBA_6x6, + GraphicsFormat.RGBA_ASTC6X6_UNorm: TextureFormat.ASTC_RGBA_6x6, + GraphicsFormat.RGBA_ASTC8X8_SRGB: TextureFormat.ASTC_RGBA_8x8, + GraphicsFormat.RGBA_ASTC8X8_UNorm: TextureFormat.ASTC_RGBA_8x8, + GraphicsFormat.RGBA_ASTC10X10_SRGB: TextureFormat.ASTC_RGBA_10x10, + GraphicsFormat.RGBA_ASTC10X10_UNorm: TextureFormat.ASTC_RGBA_10x10, + GraphicsFormat.RGBA_ASTC12X12_SRGB: TextureFormat.ASTC_RGBA_12x12, + GraphicsFormat.RGBA_ASTC12X12_UNorm: TextureFormat.ASTC_RGBA_12x12, + GraphicsFormat.YUV2: TextureFormat.YUY2, + # GraphicsFormat.RGBA_ASTC4X4_UFloat: TextureFormat.ASTC_RGBA_4x4, + # GraphicsFormat.RGBA_ASTC5X5_UFloat: TextureFormat.ASTC_RGBA_5x5, + # GraphicsFormat.RGBA_ASTC6X6_UFloat: TextureFormat.ASTC_RGBA_6x6, + # GraphicsFormat.RGBA_ASTC8X8_UFloat: TextureFormat.ASTC_RGBA_8x8, + # GraphicsFormat.RGBA_ASTC10X10_UFloat: TextureFormat.ASTC_RGBA_10x10, + # GraphicsFormat.RGBA_ASTC12X12_UFloat: TextureFormat.ASTC_RGBA_12x12, +} diff --git a/UnityPy/enums/MeshTopology.py b/UnityPy/enums/MeshTopology.py new file mode 100644 index 00000000..0baaedd8 --- /dev/null +++ b/UnityPy/enums/MeshTopology.py @@ -0,0 +1,10 @@ +from enum import IntEnum + + +class MeshTopology(IntEnum): + Triangles = 0 + TriangleStrip = 1 # deprecated + Quads = 2 + Lines = 3 + LineStrip = 4 + Points = 5 diff --git a/UnityPy/enums/ShaderGpuProgramType.py b/UnityPy/enums/ShaderGpuProgramType.py index 170bf307..2a42b7e6 100644 --- a/UnityPy/enums/ShaderGpuProgramType.py +++ b/UnityPy/enums/ShaderGpuProgramType.py @@ -2,6 +2,7 @@ class ShaderGpuProgramType(IntEnum): + """Enum of GPU program types.""" kShaderGpuProgramUnknown = 0 kShaderGpuProgramGLLegacy = 1 kShaderGpuProgramGLES31AEP = 2 diff --git a/UnityPy/enums/TextureFormat.py b/UnityPy/enums/TextureFormat.py index 1e3085ae..d34649b1 100644 --- a/UnityPy/enums/TextureFormat.py +++ b/UnityPy/enums/TextureFormat.py @@ -2,14 +2,18 @@ class TextureFormat(IntEnum): + Empty = 0 Alpha8 = 1 ARGB4444 = 2 RGB24 = 3 RGBA32 = 4 ARGB32 = 5 + ARGBFloat = 6 RGB565 = 7 + BGR24 = 8 R16 = 9 DXT1 = 10 + DXT3 = 11 DXT5 = 12 RGBA4444 = 13 BGRA32 = 14 @@ -21,10 +25,11 @@ class TextureFormat(IntEnum): RGBAFloat = 20 YUY2 = 21 RGB9e5Float = 22 - BC4 = 26 - BC5 = 27 + RGBFloat = 23 BC6H = 24 BC7 = 25 + BC4 = 26 + BC5 = 27 DXT1Crunched = 28 DXT5Crunched = 29 PVRTC_RGB2 = 30 @@ -65,3 +70,14 @@ class TextureFormat(IntEnum): ASTC_HDR_8x8 = 69 ASTC_HDR_10x10 = 70 ASTC_HDR_12x12 = 71 + RG32 = 72 + RGB48 = 73 + RGBA64 = 74 + R8_SIGNED = 75 + RG16_SIGNED = 76 + RGB24_SIGNED = 77 + RGBA32_SIGNED = 78 + R16_SIGNED = 79 + RG32_SIGNED = 80 + RGB48_SIGNED = 81 + RGBA64_SIGNED = 82 diff --git a/UnityPy/enums/VertexFormat.py b/UnityPy/enums/VertexFormat.py new file mode 100644 index 00000000..88e1295c --- /dev/null +++ b/UnityPy/enums/VertexFormat.py @@ -0,0 +1,80 @@ +from enum import IntEnum + + +class VertexChannelFormat(IntEnum): + kChannelFormatFloat = 0 + kChannelFormatFloat16 = 1 + kChannelFormatColor = 2 + kChannelFormatByte = 3 + kChannelFormatUInt32 = 4 + + +class VertexFormat2017(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 VertexFormat(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 + + +VERTEX_CHANNEL_FORMAT_STRUCT_TYPE_MAP = { + VertexChannelFormat.kChannelFormatFloat: "f", + VertexChannelFormat.kChannelFormatFloat16: "e", + VertexChannelFormat.kChannelFormatColor: "B", + VertexChannelFormat.kChannelFormatByte: "B", + VertexChannelFormat.kChannelFormatUInt32: "I", +} + +VERTEX_FORMAT_2017_STRUCT_TYPE_MAP = { + VertexFormat2017.kVertexFormatFloat: "f", + VertexFormat2017.kVertexFormatFloat16: "h", + VertexFormat2017.kVertexFormatColor: "B", + VertexFormat2017.kVertexFormatUNorm8: "B", + VertexFormat2017.kVertexFormatSNorm8: "b", + VertexFormat2017.kVertexFormatUNorm16: "H", + VertexFormat2017.kVertexFormatSNorm16: "h", + VertexFormat2017.kVertexFormatUInt8: "B", + VertexFormat2017.kVertexFormatSInt8: "b", + VertexFormat2017.kVertexFormatUInt16: "H", + VertexFormat2017.kVertexFormatSInt16: "h", + VertexFormat2017.kVertexFormatUInt32: "I", + VertexFormat2017.kVertexFormatSInt32: "i", +} + +VERTEX_FORMAT_STRUCT_TYPE_MAP = { + VertexFormat.kVertexFormatFloat: "f", + VertexFormat.kVertexFormatFloat16: "h", + VertexFormat.kVertexFormatUNorm8: "B", + VertexFormat.kVertexFormatSNorm8: "b", + VertexFormat.kVertexFormatUNorm16: "H", + VertexFormat.kVertexFormatSNorm16: "h", + VertexFormat.kVertexFormatUInt8: "B", + VertexFormat.kVertexFormatSInt8: "b", + VertexFormat.kVertexFormatUInt16: "H", + VertexFormat.kVertexFormatSInt16: "h", + VertexFormat.kVertexFormatUInt32: "I", + VertexFormat.kVertexFormatSInt32: "i", +} diff --git a/UnityPy/enums/__init__.py b/UnityPy/enums/__init__.py index 85706721..5187a736 100644 --- a/UnityPy/enums/__init__.py +++ b/UnityPy/enums/__init__.py @@ -2,11 +2,11 @@ from .BuildTarget import BuildTarget from .BundleFile import ArchiveFlags, ArchiveFlagsOld, CompressionFlags from .ClassIDType import ClassIDType -from .FileType import FileType +from .FileType import FileType, FileIDType from .TextureFormat import TextureFormat from .SpriteMeshType import SpriteMeshType from .GfxPrimitiveType import GfxPrimitiveType -from .CommonString import CommonString +from .CommonString import CommonString, CommonStringReversed from .ShaderCompilerPlatform import ShaderCompilerPlatform from .ShaderGpuProgramType import ShaderGpuProgramType from .SerializedPropertyType import SerializedPropertyType @@ -14,3 +14,4 @@ from .SpritePackingRotation import SpritePackingRotation from .TextureDimension import TextureDimension from .PassType import PassType +from .GraphicsFormat import GraphicsFormat diff --git a/UnityPy/environment.py b/UnityPy/environment.py index bfec2fbc..56dbbe24 100644 --- a/UnityPy/environment.py +++ b/UnityPy/environment.py @@ -1,21 +1,32 @@ import io import os -import ntpath import re +import weakref +#import ntpath from typing import List, Callable, Dict, Union +from collections import Counter +from collections.abc import Callable from zipfile import ZipFile - -from fsspec import AbstractFileSystem -from fsspec.implementations.local import LocalFileSystem - - +from . import files +from zipfile import ZipFile +#from fsspec import AbstractFileSystem +#from fsspec.implementations.local import LocalFileSystem from .files import File, ObjectReader, SerializedFile from .enums import FileType from .helpers import ImportHelper -from .streams import EndianBinaryReader +from .helpers.ResourceReader import search_resource_file +from .streams import EndianBinaryReader, EndianBinaryWriter +from . import config + + +RE_SPLIT = re.compile(r"(.*?([^\/\\]+?))\.split\d+") +RE_ARCHIVE = re.compile(r"archive:\/([^\/]+)\/.+") -reSplit = re.compile(r"(.*?([^\/\\]+?))\.split\d+") +IGNORE_DIR_COUNT = 0 +DEFAULT_TYPES = ['MonoBehaviour', 'Texture2D', 'TextAsset', 'PlayerSettings'] +def default_progress(skip_progress): + return skip_progress class Environment: files: dict @@ -23,28 +34,42 @@ class Environment: path: str local_files: List[str] local_files_simple: List[str] + GLOBAL_FILE_MAP = {} - def __init__(self, *args, fs: AbstractFileSystem = None): + def print_env_map(self): + import json + print(json.dumps(self.GLOBAL_FILE_MAP, ensure_ascii=True, indent=2)) + + def __init__(self, *args, **kwargs): self.files = {} self.cabs = {} self.path = None - self.fs = fs or LocalFileSystem() + self._cwd = os.getcwd() + self.out_path = os.path.join(self._cwd, "output") + self.ignore_dir_lvls = IGNORE_DIR_COUNT + self.progress_function = default_progress + #self.fs = kwargs.get("fs", None) or LocalFileSystem() self.local_files = [] self.local_files_simple = [] + self.ignore_dependencies = kwargs.get("ignore_dependencies", False) + self.extended_search = kwargs.get("extended_search", False) + globalmap = kwargs.get("globalmap", None) + if globalmap: + self.GLOBAL_FILE_MAP.update(globalmap) if args: for arg in args: if isinstance(arg, str): - if self.fs.isfile(arg): - if ntpath.splitext(arg)[-1] in [".apk", ".zip"]: + if os.path.isfile(arg): + if os.path.splitext(arg)[-1] in [".apk", ".zip"]: self.load_zip_file(arg) else: - self.path = ntpath.dirname(arg) - if reSplit.match(arg): + self.path = os.path.dirname(arg) + if RE_SPLIT.match(arg): self.load_files([arg]) else: self.load_file(arg) - elif self.fs.isdir(arg): + elif os.path.isdir(arg): self.path = arg self.load_folder(arg) else: @@ -54,9 +79,29 @@ def __init__(self, *args, fs: AbstractFileSystem = None): if len(self.files) == 1: self.file = list(self.files.values())[0] - if self.path == "": + if not self.path: self.path = os.getcwd() + @staticmethod + def prepare_global_map(assets, progress_fn=lambda r: r, map_name="globalmap.json", encoding="utf-8-sig"): + import json + print_info("Preparing file map...") + GLOBAL_MAP = {} + if not os.path.isfile("globalmap.json"): + for file_name in progress_fn(assets): + am = Environment(globalmap=GLOBAL_MAP) + asset = am.load_file(file_name, name=file_name, dry_run=True) + GLOBAL_MAP.update(am.GLOBAL_FILE_MAP) + asset.close() + am.close() + with open(map_name, "w", encoding=encoding) as p: + json.dump(GLOBAL_MAP, p) + else: + with open(map_name, "r", encoding=encoding) as p: + GLOBAL_MAP = json.load(p) + return GLOBAL_MAP + + def load_files(self, files: List[str]): """Loads all files (list) into the Environment and merges .split files for common usage.""" self.load_assets(files, lambda x: open(x, "rb")) @@ -65,8 +110,8 @@ def load_folder(self, path: str): """Loads all files in the given path and its subdirs into the Environment.""" self.load_files( [ - self.fs.sep.join([root, f]) - for root, dirs, files in self.fs.walk(path) + os.path.join(root, f) + for root, _, files in os.walk(path) for f in files ] ) @@ -75,68 +120,108 @@ def load(self, files: list): """Loads all files into the Environment.""" self.files.update( { - ntpath.basename(f): self.load_file(self.fs.open(f, "rb"), self, f) + os.path.basename(f): self.load_file(open(f, "rb"), self, f) for f in files - if self.fs.exists(f) + if os.path.exists(f) } ) def load_file( self, - file: Union[io.IOBase, str], + file: Union[io.IOBase, str, EndianBinaryReader], parent: Union["Environment", File] = None, name: str = None, is_dependency: bool = False, + **kwargs ): + if not file: + return None + if not parent: parent = self if isinstance(file, str): - split_match = reSplit.match(file) + split_match = RE_SPLIT.match(file) + archive_match = RE_ARCHIVE.match(file) if split_match: - basepath, basename = split_match.groups() + basepath, _ = split_match.groups() file = [] for i in range(0, 999): item = f"{basepath}.split{i}" - if self.fs.exists(item): - with self.fs.open(item, "rb") as f: + if item in files and os.path.exists(item): + with open(item, "rb") as f: file.append(f.read()) elif i: break name = basepath file = b"".join(file) + elif archive_match or file.startswith('CAB-'): + if archive_match: + file = archive_match.group(1) + result = self.get_cab(file) + if result: + return result + efile = self.GLOBAL_FILE_MAP.get(simplify_name(file), None) + if not efile or not os.path.isfile(efile): + return None + else: + if self.ignore_dependencies: + return None + typ, reader = ImportHelper.check_file_type(efile) + if typ == FileType.BundleFile: + f = ImportHelper.parse_file( + reader, self, name=file, typ=typ, + is_dependency=True, + **kwargs + ) + if f and f.files and isinstance(f.files[file], (SerializedFile, EndianBinaryReader)): + self.register_cab(file, f.files[file]) + return self.get_cab(file) else: name = file - file = self.fs.open(file, "rb") + if file and not os.path.isfile(file): + # should have fallback, because why do it manually... + if config.EXTENDED_SEARCH or self.extended_search: + file = search_resource_file(self._cwd, file) + else: + return None + if file and os.path.isfile(file): + file = open(file, "rb") + else: + #print_debug(name, "not found anywhere") + return None typ, reader = ImportHelper.check_file_type(file) - - stream_name = ( - name - if name - else getattr( - file, - "name", - str(file.__hash__()) if hasattr(file, "__hash__") else "", - ) - ) - + if name: + stream_name = name + else: + stream_name = getattr(file, "name", None) + if not stream_name: + stream_name = str(abs(file.__hash__())) if hasattr(file, "__hash__") else "" + f = None if typ == FileType.ZIP: f = self.load_zip_file(file) else: + if self.ignore_dependencies and is_dependency: + return None f = ImportHelper.parse_file( - reader, self, name=stream_name, typ=typ, is_dependency=is_dependency - ) - - if isinstance(f, (SerializedFile, EndianBinaryReader)): - self.register_cab(stream_name, f) - - self.files[stream_name] = f - + reader, self, name=stream_name, typ=typ, + is_dependency=is_dependency, **kwargs + ) + if f: + if isinstance(f, (SerializedFile, EndianBinaryReader)): + self.register_cab(stream_name, f) + self.files[stream_name] = f + if hasattr(f, "blocks") and hasattr(f.blocks, "m_DirectoryInfo"): + for fmap in f.blocks.m_DirectoryInfo: + fmap = simplify_name(fmap.path) + if not fmap in self.GLOBAL_FILE_MAP: + self.GLOBAL_FILE_MAP[fmap] = stream_name + return f def load_zip_file(self, value): buffer = None - if isinstance(value, str) and self.fs.exists(value): + if isinstance(value, str) and os.path.exists(value): buffer = open(value, "rb") elif isinstance(value, (bytes, bytearray)): buffer = io.BytesIO(value) @@ -147,22 +232,91 @@ def load_zip_file(self, value): self.load_assets(z.namelist(), lambda x: z.open(x, "r")) z.close() - def save(self, pack="none", out_path="output"): + def close(self): + self.unregister_all_cabs() + for f in list(self.files): + self.files[f].close() + self.files = {} + + def save(self, pack: str = "none", writer_generator: Callable = None, out_path=None): """Saves all changed assets. - Mark assets as changed using `.mark_changed()`. - pack = "none" (default) or "lz4" + Mark assets as changed using `.mark_changed()` if they aren't auto-marked. + pack = "none" (default), "lz4" or "original" """ - for fname, fitem in self.files.items(): - if getattr(fitem, "is_changed", False): - with open( - self.fs.sep.join([out_path, ntpath.basename(f)]), "wb" - ) as out: - out.write(fitem.save(packer=pack)) + for f in self.files: + opath = self.out_path + if out_path: + opath = out_path + if getattr(self.files[f], "is_changed", False):# and not getattr(self.files[f], "is_dependency", False): + fn = os.path.join(opath, os.path.basename(f)) + if writer_generator: + self.files[f].save(packer=pack, writer=writer_generator(fn)) + else: + os.makedirs(os.path.dirname(fn), exist_ok=True) + with open(fn, 'w+b') as wf: + self.files[f].save(packer=pack, writer=EndianBinaryWriter(wf)) + self.files[f].is_changed = False + + 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 = len(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_cont > 1: + all_cobjs = self.progress_function(cobjs) + if is_bundle: + postfix = asset_name + else: + all_cobjs = cobjs + + for asset_path, obj in all_cobjs: + local_path = 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 + + extracted = [] + if num_objs > 1: + all_objs = self.progress_function(objs) + if is_bundle: + 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()) + + 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:]) + else: + local_path = '' + + for obj in all_objs: + if obj.path_id not in extracted and obj: + extracted.extend(obj_modify(obj, asset_name, local_path=local_path, **kwargs)) + @property def objects(self) -> List[ObjectReader]: """Returns a list of all objects in the Environment.""" - def search(item): ret = [] if not isinstance(item, Environment) and getattr(item, "objects", None): @@ -196,7 +350,6 @@ 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 getattr(f, "is_dependency", False): @@ -206,7 +359,6 @@ def gen_all_asset_files(file, ret=[]): else: gen_all_asset_files(f, ret) return ret - return gen_all_asset_files(self) def get(self, key: str, default=None): @@ -225,6 +377,26 @@ def register_cab(self, name: str, item: File) -> None: """ self.cabs[simplify_name(name)] = item + def unregister_all_cabs(self): + for cab in list(self.cabs.keys()): + self.unregister_cab(cab) + self.cabs = {} + + def unregister_cab(self, name: str): + """ + Removes a cab from internal listing. + + Parameters + ---------- + name : str + The name of the cab file to unregister. + """ + cab = self.cabs.get(simplify_name(name), None) + if cab: + cab.close() + del self.cabs[simplify_name(name)] + + def get_cab(self, name: str) -> File: """ Returns the cab file with the given name. @@ -237,7 +409,7 @@ def get_cab(self, name: str) -> File: Returns ------- File - The cab file. + The parsed cab file. """ return self.cabs.get(simplify_name(name), None) @@ -255,9 +427,9 @@ def load_assets(self, assets: List[str], open_f: Callable[[str], io.IOBase]): """ split_files = [] for path in assets: - splitMatch = reSplit.match(path) + splitMatch = RE_SPLIT.match(path) if splitMatch: - basepath, basename = splitMatch.groups() + basepath, _ = splitMatch.groups() if basepath in split_files: continue @@ -274,7 +446,7 @@ def load_assets(self, assets: List[str], open_f: Callable[[str], io.IOBase]): data = b"".join(data) path = basepath else: - data = open_f(path).read() + data = open_f(path) self.load_file(data, name=path) def find_file(self, name: str, is_dependency: bool = True) -> Union[File, None]: @@ -310,8 +482,9 @@ def find_file(self, name: str, is_dependency: bool = True) -> Union[File, None]: else: raise FileNotFoundError(f"File {name} not found in {self.path}") - f = self.load_file(fp, name=name, is_dependency=is_dependency) - return f + if self.ignore_dependencies and is_dependency: + return None + return self.load_file(fp, name=name, is_dependency=is_dependency) def simplify_name(name: str) -> str: @@ -320,4 +493,4 @@ def simplify_name(name: str) -> str: - removing the path - converting to lowercase """ - return ntpath.basename(name).lower() + return os.path.basename(name).lower() diff --git a/UnityPy/exceptions.py b/UnityPy/exceptions.py index 1eda4850..85c0d0ba 100644 --- a/UnityPy/exceptions.py +++ b/UnityPy/exceptions.py @@ -1,5 +1,23 @@ +from . import config + +MAX_SANE = 0xFFFFF + class TypeTreeError(Exception): - def __init__(self, message, nodes): - # Call the base class constructor with the parameters it needs + def __init__(self, message, nodes): super().__init__(message) - self.nodes = nodes \ No newline at end of file + self.nodes = nodes + +def sanity_check(value_name: str, value: int, max_value: int=MAX_SANE) -> None: + if config.DEBUG and (value > max_value): + raise ValueIsTooBig(f"{value_name} length", value) + +class ValueIsTooBig(Exception): + def __init__(self, variable_name, value): + sep_value = f"{value:,}"#.replace(',', ' ') + super().__init__(f"{variable_name} value = {sep_value} is unrealistically big") + self.value = value + +class ReadingPastObject(Exception): + def __init__(self, obj): + super().__init__(f"read data past {obj} length") + self.value = obj diff --git a/UnityPy/export/AudioClipConverter.py b/UnityPy/export/AudioClipConverter.py index 16d2a02d..70c627b6 100644 --- a/UnityPy/export/AudioClipConverter.py +++ b/UnityPy/export/AudioClipConverter.py @@ -1,116 +1,138 @@ +from __future__ import annotations + import ctypes import os import platform +from typing import TYPE_CHECKING, Dict, Union + from UnityPy.streams import EndianBinaryWriter -# pyfmodex loads the dll/so/dylib on import -# so we have to adjust the environment vars -# before importing it +from ..helpers.ResourceReader import get_resource_data + +try: + import numpy as np +except ImportError: + np = None + import struct + +if TYPE_CHECKING: + from ..classes import AudioClip +# pyfmodex loads the dll/so/dylib on import so we have to adjust +# the environment vars before importing it # This is done in import_pyfmodex() # which will replace the global pyfmodex var pyfmodex = None +NO_MODEX = False + + +def get_fmod_path( + system: Union["Windows", "Linux", "Darwin"], arch: ["x64", "x86", "arm", "arm64"] +) -> str: + if system == "Darwin": + # universal dylib + return "lib/FMOD/Darwin/libfmod.dylib" + + if system == "Windows": + return f"lib/FMOD/Windows/{arch}/fmod.dll" + + if system == "Linux": + if arch == "x64": + arch = "x86_64" + return f"lib/FMOD/Linux/{arch}/libfmod.so" + + raise NotImplementedError(f"Unsupported system: {system}") def import_pyfmodex(): - global pyfmodex - if pyfmodex is not None: + global pyfmodex, NO_MODEX + if pyfmodex is not None or NO_MODEX: 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] + machine = platform.machine() - 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 - pyfmodex = 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: - pyfmodex = False - raise NotImplementedError( - "Couldn't find a correct FMOD library for your system ({system} - {arch})." - ) + if "arm" in machine: + arch = "arm" + elif "aarch64" in machine: + if system == "Linux": + arch = "arm64" + else: + arch = "arm" + elif arch == "32bit": + arch = "x86" + elif arch == "64bit": + arch = "x64" + + fmod_rel_path = get_fmod_path(system, arch) + fmod_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.realpath(__file__))), fmod_rel_path + ) + os.environ["PYFMODEX_DLL_PATH"] = fmod_path # build path and load library - LIB_PATH = os.path.join(ROOT, "lib", "FMOD", system, arch) - # prepare the environment for pyfmodex - if system == "Windows": - # register fmod.dll, so that windll.fmod in pyfmodex can find it - ctypes.WinDLL(os.path.join(LIB_PATH, "fmod.dll")) - import pyfmodex - else: - # It's a bit more complicated on Linux and Mac - # as CDLL doesn't cache the loaded libraries. - # So our only option is to hook into the ctypes loader - # and add the path to the library there. - CDLL = ctypes.CDLL - - def cdll_hook(name, *args, **kwargs): - if name.startswith("libfmod"): - name = os.path.join(LIB_PATH, name) - return CDLL(name, *args, **kwargs) - - ctypes.CDLL = cdll_hook + if system != "Windows": + # hotfix ctypes for pyfmodex for non windows systems + ctypes.windll = None + + try: import pyfmodex + except: + NO_MODEX = True - ctypes.CDLL = CDLL -def extract_audioclip_samples(audio) -> dict: +def extract_audioclip_samples( + audio: AudioClip, convert_pcm_float: bool = True +) -> Dict[str, bytes]: """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 {} + if audio.m_AudioData: + audio_data = audio.m_AudioData + else: + resource = audio.m_Resource + audio_data = get_resource_data( + resource.m_Source, + audio.object_reader.assets_file, + resource.m_Offset, + resource.m_Size, + ) - magic = memoryview(audio.m_AudioData)[:4] - if magic == b"OggS": - return {"%s.ogg" % audio.name: audio.m_AudioData} - elif magic == b"RIFF": - return {"%s.wav" % audio.name: audio.m_AudioData} - return dump_samples(audio) + magic = memoryview(audio_data)[:8] + if magic[:4] == b"OggS": + return {f"{audio.m_Name}.ogg": audio_data} + elif magic[:4] == b"RIFF": + return {f"{audio.m_Name}.wav": audio_data} + elif magic[4:8] == b"ftyp": + return {f"{audio.m_Name}.m4a": audio_data} + return dump_samples(audio, audio_data, convert_pcm_float) -def dump_samples(clip): +def dump_samples( + clip: AudioClip, audio_data: bytes, convert_pcm_float: bool = True +) -> Dict[str, bytes]: if pyfmodex is None: import_pyfmodex() if not pyfmodex: return {} + # init system system = pyfmodex.System() system.init(clip.m_Channels, pyfmodex.flags.INIT_FLAGS.NORMAL, None) sound = system.create_sound( - bytes(clip.m_AudioData), + bytes(audio_data), pyfmodex.flags.MODE.OPENMEMORY, - exinfo=pyfmodex.system.CREATESOUNDEXINFO( - length=clip.m_Size, + exinfo=pyfmodex.structure_declarations.CREATESOUNDEXINFO( + length=len(audio_data), numchannels=clip.m_Channels, defaultfrequency=clip.m_Frequency, ), @@ -120,11 +142,11 @@ def dump_samples(clip): samples = {} for i in range(sound.num_subsounds): if i > 0: - filename = "%s-%i.wav" % (clip.name, i) + filename = "%s-%i.wav" % (clip.m_Name, i) else: - filename = "%s.wav" % clip.name + filename = "%s.wav" % clip.m_Name subsound = sound.get_subsound(i) - samples[filename] = subsound_to_wav(subsound) + samples[filename] = subsound_to_wav(subsound, convert_pcm_float) subsound.release() sound.release() @@ -132,35 +154,67 @@ def dump_samples(clip): return samples -def subsound_to_wav(subsound): +def subsound_to_wav(subsound, convert_pcm_float: bool = True) -> bytes: # get sound settings - length = subsound.get_length(0x00000004) # TIMEUNIT.PCMBYTES + sound_format = subsound.format.format + sound_data_length = subsound.get_length(pyfmodex.enums.TIMEUNIT.PCMBYTES) channels = subsound.format.channels bits = subsound.format.bits sample_rate = int(subsound.default_frequency) - # write to buffer - w = EndianBinaryWriter(endian="<") + if sound_format in [ + pyfmodex.enums.SOUND_FORMAT.PCM8, + pyfmodex.enums.SOUND_FORMAT.PCM16, + pyfmodex.enums.SOUND_FORMAT.PCM24, + pyfmodex.enums.SOUND_FORMAT.PCM32, + ]: + audio_format = 1 + wav_data_length = sound_data_length + convert_pcm_float = False + elif sound_format == pyfmodex.enums.SOUND_FORMAT.PCMFLOAT: + if convert_pcm_float: + audio_format = 1 + bits = 16 + wav_data_length = sound_data_length // 2 + else: + audio_format = 3 + wav_data_length = sound_data_length + else: + raise NotImplementedError("Sound format " + sound_format + " is not supported.") + + writer = EndianBinaryWriter(endian="<") # riff chucnk - w.write(b"RIFF") - w.write_int(length + 36) # sizeof(FmtChunk) + sizeof(RiffChunk) + length - w.write(b"WAVE") + writer.write(b"RIFF") + # sizeof(FmtChunk) + sizeof(RiffChunk) + length + writer.write_int(wav_data_length + 36) + writer.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 = ctypes.string_at(ptr, length.value) - w.write(ptr_data) + writer.write(b"fmt ") + writer.write_int(16) # sizeof(FmtChunk) - sizeof(RiffChunk) + writer.write_short(audio_format) + writer.write_short(channels) + writer.write_int(sample_rate) + writer.write_int(sample_rate * channels * bits // 8) + writer.write_short(channels * bits // 8) + writer.write_short(bits) + + # data chunk - sub chunk 2 + writer.write(b"data") # sub chunk 2 id + writer.write_int(wav_data_length) # sub chunk 2 size + # sub chunk 2 data + lock = subsound.lock(0, sound_data_length) + for ptr, sound_data_length in lock: + ptr_data = ctypes.string_at(ptr, sound_data_length.value) + if convert_pcm_float: + if np is not None: + ptr_data = np.frombuffer(ptr_data, dtype=np.float32) + ptr_data = (ptr_data * 2**15).astype(np.int16).tobytes() + else: + ptr_data = struct.unpack("<%df" % (len(ptr_data) // 4), ptr_data) + ptr_data = struct.pack( + "<%dh" % len(ptr_data), *[int(f * 2**15) for f in ptr_data] + ) + writer.write(ptr_data) subsound.unlock(*lock) - return w.bytes + + return writer.save() diff --git a/UnityPy/export/MeshExporter.py b/UnityPy/export/MeshExporter.py index db68c8a9..34723b28 100644 --- a/UnityPy/export/MeshExporter.py +++ b/UnityPy/export/MeshExporter.py @@ -1,4 +1,4 @@ -from UnityPy.classes import Mesh +from ..classes import Mesh def export_mesh(m_Mesh: Mesh, format="obj") -> str: diff --git a/UnityPy/export/MeshRendererExporter.py b/UnityPy/export/MeshRendererExporter.py index 233b5878..3cd9b358 100644 --- a/UnityPy/export/MeshRendererExporter.py +++ b/UnityPy/export/MeshRendererExporter.py @@ -18,7 +18,7 @@ def get_mesh(meshR: Renderer): def export_mesh_renderer(obj: Renderer, export_dir: str) -> None: env = mesh.assets_file.enviroment - env.fs.makedirs(export_dir, exist_ok=True) + os.makedirs(export_dir, exist_ok=True) meshR = obj.read() mesh = get_mesh(meshR) if not mesh: @@ -50,24 +50,24 @@ def export_mesh_renderer(obj: Renderer, export_dir: str) -> None: continue tex = texEnv.m_Texture.read() texName = f"{tex.m_Name if tex.m_Name else key}.png" - with env.fs.open(env.fs.sep.join([export_dir, texName]), "wb") as f: + with env.fs.open(os.path.join(export_dir, texName), "wb") as f: tex.read().image.save(f) # save .obj - with env.fs.open( - env.fs.sep.join([export_dir, f"{mesh.m_Name}.obj"]), + with open( + os.path.join(export_dir, f"{mesh.name}.obj"), "wt", encoding="utf8", - newline="", + newline='' ) as f: f.write(export_mesh_obj(mesh, material_names)) # save .mtl - with env.fs.open( - env.fs.sep.join([export_dir, f"{mesh.m_Name}.mtl"]), + with open( + os.path.join(export_dir, f"{mesh.name}.mtl"), "wt", encoding="utf8", - newline="", + newline='' ) as f: f.write("\n".join(materials)) diff --git a/UnityPy/export/ShaderConverter.py b/UnityPy/export/ShaderConverter.py index fb59ba61..d41597e7 100644 --- a/UnityPy/export/ShaderConverter.py +++ b/UnityPy/export/ShaderConverter.py @@ -35,10 +35,13 @@ def ConvertSerializedShader(m_Shader): platformNumber = len(m_Shader.platforms) for i in range(platformNumber): + if i >= len(m_Shader.compressedLengths) or i >= len(m_Shader.decompressedLengths): + # m_Shader.platforms shouldn't be longer than m_shader.[de]compressedLengths, but it is + break 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] + + decompressedSize = m_Shader.decompressedLengths[i] decompressedBytes = CompressionHelper.decompress_lz4(compressedBytes, decompressedSize) shaderPrograms.append(ShaderProgram(EndianBinaryReader(decompressedBytes, endian="<"), m_Shader.version)) @@ -62,7 +65,7 @@ def ConvertSerializedShaderParsedForm(m_ParsedForm, platforms, shaderPrograms): sb.append("CustomEditor \"{0}\"\n".format(m_ParsedForm.m_CustomEditorName)) sb.append("}") - return "".join(sb) + return ''.join(sb) def ConvertSerializedSubShader(m_SubShader, platforms, shaderPrograms): sb = [] @@ -77,7 +80,7 @@ def ConvertSerializedSubShader(m_SubShader, platforms, shaderPrograms): sb.append(ConvertSerializedPass(m_Passe, platforms, shaderPrograms)) sb.append("}\n") - return "".join(sb) + return ''.join(sb) def ConvertSerializedPass(m_Passe, platforms, shaderPrograms): sb = [] @@ -127,7 +130,7 @@ def ConvertSerializedPass(m_Passe, platforms, shaderPrograms): sb.append("}\n") - return "".join(sb) + return ''.join(sb) def ConvertSerializedSubPrograms(m_SubPrograms, platforms, shaderPrograms): sb = [] @@ -145,35 +148,32 @@ def ConvertSerializedSubPrograms(m_SubPrograms, platforms, shaderPrograms): subPrograms = list(_programList) isTier = len(subPrograms) > 1 for i in range(len(platforms)): + if i >= len(shaderPrograms): + # platforms shouldn't be longer than shaderPrograms, but it is + break 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) + 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: @@ -226,13 +226,13 @@ def ConvertSerializedShaderState(m_State): 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) + return ''.join(sb) def ConvertSerializedShaderRTBlendState(rbBlend): # TODO Blend sb = [] - return "".join(sb) + return ''.join(sb) def ConvertSerializedTagMap(m_Tags, intent: int): sb = [] @@ -243,7 +243,7 @@ def ConvertSerializedTagMap(m_Tags, intent: int): sb.append("\"{0}\" = \"{1}\" ".format(key, value)) sb.append("}\n") - return "".join(sb) + return ''.join(sb) def ConvertSerializedProperties(m_PropInfo): sb = [] @@ -253,7 +253,7 @@ def ConvertSerializedProperties(m_PropInfo): sb.append(ConvertSerializedProperty(m_Prop)) sb.append("}\n") - return "".join(sb) + return ''.join(sb) def ConvertSerializedProperty(m_Prop): sb = [] @@ -302,7 +302,7 @@ def ConvertSerializedProperty(m_Prop): raise ValueError(m_Prop.m_Type) sb.append("\n") - return "".join(map(str,sb)) + return ''.join(map(str,sb)) def CheckGpuProgramUsable(platform, programType): if platform == ShaderCompilerPlatform.kShaderCompPlatformGL: @@ -582,4 +582,4 @@ def Export(self): sb.append("//shader disassembly not supported on {0}".format(self.m_ProgramType)) sb.append('"') - return "".join(sb) + return ''.join(sb) diff --git a/UnityPy/export/SpriteHelper.py b/UnityPy/export/SpriteHelper.py index ffcbf98e..9602e9a3 100644 --- a/UnityPy/export/SpriteHelper.py +++ b/UnityPy/export/SpriteHelper.py @@ -66,10 +66,10 @@ def get_image_from_sprite(m_Sprite) -> Image.Image: if settings_raw.packed == 1: rotation = settings_raw.packingRotation if rotation == SpritePackingRotation.kSPRFlipHorizontal: - sprite_image = sprite_image.transpose(Image.FLIP_TOP_BOTTOM) + sprite_image = sprite_image.transpose(Image.FLIP_LEFT_RIGHT) # spriteImage = RotateFlip(RotateFlipType.RotateNoneFlipX) elif rotation == SpritePackingRotation.kSPRFlipVertical: - sprite_image = sprite_image.transpose(Image.FLIP_LEFT_RIGHT) + sprite_image = sprite_image.transpose(Image.FLIP_TOP_BOTTOM) # spriteImage.RotateFlip(RotateFlipType.RotateNoneFlipY) elif rotation == SpritePackingRotation.kSPRRotate180: sprite_image = sprite_image.transpose(Image.ROTATE_180) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index b757a3c7..a71fe9e5 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -1,49 +1,95 @@ -īģŋimport texture2ddecoder -import etcpak -from PIL import Image +īģŋfrom __future__ import annotations + +import struct from copy import copy from io import BytesIO -import struct -from ..enums import TextureFormat, BuildTarget +from typing import TYPE_CHECKING, Dict, Tuple, Union + +import texture2ddecoder +from PIL import Image + +from ..enums import BuildTarget, TextureFormat +from ..helpers import TextureSwizzler + +if TYPE_CHECKING: + from ..classes import Texture2D + TF = TextureFormat -def image_to_texture2d(img: Image.Image, target_texture_format: TF, flip: bool = True): +def image_to_texture2d( + img: Image.Image, target_texture_format: Union[TF, int], flip: bool = True +) -> Tuple[bytes, TextureFormat]: + if isinstance(target_texture_format, int): + target_texture_format = TextureFormat(target_texture_format) + + import etcpak + if flip: img = img.transpose(Image.FLIP_TOP_BOTTOM) # DXT if target_texture_format in [TF.DXT1, TF.DXT1Crunched]: - raw_img = img.convert("RGBA").tobytes() - enc_img = etcpak.compress_to_dxt1(raw_img, img.width, img.height) + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_bc1(raw_img, img.width, img.height) tex_format = TF.DXT1 elif target_texture_format in [TF.DXT5, TF.DXT5Crunched]: - raw_img = img.convert("RGBA").tobytes() - enc_img = etcpak.compress_to_dxt5(raw_img, img.width, img.height) + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_bc3(raw_img, img.width, img.height) tex_format = TF.DXT5 + elif target_texture_format in [TF.BC4]: + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_bc4(raw_img, img.width, img.height) + tex_format = TF.BC4 + elif target_texture_format in [TF.BC5]: + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_bc5(raw_img, img.width, img.height) + tex_format = TF.BC5 + elif target_texture_format in [TF.BC7]: + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_bc7(raw_img, img.width, img.height) + tex_format = TF.BC7 # ETC elif target_texture_format in [TF.ETC_RGB4, TF.ETC_RGB4Crunched, TF.ETC_RGB4_3DS]: - img = assert_rgba(img, target_texture_format) - r, g, b, a = img.split() - raw_img = Image.merge("RGBA", (b, g, r, a)).tobytes() - enc_img = etcpak.compress_to_etc1(raw_img, img.width, img.height) + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_etc1_rgb(raw_img, img.width, img.height) tex_format = TF.ETC_RGB4 elif target_texture_format == TF.ETC2_RGB: - img = assert_rgba(img, target_texture_format) - r, g, b, a = img.split() - raw_img = Image.merge("RGBA", (b, g, r, a)).tobytes() - enc_img = etcpak.compress_to_etc2_rgb(raw_img, img.width, img.height) + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_etc2_rgb(raw_img, img.width, img.height) tex_format = TF.ETC2_RGB elif ( target_texture_format in [TF.ETC2_RGBA8, TF.ETC2_RGBA8Crunched, TF.ETC2_RGBA1] or "_RGB_" in target_texture_format.name ): - img = assert_rgba(img, target_texture_format) - r, g, b, a = img.split() - raw_img = Image.merge("RGBA", (b, g, r, a)).tobytes() - enc_img = etcpak.compress_to_etc2_rgba(raw_img, img.width, img.height) + raw_img = img.tobytes("raw", "RGBA") + enc_img = etcpak.compress_etc2_rgba(raw_img, img.width, img.height) tex_format = TF.ETC2_RGBA8 + elif target_texture_format.name.startswith("ASTC"): + import astc_encoder + + raw_img = img.tobytes("raw", "RGBA") + + block_size = tuple( + map(int, target_texture_format.name.rsplit("_", 1)[1].split("x")) + ) + + config = astc_encoder.ASTCConfig( + astc_encoder.ASTCProfile.LDR, *block_size, 1, 100 + ) + context = astc_encoder.ASTCContext(config) + raw_img = astc_encoder.ASTCImage( + astc_encoder.ASTCType.U8, img.width, img.height, 1, raw_img + ) + if img.mode == "RGB": + tex_format = getattr(TF, f"ASTC_RGB_{block_size[0]}x{block_size[1]}") + else: + tex_format = getattr(TF, f"ASTC_RGBA_{block_size[0]}x{block_size[1]}") + + swizzle = astc_encoder.ASTCSwizzle.from_str("RGBA") + enc_img = context.compress(raw_img, swizzle) + tex_format = target_texture_format # A elif target_texture_format == TF.Alpha8: enc_img = img.tobytes("raw", "A") @@ -69,26 +115,30 @@ def image_to_texture2d(img: Image.Image, target_texture_format: TF, flip: bool = TF.PVRTC_RGB4, TF.ATC_RGB4, ]: - enc_img = img.tobytes("raw", "RGB") + if img.mode != "RGB": + img = img.convert("RGB") + enc_img = img.tobytes("raw") tex_format = TF.RGB24 # everything else defaulted to RGBA else: - enc_img = img.tobytes("raw", "RGBA") + if img.mode != "RGBA": + img = img.convert("RGBA") + enc_img = img.tobytes("raw") tex_format = TF.RGBA32 return enc_img, tex_format -def assert_rgba(img: Image.Image, target_texture_format: TextureFormat): - if img.mode == "RGB": - img = img.convert("RGBA") - assert ( - img.mode == "RGBA" - ), f"{target_texture_format} compression only supports RGB & RGBA images" # noqa: E501 +def assert_rgba(img: Image.Image, target_texture_format: TextureFormat) -> Image.Image: + if img.mode == "RGB": img = img.convert("RGBA") + assert ( img.mode == "RGBA"), f"{target_texture_format} compression only supports RGB & RGBA images" return img -def get_image_from_texture2d(texture_2d, flip=True) -> Image.Image: +def get_image_from_texture2d( + texture_2d: Texture2D, + flip: bool = True, +) -> Image.Image: """converts the given texture into PIL.Image :param texture_2d: texture to be converterd @@ -98,15 +148,47 @@ def get_image_from_texture2d(texture_2d, flip=True) -> Image.Image: :return: PIL.Image object :rtype: Image """ - image_data = copy(bytes(texture_2d.image_data)) - if not image_data: - raise ValueError("Texture2D has no image data") + assert texture_2d is not None, "Must have texture_2d parameter in get_image_from_texture2d" + + width = texture_2d.m_Width + height = texture_2d.m_Height + if width == 0 or height == 0: + raise Exception("Zero dimensions Texture2D") texture_format = ( texture_2d.m_TextureFormat if isinstance(texture_2d.m_TextureFormat, TF) else TF(texture_2d.m_TextureFormat) ) + + image_data = texture_2d.image_data + if not image_data: + raise Exception(f"No image data available for Texture2D {'; path: ' + texture_2d.m_StreamData.path if hasattr(texture_2d, 'm_StreamData') else ''})") + + return parse_image_data( + bytes(image_data), + width, + height, + texture_format, + texture_2d.version, + texture_2d.platform, + getattr(texture_2d, "m_PlatformBlob", None), + flip + ) + +def parse_image_data( + image_data: bytes, + width: int, + height: int, + texture_format: Union[int, TextureFormat], + version: tuple, + platform: int, + platform_blob: bytes = None, + flip: bool = True, +) -> Image.Image: + if not image_data: + raise ValueError("Texture2D has no image data") + selection = CONV_TABLE[texture_format] if len(selection) == 0: @@ -114,11 +196,22 @@ def get_image_from_texture2d(texture_2d, flip=True) -> Image.Image: f"Not implemented texture format: {texture_format.name}" ) - if texture_format in XBOX_SWAP_FORMATS: - image_data = swap_bytes_for_xbox(image_data, texture_2d.platform) + if platform == BuildTarget.XBOX360 and texture_format in XBOX_SWAP_FORMATS: + image_data = swap_bytes_for_xbox(image_data) + elif platform == BuildTarget.Switch and platform_blob is not None: + gobsPerBlock = TextureSwizzler.get_switch_gobs_per_block(platform_blob) + block_size = TextureSwizzler.TEXTUREFORMAT_BLOCK_SIZE_MAP[texture_format] + padded_size = TextureSwizzler.get_padded_texture_size( + width, height, *block_size, gobsPerBlock + ) + image_data = TextureSwizzler.deswizzle( + image_data, *padded_size, *block_size, gobsPerBlock + ) + if not isinstance(texture_format, TextureFormat): + texture_format = TextureFormat(texture_format) if "Crunched" in texture_format.name: - version = texture_2d.version + version = version if ( version[0] > 2017 or (version[0] == 2017 and version[1] >= 3) # 2017.3 and up @@ -129,31 +222,20 @@ def get_image_from_texture2d(texture_2d, flip=True) -> Image.Image: else: image_data = texture2ddecoder.unpack_crunch(image_data) - img = selection[0]( - image_data, texture_2d.m_Width, texture_2d.m_Height, *selection[1:] - ) + img = selection[0](image_data, width, height, *selection[1:]) if img and flip: return img.transpose(Image.FLIP_TOP_BOTTOM) + return img -def swap_bytes_for_xbox(image_data: bytes, build_target: BuildTarget) -> bytes: +def swap_bytes_for_xbox(image_data: bytes) -> bytes: """swaps the texture bytes This is required for textures deployed on XBOX360. - - :param image_data: texture data - :type image_data: bytes - :param build_target: platform of the asset - :type build_target: BuildTarget - :return: swapped data if platform = XBOX360 else data - :rtype: bytes """ - if ( - 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] + for i in range(0, len(image_data), 2): + image_data[i : i + 2] = image_data[i : i + 2][::-1] return image_data @@ -186,17 +268,48 @@ def atc(image_data: bytes, width: int, height: int, alpha: bool) -> Image.Image: return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") +ASTC_CONTEXTS: Dict[Tuple[int, int], astc_encoder.ASTCContext] = {} + + def astc(image_data: bytes, width: int, height: int, block_size: tuple) -> Image.Image: - image_data = texture2ddecoder.decode_astc(image_data, width, height, *block_size) - return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") + context = ASTC_CONTEXTS.get(block_size) + if context is None: + config = astc_encoder.ASTCConfig( + astc_encoder.ASTCProfile.LDR, + *block_size, + 1, + 100, + astc_encoder.ASTCConfigFlags.USE_DECODE_UNORM8, + ) + context = ASTC_CONTEXTS[block_size] = astc_encoder.ASTCContext(config) + + image = astc_encoder.ASTCImage(astc_encoder.ASTCType.U8, width, height, 1) + texture_size = calculate_astc_compressed_size(width, height, block_size) + if len(image_data) < texture_size: + raise ValueError(f"Invalid ASTC data size: {len(image_data)} < {texture_size}") + context.decompress( + image_data[:texture_size], image, astc_encoder.ASTCSwizzle.from_str("RGBA") + ) + + return Image.frombytes("RGBA", (width, height), image.data, "raw", "RGBA") -def pvrtc(image_data: bytes, width: int, height: int, fmt: bool): +def calculate_astc_compressed_size(width: int, height: int, block_size: tuple) -> int: + """Calculate the size of the compressed data for ASTC.""" + # calculate the number of blocks + block_count_x = (width + block_size[0] - 1) // block_size[0] + block_count_y = (height + block_size[1] - 1) // block_size[1] + # ignore depth for 2D textures + # calculate the size of the compressed data + return block_count_x * block_count_y * 16 + + +def pvrtc(image_data: bytes, width: int, height: int, fmt: bool) -> Image.Image: image_data = texture2ddecoder.decode_pvrtc(image_data, width, height, fmt) return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") -def etc(image_data: bytes, width: int, height: int, fmt: list): +def etc(image_data: bytes, width: int, height: int, fmt: list) -> Image.Image: if fmt[0] == 1: image_data = texture2ddecoder.decode_etc1(image_data, width, height) elif fmt[0] == 2: @@ -211,7 +324,7 @@ def etc(image_data: bytes, width: int, height: int, fmt: list): return Image.frombytes("RGBA", (width, height), image_data, "raw", "BGRA") -def eac(image_data: bytes, width: int, height: int, fmt: list): +def eac(image_data: bytes, width: int, height: int, fmt: list) -> Image.Image: if fmt == "EAC_R": image_data = texture2ddecoder.decode_eacr(image_data, width, height) elif fmt == "EAC_R_SIGNED": @@ -241,72 +354,128 @@ def half( return pillow(image_data, width, height, mode, codec, args, swap) +RG_PADDING_MAP = { + "RGE": 16, + "RGF": 32, + "RG;16": 16, + "RG;16s": 16, + "RG;8s": 8, +} + + +def rg( + image_data: bytes, width: int, height: int, mode: str, codec: str, args +) -> Image.Image: + # convert rg to rgb by adding in zeroes + padding_size = RG_PADDING_MAP[codec] + stream = BytesIO(image_data) + padding = bytes(padding_size) + rgb_data = b"".join( + stream.read(padding_size * 2) + padding + for _ in range(image_data / (2 * padding_size)) + ) + if codec == "RGE": + return half(rgb_data, width, height, mode, "RGB", args) + else: + return pillow(rgb_data, width, height, mode, codec.replace("RG", "RGB"), args) + + +def rgb9e5float(image_data: bytes, width: int, height: int) -> Image.Image: + rgb = bytearray(width * height * 3) + for i, (n,) in enumerate(struct.iter_unpack("> 27 & 0x1F + scalef = 2 ** (scale - 24) + scaleb = scalef * 255.0 + b = (n >> 18 & 0x1FF) * scaleb + g = (n >> 9 & 0x1FF) * scaleb + r = (n & 0x1FF) * scaleb + + offset = i * 3 + rgb[offset : offset + 3] = [r, g, b] + + return Image.frombytes("RGB", (width, height), rgb, "raw", "RGB") + + CONV_TABLE = { -# FORMAT FUNC #ARGS..... -#----------------------- -------- -------- ------------ ----------------- ------------ ---------- -( TF.Alpha8, pillow, "RGBA", "raw", "A" ), -( TF.ARGB4444, pillow, "RGBA", "raw", "RGBA;4B", (2,1,0,3) ), -( TF.RGB24, pillow, "RGB", "raw", "RGB" ), -( TF.RGBA32, pillow, "RGBA", "raw", "RGBA" ), -( TF.ARGB32, pillow, "RGBA", "raw", "ARGB" ), -( TF.RGB565, pillow, "RGB", "raw", "BGR;16" ), -( TF.R8, pillow, "RGB", "raw", "R" ), -( TF.R16, pillow, "RGB", "raw", "R;16" ), -( TF.RG16, ), -( TF.DXT1, pillow, "RGBA", "bcn", 1 ), -( TF.DXT5, pillow, "RGBA", "bcn", 3 ), -( TF.RGBA4444, pillow, "RGBA", "raw", 'RGBA;4B', (3,2,1,0) ), -( TF.BGRA32, pillow, "RGBA", "raw", "BGRA" ), -( TF.RHalf, half, "R", "raw", "R" ), -( TF.RGHalf, ), -( TF.RGBAHalf, half, "RGB", "raw", "RGB" ), -( TF.RFloat, pillow, "RGB", "raw", "RF" ), -( TF.RGFloat, ), -( TF.RGBAFloat, pillow, "RGBA", "raw", "RGBAF" ), -( TF.YUY2, ), -( TF.RGB9e5Float, ), -( TF.BC4, pillow, "L", "bcn", 4 ), -( TF.BC5, pillow, "RGB", "bcn", 5 ), -( TF.BC6H, pillow, "RGBA", "bcn", 6 ), -( TF.BC7, pillow, "RGBA", "bcn", 7 ), -( TF.DXT1Crunched, pillow, "RGBA", "bcn", 1 ), -( TF.DXT5Crunched, pillow, "RGBA", "bcn", 3 ), -( TF.PVRTC_RGB2, pvrtc, True ), -( TF.PVRTC_RGBA2, pvrtc, True ), -( TF.PVRTC_RGB4, pvrtc, False ), -( TF.PVRTC_RGBA4, pvrtc, False ), -( TF.ETC_RGB4, etc, (1,) ), -( TF.ATC_RGB4, atc, False ), -( TF.ATC_RGBA8, atc, True ), -( TF.EAC_R, eac, "EAC_R" ), -( TF.EAC_R_SIGNED, eac, "EAC_R:SIGNED" ), -( TF.EAC_RG, eac, "EAC_RG" ), -( TF.EAC_RG_SIGNED, eac, "EAC_RG_SIGNED" ), -( TF.ETC2_RGB, etc, (2,"RGB") ), -( TF.ETC2_RGBA1, etc, (2, "A1") ), -( TF.ETC2_RGBA8, etc, (2, "A8") ), -( TF.ASTC_RGB_4x4, astc, (4,4) ), -( TF.ASTC_RGB_5x5, astc, (5,5) ), -( TF.ASTC_RGB_6x6, astc, (6,6) ), -( TF.ASTC_RGB_8x8, astc, (8,8) ), -( TF.ASTC_RGB_10x10, astc, (10,10) ), -( TF.ASTC_RGB_12x12, astc, (12,12) ), -( TF.ASTC_RGBA_4x4, astc, (4,4) ), -( TF.ASTC_RGBA_5x5, astc, (5,5) ), -( TF.ASTC_RGBA_6x6, astc, (6,6) ), -( TF.ASTC_RGBA_8x8, astc, (8,8) ), -( TF.ASTC_RGBA_10x10, astc, (10,10) ), -( TF.ASTC_RGBA_12x12, astc, (12,12) ), -( TF.ETC_RGB4_3DS, etc, (1,) ), -( TF.ETC_RGBA8_3DS, etc, (1,) ), -( TF.ETC_RGB4Crunched, etc, (1,) ), -( TF.ETC2_RGBA8Crunched, etc, (2, "A8") ), -( TF.ASTC_HDR_4x4, astc, (4,4) ), -( TF.ASTC_HDR_5x5, astc, (5,5) ), -( TF.ASTC_HDR_6x6, astc, (6,6) ), -( TF.ASTC_HDR_8x8, astc, (8,8) ), -( TF.ASTC_HDR_10x10, astc, (10,10) ), -( TF.ASTC_HDR_12x12, astc, (12,12) ), + # FORMAT FUNC #ARGS..... + # ----------------------- -------- -------- ------------ ----------------- ------------ ---------- + (TF.Alpha8, pillow, "RGBA", "raw", "A"), + (TF.ARGB4444, pillow, "RGBA", "raw", "RGBA;4B", (2, 1, 0, 3)), + (TF.RGB24, pillow, "RGB", "raw", "RGB"), + (TF.RGBA32, pillow, "RGBA", "raw", "RGBA"), + (TF.ARGB32, pillow, "RGBA", "raw", "ARGB"), + (TF.ARGBFloat, pillow, "RGBA", "raw", "RGBAF", (2, 1, 0, 3)), + (TF.RGB565, pillow, "RGB", "raw", "BGR;16"), + (TF.BGR24, pillow, "RGB", "raw", "BGR"), + (TF.R8, pillow, "RGB", "raw", "R"), + (TF.R16, pillow, "RGB", "raw", "R;16"), + (TF.RG16, rg, "RGB", "raw", "RG"), + (TF.DXT1, pillow, "RGBA", "bcn", 1), + (TF.DXT3, pillow, "RGBA", "bcn", 2), + (TF.DXT5, pillow, "RGBA", "bcn", 3), + (TF.RGBA4444, pillow, "RGBA", "raw", "RGBA;4B", (3, 2, 1, 0)), + (TF.BGRA32, pillow, "RGBA", "raw", "BGRA"), + (TF.RHalf, half, "R", "raw", "R"), + (TF.RGHalf, rg, "RGB", "raw", "RGE"), + (TF.RGBAHalf, half, "RGB", "raw", "RGB"), + (TF.RFloat, pillow, "RGB", "raw", "RF"), + (TF.RGFloat, rg, "RGB", "raw", "RGF"), + (TF.RGBAFloat, pillow, "RGBA", "raw", "RGBAF"), + (TF.YUY2,), + (TF.RGB9e5Float, rgb9e5float), + (TF.BC4, pillow, "L", "bcn", 4), + (TF.BC5, pillow, "RGB", "bcn", 5), + (TF.BC6H, pillow, "RGBA", "bcn", 6), + (TF.BC7, pillow, "RGBA", "bcn", 7), + (TF.DXT1Crunched, pillow, "RGBA", "bcn", 1), + (TF.DXT5Crunched, pillow, "RGBA", "bcn", 3), + (TF.PVRTC_RGB2, pvrtc, True), + (TF.PVRTC_RGBA2, pvrtc, True), + (TF.PVRTC_RGB4, pvrtc, False), + (TF.PVRTC_RGBA4, pvrtc, False), + (TF.ETC_RGB4, etc, (1,)), + (TF.ATC_RGB4, atc, False), + (TF.ATC_RGBA8, atc, True), + (TF.EAC_R, eac, "EAC_R"), + (TF.EAC_R_SIGNED, eac, "EAC_R:SIGNED"), + (TF.EAC_RG, eac, "EAC_RG"), + (TF.EAC_RG_SIGNED, eac, "EAC_RG_SIGNED"), + (TF.ETC2_RGB, etc, (2, "RGB")), + (TF.ETC2_RGBA1, etc, (2, "A1")), + (TF.ETC2_RGBA8, etc, (2, "A8")), + (TF.ASTC_RGB_4x4, astc, (4, 4)), + (TF.ASTC_RGB_5x5, astc, (5, 5)), + (TF.ASTC_RGB_6x6, astc, (6, 6)), + (TF.ASTC_RGB_8x8, astc, (8, 8)), + (TF.ASTC_RGB_10x10, astc, (10, 10)), + (TF.ASTC_RGB_12x12, astc, (12, 12)), + (TF.ASTC_RGBA_4x4, astc, (4, 4)), + (TF.ASTC_RGBA_5x5, astc, (5, 5)), + (TF.ASTC_RGBA_6x6, astc, (6, 6)), + (TF.ASTC_RGBA_8x8, astc, (8, 8)), + (TF.ASTC_RGBA_10x10, astc, (10, 10)), + (TF.ASTC_RGBA_12x12, astc, (12, 12)), + (TF.ETC_RGB4_3DS, etc, (1,)), + (TF.ETC_RGBA8_3DS, etc, (1,)), + (TF.ETC_RGB4Crunched, etc, (1,)), + (TF.ETC2_RGBA8Crunched, etc, (2, "A8")), + (TF.ASTC_HDR_4x4, astc, (4, 4)), + (TF.ASTC_HDR_5x5, astc, (5, 5)), + (TF.ASTC_HDR_6x6, astc, (6, 6)), + (TF.ASTC_HDR_8x8, astc, (8, 8)), + (TF.ASTC_HDR_10x10, astc, (10, 10)), + (TF.ASTC_HDR_12x12, astc, (12, 12)), + (TF.RG32, rg, "RGB", "raw", "RG;16"), + (TF.RGB48, pillow, "RGB", "raw", "RGB;16"), + (TF.RGBA64, pillow, "RGBA", "raw", "RGBA;16"), + (TF.R8_SIGNED, pillow, "R", "raw", "R;8s"), + (TF.RG16_SIGNED, rg, "RGB", "raw", "RG;8s"), + (TF.RGB24_SIGNED, pillow, "RGB", "raw", "RGB;8s"), + (TF.RGBA32_SIGNED, pillow, "RGBA", "raw", "RGBA;8s"), + (TF.R16_SIGNED, pillow, "R", "raw", "R;16s"), + (TF.RG32_SIGNED, rg, "RGB", "raw", "RG;16s"), + (TF.RGB48_SIGNED, pillow, "RGB", "raw", "RGB;16s"), + (TF.RGBA64_SIGNED, pillow, "RGBA", "raw", "RGBA;16s"), } # format conv_table to a dict diff --git a/UnityPy/files/AssetsFileHeader.py b/UnityPy/files/AssetsFileHeader.py new file mode 100644 index 00000000..9be385ce --- /dev/null +++ b/UnityPy/files/AssetsFileHeader.py @@ -0,0 +1,44 @@ +class AssetsFileHeader: + def __init__(self, reader): + self.metadataSize = 0 + self.fileSize = 0 + self.version = 0 + self.dataOffset = 0 + self.endian = False + if reader: + self.read(reader) + + def read(self, reader): + reader.endian = "<" + self.metadataSize = reader.read_uint() + self.fileSize = reader.read_uint() + self.version = reader.read_uint() + self.dataOffset = reader.read_uint() + self.endian = reader.ReadBoolean() + reader.Position += 3 # unused bytes + + if self.Version >= 0x16: + self.metadataSize = reader.read_uint() + self.fileSize = reader.read_long() + self.dataOffset = reader.read_long() + reader.Position += 8 # unused bytes + + def save(self, writer): + if self.version >= 0x16: + writer.write(b"\0\0") + writer.write_uint(self.version) + writer.write(b'\0') + else: + writer.write_uint(self.metadataSize) + writer.write_uint(self.fileSize) + writer.write_uint(self.Version) + writer.write_uint(self.dataOffset) + + writer.write_uint(self.endian) + writer.write(b'\0' * 3) + + if self.Version >= 0x16: + writer.write_long(self.metadataSize) + writer.write_long(self.fileSize) + writer.write_long(self.dataOffset) + writer.write(b'\0' * 8) \ No newline at end of file diff --git a/UnityPy/files/BlockStream.py b/UnityPy/files/BlockStream.py new file mode 100644 index 00000000..ea9f8db6 --- /dev/null +++ b/UnityPy/files/BlockStream.py @@ -0,0 +1,732 @@ +from typing import Tuple, Dict, Union, Callable, List +from re import compile as re_compile +from io import BytesIO +from tempfile import SpooledTemporaryFile +from ..enums import ArchiveFlags, ArchiveFlagsOld, CompressionFlags +from ..helpers import ArchiveStorageManager, CompressionHelper +from ..streams import EndianBinaryReader, EndianBinaryWriter +from ..helpers.ForceCRC32 import bit_reverse, multiply_mod, reciprocal_mod, pow_mod, MASK +from zlib import crc32 +from collections import namedtuple +from .. import config + +Surroundings = namedtuple('Surroundings', ['prev', 'next']) + +class BlockInfo: + __slots__ = ['uncompressedSize', 'compressedSize', 'flags', 'origin', 'shared'] + def __init__(self, uncompressedSize: int, compressedSize: int, flags: int, origin: int, shared: bool): + self.uncompressedSize = uncompressedSize + self.compressedSize = compressedSize + self.flags = flags + self.origin = origin + self.shared = shared + + def __repr__(self): + return f"<{self.__class__.__name__} uncompressedSize={self.uncompressedSize} compressedSize={self.compressedSize} flags={self.flags} origin={self.origin} shared={self.shared}> " + +class DirectoryInfoFS: + __slots__ = ['offset', 'size', 'flags', 'path'] + def __init__(self, offset: int, size: int, flags: int, path: str): + self.offset = offset + self.size = size + self.flags = flags + self.path = path + + def __repr__(self): + return f"<{self.path} ({self.size} B) at {self.offset} flags={self.flags}>" + +class BlockMeta: + __slots__ = ['n', 'block_offset', 'base', 'origin'] + def __init__(self, n: int, block_offset: int, base: int, origin: int): + self.n = n + self.block_offset = block_offset + self.base = base + self.origin = origin + + def __repr__(self): + return f"" + +class FileBlocksMeta: + __slots__ = ['start', 'end', 'size'] + def __init__(self, start: BlockMeta, end: BlockMeta, size: int): + self.start = start + self.end = end + self.size = size + + def __repr__(self): + return f"" + +class CryptoFuncs: + __slots__ = ['encrypt', 'decrypt'] + def __init__(self, encrypt, decrypt): + self.encrypt = encrypt + self.decrypt = decrypt + + def __repr__(self): + if self.encrypt is None and self.encrypt is None: + r = 'are not provided' + else: + r = 'provided: ' + if self.encrypt: + r += 'encrypt' + if self.decrypt: + r += ' ' if self.encrypt else '' + 'decrypt' + return f"<{self.__class__.__name__} functions {r}> " + +class FileBlocksReader: + def __init__(self, stream, name: str, real_length: int): + self.block_stream: BlockStream = stream + self.name = name + self.Length = real_length + self._position = 0 + + + def get_position(self): + """Gets the virtual Position inside the file's blockstream""" + return self._position + + def set_position(self, value): + """Sets the virtual Position inside the file's blockstream""" + self._position = max(0, min(value, self.Length)) + + Position = property(get_position, set_position) + + + def seek(self, offset: int, origin: int=0) -> int: + if origin == 0: + self._position = offset + elif origin == 1: + self._position += offset + elif origin == 2: + self._position = self.Length + offset + else: + raise ValueError("Invalid origin") + if self._position > self.Length: + self._position = self.Length + #raise IOError("offset is outside of the stream") + elif self._position < 0: + self._position = 0 + return self._position + + + # NOTE: Didn't implement read() since we only need read_bytes for big resS files + # unless there are some extra big serialized assets, which is improbable, + # the rest will be unpacked before processing and read trough EBRs + def read_bytes(self, size): + """Reads [size] bytes from the file's blockstream""" + res_offset = None + res_size = 0 + for di in self.block_stream.m_DirectoryInfo: + if di.path.lower() == self.name.lower(): + res_offset = di.offset + res_size = di.size + break + if res_offset is None: + return None + + start = self.block_stream.block_by_offset(res_offset + self._position) + end = self.block_stream.block_by_offset( + min(res_offset + self._position + size, res_offset + res_size), True) + is_start = start.n >= 0 or ( + start.n == end.n and size <= end.block_offset - start.block_offset) + is_end = end.n <= len(self.block_stream.m_BlocksInfo) - 1 + if is_start: + self.block_stream.m_BlocksInfo[start.n].shared = True + elif is_end: + self.block_stream.m_BlocksInfo[end.n].shared = True + + self.block_stream.base_stream.seek( + self.block_stream.m_BlocksInfo[start.n].origin) + data = b''.join(self.block_stream.read_owned_block( + self.block_stream.base_stream, self.m_BlocksInfo, index, + FileBlocksMeta(start, end, size) + )[0] for index in range(start.n, end.n) + ) + + self.Position += len(data) + if is_start: + self.block_stream.m_BlocksInfo[start.n].shared = False + elif is_end: + self.block_stream.m_BlocksInfo[end.n].shared = False + return data + + + def __getattr__(self, name: str): + if self.block_stream is not None and hasattr(self.block_stream, name): + return getattr(self.block_stream, name) + else: + raise AttributeError(f"{self.__class__.__name__} has not attribute {name}") + + +CHUNK_SIZE = 0x20000 +RE_UNITY_VERSION = re_compile(r"(\d+)\.(\d+)\.(\d+)\w.+") + +class BlockStream: + Length: int + Position: int + BaseOffset: int + Crypto: CryptoFuncs + + def __init__(self, stream: EndianBinaryReader, offset: int=0, size: int=0, + crypto_funcs:Callable=None, version:int=None, unity_version:str=None, **kwargs): + self.modify_crc = kwargs.get("modify_crc", False) + self.crc32 = None + self.base_stream: EndianBinaryReader = stream + self.Crypto = crypto_funcs + self.BaseOffset = offset + self.Position = 0 + #self.Length = size + self.m_BlocksInfo: list[BlockInfo] = [] + self.m_DirectoryInfo: list[DirectoryInfoFS] = [] + self.file_blocks_map : Dict[FileBlocksMeta] = {} + self._fs_flags = 0 + self._uses_block_alignment = False + + self.version = version + + if not unity_version or unity_version == "0.0.0": + unity_version = config.get_fallback_version() + self.unity_version = tuple(map(int, RE_UNITY_VERSION.match(unity_version).groups())) + + + @property + def _blocks_info_flag(self): + if self.m_BlocksInfo and len(self.m_BlocksInfo) > 0: + return self.m_BlocksInfo[0].flags + return 0 + + + def get_flags(self): + # Unity CN introduced encryption before the Switch file alignment fix. + # It used the same flag for the encryption as later on the alignment fix, + # so we have to check the version to determine the correct flag set. + unity_version = self.unity_version + if ( + # < 2000, < 2020.3.34, < 2021.3.2, < 2022.1.1 + unity_version < (2020,) + or (unity_version[0] == 2020 and unity_version < (2020, 3, 34)) + or (unity_version[0] == 2021 and unity_version < (2021, 3, 2)) + or (unity_version[0] == 2022 and unity_version < (2022, 1, 1)) + ): + return ArchiveFlagsOld(self._fs_flags) + else: + return ArchiveFlags(self._fs_flags) + + def set_flags(self, value): + self._fs_flags = value + + fsFlags = property(get_flags, set_flags) + + + def pre_align(self): + # check if we need to align the reader + # - align to 16 bytes and check if all are 0 + # - if not, reset the reader to the previous position + if self.version >= 7: + self.base_stream.align_stream(16) + self._uses_block_alignment = True + elif self.unity_version >= (2019, 4): + pre_align = self.base_stream.Position + align_data = self.base_stream.read((16 - pre_align % 16) % 16) + if any(align_data): + self.base_stream.Position = pre_align + else: + self._uses_block_alignment = True + + + def read_meta(self): + fsSize = self.base_stream.read_long() + #assert fsSize <= self.base_stream.Length, f"File is truncated" + metaCompressedSize = self.base_stream.read_u_int() + metaUncompressedSize = self.base_stream.read_u_int() + self.fsFlags = self.base_stream.read_u_int() + + if self._fs_flags & self.fsFlags.UsesAssetBundleEncryption: + self.Crypto = ArchiveStorageManager.ArchiveStorageDecryptor(self.base_stream) + + self.pre_align() + + start = self.base_stream.Position + if (self.fsFlags & ArchiveFlags.BlocksInfoAtTheEnd): + # kArchiveBlocksInfoAtTheEnd + self.base_stream.Position = self.base_stream.Length - metaCompressedSize + blocksInfoBytes = self.base_stream.read_bytes(metaCompressedSize) + self.base_stream.Position = start + else: + # 0x40 kArchiveBlocksAndDirectoryInfoCombined + blocksInfoBytes = self.base_stream.read_bytes(metaCompressedSize) + + blocksInfoBytes = self.decompress_block( + blocksInfoBytes, None, flags=self.fsFlags, uncompressed_size=metaUncompressedSize) + assert len(blocksInfoBytes) == metaUncompressedSize, "Wrong unpacked metadata size" + blocksInfoReader = EndianBinaryReader(blocksInfoBytes, offset=start) + + uncompressedDataHash = blocksInfoReader.read_bytes(16) + self.read_block_infos(blocksInfoReader) + self.read_files_dir(blocksInfoReader) + + if (isinstance(self.fsFlags, ArchiveFlags) + and self.fsFlags & ArchiveFlags.BlockInfoNeedPaddingAtStart): + self.base_stream.align_stream(16) + + # calculate blocks absolute offsets from the file start + self.BaseOffset = self.base_stream.Position + for i, bi in enumerate(self.m_BlocksInfo): + self.m_BlocksInfo[i].origin = bi.origin + self.BaseOffset + self.map_directory_to_blocks() + + self.base_stream.Position = self.BaseOffset + if self.modify_crc and self.crc32 is None: + self.crc32 = self.crc32_from_blocks(self.base_stream, self.m_BlocksInfo) + #print_debug(f"Original CRC32 is {hex(self.crc32)}") + self.base_stream.Position = self.BaseOffset + + + def crc32_from_blocks(self, stream, block_infos: list[BlockInfo]) -> int: + crc = 0 + for index in range(len(block_infos)): + buf, _ = self.read_owned_block(stream, block_infos, index) + crc = crc32(buf, crc) + return crc & MASK + + def create_crc_patch_block(self, stream: Union[EndianBinaryWriter, EndianBinaryReader], + block_infos: list[BlockInfo]) -> Tuple[bytes, BlockInfo]: + modded_crc = self.crc32_from_blocks(stream, block_infos) + #print_info(f"Patched CRC32 is {hex(modded_crc)}") + if modded_crc == self.crc32: + return (None, None) + + padded_crc = bit_reverse(crc32(b'\0' * 4, modded_crc) & MASK) # pad for correction + make_crc = bit_reverse(self.crc32) + base_delta = padded_crc ^ make_crc + delta = multiply_mod(reciprocal_mod(pow_mod(2, 4 * 8)), base_delta) + patch_bytes = bytearray([(bit_reverse(delta) >> (i * 8)) & 0xFF for i in range(4)]) + + #new_crc = crc32(patch_bytes, modded_crc) & MASK # pad for correction + #print_info(f"New CRC {hex(new_crc)} {('!' if new_crc != self.crc32 else '=')}= supposed {hex(self.crc32)}") + return (patch_bytes, BlockInfo(4, 4, 0, None, False)) + + + def write_meta_and_files(self, writer: EndianBinaryWriter, files: list, + meta_flags=None, block_flags=None, block_size=CHUNK_SIZE): + if meta_flags is None: + meta_flags = self._fs_flags + + fs_start_position = writer.Position + writer.write_bytes(b'\0' * self.estimate_header_size()) # header space + if self._uses_block_alignment: + writer.align_stream(16) + + data_writer = EndianBinaryWriter() + if meta_flags & ArchiveFlags.BlocksInfoAtTheEnd: # at end of file + if meta_flags & ArchiveFlags.BlockInfoNeedPaddingAtStart: + writer.align_stream(16) + # NOTE: we could write directly to the file with "writer" here but crc32 needs data + dirinfos, blockinfos = self.write_all_files(data_writer, files, block_flags, block_size) + else: + # metadata dirs placeholder + dirinfos, blockinfos = self.write_all_files(data_writer, files, block_flags, block_size) + + if self.modify_crc and self.crc32 is not None: + pos = data_writer.Position + data_writer.Position = 0 + patch_data, patch_block = self.create_crc_patch_block(data_writer, blockinfos) + if patch_data: + data_writer.Position = pos + data_writer.write(patch_data) + #blockinfos[-1].uncompressedSize += patch_block.uncompressedSize + # this assumes the file is really last but we can also recompress the last block + dirinfos[-1].size += patch_block.uncompressedSize + blockinfos.append(patch_block) + + meta_stream = EndianBinaryWriter(b'\0' * 16) # uncompressedDataHash + self.write_block_infos(meta_stream, blockinfos) + self.write_files_dir(meta_stream, dirinfos) + + meta_bytes = meta_stream.save() + meta_stream.close() + uncompressed_meta_len = len(meta_bytes) + meta_bytes, meta_flags = self.compress_block(meta_bytes, flags=meta_flags) + compressed_meta_len = len(meta_bytes) + + if not meta_flags & ArchiveFlags.BlocksInfoAtTheEnd: + writer.write(meta_bytes) + if meta_flags & ArchiveFlags.BlockInfoNeedPaddingAtStart: + writer.align_stream(16) + writer.write(data_writer.save()) + data_writer.close() + else: + writer.write(data_writer.save()) + data_writer.close() + writer.write(meta_bytes) + + writer_end_position = writer.Position + writer.Position = fs_start_position + writer.write_long(writer.Length) # total file size + writer.write_u_int(compressed_meta_len) # compressed blockInfoBytes size + writer.write_u_int(uncompressed_meta_len) # uncompressed blockInfoBytes size + writer.write_u_int(meta_flags) # compression and file layout flag + writer.Position = writer_end_position + + + def read_block_infos(self, reader): + blocksInfo_count = reader.read_int() + self.m_BlocksInfo: list[BlockInfo] = [] + origin = 0 + for _ in range(blocksInfo_count): + uncompressedSize = reader.read_u_int() + compressedSize = reader.read_u_int() + flags = reader.read_u_short() + self.m_BlocksInfo.append( + BlockInfo(uncompressedSize, compressedSize, flags, origin, False) + ) + origin += compressedSize + + + def estimate_header_size(self): + return 8 + 4 + 4 + 4 + + + def estimate_block_dir(self, block_infos): + return 4 + len(block_infos) * (4 + 4 + 2) + + + def estimate_files_dir(self, dirs_info): + return 4 + len(dirs_info) * (8 + 8 + 4) + sum( + [len(di.path.encode('utf-8')) + 1 for di in dirs_info]) + + + def write_block_infos(self, writer: EndianBinaryWriter, block_infos: list[BlockInfo]): + writer.write_int(len(block_infos)) + for bi in block_infos: + writer.write_u_int(bi.uncompressedSize) + writer.write_u_int(bi.compressedSize) + writer.write_u_short(bi.flags) + + + def read_files_dir(self, reader: EndianBinaryReader): + DirectoryInfo_count = reader.read_int() + self.m_DirectoryInfo: list[DirectoryInfoFS] = [ + DirectoryInfoFS( + reader.read_long(), # offset + reader.read_long(), # size + reader.read_u_int(), # flags + reader.read_string_to_null(), # path + ) + for _ in range(DirectoryInfo_count) + ] + + + def write_files_dir(self, writer: EndianBinaryWriter, dir_infos: list[DirectoryInfoFS]): + writer.write_int(len(dir_infos)) + for di in dir_infos: + writer.write_long(di.offset) + writer.write_long(di.size) + writer.write_u_int(di.flags) + writer.write_string_to_null(di.path) + + + def block_by_offset(self, offset, is_end=False) -> BlockMeta: + """ NOTE: If all blocks guaranteed to have same max block size we can + do with simple integer math but they are probably not. + """ + # block_n = offset // CHUNK_SIZE + pos = 0 + block_n = 0 + for bi in self.m_BlocksInfo: + pos += bi.uncompressedSize + if (pos >= offset if is_end else pos > offset): + break + block_n += 1 + return BlockMeta(block_n, bi.uncompressedSize - (pos - offset), pos - bi.uncompressedSize, bi.origin) + + + def map_directory_to_blocks(self): + for di in self.m_DirectoryInfo: + start = self.block_by_offset(di.offset) + end = self.block_by_offset(di.offset + di.size, True) + self.file_blocks_map[di.path] = FileBlocksMeta(start, end, di.size) + #print_debug("map_directory_to_blocks(): ", end='') + for i, di in enumerate(self.m_DirectoryInfo): + # NOTE: this assumes continuous file layout corresponding to an ordered dict. + # We check if the end and start blocks of the files are the same. + blocks = self.file_blocks_map[di.path] + if i > 0 and self.file_blocks_map[self.m_DirectoryInfo[i-1].path].end.n == ( + blocks.start.n): + # the case of having the last file's end and the current's start overlap + self.m_BlocksInfo[blocks.start.n].shared = True + #print_debug(i, blocks.start.n, self.m_BlocksInfo[blocks.start.n], end=' ') + elif i < len(self.m_DirectoryInfo) - 2 and blocks.end.n == ( + self.file_blocks_map[self.m_DirectoryInfo[i+1].path].start.n): + # the case of having the current file's end and the next's start overlap + self.m_BlocksInfo[blocks.end.n].shared = True + #print_debug(i, blocks.end.n, self.m_BlocksInfo[blocks.end.n], end='') + pass + + + def save(self, writer): + raise Exception("Use write_all_files/write_original_file/write_file instead") + + + def __getattr__(self, name: str): + # proxy calls to the base EBR + if hasattr(self.base_stream, name): + return getattr(self.base_stream, name) + else: + raise AttributeError(f"{self.__class__.__name__} has not attribute {name}") + + + def get_size_from_block_range(self, own_range: FileBlocksMeta): + return (sum([self.m_BlocksInfo[i].uncompressedSize for i in range( + own_range.start.n, own_range.end.n)])) + own_range.end.block_offset - own_range.start.block_offset + + + def is_uncompressed_block_range(self, own_range: FileBlocksMeta): + return all([CompressionFlags( + self.m_BlocksInfo[i].flags & ArchiveFlags.CompressionTypeMask) == CompressionFlags.NO and not ( + self.m_BlocksInfo[i].flags & ArchiveFlags.UnityCNEncryption) for i in range( + own_range.start.n, own_range.end.n + 1)]) + + + def get_file_reader(self, name: str): + blocks: FileBlocksMeta = self.file_blocks_map[name] + real_size = blocks.size + """ + assert blocks.start.n <= blocks.end.n < len(self.m_BlocksInfo), ( + "BlockStream file is not loaded or incorrect "+ + f"{blocks.start.n} < {blocks.end.n} < {len(self.m_BlocksInfo)}") + assert real_size == self.get_size_from_block_range(blocks), ( + f"{real_size} != {self.get_size_from_block_range(blocks)}") + """ + if abs(config.BIG_OBJECT_GUARD) > 0 and real_size > abs(config.BIG_OBJECT_GUARD): + # Return wrapped (to preserve name and file size) BlockStream since it's too big to unpack. + # We will only read it on actual read_bytes() operations and if value is negative. + print_warning(f"{name} is too big {'ignoring' if config.BIG_OBJECT_GUARD > 0 else 'assigning block reader'}") + return None if config.BIG_OBJECT_GUARD > 0 else FileBlocksReader(self, name, real_size) + if self.is_uncompressed_block_range(blocks): + # If it's entirely uncompressed read directly from the disk + return EndianBinaryReader(self.base_stream, offset=self.base_stream.Position, + crypto_func=self.base_stream.Crypto) + else: + # otherwise create a memory stream that dumps to disk if it's quite big + temp_file = SpooledTemporaryFile(max_size=400000000, prefix="unity_bundle_data_") + self.base_stream.seek(self.m_BlocksInfo[blocks.start.n].origin) + for index in range(blocks.start.n, blocks.end.n + 1): + data = self.read_owned_block(self.base_stream, self.m_BlocksInfo, index, blocks)[0] + temp_file.write(data) + return EndianBinaryReader(temp_file, offset=0) + + + def write_block(self, blocks_info, index: int, writer: EndianBinaryWriter): + """Writes a block without any modifications""" + uncompressedSize = blocks_info[index].uncompressedSize + compressedSize = blocks_info[index].compressedSize + block_flags = blocks_info[index].flags + block_data = self.read_block(self.base_stream, blocks_info, index) + written = writer.write(block_data) + assert compressedSize == written, "write_owned_block(): not all written" + return BlockInfo(uncompressedSize, compressedSize, block_flags, None, False) + + + def write_owned_block(self, blocks_info, index: int, writer: EndianBinaryWriter, + own_range: FileBlocksMeta): + """Writes a block data that only belong to the current file""" + uncompressedSize = blocks_info[index].uncompressedSize + compressedSize = blocks_info[index].compressedSize + + block_data, is_decompressed = self.read_owned_block( + self.base_stream, blocks_info, index, own_range, force_decompress=False) + + block_flags = blocks_info[index].flags + if is_decompressed and block_flags & ArchiveFlags.CompressionTypeMask: + uncompressedSize = len(block_data) + block_data, block_flags = self.compress_block(block_data, blocks_info, block_flags) + compressedSize = len(block_data) + + written = writer.write(block_data) + assert compressedSize == written, "write_owned_block(): not all written" + return BlockInfo(uncompressedSize, compressedSize, block_flags, None, False) + + + def write_original_file(self, name: str, writer: EndianBinaryWriter, unchanged:Surroundings): + own_range: FileBlocksMeta = self.file_blocks_map[name] + blocks = [] + real_size = self.get_size_from_block_range(own_range) + self.base_stream.Position = own_range.start.origin + for index in range(own_range.start.n, own_range.end.n + 1): + if self.m_BlocksInfo[index].shared and own_range.start.n != own_range.end.n and ( + unchanged.prev) and index > 0 and index == own_range.start.n: + # We always drop a first shared block if the file before is unchanged + # since it's equal to the last block of that file + print_debug(f" Skipping unmodified block {index} for original {name}...") + continue + self.base_stream.Position = self.m_BlocksInfo[index].origin + # NOTE: Cases when we modify a shared block even if asked to write it directly: + # 1) the previous file changed and the index is the same as the starting + # 2) the next file changed and the index is the same as the ending + # 3) the file is contained within a single shared block + if self.m_BlocksInfo[index].shared and not unchanged.prev and ( + index == own_range.start.n) or own_range.start.n == own_range.end.n or ( + not unchanged.next and index == own_range.end.n): + print_debug(f" Writing modified block {index} for original {name}...") + block_info = self.write_owned_block(self.m_BlocksInfo, index, writer, own_range) + else: + if index in [own_range.start.n + 1, own_range.end.n-1, own_range.end.n] or ( + own_range.end.n - own_range.start.n < 6): + append = '\n...' if own_range.end.n - own_range.start.n >= 6 and index < own_range.end.n-1 else '' + print_debug(f" Writing unmodified block {index} for original {name}...{append}") + block_info = self.write_block(self.m_BlocksInfo, index, writer) + if block_info: + blocks.append(block_info) + + return (blocks, real_size) + + + def write_file(self, name: str, data: bytes, writer: EndianBinaryWriter, + block_flags: int, block_size=CHUNK_SIZE) -> Tuple[bytes, FileBlocksMeta]: + own_range = self.file_blocks_map[name] + size = len(data) + if not block_size or block_size == -1: + block_size = size + if block_flags is None: + block_flags = 0 + length = 0 + index = own_range.start.n + blocks = [] + + while length < len(data): + block_data = data[length : length + block_size] + uncompressedSize = len(block_data) + block_data, block_flags = self.compress_block(block_data, self.m_BlocksInfo, block_flags) + compressedSize = len(block_data) + written = writer.write(block_data) + assert compressedSize == written, "write_file(): not all data written" + blocks.append( + BlockInfo(uncompressedSize, compressedSize, block_flags, None, False) + ) + length += block_size + print_debug(f" Writing new block (part of {index}) for modified {name}...") + index += 1 + + return (blocks, size) + + + def write_all_files(self, writer: EndianBinaryWriter, files: dict, block_flags: int, + block_size: int) -> Tuple[List[DirectoryInfoFS], List[BlockInfo]]: + blockinfos = [] + dirinfos = [] + offset = 0 + + unchanged_array = [] + for name, f in files: + own_range = self.file_blocks_map[name] + unchanged_array.append(not getattr(f, "is_changed", False) and own_range.start.n != own_range.end.n) + + i = 0 + for name, f in files: + if not unchanged_array[i]: + data = f.save() + cur_blockinfos, real_size = self.write_file(name, data, writer, block_flags, block_size) + else: + cur_blockinfos, real_size = self.write_original_file( + name, writer, unchanged=Surroundings( + unchanged_array[i-1] if i > 0 else True, + unchanged_array[i+1] if i < len(unchanged_array) - 1 else True + ) + ) + blockinfos += cur_blockinfos + i += 1 + + dirinfos.append(DirectoryInfoFS(offset, real_size, f.flags, name)) + offset += real_size + + return (dirinfos, blockinfos) + + + def read_block(self, stream: Union[EndianBinaryReader, EndianBinaryWriter], + blocks_info: list[BlockInfo], index: int) -> bytes: + """Reads a block without any modifications (not auto-positioned)""" + if not (0 <= index <= len(blocks_info) - 1): + raise Exception(f"read_block(): index = {index} is outside of m_BlocksInfo length") + return stream.read_bytes(blocks_info[index].compressedSize) + + + def read_owned_block(self, stream, blocks_info, index: int, own_range: FileBlocksMeta=None, + force_decompress: bool=True) -> Tuple[bytes, bool]: + """Reads a block and truncates its unpacked data so that only + the data in the current file is returned""" + data = self.read_block(stream, blocks_info, index) + is_shared = blocks_info[index].shared + is_decompressed = False + + # NOTE: we always decompress border blocks since they contain both files; + # own_range can be None in case we read the entire block + if (own_range == None or force_decompress or is_shared): + data = self.decompress_block(data, blocks_info, index=index) + is_decompressed = True + + if own_range and is_shared: # last and first blocks will never be shared unless mixed + if (own_range.end.n == own_range.start.n): # always cut mixed blocks + data = data[own_range.start.block_offset : own_range.end.block_offset] + #print_debug(f"cut single index = {index} data[{own_range.start.block_offset} : {own_range.end.block_offset}]: {data[:8]}") + elif index == own_range.end.n: + data = data[:own_range.end.block_offset] # last chink borderd right + #print_debug(f"cut end index = {index} data[0 : {own_range.end.block_offset}]: {data[:8]}") + elif index == own_range.start.n: + data = data[own_range.start.block_offset:] # first chink borderd left + #print_debug(f"cut start index = {index} data[{own_range.start.block_offset} : {len(data)}]: {data[:8]}") + + return (data, is_decompressed) + + + def decompress_block(self, data: bytes, blocks_info: list[BlockInfo], + flags: int = None, index: int = None, uncompressed_size: int = None + ) -> bytes: + if flags is None and blocks_info: + flags = blocks_info[index].flags if index is not None else 0 + if uncompressed_size is None: + if index is not None and blocks_info: + uncompressed_size = blocks_info[index].uncompressedSize + else: + raise ValueError("decompress_block(): uncompressed_size not provided") + comp_flag = flags & ArchiveFlags.CompressionTypeMask + if self.Crypto is not None and flags & ArchiveFlags.UnityCNEncryption and index is not None: + data = self.Crypto.decrypt(data, index) + match comp_flag: + case CompressionFlags.LZMA: + data = CompressionHelper.decompress_lzma(data) + case CompressionFlags.LZ4 | CompressionFlags.LZ4HC: + data = CompressionHelper.decompress_lz4(data, uncompressed_size) + case CompressionFlags.LZHAM: + if CompressionHelper.supports_lzham(): + data = CompressionHelper.decompress_lzham(data, uncompressed_size) + else: + raise ModuleNotFoundError("LZHAM decompression module (pylzham) not found") + return data + + + def compress_block(self, data: bytes, blocks_info: list[BlockInfo] = None, + flags: Union[int, ArchiveFlags, ArchiveFlagsOld] = 0, index: int = None + ) -> bytes: + if flags is None and blocks_info: + flags = blocks_info[index].flags if index is not None else 0 + match flags & ArchiveFlags.CompressionTypeMask: + case CompressionFlags.LZMA: + data = CompressionHelper.compress_lzma(data) + #case CompressionFlags.LZ4: + #data = CompressionHelper.compress_lz4(data) + case CompressionFlags.LZ4 | CompressionFlags.LZ4HC: + data = CompressionHelper.compress_lz4hc(data) + flags = CompressionFlags.LZ4HC | (flags & ~ArchiveFlags.CompressionTypeMask) + case CompressionFlags.LZHAM: + if CompressionHelper.supports_lzham(): + data = CompressionHelper.compress_lzham(data) + else: + #print_debug("UnityFS - Packer: LZHAM not implemented, using LZ4") + data = CompressionHelper.compress_lz4hc(data) + flags = CompressionFlags.LZ4HC | (flags & ~ArchiveFlags.CompressionTypeMask) + if self.Crypto is not None and flags & ArchiveFlags.UnityCNEncryption and index is not None: + data = self.Crypto.encrypt(data, index) + # else no compression - data stays the same + return data, flags + diff --git a/UnityPy/files/BundleFile.py b/UnityPy/files/BundleFile.py index 4c2d8498..c3e64289 100644 --- a/UnityPy/files/BundleFile.py +++ b/UnityPy/files/BundleFile.py @@ -1,20 +1,10 @@ -# TODO: implement encryption for saving files -from collections import namedtuple -import re -from typing import Tuple, Union - +īģŋfrom typing import Tuple, Union from . import File +from .BlockStream import BlockStream from ..enums import ArchiveFlags, ArchiveFlagsOld, CompressionFlags from ..helpers import ArchiveStorageManager, CompressionHelper from ..streams import EndianBinaryReader, EndianBinaryWriter -from .. import config - -BlockInfo = namedtuple("BlockInfo", "uncompressedSize compressedSize flags") -DirectoryInfoFS = namedtuple("DirectoryInfoFS", "offset size flags path") -reVersion = re.compile(r"(\d+)\.(\d+)\.(\d+)\w.+") - - class BundleFile(File.File): format: int is_changed: bool @@ -29,28 +19,43 @@ def __init__( self, reader: EndianBinaryReader, parent: File, name: str = None, **kwargs ): super().__init__(parent=parent, name=name, **kwargs) + self.blocksReader = None + 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() + dry_run = kwargs.get("dry_run", False) + if signature == "UnityArchive": raise NotImplementedError("BundleFile - UnityArchive") elif signature in ["UnityWeb", "UnityRaw"]: - m_DirectoryInfo, blocksReader = self.read_web_raw(reader) + self.blocksReader = self.read_web_raw(reader) elif signature == "UnityFS": - m_DirectoryInfo, blocksReader = self.read_fs(reader) + self.blocksReader = self.read_fs(reader, **kwargs) else: - raise NotImplementedError(f"Unknown Bundle signature: {signature}") + raise NotImplementedError(f"Unknown Bundle {name} signature:\n{signature[:80]}") - self.read_files(blocksReader, m_DirectoryInfo) + if not dry_run and self.blocksReader: + self.read_files(self.blocksReader, self.blocks.m_DirectoryInfo, **kwargs) + + def close(self): + if hasattr(self, "blocksReader") and self.blocksReader: + self.blocksReader.close() + self.blocksReader = None + if hasattr(self, "m_BlocksInfo"): + del self.m_BlocksInfo + if hasattr(self, "m_DirectoryInfo"): + del self.m_DirectoryInfo + super().close() def read_web_raw(self, reader: EndianBinaryReader): # def read_header_and_blocks_info(self, reader:EndianBinaryReader): version = self.version if version >= 4: - _hash = reader.read_bytes(16) - crc = reader.read_u_int() + self._hash= reader.read_bytes(16) + self.crc32 = reader.read_u_int() minimumStreamedBytes = reader.read_u_int() headerSize = reader.read_u_int() @@ -75,7 +80,7 @@ def read_web_raw(self, reader: EndianBinaryReader): blocksReader = EndianBinaryReader(uncompressedBytes, offset=headerSize) nodesCount = blocksReader.read_int() - m_DirectoryInfo = [ + self.m_DirectoryInfo = [ File.DirectoryInfo( blocksReader.read_string_to_null(), # path blocksReader.read_u_int(), # offset @@ -84,111 +89,26 @@ def read_web_raw(self, reader: EndianBinaryReader): for _ in range(nodesCount) ] - return m_DirectoryInfo, blocksReader + return blocksReader - def read_fs(self, reader: EndianBinaryReader): - size = reader.read_long() + def read_fs(self, reader: EndianBinaryReader, dry_run: bool = False, **kwargs): + #assert reader != None, "Unity file system reader must be set" - # header - compressedSize = reader.read_u_int() - uncompressedSize = reader.read_u_int() - self.dataflags = reader.read_u_int() - - version = self.get_version_tuple() - # https://issuetracker.unity3d.com/issues/files-within-assetbundles-do-not-start-on-aligned-boundaries-breaking-patching-on-nintendo-switch - # Unity CN introduced encryption before the alignment fix was introduced. - # Unity CN used the same flag for the encryption as later on the alignment fix, - # so we have to check the version to determine the correct flag set. - if ( - version < (2020,) - or (version[0] == 2020 and version < (2020, 3, 34)) - or (version[0] == 2021 and version < (2021, 3, 2)) - or (version[0] == 2022 and version < (2022, 1, 1)) - ): - self.dataflags = ArchiveFlagsOld(self.dataflags) - else: - self.dataflags = ArchiveFlags(self.dataflags) - - if self.dataflags & self.dataflags.UsesAssetBundleEncryption: - self.decryptor = ArchiveStorageManager.ArchiveStorageDecryptor(reader) - - # check if we need to align the reader - # - align to 16 bytes and check if all are 0 - # - if not, reset the reader to the previous position - if self.version >= 7: - reader.align_stream(16) - self._uses_block_alignment = True - elif version >= (2019, 4): - pre_align = reader.Position - align_data = reader.read((16 - pre_align % 16) % 16) - if any(align_data): - reader.Position = pre_align - else: - self._uses_block_alignment = True - - start = reader.Position - if ( - self.dataflags & ArchiveFlags.BlocksInfoAtTheEnd - ): # kArchiveBlocksInfoAtTheEnd - reader.Position = reader.Length - compressedSize - blocksInfoBytes = reader.read_bytes(compressedSize) - reader.Position = start - else: # 0x40 kArchiveBlocksAndDirectoryInfoCombined - blocksInfoBytes = reader.read_bytes(compressedSize) - - blocksInfoBytes = self.decompress_data( - blocksInfoBytes, uncompressedSize, self.dataflags + self.blocks = BlockStream( + reader, reader.Position, reader.Length, + version=self.version, unity_version=self.version_engine, + **kwargs ) - blocksInfoReader = EndianBinaryReader(blocksInfoBytes, offset=start) - uncompressedDataHash = blocksInfoReader.read_bytes(16) - blocksInfoCount = blocksInfoReader.read_int() + self.blocks.read_meta() - m_BlocksInfo = [ - BlockInfo( - blocksInfoReader.read_u_int(), # uncompressedSize - blocksInfoReader.read_u_int(), # compressedSize - blocksInfoReader.read_u_short(), # flags - ) - for _ in range(blocksInfoCount) - ] + if kwargs.get("dry_run", False): + reader.close() + return None - nodesCount = blocksInfoReader.read_int() - m_DirectoryInfo = [ - DirectoryInfoFS( - blocksInfoReader.read_long(), # offset - blocksInfoReader.read_long(), # size - blocksInfoReader.read_u_int(), # flags - blocksInfoReader.read_string_to_null(), # path - ) - for _ in range(nodesCount) - ] + return self.blocks - if m_BlocksInfo: - self._block_info_flags = m_BlocksInfo[0].flags - - if ( - isinstance(self.dataflags, ArchiveFlags) - and self.dataflags & ArchiveFlags.BlockInfoNeedPaddingAtStart - ): - reader.align_stream(16) - - blocksReader = EndianBinaryReader( - b"".join( - self.decompress_data( - reader.read_bytes(blockInfo.compressedSize), - blockInfo.uncompressedSize, - blockInfo.flags, - i, - ) - for i, blockInfo in enumerate(m_BlocksInfo) - ), - offset=(blocksInfoReader.real_offset()), - ) - - return m_DirectoryInfo, blocksReader - - def save(self, packer=None): + def save(self, packer=None, writer=None): """ Rewrites the BundleFile and returns it as bytes object. @@ -200,12 +120,15 @@ def save(self, packer=None): 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) - writer = EndianBinaryWriter() + """ + file_header + signature (string_to_null) + format (int) + version_player (string_to_null) + version_engine (string_to_null) + """ + if writer is None: + writer = EndianBinaryWriter() writer.write_string_to_null(self.signature) writer.write_u_int(self.version) @@ -218,224 +141,86 @@ def save(self, packer=None): raise NotImplementedError( "Saving Unity Web and Raw bundles isn't supported yet" ) - # self.save_web_raw(writer) + self.save_web_raw(writer) elif self.signature == "UnityFS": if not packer or packer == "none": - self.save_fs(writer, 64, 64) + self.save_fs(writer, + meta_flags=CompressionFlags.META_UNCOMPRESSED, + block_flags=CompressionFlags.NO) + elif packer == "none+original": + self.save_fs(writer) + elif packer == "lz4+original": + self.save_fs( + writer, + meta_flags=self.blocks._fs_flags, + block_flags=CompressionFlags.LZ4HC) elif packer == "original": self.save_fs( writer, - data_flag=self.dataflags, - block_info_flag=self._block_info_flags, - ) + meta_flags=self.blocks._fs_flags, + block_flags=self.blocks._blocks_info_flag) elif packer == "lz4": - self.save_fs(writer, data_flag=194, block_info_flag=2) + self.save_fs(writer, + meta_flags=CompressionFlags.META_LZ4, + block_flags=CompressionFlags.LZ4HC) elif isinstance(packer, tuple): self.save_fs(writer, *packer) else: - raise NotImplementedError("UnityFS - Packer:", packer) - return writer.bytes - - def save_fs(self, writer: EndianBinaryWriter, data_flag: int, block_info_flag: int): - # header - # compressed blockinfo (block details & directionary) - # compressed assets - - # 0b1000000 / 0b11000000 | 64 / 192 - uncompressed - # 0b11000010 | 194 - lz4 - # 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 - - # header: - # 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, - - # block_info: - # *flag & 0x80 ? at the end : right after header - # *decompression via flag & 0x3F - # *read compressed_size -> uncompressed_size - # 0x10 offset - # *read blocks infos of the data stream - # count (int) - # ( - # uncompressed_size(uint) - # compressed_size (uint) - # flag(short) - # ) - # *decompression via info.flag & 0x3F - - # *afterwards the file positions - # file_count (int) - # ( - # 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 = [ + raise NotImplementedError(f"UnityFS: packer \"{packer}\" not implemented") + return writer + + def save_fs(self, writer: EndianBinaryWriter, meta_flags: int=None, block_flags: int=None, block_size:int=-1): + """ Saves UnityFS format file """ + """ + header + compressed blockinfo (block details & directionary) + compressed assets + + 0b1000000 / 0b11000000 | 64 / 192 - uncompressed + 0b11000010 | 194 - lz4 + block_info_flag + + 0 / 0b1000000 | 0 / 64 - uncompressed + 0b1 | 1 - lzma + 0b10 | 2 - lz4 + 0b11 | 3 - lz4hc + 0b100 | 4 - lzham + data_flag + + header: + 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, + + block_info: + *flag & 0x80 ? at the end : right after header + *decompression via flag & 0x3F + *read compressed_size -> uncompressed_size + 0x10 offset + *read blocks infos of the data stream + count (int) ( - name, - f.flags, - data_writer.write_bytes( - f.bytes - if isinstance(f, (EndianBinaryReader, EndianBinaryWriter)) - else f.save() - ), + uncompressed_size(uint) + compressed_size (uint) + flag(short) ) - for name, f in self.files.items() - ] + *decompression via info.flag & 0x3F - file_data = data_writer.bytes - data_writer.dispose() - uncompressed_data_size = len(file_data) - - # compress the data - switch = block_info_flag & 0x3F - if switch == 1: # LZMA - file_data = CompressionHelper.compress_lzma(file_data) - elif switch in [2, 3]: # LZ4, LZ4HC - file_data = CompressionHelper.compress_lz4(file_data) - elif switch == 4: # LZHAM - raise NotImplementedError - # else no compression - data stays the same - compressed_data_size = len(file_data) - - # write the block_info - # uncompressedDataHash - block_writer = EndianBinaryWriter(b"\x00" * 0x10) - # data block info - # block count - block_writer.write_int(1) - # uncompressed size - block_writer.write_u_int(uncompressed_data_size) - # compressed size - block_writer.write_u_int(compressed_data_size) - # flag - block_writer.write_u_short(block_info_flag) - - # file block info - if not data_flag & 0x40: - raise NotImplementedError( - "UnityPy always writes DirectoryInfo, so data_flag must include 0x40" + *afterwards the file positions + file_count (int) + ( + offset (long) + size (long) + flag (int) + name (string_to_null) ) - # file count - block_writer.write_int(len(files)) - offset = 0 - for f_name, f_flag, f_len in files: - # offset - block_writer.write_long(offset) - # size - block_writer.write_long(f_len) - offset += f_len - # flag - block_writer.write_u_int(f_flag) - # name - block_writer.write_string_to_null(f_name) - - # compress the block data - block_data = block_writer.bytes - block_writer.dispose() - - uncompressed_block_data_size = len(block_data) - - switch = data_flag & 0x3F - if switch == 1: # LZMA - block_data = CompressionHelper.compress_lzma(block_data) - elif switch in [2, 3]: # LZ4, LZ4HC - block_data = CompressionHelper.compress_lz4(block_data) - elif switch == 4: # LZHAM - raise NotImplementedError - - compressed_block_data_size = len(block_data) - - # write the header info - ## file size - 0 for now, will be set at the end - writer_header_pos = writer.Position - writer.write_long(0) - # compressed blockInfoBytes size - writer.write_u_int(compressed_block_data_size) - # uncompressed size - writer.write_u_int(uncompressed_block_data_size) - # compression and file layout flag - writer.write_u_int(data_flag) - - if self._uses_block_alignment: - # UnityFS\x00 - 8 - # size 8 - # comp sizes 4+4 - # flag 4 - # sum : 28 -> +8 alignment - writer.align_stream(16) - - if data_flag & 0x80: # at end of file - if data_flag & 0x200: - writer.align_stream(16) - writer.write(file_data) - writer.write(block_data) - else: - writer.write(block_data) - if data_flag & 0x200: - writer.align_stream(16) - writer.write(file_data) - - writer_end_pos = writer.Position - writer.Position = writer_header_pos - # correct file size - writer.write_long(writer_end_pos) - writer.Position = writer_end_pos - - def decompress_data( - self, - compressed_data: bytes, - uncompressed_size: int, - flags: Union[int, ArchiveFlags, ArchiveFlagsOld], - index: int = 0, - ) -> bytes: + + file list & file data + prep nodes and build up block data """ - Parameters - ---------- - compressed_data : bytes - The compressed data. - uncompressed_size : int - The uncompressed size of the data. - flags : int - The flags of the data. - - Returns - ------- - bytes - The decompressed data.""" - comp_flag = CompressionFlags(flags & ArchiveFlags.CompressionTypeMask) - - if comp_flag == CompressionFlags.LZMA: # LZMA - return CompressionHelper.decompress_lzma(compressed_data) - elif comp_flag in [CompressionFlags.LZ4, CompressionFlags.LZ4HC]: # LZ4, LZ4HC - if self.decryptor is not None and flags & 0x100: - compressed_data = self.decryptor.decrypt_block(compressed_data, index) - return CompressionHelper.decompress_lz4(compressed_data, uncompressed_size) - elif comp_flag == CompressionFlags.LZHAM: # LZHAM - raise NotImplementedError("LZHAM decompression not implemented") - else: - return compressed_data - - def get_version_tuple(self) -> Tuple[int, int, int]: - """Returns the version as a tuple.""" - version = self.version_engine - if not version or version == "0.0.0": - version = config.get_fallback_version() - return tuple(map(int, reVersion.match(version).groups())) + self.blocks.write_meta_and_files(writer, self.files.items(), meta_flags, block_flags, block_size) + #writer.close() diff --git a/UnityPy/files/File.py b/UnityPy/files/File.py index 2d496b9d..23bc651b 100644 --- a/UnityPy/files/File.py +++ b/UnityPy/files/File.py @@ -1,12 +1,15 @@ -from ..enums import FileType +from typing import Union, Type, TYPE_CHECKING from ..helpers import ImportHelper from ..streams import EndianBinaryReader, EndianBinaryWriter +if TYPE_CHECKING: + from ..environment import Environment -from collections import namedtuple -from os.path import basename - -DirectoryInfo = namedtuple("DirectoryInfo", "path offset size") +from os.path import basename, isfile, join, dirname +from os import makedirs, sep, path +from re import sub +import weakref +#from .. import config class File(object): name: str @@ -21,21 +24,35 @@ class File(object): # parent: File # environment: Environment - def __init__(self, parent=None, name: str = None, is_dependency: bool = False): + def __init__(self, parent=None, name: str = None, is_dependency: bool = False, **kwargs): self.files = {} self.is_changed = False self.cab_file = "CAB-UnityPy_Mod.resS" - self.parent = parent - self.environment = self.environment = ( - getattr(parent, "environment", parent) if parent else None - ) + if parent: + self.parent = weakref.proxy(parent) + self.environment = getattr(self.parent, "environment", self.parent) + else: + self.parent = None + self.environment = None + self.name = basename(name) if isinstance(name, str) else "" self.is_dependency = is_dependency + @staticmethod + def allowed_path(path_name): + return sub(r'[^\w(){}[]\-_\. ]|[\*\?\!]', '_', path_name) + + @staticmethod + def make_path(*args): + fp = join(*args) + if sep in fp or "/" in fp: + fp = fp.replace("/", sep) + makedirs(File.allowed_path(dirname(fp)), exist_ok=True) + return fp + 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(): @@ -43,18 +60,22 @@ def get_assets(self): elif isinstance(f, SerializedFile.SerializedFile): yield f - def get_filtered_objects(self, obj_types=[]): - if len(obj_types) == 0: + def get_filtered_assets(self, obj_types=[]): + if not obj_types: 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 isinstance(self, (BundleFile.BundleFile, WebFile.WebFile)): + for f in self.files: + if not isinstance(self.files[f], SerializedFile.SerializedFile): + continue + for obj in self.files[f].objects.values(): if obj.type in obj_types: yield obj + elif isinstance(self, SerializedFile.SerializedFile): + for obj in self.objects.values(): + if obj.type in obj_types: + yield obj + elif isinstance(f, ObjectReader.ObjectReader): + yield f def get_objects(self): for f in self.files.values(): @@ -62,32 +83,81 @@ def get_objects(self): for obj in f.objects: yield obj elif isinstance(f, SerializedFile.SerializedFile): - for obj in f.objects.values(): + for obj in f.objects: yield obj elif isinstance(f, ObjectReader.ObjectReader): yield f - def read_files(self, reader: EndianBinaryReader, files: list): + def close(self): + for f in self.files: + if self.files[f]: + self.files[f].close() + self.files = {} + self.environment = None + self.parent = None + + def dump(self, reader, embedded_file): + a_name = self.allowed_path(embedded_file.path) + size = embedded_file.size + if not isfile(a_name) or path.getsize(a_name) != size: + self.make_path(a_name) + READ_BLOCK_MAX = 314572800 + with open(a_name, "wb") as d: + block_size = min(size, READ_BLOCK_MAX) + i = size // READ_BLOCK_MAX + j = 0 + while (i >= 0): + i -= 1 + j += 1 + if size < block_size * j: + #remainder since we read from the entire file + block_size = size % READ_BLOCK_MAX + if block_size == 0: break + d.write(reader.read(block_size)) + return a_name + + def read_files(self, reader, files: list, **kwargs): + if reader is None or len(files) == 0: + return + dump = kwargs.get("dump", False) + # read file data and convert it - for node in files: - reader.Position = node.offset - name = node.path - node_reader = EndianBinaryReader( - reader.read(node.size), offset=(reader.BaseOffset + node.offset) - ) + for embedded_file in files: + name = embedded_file.path + reader.Position = embedded_file.offset + size = embedded_file.size + if isinstance(reader, BlockStream.BlockStream): + cur_reader = reader.get_file_reader(name) + else: + cur_reader = reader + #assert cur_reader.Length == size, f"Wrong unpacked file size for {name} reader.Length ({reader.Length}) != size ({size}) diff = {abs(reader.Length-size)}" + if dump: + a_name = self.dump(cur_reader, embedded_file) + node_reader = EndianBinaryReader(a_name) + else: + if not name.lower().endswith((".ress", ".resource")) and cur_reader: + node_reader = EndianBinaryReader( + cur_reader.read(size), + offset=(cur_reader.BaseOffset + embedded_file.offset) + ) + else: + node_reader = cur_reader + if not node_reader: + return f = ImportHelper.parse_file( - node_reader, self.parent, name, is_dependency=self.is_dependency + node_reader, self, name, is_dependency=self.is_dependency ) - - if isinstance(f, (EndianBinaryReader, SerializedFile.SerializedFile)): + if isinstance(f, (BlockStream.FileBlocksReader, EndianBinaryReader, + SerializedFile.SerializedFile)): if self.environment: self.environment.register_cab(name, f) # required for BundleFiles - f.flags = getattr(node, "flags", 0) + if f: + f.flags = getattr(embedded_file, "flags", 0) self.files[name] = f - def get_writeable_cab(self, name: str = None): + def get_writeable_cab(self, name: str = None, writer: EndianBinaryWriter = None): """ Creates a new cab file in the bundle that contains the given data. This is usefull for asset types that use resource files. @@ -107,7 +177,9 @@ def get_writeable_cab(self, name: str = None): "This cab already exists and isn't an EndianBinaryWriter" ) - writer = EndianBinaryWriter() + if writer is None: + writer = EndianBinaryWriter() + # try to find another resource file to copy the flags from for fname, f in self.files.items(): if fname.endswith(".resS"): @@ -148,10 +220,10 @@ def __repr__(self): return f"<{self.__class__.__name__}>" def mark_changed(self): - if isinstance(self.parent, File): + if self.parent is not None and hasattr(self.parent, "is_changed"): self.parent.mark_changed() self.is_changed = True # recursive import requires the import down here -from . import BundleFile, SerializedFile, WebFile, ObjectReader +from . import BundleFile, SerializedFile, WebFile, ObjectReader, BlockStream \ No newline at end of file diff --git a/UnityPy/files/ObjectReader.py b/UnityPy/files/ObjectReader.py index 2ebee31a..b1193c7c 100644 --- a/UnityPy/files/ObjectReader.py +++ b/UnityPy/files/ObjectReader.py @@ -1,12 +1,13 @@ from ..enums import ClassIDType - -from . import SerializedFile -from .. import classes from ..classes.Object import NodeHelper +from .. import classes from ..streams import EndianBinaryReader, EndianBinaryWriter from ..helpers import TypeTreeHelper -from ..helpers.Tpk import get_typetree_nodes -from ..exceptions import TypeTreeError +from ..helpers.Tpk import get_typetree_nodes_from_tpk +from ..exceptions import TypeTreeError, sanity_check, ReadingPastObject +from ..files import SerializedFile +from typing import Union +from .. import config class ObjectReader: @@ -16,24 +17,25 @@ class ObjectReader: class_id: int type: ClassIDType path_id: int + reader: EndianBinaryReader # serialized_type: SerializedType - _read_until: int + _last_read_pos: 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): + def __init__(self, assets_file: SerializedFile, reader: EndianBinaryReader): self.assets_file = assets_file self.reader = reader - self.data = b"" + 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 + stypes = assets_file.serialized_types # AssetStudio ObjectInfo init if assets_file.big_id_enabled: @@ -63,14 +65,14 @@ def __init__(self, assets_file, reader: EndianBinaryReader): if header.version < 16: self.class_id = reader.read_u_short() self.serialized_type = None - for typ in types: - if typ.class_id == self.type_id: - self.serialized_type = typ + for stype in stypes: + if stype.class_id == self.type_id: + self.serialized_type = stype break else: - typ = types[self.type_id] - self.serialized_type = typ - self.class_id = typ.class_id + stype = stypes[self.type_id] + self.serialized_type = stype + self.class_id = stype.class_id self.type = ClassIDType(self.class_id) @@ -101,12 +103,11 @@ def write( # 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 - # -> following solution has let to some problems, so it will be removed for now - # if self.type != ClassIDType.MonoBehaviour: - # end_pos = self.byte_start + self.byte_size - # if self._read_until and self._read_until != end_pos: - # self.reader.Position = self._read_until - # data += self.reader.read_bytes(end_pos - self._read_until) + if self.type == ClassIDType.TextAsset: + end_pos = self.byte_start + self.byte_size + if self._last_read_pos and self._last_read_pos < end_pos: + self.reader.Position = self._last_read_pos + data += self.reader.read_bytes(end_pos - self._last_read_pos) else: self.reset() data = self.reader.read(self.byte_size) @@ -133,54 +134,88 @@ def write( 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 - ) + return self.assets_file._container.path_dict.get(self.path_id) @property - def Position(self): + def Position(self) -> int: return self.reader.Position @Position.setter - def Position(self, pos): - self.reader.Position = pos + def Position(self, value: int): + self.reader.Position = value + + @property + def already_read(self) -> int: + return self.reader.Position - self.byte_start def reset(self): self.reader.Position = self.byte_start - def read(self, return_typetree_on_error: bool = True): + def read(self, return_typetree_on_error=False, safe=True): cls = getattr(classes, self.type.name, None) - obj = None if cls: - try: + if safe: + try: + obj = cls(self) + except Exception as e: + if return_typetree_on_error and config.ENABLE_TYPETREEHELPER_FALLBACK: + print_info(f"Error during the parsing of <{self.type.name} " + + f"path_id: {self.path_id}; asset_file: {self.assets_file.name}>") + print_debug(e) + print_info("Trying to return its TypeTree...") + else: + raise + else: obj = cls(self) - except Exception as e: - if return_typetree_on_error: - print(f"Error during the parsing of object {self.path_id}") - print(e) - print("Returning the typetree") - else: - raise e - if not obj: + if not obj and config.ENABLE_TYPETREEHELPER_FALLBACK: obj = self.read_typetree(wrap=True) - self._read_until = self.reader.Position + self._last_read_pos = self.reader.Position return obj + def get_raw_data(self) -> bytes: + """Gets raw ObjectReader's data (from the storage)""" + pos = self.Position + self.reset() + ret = self.reader.read_bytes(self.byte_size) + self.Position = pos + return ret + + def set_raw_data(self, data: Union[memoryview, bytes, EndianBinaryReader, "ObjectReader", EndianBinaryWriter]): + """Sets raw ObjectReader's data (without writing it to the storage)""" + if isinstance(data, (ObjectReader, EndianBinaryReader, EndianBinaryWriter)): + self.data = data.save() + else: + self.data = data + self.mark_changed() + + def mark_changed(self): + if self.assets_file: + self.assets_file.mark_changed() + + def read_the_rest(self) -> bytes: + """Returns the rest of the ObjectReader's bytes""" + if False and DEBUG and self.BaseOffset + self.Position - self.byte_start > self.byte_size: + raise ReadingPastObject(reader) + return self.read_bytes(self.byte_size - (self.Position - self.byte_start)) + + #raw_data = property(get_raw_data, set_raw_data) # don't really need it outside debugging + 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 __getattr__(self, name: str): + if hasattr(self.reader, name): + return getattr(self.reader, name) + elif name.startswith("m_"): + non_m = name[2:].lower() + if hasattr(self.reader, non_m): + return getattr(self.reader, non_m) + else: + return None + raise AttributeError(f"{self.__class__.__name__} has not attribute {name}") def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.type.name) @@ -204,40 +239,29 @@ def dump_typetree_structure(self) -> str: def get_typetree_nodes(self, nodes: list = None) -> list: if nodes: return nodes - if self.serialized_type: nodes = self.serialized_type.nodes if not nodes: - nodes = get_typetree_nodes(self.class_id, self.version) + nodes = get_typetree_nodes_from_tpk(self.class_id, self.version) if not nodes: raise TypeTreeError("There are no TypeTree nodes for this object.") return nodes - def read_typetree(self, nodes: list = None, wrap: bool = False) -> dict: - self.reset() + def read_typetree(self, nodes: list = None, wrap: bool = False, all_trees=None) -> dict: nodes = self.get_typetree_nodes(nodes) - res = TypeTreeHelper.read_typetree(nodes, self) + if not all_trees: + all_trees = self.assets_file.get_all_typetrees() + self.reset() + res = TypeTreeHelper.read_typetree(nodes, self, all_trees=all_trees) return NodeHelper(res, self.assets_file) if wrap else res def save_typetree( - self, tree: dict, nodes: list = None, writer: EndianBinaryWriter = None + self, tree: dict, nodes: list = None, writer: EndianBinaryWriter = None, all_trees:dict = None ): nodes = self.get_typetree_nodes(nodes) if not writer: writer = EndianBinaryWriter(endian=self.reader.endian) - writer = TypeTreeHelper.write_typetree(tree, nodes, writer) - data = writer.bytes + writer = TypeTreeHelper.write_typetree(tree, nodes, writer, all_trees) + data = writer.save() 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/ObjectReader.pyi b/UnityPy/files/ObjectReader.pyi new file mode 100644 index 00000000..7ed32e77 --- /dev/null +++ b/UnityPy/files/ObjectReader.pyi @@ -0,0 +1,99 @@ +from .. import classes as classes +from ..classes.Object import NodeHelper as NodeHelper +from ..enums import ClassIDType as ClassIDType +from ..exceptions import TypeTreeError as TypeTreeError +from ..files import SerializedFile as SerializedFile +from ..helpers import TypeTreeHelper as TypeTreeHelper +from ..helpers.Tpk import get_typetree_nodes as get_typetree_nodes +from ..streams import EndianBinaryReader as EndianBinaryReader, EndianBinaryWriter as EndianBinaryWriter +from _typeshed import Incomplete +from typing import Dict, List +from ..math import Quaternion, Vector2, Vector3, Vector4, Rectangle, Matrix4x4, Color + +DEBUG_TYPETREES: bool + +class ObjectReader: + byte_start: int + byte_size: int + type_id: int + class_id: int + type: ClassIDType + path_id: int + reader: EndianBinaryReader + assets_file: Incomplete + data: bytes + version: Incomplete + version2: Incomplete + platform: Incomplete + build_type: Incomplete + byte_start_offset: Incomplete + byte_header_offset: Incomplete + byte_base_offset: Incomplete + byte_size_offset: Incomplete + serialized_type: Incomplete + is_destroyed: Incomplete + stripped: Incomplete + def __init__(self, assets_file: SerializedFile, reader: EndianBinaryReader) -> None: ... + def write(self, header, writer: EndianBinaryWriter, data_writer: EndianBinaryWriter): ... + def set_raw_data(self, data) -> None: ... + @property + def container(self): ... + @property + def Position(self): ... + @property + def already_read(self) -> int: ... + def reset(self) -> None: ... + def read(self, return_typetree_on_error: bool = ...): ... + def get(self, key, default: Incomplete | None = ...): ... + def __getattr__(self, item: str): ... + def dump_typetree(self, nodes: list = ...) -> str: ... + def dump_typetree_structure(self) -> str: ... + def get_typetree_nodes(self, nodes: list = ...) -> list: ... + def read_typetree(self, nodes: list = ..., wrap: bool = ...) -> dict: ... + def save_typetree(self, tree: dict, nodes: list = ..., writer: EndianBinaryWriter = ...): ... + def get_raw_data(self) -> bytes: ... + def set_raw_data(self, data) -> None: ... + + def read(self, *args) -> None: ... + def within_size(self): ... + def read_bytes(self, num) -> bytes: ... + def read_byte(self) -> int: ... + def read_u_byte(self) -> int: ... + def read_short(self) -> int: ... + def read_int(self) -> int: ... + def read_long(self) -> int: ... + def read_u_short(self) -> int: ... + def read_u_int(self) -> int: ... + def read_u_long(self) -> int: ... + def read_float(self) -> float: ... + def read_double(self) -> float: ... + def read_boolean(self) -> bool: ... + def read_string(self, size: Incomplete | None = ..., encoding: str = ...) -> str: ... + def read_string_to_null(self, max_length: int = ...) -> str: ... + def read_aligned_string(self) -> str: ... + def align_stream(self, alignment: int = ...) -> None: ... + def read_quaternion(self) -> Quaternion: ... + def read_vector2(self) -> Vector2: ... + def read_vector3(self) -> Vector3: ... + def read_vector4(self) -> Vector4: ... + def read_rectangle_f(self) -> Rectangle: ... + def read_color4(self) -> Color: ... + def read_byte_array(self) -> bytes: ... + def read_matrix(self) -> Matrix4x4: ... + def read_array(self, command, length: int) -> list: ... + def read_array_struct(self, param: str, length: int = ...) -> list: ... + def read_boolean_array(self, length: int = ...) -> List[bool]: ... + def read_u_short_array(self, length: int = ...) -> List[int]: ... + def read_short_array(self, length: int = ...) -> List[int]: ... + def read_int_array(self, length: int = ...) -> List[int]: ... + def read_u_int_array(self, length: int = ...) -> List[int]: ... + def read_u_int_array_array(self, length: int = ...) -> List[int]: ... + def read_long_array(self, length: int = ...) -> List[int]: ... + def read_u_long_array(self, length: int = ...) -> List[int]: ... + def read_float_array(self, length: int = ...) -> List[float]: ... + def read_string_array(self, length: int = ...) -> List[str]: ... + def read_vector2_array(self, length: int = ...) -> List[Vector2]: ... + def read_vector4_array(self, length: int = ...) -> List[Vector4]: ... + def read_matrix_array(self, length: int = ...) -> List[Matrix4x4]: ... + def real_offset(self) -> int: ... + def read_the_rest(self, reader: ObjectReader) -> bytes: ... diff --git a/UnityPy/files/SerializedFile.py b/UnityPy/files/SerializedFile.py index 6177c795..d4fe2cba 100644 --- a/UnityPy/files/SerializedFile.py +++ b/UnityPy/files/SerializedFile.py @@ -1,189 +1,33 @@ -īģŋfrom ntpath import basename -import re +īģŋimport os, re +from typing import Union -from . import File, ObjectReader -from ..enums import BuildTarget, ClassIDType, CommonString +from . import File, ObjectReader, BundleFile +from .SerializedType import SerializedType +from ..enums import BuildTarget, ClassIDType, FileIDType from ..streams import EndianBinaryReader, EndianBinaryWriter -from ..helpers.TypeTreeHelper import TypeTreeNode - -from struct import Struct - -from .. import config - - -class SerializedFileHeader: - metadata_size: int - file_size: int - version: int - data_offset: int - endian: bytes - reserved: bytes - - def __init__(self, reader: EndianBinaryReader): - ( - self.metadata_size, - self.file_size, - self.version, - self.data_offset, - ) = reader.read_u_int_array(4) - - -class LocalSerializedObjectIdentifier: # script type - local_serialized_file_index: int - local_identifier_in_file: int - - def __init__(self, header: SerializedFileHeader, reader: EndianBinaryReader): - self.local_serialized_file_index = reader.read_int() - if header.version < 14: - self.local_identifier_in_file = reader.read_int() - else: - reader.align_stream() - self.local_identifier_in_file = reader.read_long() - - def write(self, header: SerializedFileHeader, writer: EndianBinaryWriter): - writer.write_int(self.local_serialized_file_index) - if header.version < 14: - writer.write_int(self.local_identifier_in_file) - else: - writer.align_stream() - writer.write_long(self.local_identifier_in_file) - - -class FileIdentifier: # external - guid: bytes - type: int - # enum { kNonAssetType = 0, kDeprecatedCachedAssetType = 1, kSerializedAssetType = 2, kMetaAssetType = 3 }; - path: str - - @property - def name(self): - return 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() - if header.version >= 5: - self.guid = reader.read_bytes(16) - self.type = reader.read_int() - self.path = reader.read_string_to_null() - - def write(self, header: SerializedFileHeader, writer: EndianBinaryWriter): - if header.version >= 6: - writer.write_string_to_null(self.temp_empty) - if header.version >= 5: - writer.write_bytes(self.guid) - writer.write_int(self.type) - writer.write_string_to_null(self.path) - - -class BuildType: - build_type: str - - def __init__(self, build_type): - self.build_type = build_type - - @property - def IsAlpha(self): - return self.build_type == "a" - - @property - def IsPatch(self): - return self.build_type == "p" - - -class SerializedType: - 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, is_ref_type: bool): - 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 ( - (is_ref_type and self.script_type_index >= 0) - or (version < 16 and self.class_id < 0) - or (version >= 16 and self.class_id == 114) - ): - self.script_id = reader.read_bytes(16) - self.old_type_hash = reader.read_bytes(16) - - if serialized_file._enable_type_tree: - if version >= 12 or version == 10: - self.nodes, self.string_data = serialized_file.read_type_tree_blob() - else: - self.nodes = serialized_file.read_type_tree() - - if version >= 21: - if is_ref_type: - self.m_ClassName = reader.read_string_to_null() - self.m_NameSpace = reader.read_string_to_null() - self.m_AssemblyName = reader.read_string_to_null() - else: - self.type_dependencies = reader.read_int_array() - - def write(self, serialized_file, writer, is_ref_type): - 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 ( - (is_ref_type and self.script_type_index >= 0) - or (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) - - if version >= 21: - if is_ref_type: - writer.write_string_to_null(self.m_ClassName) - writer.write_string_to_null(self.m_NameSpace) - writer.write_string_to_null(self.m_AssemblyName) - else: - writer.write_int_array(self.type_dependencies) +from ..classes import AssetBundle +from ..helpers.TypeTreeHelper import dump_typetree +from .. import config, classes +from ..exceptions import sanity_check +UNITY_ZERO = "0.0.0" class SerializedFile(File.File): reader: EndianBinaryReader is_changed: bool unity_version: str version: tuple - build_type: BuildType + build_type: "BuildType" target_platform: BuildTarget - types: list + serialized_types: list[SerializedType] + reference_types: list[SerializedType] script_types: list externals: list objects: dict _cache: dict assetbundle: "AssetBundle" container: "ContainerHelper" - header: SerializedFileHeader + header: "SerializedFileHeader" @property def files(self): @@ -199,12 +43,14 @@ def __init__(self, reader: EndianBinaryReader, parent=None, name=None, **kwargs) super().__init__(parent=parent, name=name, **kwargs) self.reader = reader - self.unity_version = "2.5.0f5" + self.unity_version = UNITY_ZERO self.version = (0, 0, 0, 0) self.build_type = BuildType("") self.target_platform = BuildTarget.UnknownPlatform - self._enable_type_tree = True - self.types = [] + self.type_trees_saved = False + self._use_type_trees = True + self.serialized_types = [] + self.reference_types = [] self.script_types = [] self.externals = [] self.objects = {} @@ -214,13 +60,20 @@ def __init__(self, reader: EndianBinaryReader, parent=None, name=None, **kwargs) # used by: Sprite (Texture2D (with alpha) cached), self._cache = {} self.unknown = 0 + self.assetbundle = None + self._container = ContainerHelper([]) + self._type_trees_cache = None # ReadHeader header = SerializedFileHeader(reader) self.header = header + if config.SERIALIZED_FILE_PARSE_TYPETREE is False: + self._use_type_trees = False + if header.version >= 9: - header.endian = ">" if reader.read_boolean() else "<" + is_endian = reader.read_boolean() + header.endian = ">" if is_endian else "<" header.reserved = reader.read_bytes(3) if header.version >= 22: header.metadata_size = reader.read_u_int() @@ -242,18 +95,22 @@ def __init__(self, reader: EndianBinaryReader, parent=None, name=None, **kwargs) self.target_platform = BuildTarget(self._m_target_platform) if header.version >= 13: - self._enable_type_tree = reader.read_boolean() + self.type_trees_saved = reader.read_boolean() - # ReadTypes - type_count = reader.read_int() - self.types = [SerializedType(reader, self, False) for _ in range(type_count)] + # Read Types + type_count = reader.read_u_int() + sanity_check(f"type_count of {self.name}", type_count, 0xFFFFF) + self.serialized_types = [None] * type_count + for i in range(type_count): # for debugging + self.serialized_types[i] = SerializedType(reader, self, False) self.big_id_enabled = 0 if 7 <= header.version < 14: self.big_id_enabled = reader.read_int() - # ReadObjects - object_count = reader.read_int() + # Read Objects + object_count = reader.read_u_int() + sanity_check(f"object_count of {self.name}", object_count, 0xFFFFF) self.objects = {} for _ in range(object_count): obj = ObjectReader.ObjectReader(self, reader) @@ -261,39 +118,57 @@ def __init__(self, reader: EndianBinaryReader, parent=None, name=None, **kwargs) # Read Scripts if header.version >= 11: - script_count = reader.read_int() + script_count = reader.read_u_int() + sanity_check(f"script_count of {self.name}", script_count, 0xFFFFF) self.script_types = [ LocalSerializedObjectIdentifier(header, reader) for _ in range(script_count) ] # Read Externals - externals_count = reader.read_int() + externals_count = reader.read_u_int() + sanity_check(f"externals_count of {self.name}", externals_count, 0xFFFFF) self.externals = [ FileIdentifier(header, reader) for _ in range(externals_count) ] + # Read Reference Types if header.version >= 20: - ref_type_count = reader.read_int() - self.ref_types = [ + ref_type_count = reader.read_u_int() + sanity_check(f"ref_type_count of {self.name}", ref_type_count, 0xFFFFF) + self.reference_types = [ SerializedType(reader, self, True) for _ in range(ref_type_count) ] - if config.SERIALIZED_FILE_PARSE_TYPETREE is False: - self._enable_type_tree = False - if header.version >= 5: self.userInformation = reader.read_string_to_null() # read the asset_bundles to get the containers + self.get_containers() + pass + + def get_containers(self): for obj in self.objects.values(): if obj.type == ClassIDType.AssetBundle: - self.assetbundle = obj.read_typetree(wrap=True) + try: + cls = getattr(classes, obj.type.name, None) + self.assetbundle = cls(obj) + except Exception as e: + print_info(f"Error during the parsing of <{obj.type.name} path_id: {obj.path_id}; " + + f"asset_file: {obj.assets_file.name}>") + print_debug(str(e)) + if config.ENABLE_TYPETREEHELPER_FALLBACK: + print_info("Trying to return its TypeTree...") + self.assetbundle = obj.read_typetree(wrap=True) + else: + self.assetbundle = None + self._container = ContainerHelper([]) + break self._container = ContainerHelper(self.assetbundle.m_Container) break else: self.assetbundle = None - self._container = ContainerHelper({}) + self._container = ContainerHelper([]) @property def container(self): @@ -304,106 +179,54 @@ def load_dependencies(self, possible_dependencies: list = []): Parameters ---------- - possible_dependencies : list + possible_dependencies : list[str] List of possible dependencies for cases where the target file is not listed as external. """ for file_id in self.externals: self.environment.load_file(file_id.path, True) + for dependency in possible_dependencies: - try: - self.environment.load_file(dependency, True) - except FileNotFoundError: - pass + if not os.path.isfile(dependency): + continue + self.environment.load_file(dependency, True) + + def get_ref_typetrees(self, append_ns: bool=False) -> dict: + return {v.m_NameSpace + '.' + v.m_ClassName if append_ns else ( + v.m_ClassName): v.nodes for v in self.reference_types if hasattr( + v, "m_ClassName") and hasattr(v, "m_NameSpace")} + + def get_all_typetrees(self, append_ns: bool=False) -> dict: + if self._type_trees_cache is not None: # since we can call it often + return self._type_trees_cache + self._type_trees_cache = dict() + for i, obj_type in enumerate(self.serialized_types): + name = obj_type.nodes[0].m_Type if len(obj_type.nodes) and ( + obj_type.class_id != ClassIDType.MonoBehaviour) else str(i) + self._type_trees_cache[name] = obj_type.nodes + self._type_trees_cache |= self.get_ref_typetrees(append_ns) + return self._type_trees_cache + + def get_json_typetrees(self, file_name: str=None) -> Union[str, None]: + return dump_typetree(self.get_typetrees(), file_name) def set_version(self, string_version): + #if 'XD' in string_version: + #string_version = string_version[0: string_version.find('XD')] self.unity_version = string_version - if not string_version or string_version == "0.0.0": + if not string_version or string_version == UNITY_ZERO: # weird case, but apparently can happen? # check "cant read Texture2D by 2020.3.13 f1 AssetBundle #77" for details - string_version = self.parent.version_engine - if not string_version or string_version == "0.0.0": + if isinstance(self.parent, BundleFile.BundleFile): + string_version = self.parent.version_engine + if not string_version or string_version == UNITY_ZERO: string_version = config.get_fallback_version() build_type = re.findall(r"([^\d.])", string_version) self.build_type = BuildType(build_type[0] if build_type else "") version_split = re.split(r"\D", string_version) - self.version = tuple(int(x) for x in version_split) - - 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: - level_stack[-1][1] -= 1 - - type_tree_node = TypeTreeNode( - m_Level=level, - m_Type=self.reader.read_string_to_null(), - m_Name=self.reader.read_string_to_null(), - m_ByteSize=self.reader.read_int(), - ) - - type_tree.append(type_tree_node) - if self.header.version == 2: - type_tree_node.m_VariableCount = self.reader.read_int() - - if self.header.version != 3: - type_tree_node.m_Index = self.reader.read_int() - - type_tree_node.m_TypeFlags = self.reader.read_int() - type_tree_node.m_Version = self.reader.read_int() - if self.header.version != 3: - type_tree_node.m_MetaFlag = self.reader.read_int() - - children_count = self.reader.read_int() - if children_count: - level_stack.append([level + 1, children_count]) - return type_tree - - def read_type_tree_blob(self): - reader = self.reader - number_of_nodes = self.reader.read_int() - string_buffer_size = self.reader.read_int() - - type = f"{reader.endian}hBBIIiii" - keys = [ - "m_Version", - "m_Level", - "m_TypeFlags", - "m_TypeStrOffset", - "m_NameStrOffset", - "m_ByteSize", - "m_Index", - "m_MetaFlag", - ] - if self.header.version >= 19: - type += "Q" - keys.append("m_RefTypeHash") - - node_struct = Struct(type) - struct_data = reader.read(node_struct.size * number_of_nodes) - string_buffer_reader = EndianBinaryReader( - reader.read(string_buffer_size), reader.endian - ) - - if not config.SERIALIZED_FILE_PARSE_TYPETREE: - return [], string_buffer_reader.bytes - - type_tree = [ - TypeTreeNode( - **dict(zip(keys, raw_node)), - m_Type=read_string(string_buffer_reader, raw_node[3]), - m_Name=read_string(string_buffer_reader, raw_node[4]), - ) - for i, raw_node in enumerate(node_struct.iter_unpack(struct_data)) - ] - - return type_tree, string_buffer_reader.bytes + self.version = tuple(int(x) for x in version_split[:4]) - def get_writeable_cab(self, name: str = "CAB-UnityPy_Mod.resS"): + 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. @@ -432,69 +255,81 @@ class FileIdentifierFake: return cab - def save(self, packer: str = None) -> bytes: + def save(self, + packer: str = None, writer = None, + meta_writer = None, objects_writer = None, + **kwargs) -> bytes: + # 1. header -> has to be delayed until the very end # 2. data -> types, objects, scripts, ... + # so we write the data first + + own_meta_writer = False + own_data_writer = False + if writer is None: + writer = EndianBinaryWriter() + if meta_writer is None: + meta_writer = EndianBinaryWriter() + own_meta_writer = True + if objects_writer is None: + objects_writer = EndianBinaryWriter() + own_data_writer = True - # so write the data first header = self.header - meta_writer = EndianBinaryWriter(endian=header.endian) - data_writer = EndianBinaryWriter(endian=header.endian) + meta_writer.endian = header.endian + objects_writer.endian = header.endian if header.version >= 7: meta_writer.write_string_to_null(self.unity_version) - if header.version >= 8: meta_writer.write_int(self._m_target_platform) - if header.version >= 13: - meta_writer.write_boolean(self._enable_type_tree) + meta_writer.write_boolean(self.type_trees_saved) - # ReadTypes - meta_writer.write_int(len(self.types)) - for typ in self.types: - typ.write(self, meta_writer, False) + # Save Types + meta_writer.write_int(len(self.serialized_types)) + for ser_type in self.serialized_types: + ser_type.save(meta_writer) if 7 <= header.version < 14: meta_writer.write_int(self.big_id_enabled) - # ReadObjects + # Save Objects 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) + obj.write(header, meta_writer, objects_writer) + objects_writer.align_stream(8) - # Read Scripts + # Save Scripts if header.version >= 11: meta_writer.write_int(len(self.script_types)) for script_type in self.script_types: script_type.write(header, meta_writer) - # Read Externals + # Save Externals meta_writer.write_int(len(self.externals)) for external in self.externals: external.write(header, meta_writer) 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, True) + meta_writer.write_int(len(self.reference_types)) + for ref_type in self.reference_types: + ref_type.save(meta_writer) if header.version >= 5: meta_writer.write_string_to_null(self.userInformation) # prepare header - writer = EndianBinaryWriter() header_size = 16 # 4*4 metadata_size = meta_writer.Length - data_size = data_writer.Length + objects_size = objects_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 + file_size = data_offset + objects_size if header.version < 22: writer.write_u_int(metadata_size) writer.write_u_int(file_size) @@ -517,131 +352,29 @@ def save(self, packer: str = None) -> bytes: writer.write_long(data_offset) writer.write_long(self.unknown) - writer.write_bytes(meta_writer.bytes) + writer.write_bytes(meta_writer.save()) + if own_meta_writer: + meta_writer.close() + writer.align_stream(16) - writer.write_bytes(data_writer.bytes) + writer.write_bytes(objects_writer.save()) + if own_data_writer: + objects_writer.close() else: metadata_size += 1 # endian boolean - file_size = header_size + metadata_size + data_size + file_size = header_size + metadata_size + objects_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) + # so data follows right after this header -> after 32 (0x20) + writer.write_u_int(0x20) + writer.write_bytes(objects_writer.save()) writer.write_boolean(">" == header.endian) - writer.write_bytes(meta_writer.bytes) - - return writer.bytes + writer.write_bytes(meta_writer.save()) - def save_serialized_type( - self, - typ: SerializedType, - header: SerializedFileHeader, - writer: EndianBinaryWriter, - ): - writer.write_int(typ.class_id) - - if header.version >= 16: - writer.write_boolean(typ.is_stripped_type) - - if header.version >= 17: - writer.write_short(typ.script_type_index) - - if header.version >= 13: - if (header.version < 16 and typ.class_id < 0) or ( - header.version >= 16 and typ.class_id == 114 - ): - writer.write_bytes(typ.script_id) # Hash128 - writer.write_bytes(typ.old_type_hash) # Hash128 - - if self._enable_type_tree: - if header.version >= 12 or header.version == 10: - self.save_type_tree5(typ.nodes, writer, typ.string_data) - else: - self.save_type_tree(typ.nodes, writer) - - def save_type_tree(self, nodes: list, writer: EndianBinaryWriter): - for i, node in nodes: - writer.write_string_to_null(node.m_Type) - writer.write_string_to_null(node.m_Name) - writer.write_int(node.byte_size) - if self.header.version == 2: - writer.write_int(node.m_VariableCount) - - if self.header.version != 3: - writer.write_int(node.m_Index) - - writer.write_int(node.m_TypeFlags) - writer.write_int(node.m_Version) - if self.header.version != 3: - writer.write_int(node.m_MetaFlag) - - # calc children count - children_count = 0 - for node2 in nodes[i + 1 :]: - if node2.m_Level == node.m_Level: - break - if node2.m_Level == node.m_Level - 1: - children_count += 1 - writer.write_int(children_count) - - def save_type_tree5(self, nodes: list, writer: EndianBinaryWriter, str_data=b""): - # node count - # stream buffer size - # node data - # string buffer - - string_buffer = EndianBinaryWriter() - string_buffer.write(str_data) - strings_values = [ - (node.m_TypeStrOffset, node.m_NameStrOffset) for node in nodes - ] - - # number of nodes - writer.write_int(len(nodes)) - # string buffer size - writer.write_int(string_buffer.Length) - - # nodes - for i, node in enumerate(nodes): - # version - writer.write_u_short(node.m_Version) - # level - writer.write_byte(node.m_Level) - # is array - writer.write_u_byte(node.m_TypeFlags) - # 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.m_ByteSize) - # index - writer.write_int(node.m_Index) - # meta flag - writer.write_int(node.m_MetaFlag) - # ref hash - if self.header.version > 19: - writer.write_u_long(node.m_RefTypeHash) - - # string buffer - writer.write(string_buffer.bytes) - - if self.header.version >= 21: - writer.write_bytes(b"\x00" * 4) - - -def read_string(string_buffer_reader: EndianBinaryReader, value: int) -> str: - is_offset = (value & 0x80000000) == 0 - if is_offset: - string_buffer_reader.Position = value - return string_buffer_reader.read_string_to_null() - - offset = value & 0x7FFFFFFF - return CommonString.get(offset, str(offset)) + return writer.save() class ContainerHelper: @@ -649,28 +382,32 @@ class ContainerHelper: without breaking compatibility with old versions""" def __init__(self, container) -> None: - self.container = container # support for getitem self.container_dict = {key: value.asset for key, value in container} - self.path_dict = {value.asset.path_id: value.asset for key, value in container} + self.path_dict = {value.asset.path_id: key for key, value in container} + self.container = container def items(self): return ((key, value.asset) for key, value in self.container) def keys(self): - return list({key for key, value in self.container}) + return list(key for key, _ in self.container) def values(self): - return list({value.asset for key, value in self.container}) + return list({value.asset for _, value in self.container}) def __getitem__(self, key): - return self.container_dict[key] + if isinstance(key, str): + return self.container_dict[key] + elif isinstance(key, int): + return self.path_dict[key] + return None def __setitem__(self, key, value): - raise NotImplementedError("Assigning to container is not allowed!") + raise NotImplementedError("Assigning to container is impossible") def __delitem__(self, key): - raise NotImplementedError("Deleting from the container is not allowed!") + raise NotImplementedError("Deleting from the container is impossible") def __iter__(self): return iter(self.keys()) @@ -689,3 +426,89 @@ def __str__(self): def __dict__(self): return self.container_dict + + +class SerializedFileHeader: + metadata_size: int + file_size: int + version: int + data_offset: int + endian: bytes + reserved: bytes + + def __init__(self, reader: EndianBinaryReader): + ( + self.metadata_size, + self.file_size, + self.version, + self.data_offset, + ) = reader.read_u_int_array(4) + + +class LocalSerializedObjectIdentifier: # script type + local_serialized_file_index: int + local_identifier_in_file: int + + def __init__(self, header: SerializedFileHeader, reader: EndianBinaryReader): + self.local_serialized_file_index = reader.read_int() + if header.version < 14: + self.local_identifier_in_file = reader.read_int() + else: + reader.align_stream() + self.local_identifier_in_file = reader.read_long() + + def write(self, header: SerializedFileHeader, writer: EndianBinaryWriter): + writer.write_int(self.local_serialized_file_index) + if header.version < 14: + writer.write_int(self.local_identifier_in_file) + else: + writer.align_stream() + writer.write_long(self.local_identifier_in_file) + + +class FileIdentifier: # external + guid: bytes + type: int + path: str + + @property + def name(self): + return os.path.basename(self.path) + + @property + def enum_type(self): + return FileIDType(self.type) + + 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() + if header.version >= 5: + self.guid = reader.read_bytes(16) + self.type = reader.read_int() + self.path = reader.read_string_to_null() + + def write(self, header: SerializedFileHeader, writer: EndianBinaryWriter): + if header.version >= 6: + writer.write_string_to_null(self.temp_empty) + if header.version >= 5: + writer.write_bytes(self.guid) + writer.write_int(self.type) + writer.write_string_to_null(self.path) + + +class BuildType: + build_type: str + + def __init__(self, build_type): + self.build_type = build_type + + @property + def IsAlpha(self): + return self.build_type == "a" + + @property + def IsPatch(self): + return self.build_type == "p" diff --git a/UnityPy/files/SerializedType.py b/UnityPy/files/SerializedType.py new file mode 100644 index 00000000..cd38848a --- /dev/null +++ b/UnityPy/files/SerializedType.py @@ -0,0 +1,334 @@ +from struct import Struct, pack +from typing import Union, Tuple, List, TYPE_CHECKING +import weakref + +from ..enums import ClassIDType, CommonString, CommonStringReversed +from ..streams import EndianBinaryReader, EndianBinaryWriter +from ..helpers.TypeTreeHelper import TypeTreeNode + +if TYPE_CHECKING: + from .SerializedFile import SerializedFileHeader + from . import SerializedFile + +from ..exceptions import sanity_check +from .. import config + +NODE_STRUCT = None +NODE_STRUCT_KEYS = None +NODE_STRUCT_MODIFIED = False + +def reset_globals(): + global NODE_STRUCT, NODE_STRUCT_KEYS, NODE_STRUCT_MODIFIED + NODE_STRUCT = "hBBIIiii" + NODE_STRUCT_KEYS = [ + "m_Version", + "m_Level", + "m_TypeFlags", + "m_TypeStrOffset", + "m_NameStrOffset", + "m_ByteSize", + "m_Index", + "m_MetaFlag", + ] + NODE_STRUCT_MODIFIED = False + +def set_globals(endian, version): + global NODE_STRUCT, NODE_STRUCT_KEYS, NODE_STRUCT_MODIFIED + if not NODE_STRUCT_MODIFIED: + NODE_STRUCT = f"{endian}" + NODE_STRUCT + if version >= 19: + # do it only once per run + NODE_STRUCT += 'Q' + NODE_STRUCT_KEYS.append("m_RefTypeHash") + NODE_STRUCT_MODIFIED = True + NODE_STRUCT = Struct(NODE_STRUCT) + +reset_globals() + +class SerializedType: + class_id: int + is_stripped_type: bool + is_changed: bool + script_type_index = -1 + nodes: list = [] # TypeTreeNode + script_id: bytes # Hash128 + old_type_hash: bytes # Hash128} + + def __init__(self, reader: EndianBinaryReader, serialized_file: "SerializedFile", is_ref_type: bool): + self._serialized_file = weakref.proxy(serialized_file) + self.is_ref_type = is_ref_type + self.string_data = None + self.is_changed = False + + version = serialized_file.header.version + set_globals(reader.endian, version) + + # read SerializedType from the stream + 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: + self.script_id = None + if ( + (is_ref_type and self.script_type_index >= 0) + or (version < 16 and self.class_id < 0) + or (version >= 16 and self.class_id == ClassIDType.MonoBehaviour) + ): + self.script_id = reader.read_bytes(16) + self.old_type_hash = reader.read_bytes(16) + + if self._serialized_file.type_trees_saved: + if version >= 12 or version == 10: + self.read_type_tree_blob(reader) + else: + self.read_type_tree(reader) + + if version >= 21: + if is_ref_type: + self.m_ClassName = reader.read_string_to_null() + self.m_NameSpace = reader.read_string_to_null() + self.m_AssemblyName = reader.read_string_to_null() + else: + self.type_dependencies = reader.read_int_array() + + def save(self, writer): + version = self._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 self.script_id is not None: + writer.write_bytes(self.script_id) # Hash128 + writer.write_bytes(self.old_type_hash) # Hash128 + + if self._serialized_file.type_trees_saved: + if version >= 12 or version == 10 and version != 22: + self.save_type_tree_blob(writer) + else: + self.save_type_tree(writer) + + if version >= 21: + if self.is_ref_type: + writer.write_string_to_null(self.m_ClassName) + writer.write_string_to_null(self.m_NameSpace) + writer.write_string_to_null(self.m_AssemblyName) + else: + writer.write_int_array(self.type_dependencies, True) + + + def read_type_tree(self, reader: EndianBinaryReader): + type_tree = [] + level_stack = [[0, 1]] + while level_stack: + level, count = level_stack[-1] + if count == 1: + level_stack.pop() + else: + level_stack[-1][1] -= 1 + + type_tree_node = TypeTreeNode( + m_Level=level, + m_Type=reader.read_string_to_null(), + m_Name=reader.read_string_to_null(), + m_ByteSize=reader.read_int(), + ) + + type_tree.append(type_tree_node) + if self.header.version == 2: + type_tree_node.m_VariableCount = reader.read_int() + if self.header.version != 3: + type_tree_node.m_Index = reader.read_int() + type_tree_node.m_TypeFlags = reader.read_int() + type_tree_node.m_Version = reader.read_int() + if self.header.version != 3: + type_tree_node.m_MetaFlag = reader.read_int() + + children_count = reader.read_int() + if children_count: + level_stack.append([level + 1, children_count]) + self.nodes = type_tree + + + def save_type_tree(self, writer: EndianBinaryWriter): + nodes = self.nodes + for i, node in nodes: + writer.write_string_to_null(node.m_Type) + writer.write_string_to_null(node.m_Name) + writer.write_int(node.byte_size) + if self.header.version == 2: + writer.write_int(node.m_VariableCount) + + if self.header.version != 3: + writer.write_int(node.m_Index) + + writer.write_int(node.m_TypeFlags) + writer.write_int(node.m_Version) + if self.header.version != 3: + writer.write_int(node.m_MetaFlag) + + # calc children count + children_count = 0 + for node2 in nodes[i + 1 :]: + if node2.m_Level == node.m_Level: + break + if node2.m_Level == node.m_Level - 1: + children_count += 1 + writer.write_int(children_count) + + + def read_type_tree_blob(self, reader: EndianBinaryReader): + """Reads serialized TypeTree blob""" + """ layout: nodes count, string data size, + struct data (32 * nodes count), string data + """ + nodes_count = reader.read_u_int() + nodes_data_size = reader.read_u_int() + sanity_check("number_of_nodes of " + + f"{self._serialized_file.name} for {ClassIDType(self.class_id)}", + nodes_count, 0xFFFFF) + self.struct_data = reader.read(NODE_STRUCT.size * nodes_count) + self.string_data = reader.read(nodes_data_size) + if not self._serialized_file._use_type_trees: + return + + strings_reader = EndianBinaryReader(self.string_data, reader.endian) + self.nodes = [None] * nodes_count + for i, raw_node in enumerate(NODE_STRUCT.iter_unpack(self.struct_data)): + m_Type = self.read_common_string(strings_reader, raw_node[3]) + m_Name = self.read_common_string(strings_reader, raw_node[4]) + self.nodes[i] = TypeTreeNode( + **dict(zip(NODE_STRUCT_KEYS, raw_node)), m_Type=m_Type, m_Name=m_Name + ) + + if False and config.DEBUG: + blob_names = [] + list(filter(lambda x: blob_names.append(x) if x not in blob_names and ( + x not in CommonStringReversed.keys()) else False, [i.m_Type for i in self.nodes])) + list(filter(lambda x: blob_names.append(x) if x not in blob_names and ( + x not in CommonStringReversed.keys()) else False, [i.m_Name for i in self.nodes])) + total_buf_size = sum(len(i.encode("utf-8")) + 1 for i in blob_names) + if total_buf_size < strings_reader.Length: + self._tail = strings_reader.read_the_rest() # data after the nodes? + + + def save_type_tree_blob(self, writer: EndianBinaryWriter): + # node count + # stream data size + # node data + # string data + nodes = self.nodes + + # number of nodes + writer.write_u_int(len(nodes)) + # string data size + raw_data_len_pos = writer.Position + writer.write_u_int(len(self.string_data)) + + if not self.is_changed: + # no need for a fancy parsing if it's the same data + writer.write(self.struct_data) + writer.write(self.string_data) + return + + # recreate strings data + string_buffer = EndianBinaryWriter(endian=writer.endian) + name_offsets = {} + type_offsets = {} + for node in nodes: + t_offset = self.get_common_offset(node.m_Type) + if t_offset is not None: + if not t_offset in type_offsets: + type_offsets[node.m_Type] = t_offset + elif not node.m_Type in type_offsets: + type_offsets[node.m_Type] = string_buffer.Position + string_buffer.write_string_to_null(node.m_Type) + n_offset = self.get_common_offset(node.m_Name) + if n_offset is not None: + if not n_offset in name_offsets: + name_offsets[node.m_Name] = n_offset + elif not node.m_Name in name_offsets: + name_offsets[node.m_Name] = string_buffer.Position + string_buffer.write_string_to_null(node.m_Name) + + # recreate nodes data + for node in nodes: + # version + writer.write_u_short(node.m_Version) + # level + writer.write_byte(node.m_Level) + # is array + writer.write_u_byte(node.m_TypeFlags) + # type str offset + writer.write_u_int(type_offsets[node.m_Type]) + # name str offset + writer.write_u_int(name_offsets[node.m_Name]) + # byte size + writer.write_int(node.m_ByteSize) + # index + writer.write_int(node.m_Index) + # meta flag + writer.write_int(node.m_MetaFlag) + # ref hash + if self._serialized_file.header.version > 19: + writer.write_u_long(node.m_RefTypeHash) + + # write the proper strings data length + after_pos = writer.Position + writer.Position = raw_data_len_pos + writer.write_u_int(string_buffer.Length) + writer.Position = after_pos + + # write the strings data + writer.write(string_buffer.save()) + + # unknown remaining data? + if getattr(self, "_tail", None) and self._tail: + string_buffer.write(self._tail) + + + @staticmethod + def compute_hash(type_tree): + import hashlib + md4 = hashlib.new("md4") + + strings_values = [ + (node.m_TypeStrOffset, node.m_NameStrOffset) for node in type_tree + ] + for i, node in enumerate(type_tree): + md4.update(strings_values[i][0].encode("utf-8")) + md4.update(strings_values[i][1].encode("utf-8")) + md4.update(pack('i', node.m_ByteSize)) + md4.update(pack('i', int(node.m_TypeFlags))) + md4.update(pack('i', int(node.m_Version))) + md4.update(pack('i', int(node.m_MetaFlags & 0x4000))) + + return md4.digest() + + + @staticmethod + def read_common_string(reader: EndianBinaryReader, offset: int) -> str: + #assert offset & 0x80000000 < reader.Length, "Wrong offset of TypeTree node string data" + is_offset = (offset & 0x80000000) == 0 + if is_offset: + reader.Position = offset + return reader.read_string_to_null() + + offset &= 0x7FFFFFFF + return CommonString.get(offset, str(offset)) + + @staticmethod + def get_common_offset(name: str) -> Union[int, None]: + name = CommonStringReversed.get(name) + if name is not None: + return name | ~0x7FFFFFFF & 0xFFFFFFFF + return None diff --git a/UnityPy/files/WebFile.py b/UnityPy/files/WebFile.py index d0d48386..0c893cc2 100644 --- a/UnityPy/files/WebFile.py +++ b/UnityPy/files/WebFile.py @@ -1,6 +1,7 @@ īģŋfrom . import File from ..helpers import CompressionHelper from ..streams import EndianBinaryReader, EndianBinaryWriter +from ..streams.EndianBinaryReader import SYS_ENDIAN class WebFile(File.File): @@ -9,19 +10,18 @@ class WebFile(File.File): files -- list of all files in the WebFile """ - - def __init__(self, reader: EndianBinaryReader, parent: File, name=None): - """Constructor Method - """ - super().__init__(parent=parent, name=name) - + + def __init__(self, reader: EndianBinaryReader, parent: File, name=None, **kwargs): + """Constructor Method""" + super().__init__(parent=parent, name=name, **kwargs) + # check compression magic = reader.read_bytes(2) reader.Position = 0 - + if magic == CompressionHelper.GZIP_MAGIC: self.packer = "gzip" - data = CompressionHelper.decompress_gzip(reader.bytes) + data = CompressionHelper.decompress_gzip(reader.save()) reader = EndianBinaryReader(data, endian="<") else: reader.Position = 0x20 @@ -29,21 +29,21 @@ def __init__(self, reader: EndianBinaryReader, parent: File, name=None): reader.Position = 0 if CompressionHelper.BROTLI_MAGIC == magic: self.packer = "brotli" - data = CompressionHelper.decompress_brotli(reader.bytes) + data = CompressionHelper.decompress_brotli(reader.save()) reader = EndianBinaryReader(data, endian="<") 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() @@ -51,32 +51,34 @@ def __init__(self, reader: EndianBinaryReader, parent: File, name=None): path_length = reader.read_int() name = bytes(reader.read_bytes(path_length)).decode("utf-8") 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", + writer = None ) -> 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() + name: f.save() if isinstance(f, EndianBinaryReader) else f.save() for name, f in files.items() } - - # create writer - writer = EndianBinaryWriter(endian="<") + + if writer is None: + # create writer + writer = EndianBinaryWriter(endian=SYS_ENDIAN) # signature writer.write_string_to_null(signature) - + # data offset offset = sum( [ @@ -88,9 +90,9 @@ def save( 4, # offset int ] ) - + writer.write_int(offset) - + # 1. file headers for name, data in files.items(): # offset @@ -103,15 +105,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) + return CompressionHelper.compress_gzip(writer.save()) elif packer == "brotli": - return CompressionHelper.compress_brotli(writer.bytes) + return CompressionHelper.compress_brotli(writer.save()) else: - return writer.bytes + return writer.save() diff --git a/UnityPy/files/__init__.py b/UnityPy/files/__init__.py index bdaecc90..7d0371a1 100644 --- a/UnityPy/files/__init__.py +++ b/UnityPy/files/__init__.py @@ -1,5 +1,7 @@ -from .File import File, DirectoryInfo +from .File import File +from .SerializedType import SerializedType from .SerializedFile import SerializedFile from .BundleFile import BundleFile from .WebFile import WebFile from .ObjectReader import ObjectReader +from .BlockStream import BlockStream, FileBlocksReader diff --git a/UnityPy/helpers/ArchiveStorageManager.py b/UnityPy/helpers/ArchiveStorageManager.py index 6ddd2a2c..06a885c0 100644 --- a/UnityPy/helpers/ArchiveStorageManager.py +++ b/UnityPy/helpers/ArchiveStorageManager.py @@ -4,6 +4,11 @@ from ..streams import EndianBinaryReader +try: + from UnityPy import UnityPyBoost +except ImportError: + UnityPyBoost = None + UNITY3D_SIGNATURE = b"#$unity3dchina!@" DECRYPT_KEY: bytes = None @@ -47,11 +52,11 @@ def brute_force_key( matches = pattern.findall(data) for i, key in enumerate(matches): if verbose: - print(f"Trying {i + 1}/{len(matches)} - {key}") + print_info(f"Trying {i + 1}/{len(matches)} - {key}") signature = decrypt_key(key_sig, data_sig, key) if signature == UNITY3D_SIGNATURE: if verbose: - print(f"Found key: {key}") + print_info(f"Found key: {key}") return key return None @@ -100,7 +105,10 @@ def __init__(self, reader: EndianBinaryReader) -> None: data[0x10 + i * 4 + j] for j in range(4) for i in range(4) ) - def decrypt_block(self, data: bytes, index: int): + def decrypt_block(self, index: int, data: bytes): + if UnityPyBoost: + return UnityPyBoost.decrypt_block(self.index, self.substitute, data, index) + offset = 0 size = len(data) data = bytearray(data) diff --git a/UnityPy/helpers/CompressionHelper.py b/UnityPy/helpers/CompressionHelper.py index f6233b5a..80a5370c 100644 --- a/UnityPy/helpers/CompressionHelper.py +++ b/UnityPy/helpers/CompressionHelper.py @@ -1,17 +1,31 @@ import gzip import lzma import struct - -import brotli +from typing import Tuple import lz4.block +NO_BROTLI= False +try: + import brotli +except: + NO_BROTLI = True +NO_LZHAM = False +try: + import lzham +except: + NO_LZHAM = True GZIP_MAGIC: bytes = b"\x1f\x8b" BROTLI_MAGIC: bytes = b"brotli" +def supports_brotli() -> bool: + return not NO_BROTLI + +def supports_lzham() -> bool: + return not NO_LZHAM # LZMA -def decompress_lzma(data: bytes) -> bytes: - """decompresses lzma-compressed data +def decompress_lzma(data: bytes, read_decompressed_size: bool = False) -> bytes: + """Decompresses LZMA-compressed data :param data: compressed data :type data: bytes @@ -21,10 +35,10 @@ def decompress_lzma(data: bytes) -> bytes: """ props, dict_size = struct.unpack(" bytes: } ], ) - return dec.decompress(data[5:]) + data_offset = 13 if read_decompressed_size else 5 + return decompressor.decompress(data[data_offset:]) -def compress_lzma(data: bytes) -> bytes: - """compresses data via lzma (unity specific) +def compress_lzma(data: bytes, write_decompressed_size: bool = False) -> bytes: + """LZMA-compresses data (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. @@ -49,19 +64,34 @@ def compress_lzma(data: bytes) -> bytes: :return: compressed data :rtype: bytes """ - ec = lzma.LZMACompressor( + dict_size = 0x800000 # 1 << 23 + compressor = 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": dict_size, + "lc": 3, + "lp": 0, + "pb": 2, + "mode": lzma.MODE_NORMAL, + "mf": lzma.MF_BT4, + "nice_len": 123, + } ], ) - ec.compress(data) - return b"]\x00\x00\x08\x00" + ec.flush() + + compressed_data = compressor.compress(data) + compressor.flush() + cdl = len(compressed_data) + if write_decompressed_size: + return struct.pack(f" bytes: # LZ4M/LZ4HC - """decompresses lz4-compressed data + """Decompresses LZ4-compressed data :param data: compressed data :type data: bytes @@ -73,9 +103,18 @@ def decompress_lz4(data: bytes, uncompressed_size: int) -> bytes: # LZ4M/LZ4HC """ return lz4.block.decompress(data, uncompressed_size) +def compress_lz4(data: bytes) -> bytes: # LZ4M + """Compresses data via lz4.block -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 + """ + return lz4.block.compress(data, mode="default", store_size=False) + +def compress_lz4hc(data: bytes) -> bytes: # LZ4HC + """Compresses data via lz4.block :param data: uncompressed data :type data: bytes @@ -86,10 +125,41 @@ def compress_lz4(data: bytes) -> bytes: # LZ4M/LZ4HC data, mode="high_compression", compression=9, store_size=False ) +# LZ4 +def decompress_lzham(data: bytes, uncompressed_size: int) -> bytes: # LZ4M/LZ4HC + """Decompresses LZHAM-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 + """ + if NO_LZHAM: + raise Exception("package pylzham is not installed") + lzham_decompressor = LZHAMDecompressor() + return lzham_decompressor.decompress(data, uncompressed_size) + + +def compress_lzham(data: bytes) -> bytes: # LZ4M/LZ4HC + """Compresses LZHAM data via lz4.block + + :param data: uncompressed data + :type data: bytes + :return: compressed data + :rtype: bytes + """ + if NO_LZHAM: + raise Exception("package pylzham is not installed") + lzham_decompressor = LZHAMDecompressor() + return lzham_decompressor.compress(data) + # Brotli def decompress_brotli(data: bytes) -> bytes: - """decompresses brotli-compressed data + """Decompresses Brotli-compressed data :param data: compressed data :type data: bytes @@ -97,23 +167,27 @@ def decompress_brotli(data: bytes) -> bytes: :return: uncompressed data :rtype: bytes """ + if NO_BROTLI: + raise Exception("package Brotli is not installed") return brotli.decompress(data) def compress_brotli(data: bytes) -> bytes: - """compresses data via brotli + """Compresses data via Brotli :param data: uncompressed data :type data: bytes :return: compressed data :rtype: bytes """ + if NO_BROTLI: + raise Exception("package Brotli is not installed") return brotli.compress(data) # GZIP def decompress_gzip(data: bytes) -> bytes: - """decompresses gzip-compressed data + """Decompresses GZip-compressed data :param data: compressed data :type data: bytes @@ -125,7 +199,7 @@ def decompress_gzip(data: bytes) -> bytes: def compress_gzip(data: bytes) -> bytes: - """compresses data via gzip + """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. @@ -135,3 +209,4 @@ def compress_gzip(data: bytes) -> bytes: :rtype: bytes """ return gzip.compress(data) + diff --git a/UnityPy/helpers/D3DDisasmHelper.py b/UnityPy/helpers/D3DDisasmHelper.py new file mode 100644 index 00000000..41ae88c0 --- /dev/null +++ b/UnityPy/helpers/D3DDisasmHelper.py @@ -0,0 +1,60 @@ +import ctypes +from ctypes import wintypes + +D3DCOMPILER_DLL = "D3dcompiler_47.dll" +D3D_DISASM_ENABLE_DEFAULT_VALUE_PRINT = 0x2 +D3D_DISASM_ENABLE_INSTRUCTION_NUMBERING = 0x4 +D3D_DISASM_ENABLE_COLOR_CODE = 0x1 +D3D_DISASM_ENABLE_COMMENT_PRINT = 0x8 +D3D_DISASM_FORCE_DWORD = 0x7fffffff + +# Load D3dcompiler_47.dll +d3dcompiler_47 = ctypes.WinDLL(D3DCOMPILER_DLL) + +# Define function prototype in Python +D3DDisassemble = d3dcompiler_47.D3DDisassemble +D3DDisassemble.argtypes = [ + wintypes.LPCVOID, # pSrcData + ctypes.c_size_t, # SrcDataSize + ctypes.c_uint, # Flags + ctypes.c_char_p, # szComments + ctypes.POINTER(ctypes.c_void_p) # ppDisassembly (ID3DBlob**) +] +D3DDisassemble.restype = wintypes.HRESULT + +class ID3DBlob(ctypes.Structure): + _fields_ = [ + ("lpVtbl", ctypes.c_void_p), + ] + +class ID3DBlobVtbl(ctypes.Structure): + _fields_ = [ + ("QueryInterface", ctypes.c_void_p), + ("AddRef", ctypes.c_void_p), + ("Release", ctypes.c_void_p), + ("GetBufferPointer", ctypes.c_void_p), + ("GetBufferSize", ctypes.c_void_p), + ] + +def decompile_shader(shader_bytes, flags=D3D_DISASM_ENABLE_DEFAULT_VALUE_PRINT): + blob = ctypes.c_void_p() + hresult = D3DDisassemble(shader_bytes, len(shader_bytes), flags, None, ctypes.byref(blob)) + if hresult != 0: + raise Exception(f"Failed to disassemble shader, hresult={hresult}") + + blob_interface = ID3DBlob.from_address(blob) + vtbl_pointer = ctypes.cast(blob_interface.lpVtbl, ctypes.POINTER(ID3DBlobVtbl)) + vtbl = vtbl_pointer.contents + + buffer_pointer_func = ctypes.cast(vtbl.GetBufferPointer, ctypes.CFUNCTYPE(ctypes.c_void_p)) + buffer = buffer_pointer_func(blob_interface) + + buffer_size_func = ctypes.cast(vtbl.GetBufferSize, ctypes.CFUNCTYPE(ctypes.c_size_t)) + size = buffer_size_func(blob_interface) + + shader_code = str(ctypes.string_at(buffer, size)) + + release_func = ctypes.cast(vtbl.Release, ctypes.CFUNCTYPE(ctypes.c_ulong)) + release_func(blob_interface) + return shader_code + diff --git a/UnityPy/helpers/ForceCRC32.py b/UnityPy/helpers/ForceCRC32.py new file mode 100644 index 00000000..6ed489c1 --- /dev/null +++ b/UnityPy/helpers/ForceCRC32.py @@ -0,0 +1,155 @@ +import zlib + +# ---- Algorithms ---- + +POLYNOMIAL = 0x104C11DB7 +MASK = (1 << 32) - 1 + +def get_reversed_crc32(stream, offset = 0): + return bit_reverse(get_crc32(stream, offset)) + +def get_crc32(stream, offset = 0, block_size=128 * 1024): + stream.seek(offset) + crc = 0 + while True: + buf = stream.read(block_size) + if len(buf) == 0: + return crc & MASK + else: + crc = zlib.crc32(buf, crc) + +def bit_reverse(x, bits=32): + y = 0 + for _ in range(bits): + y = (y << 1) | (x & 1) + x >>= 1 + return y + +# ---- Polynomial arithmetic ---- + +# Returns polynomial x multiplied by polynomial y modulo the generator polynomial. +def multiply_mod(x, y): + # Russian peasant multiplication algorithm + z = 0 + while y != 0: + z ^= x * (y & 1) + y >>= 1 + x <<= 1 + if x & (1 << 32) != 0: + x ^= POLYNOMIAL + return z + +# Returns polynomial x to the power of natural number y modulo the generator polynomial. +def pow_mod(x, y): + # Exponentiation by squaring + z = 1 + while y != 0: + if y & 1 != 0: + z = multiply_mod(z, x) + x = multiply_mod(x, x) + y >>= 1 + return z + +# Computes polynomial x divided by polynomial y, returning the quotient and remainder. +def divide_and_remainder(x, y): + if y == 0: + raise ValueError("Division by zero") + if x == 0: + return (0, 0) + + ydeg = get_degree(y) + z = 0 + for i in range(get_degree(x) - ydeg, -1, -1): + if (x & (1 << (i + ydeg)) != 0): + x ^= y << i + z |= 1 << i + return (z, x) + +# Returns the reciprocal of polynomial x with respect to the modulus polynomial m. +def reciprocal_mod(x): + # Based on a simplification of the extended Euclidean algorithm + y = x + x = POLYNOMIAL + a = 0 + b = 1 + while (y != 0): + divrem = divide_and_remainder(x, y) + c = a ^ multiply_mod(divrem[0], b) + x = y + y = divrem[1] + a = b + b = c + if x == 1: + return a + else: + raise ValueError("Reciprocal does not exist") + +def get_degree(x): + if x == 0: + return -1 + i = 0 + while True: + if x >> i == 1: + return i + i += 1 + +# ---- Main ---- + +def patch_at(stream, make_crc, offset, length): + if offset is None: + raise ValueError("Must provide valid offset to write to") + make_crc = bit_reverse(make_crc) + crc = get_reversed_crc32(stream) + + # Compute the change to make + delta = crc ^ make_crc + delta = multiply_mod(reciprocal_mod(pow_mod(2, (length - offset) * 8)), delta) + + # Patch 4 bytes in the file + stream.seek(offset) + bytes4 = bytearray(stream.read(4)) + for i in range(4): + bytes4[i] ^= (bit_reverse(delta) >> (i * 8)) & 0xFF + stream.seek(offset) + stream.write(bytes4) + new_crc = get_reversed_crc32(stream) + #print(f"New CRC {new_crc:08X} {'!' if new_crc != new_crc else '='}= supposed {make_crc:08X}") + +def patch_after(stream, make_crc, length): + make_crc = bit_reverse(make_crc) + original_crc = get_reversed_crc32(stream) + + stream.seek(length) + bytes4 = bytearray(b'\0' * 4) + stream.write(bytes4) + crc = get_reversed_crc32(stream) + + # Calculate the change to make the patched stream like the original + base_delta = crc ^ original_crc + delta = multiply_mod(reciprocal_mod(pow_mod(2, 4 * 8)), base_delta) + for i in range(4): + bytes4[i] = (bit_reverse(delta) >> (i * 8)) & 0xFF + + # Patch 4 bytes in the file + stream.seek(length) + stream.write(bytes4) + crc = get_reversed_crc32(stream) + + # Calculate the change to make the patched stream like the target + base_delta = crc ^ make_crc + delta = multiply_mod(reciprocal_mod(pow_mod(2, 4 * 8)), base_delta) + + for i in range(4): + bytes4[i] ^= (bit_reverse(delta) >> (i * 8)) & 0xFF + stream.seek(length) + stream.write(bytes4) + #new_crc = get_reversed_crc32(stream) + #print(f"New CRC {new_crc:08X} {('!' if new_crc != make_crc else '=')}= supposed {make_crc:08X}") + pass + +if __name__=="__main__": + from io import BytesIO + + test = BytesIO(b'123456789') + patch_after(test, 0xFFFFFF,9) + #patch_at(test, 0xFFFFFF, 0, 9) diff --git a/UnityPy/helpers/GameObjectNode.py b/UnityPy/helpers/GameObjectNode.py new file mode 100644 index 00000000..f9250a71 --- /dev/null +++ b/UnityPy/helpers/GameObjectNode.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +import os +from ..enums import ClassIDType +from ..classes import GameObject + +class GameObjectNode(object): + "GameObject Tree structure" + + SPACER = ' ' + BRANCH = '│ ' + TEE = '├── ' + LAST = '└── ' + + def __init__(self, name: str='', game_obj: GameObject=None, parent: GameObjectNode=None, children: list=None, attachments: list=None): + self.name: str = name + self.content: GameObject = game_obj + self.parent: GameObjectNode = parent + self.children: list = [] + self.attachments: list = [] + if children is not None: + for child in children: + self.add_child(child) + if attachments is not None: + for attachment in attachments: + self.add_attachment(attachment) + + def __repr__(self): + typ = ((':' + self.content.type.name) if self.content.type != ClassIDType.GameObject else '') + return f"" + + def add_child(self, node): + if node is None: + return + assert isinstance(node, GameObjectNode) + node.parent = self + self.children.append(node) + + def add_attachment(self, node): + if node is None: + return + self.attachments.append(node) + + def find_path_up(self): + path = [allowed_path(self.name)] if self.name else [] + p = self + while p := p.parent: + path += [allowed_path(p.name)] + return os.sep.join(reversed(path)).strip("\\") + + def find_child(self, path_id): + if not path_id: return None + if self.content.m_PathID == self.m_PathID: return self + for node in self.attachments: + if node.m_PathID == path_id: + return node + for node in self.children: + if len(node.children): + ret = node.find_child(path_id) + if ret is not None: + return ret + return None + + def print_tree(self, prefix: str=''): + contents = self.attachments + pointers = [self.TEE] * (len(contents) - 1) + ([self.LAST] if not len(self.children) else [self.TEE]) + for pointer, node in zip(pointers, contents): + yield prefix + pointer + node.type.name + f" (m_FileID: {node.m_FileID}; m_PathID: {node.m_PathID})" + contents = self.children + pointers = [self.TEE] * (len(contents) - 1) + [self.LAST] + for pointer, node in zip(pointers, contents): + yield prefix + pointer + node.name + extension = self.BRANCH if pointer == self.TEE else self.SPACER + yield from node.print_tree(prefix=prefix+extension) diff --git a/UnityPy/helpers/ImportHelper.py b/UnityPy/helpers/ImportHelper.py index 53599ef5..55142c05 100644 --- a/UnityPy/helpers/ImportHelper.py +++ b/UnityPy/helpers/ImportHelper.py @@ -1,10 +1,12 @@ -īģŋimport os +īģŋfrom __future__ import annotations +import os from typing import Union, List from .CompressionHelper import BROTLI_MAGIC, GZIP_MAGIC from ..enums import FileType from ..streams import EndianBinaryReader from .. import files +DEBUG = False def file_name_without_extension(file_name: str) -> str: return os.path.join( @@ -43,6 +45,8 @@ def find_all_files(directory: str, search_str: str) -> List[str]: def check_file_type(input_) -> Union[FileType, EndianBinaryReader]: if isinstance(input_, str) and os.path.isfile(input_): reader = EndianBinaryReader(open(input_, "rb")) + elif isinstance(input_, (files.BlockStream, files.FileBlocksReader)): + return FileType.ResourceFile, input_ elif isinstance(input_, EndianBinaryReader): reader = input_ else: @@ -55,6 +59,8 @@ def check_file_type(input_) -> Union[FileType, EndianBinaryReader]: return FileType.ResourceFile, reader signature = reader.read_string_to_null(20) + if DEBUG: + print_debug(signature) reader.Position = 0 if signature in [ @@ -127,17 +133,39 @@ def parse_file( name: str, typ: FileType = None, is_dependency=False, -): + **kwargs +) -> Union[files.File, files.FileBlockStream, EndianBinaryReader]: + lc_name = name.lower() + if lc_name.endswith( + (".config", ".xml", ".dat", ".info", ".json", ".py", ".dll", ".exe")): + return None + if lc_name.endswith((".ress", ".resource")): + return reader if typ is None: typ, _ = check_file_type(reader) - if typ == FileType.AssetsFile and not name.endswith( - (".resS", ".resource", ".config", ".xml", ".dat") - ): - f = files.SerializedFile(reader, parent, name=name, is_dependency=is_dependency) + if typ == FileType.AssetsFile: + f = files.SerializedFile(reader, parent, name=name, is_dependency=is_dependency, **kwargs) elif typ == FileType.BundleFile: - f = files.BundleFile(reader, parent, name=name, is_dependency=is_dependency) + f = files.BundleFile(reader, parent, name=name, is_dependency=is_dependency, **kwargs) elif typ == FileType.WebFile: - f = files.WebFile(reader, parent, name=name, is_dependency=is_dependency) + f = files.WebFile(reader, parent, name=name, is_dependency=is_dependency, **kwargs) else: f = reader return f + + +def find_sensitive_path(dir: str, insensitive_path: str) -> Union[str, None]: + parts = os.path.split(insensitive_path.strip(os.path.sep)) + + sensitive_path = dir + for part in parts: + part_lower = part.lower() + part = next( + (name for name in os.listdir(sensitive_path) if name.lower() == part_lower), + None, + ) + if part is None: + return None + sensitive_path = os.path.join(sensitive_path, part) + + return sensitive_path diff --git a/UnityPy/helpers/ResourceReader.py b/UnityPy/helpers/ResourceReader.py index b624957a..8dc3c295 100644 --- a/UnityPy/helpers/ResourceReader.py +++ b/UnityPy/helpers/ResourceReader.py @@ -1,6 +1,7 @@ -import ntpath +import os, glob, re from ..streams import EndianBinaryReader from ..files import File +from .. import config def get_resource_data(*args): @@ -20,32 +21,63 @@ def get_resource_data(*args): """ if len(args) == 4: res_path, assets_file, offset, size = args - basename = ntpath.basename(res_path) - name, ext = ntpath.splitext(basename) - possible_names = { + + basename = os.path.basename(res_path) + if basename == "": + basename = os.path.basename(getattr(assets_file, "name", "")) + name, _ = os.path.splitext(basename) + pure_name = basename[:basename.find('.')] + + possible_names = [ basename, - f"{name}.resource", - f"{name}.assets.resS", - f"{name}.resS", - } + f"{pure_name}.assets.resS", + f"{pure_name}.resource", + f"{pure_name}.resS", + ] + if name != pure_name: + possible_names += [ + f"{name}.resource", + f"{name}.assets.resS", + f"{name}.resS" + ] + possible_names = list(dict.fromkeys(possible_names)) + environment = assets_file.environment reader = None - for possible_name in possible_names: - reader = environment.get_cab(possible_name) + for name in possible_names: + reader = environment.get_cab(name) if reader: + if isinstance(reader, File.__class__): continue break if not reader: - assets_file.load_dependencies(possible_names) - for possible_name in possible_names: - reader = environment.get_cab(possible_name) + for name in possible_names: + if not os.path.isfile(name): continue + reader = environment.load_file(name, True) if reader: + environment.register_cab(name, reader) + break + if config.EXTENDED_SEARCH and not reader: + for name in possible_names: + result = search_resource_file(res_path, name) + reader = environment.load_file(result, True) + if reader: + environment.register_cab(name, reader) break - if not reader: - raise FileNotFoundError(f"Resource file {basename} not found") elif len(args) == 3: reader, offset, size = args else: - raise TypeError(f"3 or 4 arguments required, but only {len(args)} given") + raise TypeError(f"3 or 4 arguments required, but {len(args)} given") + if not reader: + print_warning(f"Resource file {(basename + ' ') if basename else ''}not found or no reader specified") + return None reader.Position = offset return reader.read_bytes(size) + +def search_resource_file(path, name): + #nocase = lambda w: ''.join((f'[{c.lower()}{c.upper()}]' if c.isalpha() else c) for c in w) + #name = nocase(name) + #print_debug(f"search_resource_file({path}, {name})") + files = glob.glob(os.path.join(path, "**", name), recursive=True) + return files[0] if len(files) else None + diff --git a/UnityPy/helpers/TextureSwizzler.py b/UnityPy/helpers/TextureSwizzler.py new file mode 100644 index 00000000..33f64c94 --- /dev/null +++ b/UnityPy/helpers/TextureSwizzler.py @@ -0,0 +1,138 @@ +# based on https://github.com/nesrak1/UABEA/blob/master/TexturePlugin/Texture2DSwitchDeswizzler.cs +from typing import Dict, Tuple + +from ..enums import TextureFormat + +GOB_X_TEXEL_COUNT = 4 +GOB_Y_TEXEL_COUNT = 8 +TEXEL_BYTE_SIZE = 16 +TEXELS_IN_GOB = GOB_X_TEXEL_COUNT * GOB_Y_TEXEL_COUNT +GOB_MAP = [ + (((l >> 3) & 0b10) | ((l >> 1) & 0b1), ((l >> 1) & 0b110) | (l & 0b1)) + for l in range(TEXELS_IN_GOB) +] + + +def ceil_divide(a: int, b: int) -> int: + return (a + b - 1) // b + + +def deswizzle( + data: bytes, + width: int, + height: int, + block_width: int, + block_height: int, + texels_per_block: int, +) -> bytearray: + block_count_x = ceil_divide(width, block_width) + block_count_y = ceil_divide(height, block_height) + gob_count_x = block_count_x // GOB_X_TEXEL_COUNT + gob_count_y = block_count_y // GOB_Y_TEXEL_COUNT + new_data = bytearray(len(data)) + data_view = memoryview(data) + + for i in range(gob_count_y // texels_per_block): + for j in range(gob_count_x): + base_gob_dst_x = j * 4 + for k in range(texels_per_block): + base_gob_dst_y = (i * texels_per_block + k) * GOB_Y_TEXEL_COUNT + for gob_x, gob_y in GOB_MAP: + dst_offset = ( + (base_gob_dst_y + gob_y) * block_count_x + + (base_gob_dst_x + gob_x) + ) * TEXEL_BYTE_SIZE + new_data[dst_offset : dst_offset + TEXEL_BYTE_SIZE] = data_view[ + :TEXEL_BYTE_SIZE + ] + data_view = data_view[TEXEL_BYTE_SIZE:] + return new_data + + +# def switch_swizzle( +# src_image: Image.Image, block_size: Tuple[int, int], texels_per_block: int +# ) -> Image.Image: +# dstImage = Image.new(src_image.mode, src_image.size) + +# blockCountX, blockCountY = map(ceil_divide, zip(src_image.size, block_size)) + +# gob_count_x = blockCountX / GOB_X_BLOCK_COUNT +# gob_count_y = blockCountY / GOB_Y_BLOCK_COUNT + +# dstX = 0 +# dstY = 0 + +# for i in range(gob_count_y): +# for j in range(gob_count_x): +# for k in range(texels_per_block): +# for l in range(BLOCKS_IN_GOB): +# # todo: use table for speedy boi +# gobX = ((l >> 3) & 0b10) | ((l >> 1) & 0b1) +# gobY = ((l >> 1) & 0b110) | (l & 0b1) +# gobSrcX = j * GOB_X_BLOCK_COUNT + gobX +# gobSrcY = (i * texels_per_block + k) * GOB_Y_BLOCK_COUNT + gobY +# CopyBlock( +# src_image, dstImage, gobSrcX, gobSrcY, dstX, dstY, *block_size +# ) + +# dstX += 1 +# if dstX >= blockCountX: +# dstX = 0 +# dstY += 1 + +# return dstImage + + +# this should be the amount of pixels that can fit 16 bytes +TEXTUREFORMAT_BLOCK_SIZE_MAP: Dict[TextureFormat, Tuple[int, int]] = { + TextureFormat.Alpha8: (16, 1), # 1 byte per pixel + TextureFormat.ARGB4444: (8, 1), # 2 bytes per pixel + TextureFormat.RGBA32: (4, 1), # 4 bytes per pixel + TextureFormat.ARGB32: (4, 1), # 4 bytes per pixel + TextureFormat.ARGBFloat: (1, 1), # 16 bytes per pixel (?) + TextureFormat.RGB565: (8, 1), # 2 bytes per pixel + TextureFormat.R16: (8, 1), # 2 bytes per pixel + TextureFormat.DXT1: (8, 4), # 8 bytes per 4x4=16 pixels + TextureFormat.DXT5: (4, 4), # 16 bytes per 4x4=16 pixels + TextureFormat.RGBA4444: (8, 1), # 2 bytes per pixel + TextureFormat.BGRA32: (4, 1), # 4 bytes per pixel + TextureFormat.BC6H: (4, 4), # 16 bytes per 4x4=16 pixels + TextureFormat.BC7: (4, 4), # 16 bytes per 4x4=16 pixels + TextureFormat.BC4: (8, 4), # 8 bytes per 4x4=16 pixels + TextureFormat.BC5: (4, 4), # 16 bytes per 4x4=16 pixels + TextureFormat.ASTC_RGB_4x4: (4, 4), # 16 bytes per 4x4=16 pixels + TextureFormat.ASTC_RGB_5x5: (5, 5), # 16 bytes per 5x5=25 pixels + TextureFormat.ASTC_RGB_6x6: (6, 6), # 16 bytes per 6x6=36 pixels + TextureFormat.ASTC_RGB_8x8: (8, 8), # 16 bytes per 8x8=64 pixels + TextureFormat.ASTC_RGB_10x10: (10, 10), # 16 bytes per 10x10=100 pixels + TextureFormat.ASTC_RGB_12x12: (12, 12), # 16 bytes per 12x12=144 pixels + TextureFormat.ASTC_RGBA_4x4: (4, 4), # 16 bytes per 4x4=16 pixels + TextureFormat.ASTC_RGBA_5x5: (5, 5), # 16 bytes per 5x5=25 pixels + TextureFormat.ASTC_RGBA_6x6: (6, 6), # 16 bytes per 6x6=36 pixels + TextureFormat.ASTC_RGBA_8x8: (8, 8), # 16 bytes per 8x8=64 pixels + TextureFormat.ASTC_RGBA_10x10: (10, 10), # 16 bytes per 10x10=100 pixels + TextureFormat.ASTC_RGBA_12x12: (12, 12), # 16 bytes per 12x12=144 pixels + TextureFormat.RG16: (8, 1), # 2 bytes per pixel + TextureFormat.R8: (16, 1), # 1 byte per pixel +} + + +def get_padded_texture_size( + width: int, height: int, block_width: int, block_height: int, texels_per_block: int +): + width = ( + ceil_divide(width, block_width * GOB_X_TEXEL_COUNT) + * block_width + * GOB_X_TEXEL_COUNT + ) + height = ( + ceil_divide(height, block_height * GOB_Y_TEXEL_COUNT * texels_per_block) + * block_height + * GOB_Y_TEXEL_COUNT + * texels_per_block + ) + return width, height + + +def get_switch_gobs_per_block(platform_blob: bytes) -> int: + return 1 << int.from_bytes(platform_blob[8:12], "little") diff --git a/UnityPy/helpers/Tpk.py b/UnityPy/helpers/Tpk.py index 84b65d42..4eb44549 100644 --- a/UnityPy/helpers/Tpk.py +++ b/UnityPy/helpers/Tpk.py @@ -1,29 +1,33 @@ from __future__ import annotations + from enum import IntEnum, IntFlag -from struct import Struct +from importlib.resources import open_binary from io import BytesIO -from typing import List, Tuple, Any, Dict +from struct import Struct +from typing import Any, Dict, List, Tuple from .TypeTreeHelper import TypeTreeNode +from .UnityVersion import UnityVersion TPKTYPETREE: TpkTypeTreeBlob = None -NODES_CACHE: dict = {} +CLASSES_CACHE: Dict[Tuple[int, tuple], TypeTreeNode] = {} +NODES_CACHE: Dict[TpkUnityClass, TypeTreeNode] = {} def init(): - import os + with open_binary("UnityPy.resources", "uncompressed.tpk") as f: + data = f.read() - with open( - os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "uncompressed.tpk"), "rb" - ) as f: - global TPKTYPETREE - TPKTYPETREE = TpkFile(f).GetDataBlob() + global TPKTYPETREE + with BytesIO(data) as stream: + TPKTYPETREE = TpkFile(stream).GetDataBlob() -def get_typetree_nodes(class_id: int, version: tuple): - global NODES_CACHE +def get_typetree_nodes_from_tpk(class_id: int, version: tuple): + global CLASSES_CACHE key = (class_id, version) - if key in NODES_CACHE: - return NODES_CACHE[key] + cached = CLASSES_CACHE.get(key) + if cached: + return cached class_info = TPKTYPETREE.ClassInformation[class_id].getVersionedClass( UnityVersion.fromList(*version) @@ -31,12 +35,17 @@ def get_typetree_nodes(class_id: int, version: tuple): if class_info is None: raise ValueError("Could not find class info for class id {}".format(class_id)) - nodes = generate_flat_nodes(class_info) - NODES_CACHE[key] = nodes - return nodes + node = generate_node(class_info) + CLASSES_CACHE[key] = node + return node -def generate_flat_nodes(class_info: TpkUnityClass) -> List[TypeTreeNode]: +def generate_node(class_info: TpkUnityClass, unwrap: bool = False) -> TypeTreeNode: + global NODES_CACHE + cached = NODES_CACHE.get(class_info) + if cached: + return cached + nodes = [] NODES = TPKTYPETREE.NodeBuffer.Nodes stack = [(class_info.ReleaseRootNode, 0)] @@ -57,7 +66,9 @@ def generate_flat_nodes(class_info: TpkUnityClass) -> List[TypeTreeNode]: ) stack = [(node_id, level + 1) for node_id in node.SubNodes] + stack index += 1 - return nodes + result = TypeTreeNode.from_list(nodes) if unwrap else nodes + NODES_CACHE[class_info] = result + return result ###################################################################################### @@ -74,15 +85,6 @@ class TpkCompressionType(IntEnum): Brotli = 3 -class UnityVersionType(IntEnum): - Alpha = 0 - Beta = 1 - China = 2 - Final = 3 - Patch = 4 - Experimental = 5 - - class TpkDataType(IntEnum): TypeTreeInformation = 0 Collection = 1 @@ -169,12 +171,12 @@ def GetDataBlob(self) -> TpkDataBlob: elif self.CompressionType == TpkCompressionType.Lzma: import lzma - raise Exception("LZMA compression not implemented") + decompressed = lzma.decompress(self.CompressedBytes) elif self.CompressionType == TpkCompressionType.Brotli: import brotli - decompressed = brotli.decompress(self.CompressedBytes) + decompressed: bytes = brotli.decompress(self.CompressedBytes) else: raise Exception("Invalid compression type") @@ -190,7 +192,7 @@ def GetDataBlob(self) -> TpkDataBlob: class TpkDataBlob: - __slots__ = "DataType" + __slots__ = ("DataType",) DataType: TpkDataType def __init__(self, stream: BytesIO) -> None: @@ -244,7 +246,7 @@ def __init__(self, stream: BytesIO) -> None: class TpkFileSystemBlob(TpkDataBlob): - __slots__ = "Files" + __slots__ = ("Files",) # TODO: check if dict might be better Files: List[Tuple[str, bytes]] @@ -273,52 +275,6 @@ def __init__(self, stream: BytesIO) -> None: ###################################################################################### -class UnityVersion(int): - # https://github.com/AssetRipper/VersionUtilities/blob/master/VersionUtilities/UnityVersion.cs - """ - use following static methos instead of the constructor(__init__): - UnityVersion.fromStream(stream: BytesIO) - UnityVersion.fromString(version: str) - UnityVersion.fromList(major: int, minor: int, patch: int, build: int) - """ - - @staticmethod - def fromStream(stream: BytesIO) -> UnityVersion: - (m_data,) = UINT64.unpack(stream.read(UINT64.size)) - return UnityVersion(m_data) - - @staticmethod - def fromString(version: str) -> UnityVersion: - return UnityVersion(version.split(".")) - - @staticmethod - def fromList(major: int, minor: int, patch: int, build: int) -> UnityVersion: - return UnityVersion(major << 48 | minor << 32 | patch << 16 | build) - - @property - def major(self) -> int: - return (self >> 48) & 0xFFFF - - @property - def minor(self) -> int: - return (self >> 32) & 0xFFFF - - @property - def build(self) -> int: - return (self >> 16) & 0xFFFF - - @property - def type(self) -> int: - return UnityVersionType(self >> 8) & 0xFF - - @property - def type_number(self) -> int: - return self & 0xFF - - def __repr__(self) -> str: - return f"UnityVersion {self.major}.{self.minor}.{self.build}.{self.type_number}" - - class TpkUnityClass: __slots__ = ("Name", "Base", "Flags", "EditorRootNode", "ReleaseRootNode") Struct = Struct(" None: if self.Flags & TpkUnityClassFlags.HasReleaseRootNode: (self.ReleaseRootNode,) = UINT16.unpack(stream.read(UINT16.size)) + def to_dict(self) -> Dict[str, Any]: + return { + "Name": self.Name, + "Base": self.Base, + "Flags": self.Flags, + "EditorRootNode": self.EditorRootNode, + "ReleaseRootNode": self.ReleaseRootNode, + } + def __eq__(self, other: TpkUnityClass) -> bool: - return self.__dict__ == other.__dict__ + return self.to_dict() == other.to_dict() def __hash__(self) -> int: - # TODO - return hash(self.__dict__) + return hash( + ( + self.Name, + self.Base, + self.Flags, + self.EditorRootNode, + self.ReleaseRootNode, + ) + ) class TpkClassInformation: @@ -363,6 +335,7 @@ def __init__(self, stream: BytesIO) -> None: ) for _ in range(count) ] + pass def getVersionedClass(self, version: UnityVersion) -> TpkUnityClass: return get_item_for_version(version, self.Classes) @@ -495,17 +468,18 @@ def read_data(stream: BytesIO) -> bytes: def get_item_for_version( - exactVersion: UnityVersion, items: List[Tuple[UnityVersion, Any]] + highest_version: UnityVersion, items: List[Tuple[UnityVersion, Any]] ) -> Any: ret = None - for version, item in items: - if exactVersion >= version: + usedver = None + for version, item in reversed(items): + if version <= highest_version: ret = item - else: + usedver = version break if ret: return ret - raise ValueError("Could not find exact version") + raise ValueError("Could not find matching version") init() diff --git a/UnityPy/helpers/TypeTreeHelper.py b/UnityPy/helpers/TypeTreeHelper.py index 9fa88e8d..78d56474 100644 --- a/UnityPy/helpers/TypeTreeHelper.py +++ b/UnityPy/helpers/TypeTreeHelper.py @@ -1,61 +1,23 @@ -īģŋfrom typing import Any, Dict, List, Union, Iterable, Tuple -from ..streams import EndianBinaryReader, EndianBinaryWriter +īģŋfrom typing import Any, List, Union, TYPE_CHECKING from ctypes import c_uint32 -import tabulate -from ..exceptions import TypeTreeError as TypeTreeError - -kAlignBytes = 0x4000 - - -class TypeTreeNode(object): - __slots__ = ( - "m_Version", - "m_Level", - "m_TypeFlags", - "m_ByteSize", - "m_Index", - "m_MetaFlag", - "m_Type", - "m_Name", - "m_TypeStrOffset", - "m_NameStrOffset", - "m_RefTypeHash", - "m_VariableCount", - ) - m_Type: str - m_Name: str - m_ByteSize: int - m_Index: int - m_Version: int - m_MetaFlag: int - m_Level: int - m_TypeStrOffset: int - m_NameStrOffset: int - m_RefTypeHash: str - m_TypeFlags: int - m_VariableCount: int - - def __init__(self, data: Union[dict, Iterable[Tuple]] = None, **kwargs): - if isinstance(data, dict): - items = data.items() - elif kwargs: - items = kwargs.items() - else: - items = data - - for key, val in items: - setattr(self, key, val) - - def __repr__(self): - return f"" +#import base64 +from ast import literal_eval +if TYPE_CHECKING: + from ..files import ObjectReader + from ..streams import EndianBinaryReader +from ..streams import EndianBinaryWriter +from ..exceptions import TypeTreeError as TypeTreeError +from ..enums import ClassIDType +from ..exceptions import sanity_check +from .. import config +from .TypeTreeNode import TypeTreeNode, kAlignBytes try: from ..UnityPyBoost import TypeTreeNode, read_typetree as read_typetree_c except: read_typetree_c = None - def node_dict_to_node_cls(nodes: List[dict]) -> List[TypeTreeNode]: """Converts all dict-type nodes into TypeTreeNodes @@ -105,10 +67,16 @@ def check_nodes(nodes: List[Union[dict, TypeTreeNode]]) -> List[TypeTreeNode]: elif isinstance(nodes[0], dict): return node_dict_to_node_cls(nodes) raise ValueError( - f"nodes must be a list of dict or TypeTreeNode elements, but received {type(nodes)} - {type(nodes[0]) if isinstance(nodes, list) else ''}" + "nodes must be a list of dict or TypeTreeNode elements, but received " + + f"{type(nodes)} - {type(nodes[0]) if isinstance(nodes, list) else ''}" ) +def slice_len(s): + step = s.step if s.step else 1 + return max((s.stop - s.start) // step, 1) + + def get_nodes(nodes: List[TypeTreeNode], index: int) -> list: """Copies all nodes above the level of the node at the set index. @@ -131,296 +99,289 @@ def get_nodes(nodes: List[TypeTreeNode], index: int) -> list: return nodes[index:] -def read_typetree( - nodes: List[Union[dict, TypeTreeNode]], reader: EndianBinaryReader -) -> dict: - """Reads the typetree of the object contained in the reader via the node list. - +def get_subtree_at(nodes: List[TypeTreeNode], index: c_uint32) -> slice: + """Copies all nodes with the level above the one of the node at the set index. Parameters ---------- nodes : list - List of nodes/nodes - reader : EndianBinaryReader - Reader of the object to be parsed + nodes/nodes of the typetree + index : int + index of the node Returns ------- - dict - The parsed typtree + slice + A slice of nodes """ - reader.reset() - - nodes = check_nodes(nodes) - - if read_typetree_c: - return read_typetree_c( - nodes, reader.read_bytes(reader.byte_size), reader.endian - ) - - obj = read_value(nodes, reader, c_uint32(0)) - - read = reader.Position - reader.byte_start - if read != reader.byte_size: - raise TypeTreeError( - f"Error while read type, read {read} bytes but expected {reader.byte_size} bytes", - nodes, - ) - - return obj - - -def read_value(nodes: List[TypeTreeNode], reader: EndianBinaryReader, i: c_uint32): - node = nodes[i.value] - typ = node.m_Type - align = (node.m_MetaFlag & kAlignBytes) != 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].m_MetaFlag & kAlignBytes) != 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() - value = [None] * size - for j in range(size): - 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 + i = index.value + level = nodes[i].m_Level + len_nodes = len(nodes) + while True: + i += 1 + if i >= len_nodes or nodes[i].m_Level <= level: + break + if i == index.value: + subtree = slice(1) else: - # Vector - if i.value < len(nodes) - 1 and nodes[i.value + 1].m_Type == "Array": - if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 0: - align = True - 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.m_Name] = read_value(clz, reader, j) - j.value += 1 + subtree = slice(index.value, min(i, len_nodes)) + return subtree - if align: - reader.align_stream() - return value +def read_typetree_safe(nodes: List[Union[dict, TypeTreeNode]], + reader: Union["ObjectReader", "EndianBinaryReader"], all_trees: dict=None, + pos: c_uint32=None) -> dict: + """Safely reads TypeTree of Object using its provided nodes from the reader.""" + try: + return read_typetree(nodes, reader, all_trees, pos) + except: + return None -def read_typetree_str( - sb: List[str], nodes: List[Union[dict, TypeTreeNode]], reader: EndianBinaryReader -) -> list: - """Reads the typetree of the object contained in the reader via the node list and dumps it as string. +def read_typetree(nodes: List[Union[dict, TypeTreeNode]], + reader: "ObjectReader", + all_trees: dict=None, pos: c_uint32=None) -> dict: + """Reads TypeTree of Object using its provided nodes from the reader. Parameters ---------- - sb : list - StringBuilder - a list used to build the string dump, should be empty nodes : list List of nodes/nodes - reader : EndianBinaryReader + reader : Union["ObjectReader", "EndianBinaryReader"] Reader of the object to be parsed Returns ------- - list - The sb given as input + dict + The parsed typtree """ - # reader.reset() + + reader.reset() nodes = check_nodes(nodes) - i = c_uint32(0) + if read_typetree_c: + return read_typetree_c( + nodes, reader.read_bytes(reader.byte_size), reader.endian + ) + + obj = {} + i = c_uint32(1) if pos is None else pos + while i.value < len(nodes): - read_value_str(sb, nodes, reader, i) + node = nodes[i.value] + val = read_value(nodes, reader, i, all_trees) + obj[node.m_Name] = val i.value += 1 - readed = reader.Position - reader.byte_start - if readed != reader.byte_size: + difference = reader.byte_size - reader.already_read + if difference != 0: + idobj = obj.get("m_GameObject", "") + try: + if idobj: idobj = " " + str(idobj) + except: + pass raise TypeTreeError( - f"Error while read type, read {readed} bytes but expected {reader.byte_size} bytes", - nodes, + f"object's <{nodes[0].m_Type}> TypeTree specifies {reader.already_read},"+ + f" but object is {reader.byte_size} bytes (diff: {difference} B)" + #+f"\n{json.dumps(obj, indent=4)}" + ,obj ) - - return sb + return obj -def read_value_str( - sb: List[str], nodes: List[TypeTreeNode], reader: EndianBinaryReader, i: c_uint32 -) -> list: +def read_common_type(value_type, reader: Union["ObjectReader", "EndianBinaryReader"]): + match value_type: + case "SInt8" | "sbyte": + return reader.read_byte() + case "UInt8" | "char" | "ubyte": + return reader.read_u_byte() + case "short" | "SInt16": + return reader.read_short() + case "UInt16" | "unsigned short": + return reader.read_u_short() + case "int" | "SInt32": + return reader.read_int() + case "UInt32" | "unsigned int" | "Type*": + return reader.read_u_int() + case "long long" | "SInt64": + return reader.read_long() + case "UInt64" | "unsigned long long" | "FileSize": + return reader.read_u_long() + case "float": + return reader.read_float() + case "double": + return reader.read_double() + case "bool": + return reader.read_boolean() + case _: + return None + + +def read_value(nodes: List[TypeTreeNode], reader: "ObjectReader", + i: c_uint32, all_trees:dict, value_ref=None): node = nodes[i.value] - typ = node.m_Type + _type = node.m_Type + _name = node.m_Name align = (node.m_MetaFlag & kAlignBytes) != 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.m_Level, node.m_Type, node.m_Name, value - ) - ) - elif typ == "map": - if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 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.m_Level, node.m_Type, node.m_Name) - ) - sb.append("{0}{1} {2}\r\n".format("\t" * (node.m_Level + 1), "Array", "Array")) - sb.append( - "{0}{1} {2} = {3}\r\n".format( - "\t" * (node.m_Level + 1), "int", "size", size - ) - ) - for j in range(size): - sb.append("{0}[{1}]\r\n".format("\t" * (node.m_Level + 2), j)) - sb.append( - "{0}{1} {2}\r\n".format("\t" * (node.m_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.m_Level, node.m_Type, node.m_Name) - ) - sb.append( - "{0}{1} {2} = {3}\r\n".format("\t" * node.m_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].m_Type == "Array": - if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 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" * node.m_Level, node.m_Type, node.m_Name) - ) - sb.append( - "{0}{1} {2}\r\n".format("\t" * (node.m_Level + 1), "Array", "Array") - ) - sb.append( - "{0}{1} {2} = {3}\r\n".format( - "\t" * (node.m_Level + 1), "int", "size", size - ) - ) - for j in range(size): - sb.append("{0}[{1}]\r\n".format("\t" * (node.m_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 - sb.append( - "{0}{1} {2}\r\n".format("\t" * node.m_Level, node.m_Type, node.m_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.m_Level, node.m_Type, node.m_Name, value - ) - ) + + if config.DEBUG_TYPETREES: + pos1 = reader.Position - reader.byte_start + + value = read_common_type(_type, reader) + if value is None: + match _type: + case _type if _type.startswith("PPtr<"): + fid = reader.read_int() + pid = read_common_type(nodes[i.value + 2].m_Type, reader) # old ver compat + value = {"m_FileID": fid, "m_PathID": pid} + i.value += 2 + case "String[]": + if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 0: + align = True + size_array = reader.read_int() + sanity_check(f"read_value(): size_array of String[]", size_array) + value = [None] * size_array + for s in range(size_array): + value[s] = reader.read_aligned_string() + i.value += 6 # followed by 6 entities: Array, Size, Data, Array, Size, Data(uint8[]) + case "string": + value = reader.read_aligned_string() + i.value += 3 # followed by 3 entities: Array, Size, Data(uint8[]) + case "map": # map == MultiDict + if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 0: + align = True + map_ = get_subtree_at(nodes, i) + i.value += slice_len(map_) - 1 + first = get_subtree_at(nodes[map_], c_uint32(4)) + second = get_subtree_at(nodes[map_], c_uint32(4 + slice_len(first))) + size = reader.read_int() + sanity_check(f"read_value(): size of map", size) + value = [None] * size + for j in range(size): + key = read_value(nodes[first], reader, c_uint32(0), all_trees, value_ref) + data = read_value(nodes[second], reader, c_uint32(0), all_trees, value_ref) + value[j] = (key, data) + case "TypelessData": + size = reader.read_int() + sanity_check(f"read_value(): size of TypelessData", size) + value = reader.read_bytes(size) + i.value += 2 # following by 2 entities: Size, Data(uint8[]) + case "ManagedReferencesRegistry": + registry = get_subtree_at(nodes, i) # just to count nodes + value = read_managed_ref_registry(reader, all_trees, value_ref) + i.value += slice_len(registry) - 1 + case "UnityPyBinaryBlob": + if config.ENABLE_BINARY_BLOBS: + size = None + if hasattr(nodes[i.value], "m_ByteSize"): + size = nodes[i.value].m_ByteSize + value = repr(bytes(reader.read_bytes(size)).decode('unicode_escape'))[1:-1] + else: + value = repr(bytes(reader.read_the_rest()).decode('unicode_escape'))[1:-1] + i.value += 1 + case _: + # Vector + if i.value < len(nodes) - 1 and nodes[i.value + 1].m_Type == "Array": + if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 0: + align = True + vector = get_subtree_at(nodes, i) + i.value += slice_len(vector) - 1 + size = reader.read_int() + if True or config.DEBUG_TYPETREES: + sanity_check(f"read_value(): size of Vector", size, max_value=16777216) # 4096x4096; needed? + value = [None] * size + for i in range(size): + value[i] = read_value(nodes[vector], reader, c_uint32(3), all_trees, value_ref) + pass + else: # Class + clz = get_subtree_at(nodes, i) + slen = slice_len(clz) + if slen == 1: + if reader.byte_size - reader.already_read != 0: + if all_trees and _type in all_trees: # an unspecified class present elsewhere + ref_nodes = all_trees[_type] + if ref_nodes and len(ref_nodes) > 1: + value = read_value(ref_nodes, reader, c_uint32(0), all_trees, value_ref) + else: + value = {} + else: + print_debug(f"No type definition for field {nodes[clz][0].m_Name} of type " + + f"{nodes[clz][0].m_Type}, error in the TypeTree or an empty type") + value = {} + else: + value = {} # just an empty class + else: + i.value += slen - 1 + value = {} + j = c_uint32(1) + while j.value < slen: + clz_node: TypeTreeNode = nodes[clz.start + j.value] + value[clz_node.m_Name] = read_value(nodes[clz], reader, j, all_trees, value_ref) + j.value += 1 if align: reader.align_stream() - return sb + if config.DEBUG_TYPETREES: + pos2 = reader.Position - reader.byte_start + #debug_data[str(i.value)] = + if isinstance(value, (memoryview, bytes, str)): + value_out = f"{value.__class__.__name__} of length {len(value)}" + else: + value_out = value + print_debug(f"\"{node.m_Name}\": {value_out} (offset: {pos2} - {pos1} = {pos2 - pos1})" + + f"\nlast data: {[value[k] for k in value.keys()[-2:]]}" if value else '') + return value -def dump_typetree(nodes: List[TypeTreeNode]) -> str: - """Dumps the structure of the given nodes. - Parameters - ---------- - nodes : list - List of nodes/nodes - - Returns - ------- - str - The dumped structure - """ - field_names = ["m_Level", "m_Type", "m_Name", "m_MetaFlag"] - rows = [[getattr(x, key) for key in field_names] for x in nodes] - return tabulate.tabulate(rows, headers=field_names) +def read_managed_ref_registry(reader: Union["ObjectReader", "EndianBinaryReader"], + all_trees: dict, value_ref): + value = {} + version = 2 + refid_count = 1 + if value_ref is None: + # read version and refid count only at the top level + version = reader.read_int() + value['version'] = version + if version not in [1,2]: + raise Exception(f"Unsupported ManagedReferencesRegistry version {version}") + refid_count = reader.read_int() + sanity_check("refid_count", refid_count) + value_ref = True + else: + return None # no more than one level of nesting + value['RefIds'] = [] + while refid_count > 0: + rid = None + if reader.Position >= reader.Length: + raise TypeTreeError("End of stream before enough RefId nodes", value) + if version > 1: + rid = reader.read_long() + #rid_text = f"" + ref_class = reader.read_aligned_string() + ref_ns = reader.read_aligned_string() + ref_asm = reader.read_aligned_string() + ref_nodes = None + if rid is not None and not ref_class and rid >= 0 and rid < (1 << 32): + ref_class = ClassIDType(rid).name + if all_trees and ref_class and ref_class in all_trees: + ref_nodes = all_trees[ref_class] + data = None + # rid == -2 and classless nodes can be safely ignored + if ref_nodes and (rid != -2 if rid is not None else True): + data = read_value(ref_nodes, reader, c_uint32(0), all_trees, value_ref) + class_value = {} + if rid is not None: + class_value |= { "rid": rid } + class_value |= { + "type": { "class": ref_class, "ns": ref_ns,"asm": ref_asm }, + "data": data + } + value['RefIds'].append(class_value) + refid_count -= 1 + return value -def write_typetree( - obj: dict, nodes: List[Union[dict, TypeTreeNode]], writer: EndianBinaryWriter = None +def write_typetree(obj: dict, nodes: List[Union[dict, TypeTreeNode]], + writer: EndianBinaryWriter = None, all_trees: dict = None ) -> EndianBinaryWriter: """Writes the data of the object via the given typetree of the object into the writer. @@ -446,79 +407,143 @@ def write_typetree( i = c_uint32(1) while i.value < len(nodes): value = obj[nodes[i.value].m_Name] - write_value(value, nodes, writer, i) + write_value(value, nodes, writer, i, all_trees) i.value += 1 return writer -def write_value( - value: Any, nodes: List[TypeTreeNode], writer: EndianBinaryWriter, i: c_uint32 +def write_common_type(value, value_type: str, writer: EndianBinaryWriter): + is_written = True + match value_type: + case "SInt8": + writer.write_byte(value) + case "UInt8" | "char": + writer.write_u_byte(value) + case "short" | "SInt16": + writer.write_short(value) + case "UInt16" | "unsigned short": + writer.write_u_short(value) + case "int" | "SInt32": + writer.write_int(value) + case "UInt32" | "unsigned int" | "Type*": + writer.write_u_int(value) + case "long long" | "SInt64": + writer.write_long(value) + case "UInt64" | "unsigned long long" | "FileSize": + writer.write_u_long(value) + case "float": + writer.write_float(value) + case "double": + writer.write_double(value) + case "bool": + writer.write_boolean(value) + case _: + is_written = False + return is_written + + +def write_value(value: Union[Any, int, str], nodes: List[TypeTreeNode], + writer: EndianBinaryWriter, i: c_uint32, + all_trees: dict = None, value_ref = None ): node = nodes[i.value] - typ = node.m_Type + _type = node.m_Type align = (node.m_MetaFlag & kAlignBytes) != 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].m_MetaFlag & kAlignBytes) != 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].m_Type == "Array": - if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 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].m_Name] - write_value(val, clz, writer, j) - j.value += 1 - + if not write_common_type(value, _type, writer): + match _type: + case _type if _type.startswith("PPtr<"): + # accelerate pointers write + writer.write_int(value["m_FileID"]) + write_common_type(value["m_PathID"], nodes[i.value + 2].m_Type, writer) + i.value += 2 + case "string": + writer.write_aligned_string(value) + i.value += 3 # Array, Size, Data(uint8[]) + case "map": + if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 0: + align = True + map_ = get_subtree_at(nodes, i) + i.value += len(map_) - 1 + first = get_subtree_at(nodes[map_], c_uint32(4)) + second = get_subtree_at(nodes[map_], c_uint32(4 + slice_len(first))) + # Size + writer.write_int(len(value)) + # Data + for key, val in value: + write_value(key, nodes[first], writer, c_uint32(0), all_trees, value_ref) + write_value(val, nodes[second], writer, c_uint32(0), all_trees, value_ref) + case "TypelessData": + writer.write_int(len(value)) + writer.write_bytes(value) + i.value += 2 # Size, Data(char/uint8) + case "ManagedReferencesRegistry": + registry = get_subtree_at(nodes, i) # just to count nodes + value = write_managed_ref_registry(value, writer, all_trees, value_ref) + i.value += slice_len(registry) - 1 + case "UnityPyBinaryBlob": + if config.ENABLE_BINARY_BLOBS: + value = value.replace('"','\\"') + writer.write_bytes(literal_eval(f'b"{value}"')) + i.value += 1 # Data(bytes) + case _: + # Vector + if i.value < len(nodes) - 1 and nodes[i.value + 1].m_Type == "Array": + if (nodes[i.value + 1].m_MetaFlag & kAlignBytes) != 0: + align = True + vector = get_subtree_at(nodes, i) + i.value += slice_len(vector) - 1 + writer.write_int(len(value)) + for val in value: + write_value(val, nodes[vector], writer, c_uint32(3), all_trees, value_ref) + else: # Class + clz = get_subtree_at(nodes, i) + i.value += slice_len(clz) - 1 + j = c_uint32(1) + while j.value < slice_len(clz): + val = value[nodes[clz.start + j.value].m_Name] + write_value(val, nodes[clz], writer, j, all_trees, value_ref) + j.value += 1 if align: writer.align_stream() + + +def write_managed_ref_registry(value, writer: EndianBinaryWriter, + all_trees: dict, value_ref): + version = 2 + if value_ref is None: + version = value['version'] + writer.write_int(version) + refid_count_pos = writer.Position + writer.write_int(len(value['RefIds'])) + value_ref = True + else: + return + for item in value['RefIds']: + rid = None + if version > 1: + rid = item["rid"] + writer.write_long(rid) + writer.write_aligned_string(item["type"]["class"]) + writer.write_aligned_string(item["type"]["ns"]) + writer.write_aligned_string(item["type"]["asm"]) + ref_nodes = None + ref_class = item["type"]["class"] + if rid is not None and not ref_class and rid >= 0 and rid < (1 << 32): + ref_class = ClassIDType(rid).name + if all_trees and ref_class and ref_class in all_trees: + ref_nodes = all_trees[ref_class] + if ref_nodes and (rid != -2 if rid is not None else True): + write_value(item["data"], ref_nodes, writer, c_uint32(0), all_trees, value_ref) + + +def dump_typetree(nodes: list, filename:str=None, indent=4) -> Union[str, None]: + """Dumps TypeTree as JSON. + If filename is provided then dumps to that file instead of returning it.""" + import json + data = json.dumps(nodes, ensure_ascii=False, indent=indent, default=lambda __o: __o.toJSON()) + if filename: + with open(filename, "w", encoding="utf-8") as j: + j.write(data) + else: + return data diff --git a/UnityPy/helpers/TypeTreeNode.py b/UnityPy/helpers/TypeTreeNode.py new file mode 100644 index 00000000..eaa1daa0 --- /dev/null +++ b/UnityPy/helpers/TypeTreeNode.py @@ -0,0 +1,85 @@ + +from typing import Dict, Union, Iterable, Tuple + +kAlignBytes = 0x4000 + +class TypeTreeNode(object): + __slots__ = ( + "m_Level", + "m_Type", + "m_Name", + "m_MetaFlag", + # NOTE: rarely used attributes next + "m_ByteSize", + "m_Index", + "m_Version", + "m_TypeFlags", + "m_TypeStrOffset", + "m_NameStrOffset", + "m_RefTypeHash" # > v19 + ) + m_Type: str + m_Name: str + m_Level: int + m_MetaFlag: int + m_ByteSize: int + m_Index: int + m_Version: int + # NOTE: no need to make them always visible + #m_TypeStrOffset: int + #m_NameStrOffset: int + #m_RefTypeHash: str + #m_TypeFlags: int + #m_VariableCount: int + + def __init__(self, data: Union[Dict, Iterable[Tuple]] = None, **kwargs): + self.m_Level = None + self.m_Type = None + self.m_Name = None + self.m_MetaFlag = None + + if isinstance(data, dict) and len(data) > 0: + items = data.items() + elif kwargs: + items = kwargs.items() + else: + items = data + + if items: + for key, val in items: + setattr(self, key, val) + + if (self.m_Level is None or self.m_Type is None or self.m_Name is None): + raise ValueError("TypeTreeNode must have level, name and type") + + def __getitem__(self, item): + if item == "m_Name": return self.m_Name + elif item == "m_Level": return self.m_Level + elif item == "m_Type": return self.m_Type + elif item == "m_MetaFlag": return self.m_MetaFlag + return getattr(self, item) + + def __repr__(self): + return f"{' ' * self.m_Level}" + + def toJSON(self): + return { + "m_Level": self.m_Level, + "m_Type": self.m_Type, + "m_Name": self.m_Name, + "m_MetaFlag": self.m_MetaFlag & kAlignBytes + } + +CHAR_REPLACE = {',': '_', ' ': '_', '.': '_', ':': '_', + '-': '_', '[': '_', ']': '_', '\\': '_', '/': '_', '`': '_'} +def sanitize_name(name: str) -> str: + if name.startswith("(int&)"): + name = name[6:] + if name.endswith("?"): + name = name[:-1] + name = name.translate(CHAR_REPLACE) + if name in ["pass", "from"]: + name += "_" + if name[0].isdigit(): + name = f"_{name}" + return name diff --git a/UnityPy/helpers/UnityVersion.py b/UnityPy/helpers/UnityVersion.py new file mode 100644 index 00000000..cb81f8ed --- /dev/null +++ b/UnityPy/helpers/UnityVersion.py @@ -0,0 +1,72 @@ +from io import BytesIO +from enum import IntEnum +from struct import Struct + +UINT64 = Struct(" "UnityVersion": + (m_data,) = UINT64.unpack(stream.read(UINT64.size)) + return UnityVersion(m_data) + + @staticmethod + def fromString(version: str) -> "UnityVersion": + return UnityVersion(version.split(".")) + + @staticmethod + def fromList( + major: int = 0, minor: int = 0, patch: int = 0, build: int = 0 + ) -> "UnityVersion": + return UnityVersion(major << 48 | minor << 32 | patch << 16 | build) + + @property + def major(self) -> int: + return (self >> 48) & 0xFFFF + + @property + def minor(self) -> int: + return (self >> 32) & 0xFFFF + + @property + def build(self) -> int: + return (self >> 16) & 0xFFFF + + @property + def type(self) -> int: + return UnityVersionType(self >> 8) & 0xFF + + @property + def type_number(self) -> int: + return self & 0xFF + + def __eq__(self, other: "UnityVersion"): + return self.as_tuple() == other.as_tuple() + + def __ne__(self, other: "UnityVersion"): + return self.as_tuple() != other.as_tuple() + + def __lt__(self, other: "UnityVersion"): + return self.as_tuple() < other.as_tuple() + + def as_tuple(self): + return (self.major, self.minor, self.build, self.type_number) + + def __repr__(self) -> str: + return f"UnityVersion<{self.major}.{self.minor}.{self.build}.{self.type_number}>" diff --git a/UnityPy/helpers/__init__.py b/UnityPy/helpers/__init__.py index 066f4b96..c445d322 100644 --- a/UnityPy/helpers/__init__.py +++ b/UnityPy/helpers/__init__.py @@ -1 +1 @@ -from . import ArchiveStorageManager, CompressionHelper, ImportHelper, TypeTreeHelper \ No newline at end of file +from . import ArchiveStorageManager, CompressionHelper, ImportHelper, TypeTreeHelper, TypeTreeNode \ No newline at end of file diff --git a/UnityPy/lib/FMOD/Darwin/x64/4.x64 b/UnityPy/lib/FMOD/Darwin/x64/4.x64 deleted file mode 100644 index d87cae13..00000000 Binary files a/UnityPy/lib/FMOD/Darwin/x64/4.x64 and /dev/null differ diff --git a/UnityPy/lib/FMOD/Darwin/x64/4.x86 b/UnityPy/lib/FMOD/Darwin/x64/4.x86 deleted file mode 100644 index 1309010b..00000000 Binary files a/UnityPy/lib/FMOD/Darwin/x64/4.x86 and /dev/null differ diff --git a/UnityPy/lib/FMOD/Darwin/x64/libfmod.dylib b/UnityPy/lib/FMOD/Darwin/x64/libfmod.dylib deleted file mode 100644 index 01c3e67d..00000000 Binary files a/UnityPy/lib/FMOD/Darwin/x64/libfmod.dylib and /dev/null differ diff --git a/UnityPy/lib/FMOD/Linux/arm/libfmod.so b/UnityPy/lib/FMOD/Linux/arm/libfmod.so deleted file mode 100644 index 9e3644e3..00000000 Binary files a/UnityPy/lib/FMOD/Linux/arm/libfmod.so and /dev/null differ diff --git a/UnityPy/lib/FMOD/Linux/armhf/libfmod.so b/UnityPy/lib/FMOD/Linux/armhf/libfmod.so deleted file mode 100644 index dee60d3d..00000000 Binary files a/UnityPy/lib/FMOD/Linux/armhf/libfmod.so and /dev/null differ diff --git a/UnityPy/lib/FMOD/Linux/x86/libfmod.so b/UnityPy/lib/FMOD/Linux/x86/libfmod.so deleted file mode 100644 index 9c2618d2..00000000 Binary files a/UnityPy/lib/FMOD/Linux/x86/libfmod.so and /dev/null differ diff --git a/UnityPy/lib/FMOD/Linux/x86_64/libfmod.so b/UnityPy/lib/FMOD/Linux/x86_64/libfmod.so deleted file mode 100644 index c661a41a..00000000 Binary files a/UnityPy/lib/FMOD/Linux/x86_64/libfmod.so and /dev/null differ diff --git a/UnityPy/lib/FMOD/Windows/x64/fmod.dll b/UnityPy/lib/FMOD/Windows/x64/fmod.dll deleted file mode 100644 index cce1e68a..00000000 Binary files a/UnityPy/lib/FMOD/Windows/x64/fmod.dll and /dev/null differ diff --git a/UnityPy/lib/FMOD/Windows/x86/fmod.dll b/UnityPy/lib/FMOD/Windows/x86/fmod.dll deleted file mode 100644 index 111ce17f..00000000 Binary files a/UnityPy/lib/FMOD/Windows/x86/fmod.dll and /dev/null differ diff --git a/UnityPy/lib/README.MD b/UnityPy/lib/README.MD deleted file mode 100644 index 647bfd62..00000000 --- a/UnityPy/lib/README.MD +++ /dev/null @@ -1,2 +0,0 @@ -## FMOD -Source: [FMOD Engine 2.00.10](https://fmod.com/download) diff --git a/UnityPy/math/GUID32.py b/UnityPy/math/GUID32.py new file mode 100644 index 00000000..ec9971ec --- /dev/null +++ b/UnityPy/math/GUID32.py @@ -0,0 +1,78 @@ +class GUID128: + HexToLiteral = "0123456789abcdef" + __slots__ = {"data0", "data1", "data2", "data3"} + + def __init__(self, reader=None): + if reader is not None: + self.read(reader) + + @property + def is_empty(self): + return self.data0 == 0 and self.data1 == 0 and self.data2 == 0 and self.data3 == 0 + + def read(self, reader): + self.data0 = reader.read_uint() + self.data1 = reader.read_uint() + self.data2 = reader.read_uint() + self.data3 = reader.read_uint() + + def write(self, writer): + writer.write_uint(self.data0) + writer.write_uint(self.data1) + writer.write_uint(self.data2) + writer.write_uint(self.data3) + + def __getitem__(self, i): + if i == 0: + return self.data0 + elif i == 1: + return self.data1 + elif i == 2: + return self.data2 + elif i == 3: + return self.data3 + else: + raise IndexError() + + def __setitem__(self, i, value): + if i == 0: + self.data0 = value + elif i == 1: + self.data1 = value + elif i == 2: + self.data2 = value + elif i == 3: + self.data3 = value + else: + raise IndexError() + + def __str__(self): + string_builder = [] + for i in range(3, -1, -1): + for j in range(7, -1, -1): + cur = self[i] + cur >>= (j * 4) + cur &= 0xF + string_builder.insert(0, self.HexToLiteral[cur]) + return ''.join(string_builder) + + @classmethod + def from_bytes(guid, data): + if len(data) != 32: + return None + + guid = new guid() + for i in range(4): + cur = 0 + for j in range(7, -1, -1): + cur_hex = GUID128.literal_to_hex(str[i * 8 + j]) + if cur_hex == uint.MaxValue: + return None + cur |= (cur_hex << (j * 4)) + guid[i] = cur + + return guid + + @staticmethod + def literal_to_hex(c): + return int(c) \ No newline at end of file diff --git a/UnityPy/math/Half.py b/UnityPy/math/Half.py index 6d56e1b9..affcee75 100644 --- a/UnityPy/math/Half.py +++ b/UnityPy/math/Half.py @@ -25,7 +25,7 @@ def ToHalf(*args) -> float: raise ValueError("Invalid amount of arguments") if math.isnan(val): - # print('Nan') + #print_debug('ToHalf(): NaN') return 0 elif math.isinf(val): return MaxValue @@ -90,7 +90,7 @@ def ToHalf(*args) -> float: # e -= 1 # e += 1 # f &= ~0x00000400 -# # print(s,e,f) +# # print_debug(s,e,f) # elif e == 31: # if f == 0: # return int((s << 31) | 0x7f800000) diff --git a/UnityPy/math/Hash128.py b/UnityPy/math/Hash128.py new file mode 100644 index 00000000..aa300fa3 --- /dev/null +++ b/UnityPy/math/Hash128.py @@ -0,0 +1,26 @@ +class Hash128: + def __init__(self, data=b''): + if isinstance(data, Hash128): + self.data = data.data + else: + self.data = bytes(data) + + def read(self, reader): + self.data = reader.read(16) + + def write(self, writer): + writer.write(self.data) + + @property + def is_zero(self): + return self.data is None or all([a == 0 for a in self.data]) + + def __str__(self): + return ''.join(f"{hb:02X}" for hb in self.data) + + def __bytes__(self): + return self.data + + @classmethod + def blank_hash(cls): + return cls(b'\0' * 16) \ No newline at end of file diff --git a/UnityPy/math/Matrix4x4.py b/UnityPy/math/Matrix4x4.py index a38c5967..ab8aeb9e 100644 --- a/UnityPy/math/Matrix4x4.py +++ b/UnityPy/math/Matrix4x4.py @@ -4,12 +4,12 @@ class Matrix4x4: M: list - def __init__(self, values): + def __init__(self, *values): if len(values) != 16: raise ValueError( - "There must be sixteen and only sixteen input values for Matrix." + "There must be only 16 input values for Matrix4x4!" ) - self.M = values + self.M = list(values) def __getitem__(self, index): if isinstance(index, tuple): @@ -25,10 +25,10 @@ def __setitem__(self, index, value): def __eq__(self, other): if not isinstance(other, Matrix4x4): return False - print() + return all([self.M[i] == other.M[i] for i in range(16)]) def __mul__(lhs, rhs): - res = Matrix4x4([0] * 16) + res = Matrix4x4(*([0] * 16)) res.M00 = ( lhs.M00 * rhs.M00 + lhs.M01 * rhs.M10 @@ -134,7 +134,7 @@ def __mul__(lhs, rhs): @staticmethod def Scale(vector: Vector3): return Matrix4x4( - [vector.X, 0, 0, 0, 0, vector.Y, 0, 0, 0, 0, vector.Z, 0, 0, 0, 0, 1] + vector.X, 0, 0, 0, 0, vector.Y, 0, 0, 0, 0, vector.Z, 0, 0, 0, 0, 1 ) @property diff --git a/UnityPy/py.typed b/UnityPy/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/UnityPy/resources/__init__.py b/UnityPy/resources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/UnityPy/resources/uncompressed.tpk b/UnityPy/resources/uncompressed.tpk index 5128be38..68d045be 100644 Binary files a/UnityPy/resources/uncompressed.tpk and b/UnityPy/resources/uncompressed.tpk differ diff --git a/UnityPy/streams/EndianBinaryReader.py b/UnityPy/streams/EndianBinaryReader.py index 0fe7f7db..bb9771d9 100644 --- a/UnityPy/streams/EndianBinaryReader.py +++ b/UnityPy/streams/EndianBinaryReader.py @@ -1,131 +1,99 @@ -import sys from struct import Struct, unpack import re -from typing import List, Union -from io import BytesIO, BufferedIOBase, IOBase, BufferedReader +from typing import Type, List, Union, Callable, TYPE_CHECKING +from io import BytesIO, IOBase, SEEK_END, SEEK_SET, SEEK_CUR +from sys import byteorder +from ..exceptions import sanity_check, ReadingPastObject +import weakref -reNot0 = re.compile(b"(.*?)\x00") +if TYPE_CHECKING: + from ..files.ObjectReader import ObjectReader -SYS_ENDIAN = "<" if sys.byteorder == "little" else ">" +from .. import config +DEBUG = config.DEBUG +#if DEBUG: + #import traceback + +SYS_ENDIAN = "<" if byteorder == "little" else ">" +RE_NOT_0 = re.compile(b"(.*?)\0", re.S) from ..math import Color, Matrix4x4, Quaternion, Vector2, Vector3, Vector4, Rectangle # generate unpack and unpack_from functions TYPE_PARAM_SIZE_LIST = [ - ("short", "h", 2), - ("u_short", "H", 2), - ("int", "i", 4), - ("u_int", "I", 4), - ("long", "q", 8), - ("u_long", "Q", 8), - ("half", "e", 2), - ("float", "f", 4), - ("double", "d", 8), - ("vector2", "2f", 8), - ("vector3", "3f", 12), - ("vector4", "4f", 16), + # TYPE_NAME, STRUCT_TYPE_ID, TYPE_SIZE, NATIVE_TYPE + ("bool", "?", 1, bool), + ("boolean", "?", 1, bool), + ("byte", "b", 1, None), + ("u_byte", "B", 1, None), + ("short", "h", 2, None), + ("u_short", "H", 2, None), + ("int", "i", 4, None), + ("u_int", "I", 4, None), + ("long", "q", 8, None), + ("u_long", "Q", 8, None), + ("half", "e", 2, None), + ("float", "f", 4, None), + ("double", "d", 8, None), + ("quaternion", "4f", 4 * 4, Quaternion), + ("color3", "3f", 3 * 4, Color), + ("color4", "4f", 4 * 4, Color), + ("rectangle_f", "4f", 4 * 4, Rectangle), + ("matrix", "16f", 16 * 4, Matrix4x4), + ("vector2", "2f", 2 * 4, Vector2), + ("vector3", "3f", 3 * 4, Vector3), + ("vector4", "4f", 4 * 4, Vector4), ] -LOCALS = locals() -for endian_s, endian_l in (("<", "little"), (">", "big")): - for typ, param, _ in TYPE_PARAM_SIZE_LIST: - LOCALS[f"unpack_{endian_l}_{typ}"] = Struct(f"{endian_s}{param}").unpack - LOCALS[f"unpack_{endian_l}_{typ}_from"] = Struct( - f"{endian_s}{param}" - ).unpack_from - - class EndianBinaryReader: - endian: str Length: int Position: int BaseOffset: int + Crypto: Callable def __new__( cls, item: Union[bytes, bytearray, memoryview, BytesIO, str], - endian: str = ">", - offset: int = 0, + endian=">", + offset=0, + size=-1, + crypto_func=None ): + in_memory = False if isinstance(item, (bytes, bytearray, memoryview)): - obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Memoryview) - elif isinstance(item, (IOBase, BufferedIOBase)): - obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Streamable) + in_memory = True elif isinstance(item, str): item = open(item, "rb") - obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Streamable) - elif isinstance(item, EndianBinaryReader): + elif issubclass(item.__class__, EndianBinaryReader): item = ( - item.stream - if isinstance(item, EndianBinaryReader_Streamable) - else item.view + item.stream if isinstance(item, EndianBinaryReader_Streamable) else item.view ) - return EndianBinaryReader(item, endian, offset) - elif hasattr(item, "read"): - if hasattr(item, "seek") and hasattr(item, "tell"): - obj = super(EndianBinaryReader, cls).__new__( - EndianBinaryReader_Streamable - ) - else: - item = item.read() - obj = super(EndianBinaryReader, cls).__new__( - EndianBinaryReader_Memoryview - ) - - obj.__init__(item, endian) + if in_memory: + obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Memoryview) + setattr(obj, "view", memoryview(item)) + else: + #assert issubclass(item.__calss__, IOBase), "Must be IOBase for streams" + obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Streamable) + setattr(obj, "stream", item) return obj - def __init__(self, item, endian=">", offset=0): - self.endian = endian + def __init__(self, item, endian:str=None, offset:int=0, size:int=0, crypto_func:Callable=None): + self.Crypto = crypto_func + self._endian = "" self.BaseOffset = offset self.Position = 0 - @property - def bytes(self): - # implemented by Streamable and Memoryview versions - return b"" - def read(self, *args): # implemented by Streamable and Memoryview versions - return b"" - - def read_byte(self) -> int: - return unpack(self.endian + "b", self.read(1))[0] + raise NotImplementedError("read not implemented") - def read_u_byte(self) -> int: - return unpack(self.endian + "B", self.read(1))[0] + def within_size(self): + return self.Position - self.BaseOffset < self._size def read_bytes(self, num) -> bytes: return self.read(num) - def read_short(self) -> int: - return unpack(self.endian + "h", self.read(2))[0] - - def read_int(self) -> int: - return unpack(self.endian + "i", self.read(4))[0] - - def read_long(self) -> int: - return unpack(self.endian + "q", self.read(8))[0] - - def read_u_short(self) -> int: - return unpack(self.endian + "H", self.read(2))[0] - - def read_u_int(self) -> int: - return unpack(self.endian + "I", self.read(4))[0] - - def read_u_long(self) -> int: - return unpack(self.endian + "Q", self.read(8))[0] - - def read_float(self) -> float: - return unpack(self.endian + "f", self.read(4))[0] - - def read_double(self) -> float: - return unpack(self.endian + "d", self.read(8))[0] - - def read_boolean(self) -> bool: - return bool(unpack(self.endian + "?", self.read(1))[0]) - - def read_string(self, size=None, encoding="utf8") -> str: + def read_string(self, size=None, encoding="utf-8") -> str: if size is None: ret = self.read_string_to_null() else: @@ -138,18 +106,24 @@ def read_string(self, size=None, encoding="utf8") -> str: def read_string_to_null(self, max_length=32767) -> str: ret = [] c = b"" - while c != b"\0" and len(ret) < max_length and self.Position != self.Length: + length = 0 + while c != b"\0" and len(ret) <= max_length and self.Position != self.Length: ret.append(c) c = self.read(1) + length += 1 + if False and DEBUG: + sanity_check("read_string_to_null length", length, max_length) if not c: raise ValueError("Unterminated string: %r" % ret) - return b"".join(ret).decode("utf8", "surrogateescape") + return b"".join(ret).decode("utf-8", "surrogateescape") def read_aligned_string(self) -> str: length = self.read_int() + if DEBUG: + sanity_check("read_aligned_string length", length, self.Length) if 0 < length <= self.Length - self.Position: string_data = bytes(self.read_bytes(length)) - result = string_data.decode("utf8", "surrogateescape") + result = string_data.decode("utf-8", "surrogateescape") self.align_stream() return result return "" @@ -157,81 +131,47 @@ def read_aligned_string(self) -> str: def align_stream(self, alignment=4): self.Position += (alignment - self.Position % alignment) % alignment - def read_quaternion(self) -> Quaternion: - return Quaternion( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) - - def read_vector2(self) -> Vector2: - return Vector2(self.read_float(), self.read_float()) - - def read_vector3(self) -> Vector3: - return Vector3(self.read_float(), self.read_float(), self.read_float()) - - def read_vector4(self) -> Vector4: - return Vector4( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) - - def read_rectangle_f(self) -> Rectangle: - return Rectangle( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) - - def read_color4(self) -> Color: - return Color( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) + def read_color_uint(self): + r = self.read_u_byte() + g = self.read_u_byte() + b = self.read_u_byte() + a = self.read_u_byte() + return Color(r / 255.0, g / 255.0, b / 255.0, a / 255.0) def read_byte_array(self) -> bytes: return self.read(self.read_int()) - def read_matrix(self) -> Matrix4x4: - return Matrix4x4(self.read_float_array(16)) + def read_array(self, command, length: int = None) -> list: + if length is None: + length = self.read_u_int() + return [command() for _ in range(length)] - def read_array(self, command, length: int) -> list: + def read_vector(self, command) -> list: + length = self.read_u_int() + sanity_check("read_vector", length) + if command is None: # byte/char vector + return self.read(length) return [command() for _ in range(length)] + def read_map(self, command1, command2) -> dict: + length = self.read_u_int() + sanity_check("read_map", length) + result = {} + for _ in range(length): + key = command1() # this is possible since Object impl. __hash__ and __eq__ + result[key] = command2() + return result + def read_array_struct(self, param: str, length: int = None) -> list: - if length is None: + if not length: length = self.read_int() struct = Struct(f"{self.endian}{length}{param}") return struct.unpack(self.read(struct.size)) - def read_boolean_array(self, length: int = None) -> List[bool]: - return self.read_array_struct("?", length) - - def read_u_short_array(self, length: int = None) -> List[int]: - return self.read_array_struct("h", length) - - def read_short_array(self, length: int = None) -> List[int]: - return self.read_array_struct("H", length) - - def read_int_array(self, length: int = None) -> List[int]: - return self.read_array_struct("i", length) - - def read_u_int_array(self, length: int = None) -> List[int]: - return self.read_array_struct("I", length) - - def read_u_int_array_array(self, length: int = None) -> List[List[int]]: - return self.read_array( - self.read_u_int_array, length if length is not None else self.read_int() - ) - - def read_float_array(self, length: int = None) -> List[float]: - return self.read_array_struct("f", length) - - def read_string_array(self) -> List[str]: - return self.read_array(self.read_aligned_string, self.read_int()) - - def read_vector2_array(self) -> List[Vector2]: - return self.read_array(self.read_vector2, self.read_int()) - - def read_vector4_array(self) -> List[Vector4]: - return self.read_array(self.read_vector4, self.read_int()) - - def read_matrix_array(self) -> List[Matrix4x4]: - return self.read_array(self.read_matrix, self.read_int()) + def read_string_array(self, length: int = None) -> List[str]: + if not length: + length = self.read_int() + return self.read_array(self.read_aligned_string, length) def real_offset(self) -> int: """Returns offset in the underlying file. @@ -239,50 +179,78 @@ def real_offset(self) -> int: """ return self.BaseOffset + self.Position - def read_the_rest(self, obj_start: int, obj_size: int) -> bytes: - """Returns the rest of the current reader bytes.""" - return self.read_bytes(obj_size - (self.Position - obj_start)) - + def read_the_rest(self): + return self.read(self.Length - self.Position) class EndianBinaryReader_Memoryview(EndianBinaryReader): - __slots__ = ("view", "_endian", "BaseOffset", "Position", "Length") + __slots__ = ("view", "_endian", "BaseOffset", "Position", "Length", "Crypto") view: memoryview + endian: str - def __init__(self, view, endian=">", offset=0): - self._endian = "" - super().__init__(view, endian=endian, offset=offset) - self.view = memoryview(view) + def __init__(self, view, endian=">", offset=0, size=None, crypto_func: Callable = None): + super().__init__(view, endian=endian, offset=offset, size=size, crypto_func=crypto_func) self.Length = len(view) + self.endian = endian @property def endian(self): return self._endian + def set_endian_be(self): + if ">" != self._endian: + setattr(self,"__class__", EndianBinaryReader_Memoryview_BigEndian) + self._endian = ">" + + def set_endian_le(self): + if "<" != self._endian: + setattr(self,"__class__", EndianBinaryReader_Memoryview_LittleEndian) + self._endian = "<" + @endian.setter def endian(self, value: str): - if value not in ("<", ">"): + if value not in ("<", ">", "little", "big"): raise ValueError("Invalid endian") - if value != self._endian: - setattr( - self, - "__class__", - EndianBinaryReader_Memoryview_LittleEndian - if value == "<" - else EndianBinaryReader_Memoryview_BigEndian, - ) - self._endian = value + self.set_endian_le() if value in ( + "<", "little") else self.set_endian_be() @property - def bytes(self): + def bytes(self) -> memoryview: + return self.save() + + def save(self) -> memoryview: + if self.Crypto is not None: + return self.Crypto(self.view, self.Position) return self.view - def dispose(self): - self.view.release() + def close(self): + if self.view: + if hasattr(self.view, "release"): + self.view.release() + if hasattr(self.view, "close"): + self.view.close() + self.view = None + + def seek(self, value): + if value < self.Length: + self.Position = value + else: + self.Position = self.Length + return self.Position + + def seek_relative(self, value): + new_position = self.Position + value + if new_position < self.Length: + self.Position = new_position + else: + self.Position = self.Length + return self.Position def read(self, length: int): if not length: return b"" ret = self.view[self.Position : self.Position + length] + if self.Crypto is not None: + ret = self.Crypto(ret, self.Position) self.Position += length return ret @@ -290,164 +258,51 @@ def read_aligned_string(self): length = self.read_int() if 0 < length <= self.Length - self.Position: string_data = self.read_bytes(length) - result = bytes(string_data).decode("utf8", "surrogateescape") self.align_stream() - return result + return bytes(string_data).decode("utf-8", "surrogateescape") return "" - def read_string_to_null(self, max_length=32767) -> str: - match = reNot0.search(self.view, self.Position, self.Position + max_length) - if not match: - if self.Position + max_length >= self.Length: - raise Exception("String not terminated") - else: - return bytes(self.read_bytes(max_length)).decode( - "utf8", "surrogateescape" - ) - ret = match[1].decode("utf8", "surrogateescape") - self.Position = match.end() - return ret - - class EndianBinaryReader_Memoryview_LittleEndian(EndianBinaryReader_Memoryview): - def read_u_short(self): - (ret,) = unpack_little_u_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_short(self): - (ret,) = unpack_little_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_int(self): - (ret,) = unpack_little_int_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_u_int(self): - (ret,) = unpack_little_u_int_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_long(self): - (ret,) = unpack_little_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_u_long(self): - (ret,) = unpack_little_u_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_half(self): - (ret,) = unpack_little_half_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_float(self): - (ret,) = unpack_little_float_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_double(self): - (ret,) = unpack_little_double_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_vector2(self): - (x, y) = unpack_little_vector2_from(self.view, self.Position) - self.Position += 8 - return Vector2(x, y) - - def read_vector3(self): - (x, y, z) = unpack_little_vector3_from(self.view, self.Position) - self.Position += 12 - return Vector3(x, y, z) - - def read_vector4(self): - (x, y, z, w) = unpack_little_vector4_from(self.view, self.Position) - self.Position += 16 - return Vector4(x, y, z, w) - + pass class EndianBinaryReader_Memoryview_BigEndian(EndianBinaryReader_Memoryview): - def read_u_short(self): - (ret,) = unpack_big_u_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_short(self): - (ret,) = unpack_big_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_int(self): - (ret,) = unpack_big_int_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_u_int(self): - (ret,) = unpack_big_u_int_from(self.view, self.Position) - self.Position += 4 - return ret + pass - def read_long(self): - (ret,) = unpack_big_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_u_long(self): - (ret,) = unpack_big_u_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_half(self): - (ret,) = unpack_big_half_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_float(self): - (ret,) = unpack_big_float_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_double(self): - (ret,) = unpack_big_double_from(self.view, self.Position) - self.Position += 8 - return ret +class EndianBinaryReader_Streamable(EndianBinaryReader): + __slots__ = ("stream", "_endian", "BaseOffset", "Crypto") + stream: IOBase + endian: str - def read_vector2(self): - (x, y) = unpack_big_vector2_from(self.view, self.Position) - self.Position += 8 - return Vector2(x, y) + def __init__(self, stream, endian=">", offset=0, size=-1, crypto_func: Callable = None): + super().__init__(stream, endian=endian, offset=offset, size=size, crypto_func=crypto_func) + self.endian = endian + self._size = size + self._size_checked = False + self._finalizer = weakref.finalize(self, self.close, self.stream) - def read_vector3(self): - (x, y, z) = unpack_big_vector3_from(self.view, self.Position) - self.Position += 12 - return Vector3(x, y, z) + def get_position(self): + return self.stream.tell() - self.BaseOffset - def read_vector4(self): - (x, y, z, w) = unpack_big_vector4_from(self.view, self.Position) - self.Position += 16 - return Vector4(x, y, z, w) + def set_position(self, value): + self.stream.seek(value + self.BaseOffset, SEEK_SET) + def seek(self, value): + self.stream.seek(value + self.BaseOffset, SEEK_SET) + return self.Position -class EndianBinaryReader_Streamable(EndianBinaryReader): - __slots__ = ("stream", "_endian", "BaseOffset") - stream: BufferedReader + def seek_relative(self, value): + self.stream.seek(value, SEEK_CUR) + return self.Position - def __init__(self, stream, endian=">", offset=0): - self._endian = "" - self.stream = stream - super().__init__(stream, endian=endian, offset=offset) - self.read = self.stream.read + def set_endian_be(self): + if ">" != self._endian: + setattr(self,"__class__", EndianBinaryReader_Streamable_BigEndian) + self._endian = ">" - def get_position(self): - return self.stream.tell() - - def set_position(self, value): - self.stream.seek(value + self.BaseOffset) + def set_endian_le(self): + if "<" != self._endian: + setattr(self,"__class__", EndianBinaryReader_Streamable_LittleEndian) + self._endian = "<" @property def endian(self): @@ -455,111 +310,107 @@ def endian(self): @endian.setter def endian(self, value): - if value not in ("<", ">"): - raise ValueError("Invalid endian") - if value != self._endian: - setattr( - self, - "__class__", - EndianBinaryReader_Streamable_LittleEndian - if value == "<" - else EndianBinaryReader_Streamable_BigEndian, - ) - self._endian = value + if value not in ("<", ">", "little", "big"): + raise ValueError(f"Invalid endianness: {value}") + self.set_endian_le() if value in ( + "<", "little") else self.set_endian_be() @property def Length(self): - pos = self.Position - length = self.stream.seek(0, 2) - self.BaseOffset - self.Position = pos - return length + if self._size <= 0: + pos = self.Position + self._size = self.stream.seek(0, SEEK_END) - self.BaseOffset + self.Position = pos + elif not self._size_checked and self._size > 0: + pos = self.Position + self._size = min(self.stream.seek(0, SEEK_END) - self.BaseOffset, self._size) + self.Position = pos + self._size_checked = True + return self._size Position = property(get_position, set_position) - @property - def bytes(self): + def save(self) -> bytes: last_pos = self.Position self.Position = 0 ret = self.read(self.Length) self.Position = last_pos + if self.Crypto is not None: + return self.Crypto(ret, last_pos) return ret - def dispose(self): - self.stream.close() - pass + def close(self, stream=None): + #print_debug(f"Closing stream {stream}") + if stream is None: + if self.stream: + self.stream.close() + self.stream = None + else: + stream.close() + def read(self, length: int): + if not length: + return b"" + pos = self.Position + ret = self.stream.read(length) + if self.Crypto is not None: + return self.Crypto(ret, pos) + return ret class EndianBinaryReader_Streamable_LittleEndian(EndianBinaryReader_Streamable): - def read_u_short(self): - return unpack_little_u_short(self.read(2))[0] - - def read_short(self): - return unpack_little_short(self.read(2))[0] - - def read_int(self): - return unpack_little_int(self.read(4))[0] - - def read_u_int(self): - return unpack_little_u_int(self.read(4))[0] - - def read_long(self): - return unpack_little_long(self.read(8))[0] - - def read_u_long(self): - return unpack_little_u_long(self.read(8))[0] - - def read_half(self): - return unpack_little_half(self.read(2))[0] - - def read_float(self): - return unpack_little_float(self.read(4))[0] - - def read_double(self): - return unpack_little_double(self.read(8))[0] - - def read_vector2(self): - return Vector2(*unpack_little_vector2(self.read(8))) - - def read_vector3(self): - return Vector3(*unpack_little_vector3(self.read(12))) - - def read_vector4(self): - return Vector4(*unpack_little_vector4(self.read(16))) - + pass class EndianBinaryReader_Streamable_BigEndian(EndianBinaryReader_Streamable): - def read_u_short(self): - return unpack_big_u_short(self.read(2))[0] - - def read_short(self): - return unpack_big_short(self.read(2))[0] - - def read_int(self): - return unpack_big_int(self.read(4))[0] - - def read_u_int(self): - return unpack_big_u_int(self.read(4))[0] - - def read_long(self): - return unpack_big_long(self.read(8))[0] - - def read_u_long(self): - return unpack_big_u_long(self.read(8))[0] - - def read_half(self): - return unpack_big_half(self.read(2))[0] - - def read_float(self): - return unpack_big_float(self.read(4))[0] - - def read_double(self): - return unpack_big_double(self.read(8))[0] - - def read_vector2(self): - return Vector2(*unpack_big_vector2(self.read(8))) + pass + +def generate_streamable_read_method(endian, type_id, native_type): + data_format = Struct(f"{endian}{type_id}") + size = data_format.size + if native_type is not None: + def _method_body(self): + return native_type(*data_format.unpack(self.read(size))) + else: + def _method_body(self): + return data_format.unpack(self.read(size))[0] + return _method_body + +def generate_memoryview_read_method(endian, type_id, native_type): + data_format = Struct(f"{endian}{type_id}") + size = data_format.size + if native_type is not None: + def _method_body(self): + ret = data_format.unpack_from(self.view, self.Position) + self.Position += size + return native_type(*ret) + else: + def _method_body(self): + ret = data_format.unpack_from(self.view, self.Position) + self.Position += size + return ret[0] + return _method_body + +def generate_array_read_method(type_name): + reader_method = f"read_{type_name}" + def _method_body(self, length=None): + if not length: + length = self.read_int() + if DEBUG: + sanity_check(f"{type_name} array", length) + return self.read_array(getattr(self, reader_method), length) + return _method_body - def read_vector3(self): - return Vector3(*unpack_big_vector3(self.read(12))) +# generate methods for reading each type +for endian_s, endian_l in (("<", "little"), (">", "big")): + for type_name, type_id, size, native_type in TYPE_PARAM_SIZE_LIST: + if type_name != "byte": + setattr(EndianBinaryReader, f"read_{type_name}_array", generate_array_read_method(type_name)) + if endian_l == "big": + clsm = EndianBinaryReader_Memoryview_BigEndian + clss = EndianBinaryReader_Streamable_BigEndian + else: + clsm = EndianBinaryReader_Memoryview_LittleEndian + clss = EndianBinaryReader_Streamable_LittleEndian + setattr(clsm, f"read_{type_name}", generate_memoryview_read_method(endian_s, type_id, native_type)) + setattr(clss, f"read_{type_name}", generate_streamable_read_method(endian_s, type_id, native_type)) - def read_vector4(self): - return Vector4(*unpack_big_vector4(self.read(16))) +pass \ No newline at end of file diff --git a/UnityPy/streams/EndianBinaryReader.pyi b/UnityPy/streams/EndianBinaryReader.pyi new file mode 100644 index 00000000..bc2b10ef --- /dev/null +++ b/UnityPy/streams/EndianBinaryReader.pyi @@ -0,0 +1,89 @@ +from ..math import Color as Color, Matrix4x4 as Matrix4x4, Quaternion as Quaternion, Rectangle as Rectangle, Vector2 as Vector2, Vector3 as Vector3, Vector4 as Vector4 +from ..files import ObjectReader +from io import BytesIO, IOBase +from typing import Callable, List, Union + +SYS_ENDIAN: str + +class EndianBinaryReader: + Length: int + Position: int + BaseOffset: int + Crypto: Callable + def __new__(cls, item: Union[bytes, bytearray, memoryview, BytesIO, str], endian: str = ..., offset: int = ..., size: int = ..., crypto_func: Callable | None = ...): ... + def __init__(self, item, endian: str = ..., offset: int = ..., size: int = ..., crypto_func: Callable = ...) -> None: ... + def read(self, *args) -> None: ... + def read_the_rest(self) -> bytes: ... + def within_size(self) -> bool: ... + def read_bytes(self, num) -> bytes: ... + def read_byte(self) -> int: ... + def read_u_byte(self) -> int: ... + def read_short(self) -> int: ... + def read_int(self) -> int: ... + def read_long(self) -> int: ... + def read_u_short(self) -> int: ... + def read_u_int(self) -> int: ... + def read_u_long(self) -> int: ... + def read_float(self) -> float: ... + def read_double(self) -> float: ... + def read_boolean(self) -> bool: ... + def read_string(self, size: int | None = ..., encoding: str = ...) -> str: ... + def read_string_to_null(self, max_length: int = ...) -> str: ... + def read_aligned_string(self) -> str: ... + def align_stream(self, alignment: int = ...) -> None: ... + def read_color_uint(self) -> Color: ... + def read_quaternion(self) -> Quaternion: ... + def read_vector2(self) -> Vector2: ... + def read_vector3(self) -> Vector3: ... + def read_vector4(self) -> Vector4: ... + def read_vector(self, command) -> list: ... + def read_map(self, command) -> dict: ... + def read_rectangle_f(self) -> Rectangle: ... + def read_color4(self) -> Color: ... + def read_byte_array(self) -> bytes: ... + def read_matrix(self) -> Matrix4x4: ... + def read_array(self, command, length: int) -> list: ... + def read_array_struct(self, param: str, length: int = ...) -> list: ... + def read_boolean_array(self, length: int = None) -> List[bool]: ... + def read_u_short_array(self, length: int = None) -> List[int]: ... + def read_short_array(self, length: int = None) -> List[int]: ... + def read_int_array(self, length: int = None) -> List[int]: ... + def read_u_int_array(self, length: int = None) -> List[int]: ... + def read_long_array(self, length: int = None) -> List[int]: ... + def read_u_long_array(self, length: int = None) -> List[int]: ... + def read_u_int_array_array(self, length: int = None) -> List[int]: ... + def read_float_array(self, length: int = None) -> List[float]: ... + def read_string_array(self, length: int = None) -> List[str]: ... + def read_vector2_array(self, length: int = None) -> List[Vector2]: ... + def read_vector4_array(self, length: int = None) -> List[Vector4]: ... + def read_matrix_array(self, length: int = None) -> List[Matrix4x4]: ... + def real_offset(self) -> int: ... + def seek(self, value: int = ...) -> None: ... + def seek_relative(self, value: int = ...)-> None: ... + def close(self) -> None: ... + +class EndianBinaryReader_Memoryview(EndianBinaryReader): + view: memoryview + endian: str + def __init__(self, view: memoryview, endian: str = ..., offset: int = ..., size: int | None = ..., crypto_func: Callable = ...) -> None: ... + @property + def bytes(self) -> memoryview: ... + def save(self) -> memoryview: ... + def read(self, length: int) -> memoryview: ... + +class EndianBinaryReader_Memoryview_LittleEndian(EndianBinaryReader_Memoryview): ... +class EndianBinaryReader_Memoryview_BigEndian(EndianBinaryReader_Memoryview): ... + +class EndianBinaryReader_Streamable(EndianBinaryReader): + stream: IOBase + endian: str + def __init__(self, stream: IOBase, endian: str = ..., offset: int = ..., size: int = ..., crypto_func: Callable = ...) -> None: ... + def save(self) -> bytes: ... + def read(self, length: int) -> bytes: ... + +class EndianBinaryReader_Streamable_LittleEndian(EndianBinaryReader_Streamable): ... +class EndianBinaryReader_Streamable_BigEndian(EndianBinaryReader_Streamable): ... + +def generate_streamable_read_method(endian, type_id, native_type): ... +def generate_memoryview_read_method(endian, type_id, native_type): ... +def generate_array_read_method(type_name): ... \ No newline at end of file diff --git a/UnityPy/streams/EndianBinaryWriter.py b/UnityPy/streams/EndianBinaryWriter.py index d9861401..c08d9ce7 100644 --- a/UnityPy/streams/EndianBinaryWriter.py +++ b/UnityPy/streams/EndianBinaryWriter.py @@ -1,31 +1,39 @@ import io +import sys from struct import pack - +from typing import Callable from ..math import Color, Matrix4x4, Quaternion, Vector2, Vector3, Vector4, Rectangle - +from io import IOBase, BytesIO class EndianBinaryWriter: + stream: IOBase endian: str Length: int Position: int - stream: io.BufferedReader + Crypto: Callable - def __init__(self, input_=b"", endian=">"): + def __init__(self, input_=b"", endian=">", offset=None, crypto_func=None): if isinstance(input_, (bytes, bytearray)): - self.stream = io.BytesIO(input_) + self.stream = BytesIO(input_) self.stream.seek(0, 2) - elif isinstance(input_, io.IOBase): + elif isinstance(input_, IOBase): self.stream = input_ else: raise ValueError("Invalid input type - %s." % type(input_)) self.endian = endian - self.Position = self.stream.tell() + self.Crypto = crypto_func + if offset is None: + self.Position = self.stream.tell() + else: + self.Position = offset - @property - def bytes(self): + def save(self): self.stream.seek(0) return self.stream.read() + def read_bytes(self, size = None): + return self.stream.read(size) + @property def Length(self) -> int: pos = self.stream.tell() @@ -34,15 +42,25 @@ def Length(self) -> int: self.stream.seek(pos) return l - def dispose(self): + def close(self): self.stream.close() pass + def get_position(self): + return self.stream.tell() + + def set_position(self, value): + if value != self.get_position(): + self.stream.seek(value) + + Position = property(get_position, set_position) + def write(self, *args): - if self.Position != self.stream.tell(): - self.stream.seek(self.Position) - ret = self.stream.write(*args) - self.Position = self.stream.tell() + ret = 0 + if self.Crypto is not None: + ret = self.stream.write(self.Crypto(args[0], self.Position), *args[1:]) + else: + ret = self.stream.write(*args) return ret def write_byte(self, value: int): @@ -81,20 +99,26 @@ def write_double(self, value: float): def write_boolean(self, value: bool): self.write(pack(self.endian + "?", value)) - def write_string_to_null(self, value: str): - self.write(value.encode("utf8", "surrogateescape")) + def write_bool(self, value: bool): + self.write_boolean(value) + + def write_string_to_null(self, value: [str, bytes, bytearray], encoding: str="utf-8"): + if not isinstance(value, (bytes, bytearray)): + value = value.encode(encoding, "surrogatepass") + self.write(value) self.write(b"\0") - def write_aligned_string(self, value: str): - bstring = value.encode("utf8", "surrogateescape") - self.write_int(len(bstring)) - self.write(bstring) - self.align_stream(4) + def write_aligned_string(self, value: [str, bytes, bytearray], encoding: str="utf-8"): + if not isinstance(value, (bytes, bytearray)): + value = value.encode(encoding, "surrogatepass") + self.write_u_int(len(value)) + self.write(value) + self.align_stream() def align_stream(self, alignment=4): pos = self.stream.tell() align = (alignment - pos % alignment) % alignment - self.write(b"\0" * align) + self.write(b'\0' * align) def write_quaternion(self, value: Quaternion): self.write_float(value.X) @@ -123,6 +147,12 @@ def write_rectangle_f(self, value: Rectangle): self.write_float(value.width) self.write_float(value.height) + def write_color_uint(self, value: Color): + self.write_u_byte(value.R * 255) + self.write_u_byte(value.G * 255) + self.write_u_byte(value.B * 255) + self.write_u_byte(value.A * 255) + def write_color4(self, value: Color): self.write_float(value.R) self.write_float(value.G) @@ -135,12 +165,12 @@ def write_matrix(self, value: Matrix4x4): def write_array(self, command, value: list, write_length: bool = True): if write_length: - self.write_int(len(value)) + self.write_u_int(len(value)) for val in value: command(val) def write_byte_array(self, value: bytes): - self.write_int(len(value)) + self.write_u_int(len(value)) self.write(value) def write_boolean_array(self, value: list): diff --git a/UnityPy/streams/EndianBinaryWriter.pyi b/UnityPy/streams/EndianBinaryWriter.pyi new file mode 100644 index 00000000..b0be2ab2 --- /dev/null +++ b/UnityPy/streams/EndianBinaryWriter.pyi @@ -0,0 +1,51 @@ +from ..math import Color as Color, Matrix4x4 as Matrix4x4, Quaternion as Quaternion, Rectangle as Rectangle, Vector2 as Vector2, Vector3 as Vector3, Vector4 as Vector4 +from _typeshed import Incomplete +from io import IOBase +from typing import Callable, Union + +class EndianBinaryWriter: + stream: IOBase + endian: str + Length: int + Position: int + Crypto: Callable + def __init__(self, input_: bytes = ..., endian: str = ..., offset: int | None = ..., crypto_func: Callable | None = ...) -> None: ... + def save(self) -> Union[bytes, memoryview]: ... + @property + def Length(self) -> int: ... + def close(self) -> None: ... + def write(self, *args): ... + def write_byte(self, value: int): ... + def write_u_byte(self, value: int): ... + def write_bytes(self, value: bytes): ... + def write_short(self, value: int): ... + def write_int(self, value: int): ... + def write_long(self, value: int): ... + def write_u_short(self, value: int): ... + def write_u_int(self, value: int): ... + def write_u_long(self, value: int): ... + def write_float(self, value: float): ... + def write_double(self, value: float): ... + def write_boolean(self, value: bool): ... + def write_string_to_null(self, value: [str, bytes, bytearray], encoding: str = ...): ... + def write_aligned_string(self, value: [str, bytes, bytearray], encoding: str = ...): ... + def align_stream(self, alignment: int = ...) -> None: ... + def write_quaternion(self, value: Quaternion): ... + def write_vector2(self, value: Vector2): ... + def write_vector3(self, value: Vector3): ... + def write_vector4(self, value: Vector4): ... + def write_rectangle_f(self, value: Rectangle): ... + def write_color_uint(self, value: Color): ... + def write_color4(self, value: Color): ... + def write_matrix(self, value: Matrix4x4): ... + def write_array(self, command, value: list, write_length: bool = ...): ... + def write_byte_array(self, value: bytes): ... + def write_boolean_array(self, value: list): ... + def write_u_short_array(self, value: list): ... + def write_int_array(self, value: list, write_length: bool = ...): ... + def write_u_int_array(self, value: list, write_length: bool = ...): ... + def write_float_array(self, value: list, write_length: bool = ...): ... + def write_string_array(self, value: list): ... + def write_vector2_array(self, value: list): ... + def write_vector4_array(self, value: list): ... + def write_matrix_array(self, value: list): ... diff --git a/UnityPy/streams/__init__.py b/UnityPy/streams/__init__.py index 0b713c1e..581ea1d3 100644 --- a/UnityPy/streams/__init__.py +++ b/UnityPy/streams/__init__.py @@ -1,2 +1,2 @@ from .EndianBinaryReader import EndianBinaryReader -from .EndianBinaryWriter import EndianBinaryWriter +from .EndianBinaryWriter import EndianBinaryWriter \ No newline at end of file diff --git a/UnityPy/tools/extractor.py b/UnityPy/tools/extractor.py index 86f6d98b..333182ef 100644 --- a/UnityPy/tools/extractor.py +++ b/UnityPy/tools/extractor.py @@ -1,24 +1,26 @@ -from io import BytesIO -import os import json +import os +from io import BytesIO +from pathlib import Path +from typing import Callable, Dict, List, Optional, Tuple, Union + import UnityPy from UnityPy.classes import ( + AudioClip, + Font, + GameObject, + Mesh, + MonoBehaviour, Object, PPtr, - MonoBehaviour, - TextAsset, - Font, Shader, - Mesh, Sprite, + TextAsset, Texture2D, - AudioClip, - GameObject, ) + from UnityPy.enums.ClassIDType import ClassIDType -from typing import Union, List, Dict -from pathlib import Path -from collections.abc import Callable +from UnityPy.files import SerializedFile def export_obj( @@ -27,8 +29,8 @@ def export_obj( append_name: bool = False, append_path_id: bool = False, export_unknown_as_typetree: bool = False, - asset_filter: Callable[[Object], bool] = None, -) -> List[int]: + asset_filter: Optional[Callable[[Object], bool]] = None, +) -> List[Tuple[SerializedFile, int]]: """Exports the given object to the given filepath. Args: @@ -58,7 +60,7 @@ def export_obj( return [] if append_name: - fp = os.path.join(fp, obj.name if obj.name else obj.type.name) + fp = os.path.join(fp, obj.m_Name if getattr(obj, "m_Name") else obj.type.name) fp, extension = os.path.splitext(fp) @@ -76,8 +78,8 @@ def extract_assets( ignore_first_container_dirs: int = 0, append_path_id: bool = False, export_unknown_as_typetree: bool = False, - asset_filter: Callable[[Object], bool] = None, -) -> List[int]: + asset_filter: Optional[Callable[[Object], bool]] = None, +) -> List[Tuple[SerializedFile, int]]: """Extracts some or all assets from the given source. Args: @@ -90,7 +92,7 @@ def extract_assets( asset_filter (func(object)->bool, optional): Determines whether to export an object. Defaults to all objects. Returns: - List[int]: [description] + List[Tuple[SerializedFile, int]]: [description] """ # load source env = UnityPy.load(src) @@ -115,7 +117,7 @@ def defaulted_export_index(type: ClassIDType): # the check of the various sub directories is required to avoid // in the path obj_dest = os.path.join( dst, - *(x for x in obj_path.split("/")[:ignore_first_container_dirs] if x), + *(x for x in obj_path.split("/")[ignore_first_container_dirs:] if x), ) os.makedirs(os.path.dirname(obj_dest), exist_ok=True) exported.extend( @@ -153,58 +155,56 @@ def defaulted_export_index(type: ClassIDType): ############################################################################### -def exportTextAsset(obj: TextAsset, fp: str, extension: str = ".txt") -> List[int]: +def exportTextAsset(obj: TextAsset, fp: str, extension: str = ".txt") -> List[Tuple[SerializedFile, int]]: if not extension: extension = ".txt" with open(f"{fp}{extension}", "wb") as f: - f.write(obj.script) - return [(obj.assets_file, obj.path_id)] + f.write(obj.m_Script.encode("utf-8", "surrogateescape")) + return [(obj.assets_file, obj.object_reader.path_id)] -def exportFont(obj: Font, fp: str, extension: str = "") -> List[int]: +def exportFont(obj: Font, fp: str, extension: str = "") -> List[Tuple[SerializedFile, int]]: # TODO - export glyphs if obj.m_FontData: extension = ".ttf" if obj.m_FontData[0:4] == b"OTTO": extension = ".otf" with open(f"{fp}{extension}", "wb") as f: - f.write(obj.m_FontData) - return [(obj.assets_file, obj.path_id)] + f.write(bytes(obj.m_FontData)) + return [(obj.assets_file, obj.object_reader.path_id)] -def exportMesh(obj: Mesh, fp: str, extension=".obj") -> List[int]: +def exportMesh(obj: Mesh, fp: str, extension=".obj") -> List[Tuple[SerializedFile, int]]: if not extension: extension = ".obj" with open(f"{fp}{extension}", "wt", encoding="utf8", newline="") as f: f.write(obj.export()) - return [(obj.assets_file, obj.path_id)] + return [(obj.assets_file, obj.object_reader.path_id)] -def exporShader(obj: Shader, fp: str, extension=".txt") -> List[int]: +def exportShader(obj: Shader, fp: str, extension=".txt") -> List[Tuple[SerializedFile, int]]: if not extension: extension = ".txt" with open(f"{fp}{extension}", "wt", encoding="utf8", newline="") as f: f.write(obj.export()) - return [(obj.assets_file, obj.path_id)] + return [(obj.assets_file, obj.object_reader.path_id)] def exportMonoBehaviour( obj: Union[MonoBehaviour, Object], fp: str, extension: str = "" -) -> List[int]: +) -> List[Tuple[SerializedFile, int]]: export = None # TODO - add generic way to add external typetrees - if obj.serialized_type and obj.serialized_type.nodes: - extension = ".json" - export = json.dumps(obj.read_typetree(), indent=4, ensure_ascii=False).encode( - "utf8", errors="surrogateescape" - ) + if obj.object_reader.serialized_type.node: + # a typetree is available from the SerializedFile for this object + export = obj.object_reader.read_typetree() elif isinstance(obj, MonoBehaviour): # no set typetree # check if we have a script - script = obj.m_Script - if script: + script_ptr = obj.m_Script + if script_ptr: # looks like we have a script - script = script.read() + script = script_ptr.read() # check if there is a locally stored typetree for it nodes = MONOBEHAVIOUR_TYPETREES.get(script.m_AssemblyName, {}).get( script.m_ClassName, None @@ -217,19 +217,23 @@ def exportMonoBehaviour( # if obj.name # else script.m_ClassName # ) - extension = ".json" - export = json.dumps( - obj.read_typetree(nodes), indent=4, ensure_ascii=False - ).encode("utf8", errors="surrogateescape") + export = obj.object_reader.read_typetree(nodes) + else: + export = obj.object_reader.read_typetree() if not export: extension = ".bin" - export = obj.raw_data + export = obj.object_reader.raw_data + else: + extension = ".json" + export = json.dumps(export, indent=4, ensure_ascii=False).encode( + "utf8", errors="surrogateescape" + ) with open(f"{fp}{extension}", "wb") as f: f.write(export) - return [(obj.assets_file, obj.path_id)] + return [(obj.assets_file, obj.object_reader.path_id)] -def exportAudioClip(obj: AudioClip, fp: str, extension: str = "") -> List[int]: +def exportAudioClip(obj: AudioClip, fp: str, extension: str = "") -> List[Tuple[SerializedFile, int]]: samples = obj.samples if len(samples) == 0: pass @@ -241,16 +245,16 @@ def exportAudioClip(obj: AudioClip, fp: str, extension: str = "") -> List[int]: for name, clip_data in samples.items(): with open(os.path.join(fp, f"{name}.wav"), "wb") as f: f.write(clip_data) - return [(obj.assets_file, obj.path_id)] + return [(obj.assets_file, obj.object_reader.path_id)] -def exportSprite(obj: Sprite, fp: str, extension: str = ".png") -> List[int]: +def exportSprite(obj: Sprite, fp: str, extension: str = ".png") -> List[Tuple[SerializedFile, int]]: if not extension: extension = ".png" obj.image.save(f"{fp}{extension}") exported = [ - (obj.assets_file, obj.path_id), - (obj.m_RD.texture.assets_file, obj.m_RD.texture.path_id), + (obj.assets_file, obj.object_reader.path_id), + (obj.m_RD.texture.assetsfile, obj.m_RD.texture.path_id), ] alpha_assets_file = getattr(obj.m_RD.alphaTexture, "assets_file", None) alpha_path_id = getattr(obj.m_RD.alphaTexture, "path_id", None) @@ -259,7 +263,7 @@ def exportSprite(obj: Sprite, fp: str, extension: str = ".png") -> List[int]: return exported -def exportTexture2D(obj: Texture2D, fp: str, extension: str = ".png") -> List[int]: +def exportTexture2D(obj: Texture2D, fp: str, extension: str = ".png") -> List[Tuple[SerializedFile, int]]: if not extension: extension = ".png" if obj.m_Width: @@ -268,7 +272,7 @@ def exportTexture2D(obj: Texture2D, fp: str, extension: str = ".png") -> List[in return [(obj.assets_file, obj.path_id)] -def exportGameObject(obj: GameObject, fp: str, extension: str = "") -> List[int]: +def exportGameObject(obj: GameObject, fp: str, extension: str = "") -> List[Tuple[SerializedFile, int]]: exported = [(obj.assets_file, obj.path_id)] refs = crawl_obj(obj) if refs: @@ -296,7 +300,7 @@ def exportGameObject(obj: GameObject, fp: str, extension: str = "") -> List[int] ClassIDType.Font: exportFont, ClassIDType.Mesh: exportMesh, ClassIDType.MonoBehaviour: exportMonoBehaviour, - ClassIDType.Shader: exporShader, + ClassIDType.Shader: exportShader, ClassIDType.TextAsset: exportTextAsset, ClassIDType.Texture2D: exportTexture2D, } @@ -304,13 +308,13 @@ def exportGameObject(obj: GameObject, fp: str, extension: str = "") -> List[int] MONOBEHAVIOUR_TYPETREES: Dict["Assembly-Name.dll", Dict["Class-Name", List[Dict]]] = {} -def crawl_obj(obj: Object, ret: dict = None) -> Dict[int, Union[Object, PPtr]]: +def crawl_obj(obj: Object, ret: Optional[dict] = None) -> Dict[int, Union[Object, PPtr]]: """Crawls through the data struture of the object and returns a list of all the components.""" if not ret: ret = {} if isinstance(obj, PPtr): - if obj.path_id == 0 and obj.file_id == 0 and obj.index == -2: + if obj.m_PathID == 0 and obj.m_FileID == 0 and obj.m_Index == -2: return ret try: obj = obj.read() @@ -323,8 +327,7 @@ def crawl_obj(obj: Object, ret: dict = None) -> Dict[int, Union[Object, PPtr]]: # MonoBehaviour really on their typetree # while Object denotes that the class of the object isn't implemented yet if isinstance(obj, (MonoBehaviour, Object)): - obj.read_typetree() - data = obj.type_tree.__dict__.values() + data = obj.read_typetree().__dict__.values() else: data = obj.__dict__.values() diff --git a/UnityPyBoost/AnimationClip.c b/UnityPyBoost/AnimationClip.c deleted file mode 100644 index 59ef7c98..00000000 --- a/UnityPyBoost/AnimationClip.c +++ /dev/null @@ -1,150 +0,0 @@ -#include "AnimationClip.h" -#include - -#define MIN(x, y) (((x) < (y)) ? (x) : (y)) - -/* AnimationClip.py */ -static float *UnpackFloats(uint32_t m_NumItems, float m_Range, float m_Start, uint8_t *m_Data, char m_BitSize, int itemCountInChunk, int chunkStride, int start, int numChunks) -{ - int bitPos = m_BitSize * start; - int indexPos = bitPos / 8; - bitPos %= 8; - - float scale = 1.0f / m_Range; - if (numChunks == -1) - numChunks = (int)m_NumItems / itemCountInChunk; - - // TODO: might be better to use ulong instead of uint - uint32_t end = chunkStride * numChunks / 4; - // TODO: check if this is correct - float *data = (float *)malloc(sizeof(float) * numChunks * itemCountInChunk); - float *dataStart = data; - for (uint32_t index = 0; index != end; index += chunkStride / 4) - { - for (int i = 0; i < itemCountInChunk; ++i) - { - uint32_t x = 0; - int bits = 0; - while (bits < m_BitSize) - { - x |= (uint32_t)((m_Data[indexPos] >> bitPos) << bits); - int num = MIN(m_BitSize - bits, 8 - bitPos); - bitPos += num; - bits += num; - if (bitPos == 8) - { - indexPos++; - bitPos = 0; - } - } - x &= (uint32_t)(1 << m_BitSize) - 1u; - *data++ = (x / (scale * ((1 << m_BitSize) - 1)) + m_Start); - } - } - return dataStart; -} - -// TODO: check if bitPos, bits, and num can be reduced to chars -static int *UnpackInts(uint32_t m_NumItems, uint8_t *m_Data, char m_BitSize) -{ - int *data = (int *)malloc(m_NumItems * sizeof(int)); - - int indexPos = 0; - int bitPos = 0; - for (uint32_t i = 0; i < m_NumItems; i++) - { - int bits = 0; - data[i] = 0; - while (bits < m_BitSize) - { - data[i] |= (m_Data[indexPos] >> bitPos) << bits; - int num = MIN(m_BitSize - bits, 8 - bitPos); - bitPos += num; - bits += num; - if (bitPos == 8) - { - indexPos++; - bitPos = 0; - } - } - data[i] &= (1 << m_BitSize) - 1; - } - return data; -} - -// TODO: implement unpack quaternions - -PyObject *unpack_floats(PyObject *self, PyObject *args) -{ - // define vars - uint32_t m_NumItems; - float m_Range; - float m_Start; - uint8_t *m_Data; - char m_BitSize; - int itemCountInChunk; - int chunkStride; - int start; - int numChunks; - Py_ssize_t data_size; - - start = 0; - numChunks = -1; - if (!PyArg_ParseTuple(args, "Iffy#bii|ii", &m_NumItems, &m_Range, &m_Start, &m_Data, &data_size, &m_BitSize, &itemCountInChunk, &chunkStride, &start, &numChunks)) - return NULL; - - // decode - float *array = UnpackFloats(m_NumItems, m_Range, m_Start, m_Data, m_BitSize, itemCountInChunk, chunkStride, start, numChunks); - - if (numChunks == -1) - numChunks = (int)m_NumItems / itemCountInChunk; - Py_ssize_t array_len = numChunks * itemCountInChunk; - // return - PyObject *lst = PyList_New(array_len); - if (!lst) - return NULL; - for (Py_ssize_t i = 0; i < array_len; i++) - { - PyObject *num = PyFloat_FromDouble(array[i]); - if (!num) - { - Py_DECREF(lst); - return NULL; - } - PyList_SET_ITEM(lst, i, num); // reference to num stolen - } - free(array); - return lst; -} - -PyObject *unpack_ints(PyObject *self, PyObject *args) -{ - // define vars - uint32_t m_NumItems; - uint8_t *m_Data; - char m_BitSize; - Py_ssize_t data_size; - - if (!PyArg_ParseTuple(args, "Iy#b", &m_NumItems, &m_Data, &data_size, &m_BitSize)) - return NULL; - - // decode - int *array = UnpackInts(m_NumItems, m_Data, m_BitSize); - - // return - PyObject *lst = PyList_New(m_NumItems); - if (!lst) - return NULL; - for (uint32_t i = 0; i < m_NumItems; i++) - { - PyObject *num = PyLong_FromLong(array[i]); - if (!num) - { - Py_DECREF(lst); - return NULL; - } - PyList_SET_ITEM(lst, i, num); // reference to num stolen - } - free(array); - return lst; -} \ No newline at end of file diff --git a/UnityPyBoost/AnimationClip.h b/UnityPyBoost/AnimationClip.h deleted file mode 100644 index f231d55f..00000000 --- a/UnityPyBoost/AnimationClip.h +++ /dev/null @@ -1,5 +0,0 @@ -#define PY_SSIZE_T_CLEAN -#pragma once -#include -PyObject *unpack_floats(PyObject *self, PyObject *args); -PyObject *unpack_ints(PyObject *self, PyObject *args); \ No newline at end of file diff --git a/UnityPyBoost/Mesh.c b/UnityPyBoost/Mesh.c deleted file mode 100644 index 0eb618fe..00000000 --- a/UnityPyBoost/Mesh.c +++ /dev/null @@ -1,197 +0,0 @@ -#include "Mesh.h" -#include -#include - -#define MAX(x, y) (((x) > (y)) ? (x) : (y)) - -enum -{ - kVertexFormatFloat, - kVertexFormatFloat16, - kVertexFormatUNorm8, - kVertexFormatSNorm8, - kVertexFormatUNorm16, - kVertexFormatSNorm16, - kVertexFormatUInt8, - kVertexFormatSInt8, - kVertexFormatUInt16, - kVertexFormatSInt16, - kVertexFormatUInt32, - kVertexFormatSInt32 -}; - -PyObject *unpack_vertexdata(PyObject *self, PyObject *args) -{ - // define vars - int componentByteSize; - uint32_t m_VertexCount; - uint8_t swap; - // char format; - uint8_t *vertexData; // m_VertexData.m_DataSize - uint32_t m_StreamOffset; - uint32_t m_StreamStride; - uint32_t m_ChannelOffset; - uint32_t m_ChannelDimension; - Py_ssize_t vertexDataSize; - - if (!PyArg_ParseTuple(args, "y#iIIIIIb", &vertexData, &vertexDataSize, &componentByteSize, &m_VertexCount, &m_StreamOffset, &m_StreamStride, &m_ChannelOffset, &m_ChannelDimension, &swap)) - return NULL; - - Py_ssize_t componentBytesLength = m_VertexCount * m_ChannelDimension * componentByteSize; - uint8_t *componentBytes = (uint8_t *)PyMem_Malloc(componentBytesLength + 1); - componentBytes[componentBytesLength] = 0; - - // check if max values are ok - uint32_t maxVertexDataAccess = (m_VertexCount - 1) * m_StreamStride + m_ChannelOffset + m_StreamOffset + componentByteSize * (m_ChannelDimension - 1) + componentByteSize; - if (maxVertexDataAccess > vertexDataSize) - { - PyErr_SetString(PyExc_ValueError, "Vertex data access out of bounds"); - return NULL; - } - - for (uint32_t v = 0; v < m_VertexCount; v++) - { - uint32_t vertexOffset = m_StreamOffset + m_ChannelOffset + m_StreamStride * v; - for (uint32_t d = 0; d < m_ChannelDimension; d++) - { - uint32_t vertexDataOffset = vertexOffset + componentByteSize * d; - uint32_t componentOffset = componentByteSize * (v * m_ChannelDimension + d); - memcpy(componentBytes + componentOffset, vertexData + vertexDataOffset, componentByteSize); - } - } - - if (swap) // swap bytes - { - if (componentByteSize == 2) - { - uint16_t *componentUints = (uint16_t *)componentBytes; - for (uint32_t i = 0; i < componentBytesLength; i += 2) - { - *componentUints++ = bswap16(*componentUints); - } - } - else if (componentByteSize == 4) - { - - uint32_t *componentUints = (uint32_t *)componentBytes; - for (uint32_t i = 0; i < componentBytesLength; i += 4) - { - *componentUints++ = bswap32(*componentUints); - } - } - } - - PyObject *res = PyByteArray_FromStringAndSize(componentBytes, componentBytesLength); - PyMem_Free(componentBytes); - return res; - - // fast enough in Python - // uint32_t itemCount = componentBytesLength / componentByteSize; - // PyObject *lst = PyList_New(itemCount); - // if (!lst) - // return NULL; - - // switch (format) - // { - // case kVertexFormatFloat: - // { - // float *items = (float *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)*items++)); - // } - // // result[i] = BitConverter.ToSingle(inputBytes, i * 4); - // break; - // } - // case kVertexFormatFloat16: - // { - // uint16_t *items = (uint16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // double x = _PyFloat_Unpack2(items++, 0); - // if (x == -1.0 && PyErr_Occurred()) - // { - // return NULL; - // } - // PyList_SetItem(lst, i, PyFloat_FromDouble(x)); - // } - // // result[i] = Half.ToHalf(inputBytes, i * 2); - // break; - // } - // case kVertexFormatUNorm8: - // { - // uint8_t *items = componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)(*items++ / 255.0f))); - // } - // // result[i] = inputBytes[i] / 255f; - // break; - // } - // case kVertexFormatSNorm8: - // { - // int8_t *items = (int8_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)MAX((*items++ / 127.0f), -1.0f))); - // } - // // result[i] = Math.Max((sbyte)inputBytes[i] / 127f, -1f); - // break; - // } - // case kVertexFormatUNorm16: - // { - // uint16_t *items = (uint16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)(*items++ / 65535.0f))); - // } - // // result[i] = BitConverter.ToUInt16(inputBytes, i * 2) / 65535f; - // break; - // } - // case kVertexFormatSNorm16: - // { - // int16_t *items = (int16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)MAX((*items++ / 32767.0f), -1.0f))); - // } - // // result[i] = Math.Max(BitConverter.ToInt16(inputBytes, i * 2) / 32767f, -1f); - // break; - // } - // case kVertexFormatUInt8: - // case kVertexFormatSInt8: - // { - // uint8_t *items = componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyLong_FromUnsignedLong((uint32_t)*items++)); - // } - // // result[i] = inputBytes[i]; - // break; - // } - // case kVertexFormatUInt16: - // case kVertexFormatSInt16: - // { - // uint16_t *items = (uint16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyLong_FromUnsignedLong((uint32_t)*items++)); - // } - // // result[i] = BitConverter.ToInt16(inputBytes, i * 2); - // break; - // } - // case kVertexFormatUInt32: - // case kVertexFormatSInt32: - // { - // uint32_t *items = (uint32_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyLong_FromUnsignedLong(*items++)); - // } - // // result[i] = BitConverter.ToInt32(inputBytes, i * 4); - // break; - // } - // } - // free(componentBytes); - // return lst; -} diff --git a/UnityPyBoost/Mesh.h b/UnityPyBoost/Mesh.h deleted file mode 100644 index e74f9ba1..00000000 --- a/UnityPyBoost/Mesh.h +++ /dev/null @@ -1,6 +0,0 @@ -#define PY_SSIZE_T_CLEAN -#pragma once -#include "AnimationClip.h" -#include - -PyObject *unpack_vertexdata(PyObject *self, PyObject *args); \ No newline at end of file diff --git a/UnityPyBoost/ReadMe.md b/UnityPyBoost/ReadMe.md deleted file mode 100644 index 18fb32e7..00000000 --- a/UnityPyBoost/ReadMe.md +++ /dev/null @@ -1,7 +0,0 @@ -# UnityPyBoost - -A C-extension for UnityPy that accelerates various parts of UnityPy (by a lot). - -## Structure - -The filename corresponds to where the original python functionality is situated. diff --git a/UnityPyBoost/TypeTreeHelper.c b/UnityPyBoost/TypeTreeHelper.c deleted file mode 100644 index 4b017926..00000000 --- a/UnityPyBoost/TypeTreeHelper.c +++ /dev/null @@ -1,836 +0,0 @@ -#define PY_SSIZE_T_CLEAN -#pragma once -#include -#include "structmember.h" -#include "TypeTreeHelper.h" -#include -#include -#include "swap.h" - -typedef struct -{ - char *data; - char *dataStart; - char *dataEnd; - char swap; - PyObject *obj; -} Reader; -typedef PyObject *(*read_type)(Reader *); - -#define kAlignBytesFlag 1 << 14 -#define kAnyChildUsesAlignBytesFlag 1 << 15 - -#define HASH_SInt8 235330747 -#define HASH_UInt8 237702589 -#define HASH_char 2090147939 -#define HASH_SInt16 3470947178 -#define HASH_short 274395349 -#define HASH_UInt16 3549217964 -#define HASH_unsigned_short 878862258 -#define HASH_SInt32 3470947240 -#define HASH_int 193495088 -#define HASH_UInt32 3549218026 -#define HASH_unsigned_int 2990314445 -#define HASH_TypePtr 238243313 -#define HASH_SInt64 3470947341 -#define HASH_long_long 2373003301 -#define HASH_UInt64 3549218127 -#define HASH_unsigned_long_long 857652610 -#define HASH_FileSize 2878770112 -#define HASH_float 259121563 -#define HASH_double 4181547808 -#define HASH_bool 2090120081 -#define HASH_string 479440892 -#define HASH_TypelessData 1242572536 -#define HASH_map 193499011 - -#define CHECK_LENGTH(reader, length) \ - if (reader->data + length > reader->dataEnd) \ - { \ - PyErr_Format(PyExc_ValueError, "Can't read %d bytes at position %d of %d\nError occured at %s:%d:%s", length, (int)(reader->data - reader->dataStart), (int)(reader->dataEnd - reader->dataStart), __FILE__, __LINE__, __func__); \ - return NULL; \ - } - -static char SURROGATEESCAPE[] = "surrogateescape"; - -/* function signatures */ -static int read_length(Reader *reader); -static PyObject *read_SInt8(Reader *reader); -static PyObject *read_UInt8(Reader *reader); -static PyObject *read_SInt16(Reader *reader); -static PyObject *read_UInt16(Reader *reader); -static PyObject *read_SInt32(Reader *reader); -static PyObject *read_UInt32(Reader *reader); -static PyObject *read_SInt64(Reader *reader); -static PyObject *read_UInt64(Reader *reader); -static PyObject *read_float(Reader *reader); -static PyObject *read_double(Reader *reader); -static PyObject *read_bool(Reader *reader); -static PyObject *read_string(Reader *reader); -static PyObject *read_TypelessData(Reader *reader); - -static read_type getReadFunction(int hash_value, int *index); -static PyObject *getSubNodes(PyObject *nodes, int *index); - -static PyObject *TypeTreeHelper_ReadValue(PyObject *nodes, Reader *reader, int *index); -static PyObject *TypeTreeHelper_ReadValueVector(PyObject *nodes, Reader *reader, int *index); - -PyObject *read_typetree(PyObject *self, PyObject *args); - -/* implementation */ -static inline uint32_t hash_str(const char *str) -{ - unsigned int hash = 5381; - int c; - while ((c = *str++)) - hash = ((hash << 5) + hash) + c; - return hash; -} - -static inline void initReadFuncOrNodes(PyObject *nodes, int *index, PyObject **subnodes, read_type *func, char *subalign) -{ - TypeTreeNodeObject *node = (TypeTreeNodeObject *)PyList_GetItem(nodes, *index); - *func = getReadFunction(node->typehash, index); - if (*func == NULL) - { - *subnodes = getSubNodes(nodes, index); - } - else - { - *subalign = (node->m_MetaFlag & kAlignBytesFlag) ? 1 : 0; - } -} - -static inline void align4(Reader *reader) -{ - char mod = (reader->data - reader->dataStart) % 4; - if (mod != 0) - { - reader->data += 4 - mod; - } -} - -static inline PyObject *read_bool(Reader *reader) -{ - CHECK_LENGTH(reader, 1); - return PyBool_FromLong(*(char *)reader->data++); -} - -static inline PyObject *read_SInt8(Reader *reader) -{ - CHECK_LENGTH(reader, 1); - return PyLong_FromLong(*(signed char *)reader->data++); -} - -static inline PyObject *read_UInt8(Reader *reader) -{ - CHECK_LENGTH(reader, 1); - return PyLong_FromUnsignedLong(*(unsigned char *)reader->data++); -} - -static inline PyObject *read_SInt16(Reader *reader) -{ - CHECK_LENGTH(reader, 2); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyLong_FromLong((signed short)bswap16(*(unsigned short *)reader->data)); - } - else - { - ret = PyLong_FromLong(*(signed short *)(reader->data)); - } - reader->data += 2; - return ret; -} - -static inline PyObject *read_UInt16(Reader *reader) -{ - CHECK_LENGTH(reader, 2); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyLong_FromUnsignedLong((unsigned short)bswap16(*(unsigned short *)reader->data)); - } - else - { - ret = PyLong_FromUnsignedLong(*(unsigned short *)(reader->data)); - } - reader->data += 2; - return ret; -} - -static inline PyObject *read_SInt32(Reader *reader) -{ - CHECK_LENGTH(reader, 4); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyLong_FromLong((signed int)bswap32(*(unsigned int *)reader->data)); - } - else - { - ret = PyLong_FromLong(*(signed int *)(reader->data)); - } - reader->data += 4; - return ret; -} - -static inline PyObject *read_UInt32(Reader *reader) -{ - CHECK_LENGTH(reader, 4); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyLong_FromUnsignedLong((unsigned int)bswap32(*(unsigned int *)reader->data)); - } - else - { - ret = PyLong_FromUnsignedLong(*(unsigned int *)(reader->data)); - } - reader->data += 4; - return ret; -} - -static inline PyObject *read_SInt64(Reader *reader) -{ - CHECK_LENGTH(reader, 8); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyLong_FromLongLong((signed long long)bswap64(*(unsigned long long *)reader->data)); - } - else - { - ret = PyLong_FromLongLong(*(signed long long *)(reader->data)); - } - reader->data += 8; - return ret; -} - -static inline PyObject *read_UInt64(Reader *reader) -{ - CHECK_LENGTH(reader, 8); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyLong_FromUnsignedLongLong((unsigned long long)bswap64(*(unsigned long long *)reader->data)); - } - else - { - ret = PyLong_FromUnsignedLongLong(*(unsigned long long *)(reader->data)); - } - reader->data += 8; - return ret; -} - -static inline PyObject *read_float(Reader *reader) -{ - CHECK_LENGTH(reader, 4); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyFloat_FromDouble((float)bswap32(*(unsigned int *)reader->data)); - } - else - { - ret = PyFloat_FromDouble(*(float *)(reader->data)); - } - reader->data += 4; - return ret; -} - -static inline PyObject *read_double(Reader *reader) -{ - CHECK_LENGTH(reader, 8); - PyObject *ret = NULL; - if (reader->swap) - { - ret = PyFloat_FromDouble((double)bswap64(*(unsigned long long *)reader->data)); - } - else - { - ret = PyFloat_FromDouble(*(double *)(reader->data)); - } - reader->data += 8; - return ret; -} - -static inline int read_length(Reader *reader) -{ - int length = *(int *)reader->data; - reader->data += 4; - if (reader->swap) - { - length = bswap32(length); - } - return length; -} - -static inline PyObject *read_string(Reader *reader) -{ - CHECK_LENGTH(reader, 4); - int length = read_length(reader); - CHECK_LENGTH(reader, length); - PyObject *str = PyUnicode_DecodeUTF8(reader->data, length, SURROGATEESCAPE); - reader->data += length; - // align - align4(reader); - return str; -} - -static inline PyObject *read_TypelessData(Reader *reader) -{ - CHECK_LENGTH(reader, 4); - int length = read_length(reader); - CHECK_LENGTH(reader, length); - PyObject *value = PyMemoryView_FromMemory(reader->data, length, PyBUF_READ); - reader->data += length; - return value; -} - -static inline read_type getReadFunction(int hash_value, int *index) -{ - switch (hash_value) - { - case HASH_SInt8: - return read_SInt8; - case HASH_UInt8: - case HASH_char: - return read_UInt8; - case HASH_SInt16: - case HASH_short: - return read_SInt16; - case HASH_UInt16: - case HASH_unsigned_short: - return read_UInt16; - case HASH_SInt32: - case HASH_int: - return read_SInt32; - case HASH_UInt32: - case HASH_unsigned_int: - case HASH_TypePtr: // Type* - return read_UInt32; - case HASH_SInt64: - case HASH_long_long: - return read_SInt64; - case HASH_UInt64: - case HASH_unsigned_long_long: - case HASH_FileSize: - return read_UInt64; - case HASH_float: - return read_float; - case HASH_double: - return read_double; - case HASH_bool: - return read_bool; - case HASH_string: - *index += 3; - return read_string; - case HASH_TypelessData: - *index += 2; - return read_TypelessData; - default: - return NULL; - } -} - -static PyObject *getSubNodes(PyObject *nodes, int *index) -{ - PyObject *result = NULL; - TypeTreeNodeObject *node = (TypeTreeNodeObject *)PyList_GetItem(nodes, *index); - unsigned short level = node->m_Level; - for (int i = *index + 1; i < PyList_Size(nodes); i++) - { - if (((TypeTreeNodeObject *)PyList_GetItem(nodes, i))->m_Level <= level) - { - result = PyList_GetSlice(nodes, *index, i); - *index = i - 1; - return result; - } - } - result = PyList_GetSlice(nodes, *index, PyList_Size(nodes)); - *index = PyList_Size(nodes) - 1; - return result; -} - -static inline int PyDict_SetItemString_Safe(PyObject *dict, const char *key, PyObject *value) -{ - int ret = PyDict_SetItemString(dict, key, value); - // SetItemString increases the ref count - // so we have to decrease it here again - // so that the value will be destroyed with the dict - Py_XDECREF(value); - return ret; -} - -static inline int PyList_SetItem_Safe(PyObject *list, int i, PyObject *value) -{ - int ret = PyList_SetItem(list, i, value); - // SetItem increases the ref count - // so we have to decrease it here again - // so that the value will be destroyed with the list - // Py_XDECREF(value); - return ret; -} - -static PyObject *TypeTreeHelper_ReadValue(PyObject *nodes, Reader *reader, int *index) -{ - if (*index >= PyList_Size(nodes)) - { - PyErr_SetString(PyExc_RuntimeError, "index out of range"); - return NULL; - } - TypeTreeNodeObject *node = (TypeTreeNodeObject *)PyList_GetItem(nodes, *index); - PyObject *value = NULL; - int sub_index = 0; - - char align = (node->m_MetaFlag & kAlignBytesFlag) ? 1 : 0; - // printf("RVa: %d\t%lld\t%s\t%s\t%d\t%d\n", *index, (reader->data - reader->dataStart), node->m_Name, node->m_Type, align, node->m_MetaFlag); - - int hash_value = node->typehash; - read_type func = getReadFunction(hash_value, index); - if (func) - { - value = func(reader); - } - else - { - if (hash_value == HASH_map) - { - TypeTreeNodeObject *node2 = (TypeTreeNodeObject *)PyList_GetItem(nodes, *index + 1); - if (node2->m_MetaFlag & kAlignBytesFlag) - align = 1; - - CHECK_LENGTH(reader, 4); - int size = read_length(reader); - - *index += 4; // skip self, Array, size, pair - PyObject *first_nodes = NULL; - read_type first_func = NULL; - char firstalign = 0; - initReadFuncOrNodes(nodes, index, &first_nodes, &first_func, &firstalign); - *index += 1; // move to start of second - PyObject *second_nodes = NULL; - read_type second_func = NULL; - char secondalign = 0; - initReadFuncOrNodes(nodes, index, &second_nodes, &second_func, &secondalign); - - value = PyList_New(size); - PyObject *first; - PyObject *second; - for (int i = 0; i < size; i++) - { - sub_index = 0; - first = (first_func) ? first_func(reader) : TypeTreeHelper_ReadValue(first_nodes, reader, &sub_index); - if (first == NULL) - { - Py_XDECREF(value); - return NULL; - } - if (firstalign) - align4(reader); - sub_index = 0; - second = (second_func) ? second_func(reader) : TypeTreeHelper_ReadValue(second_nodes, reader, &sub_index); - if (second == NULL) - { - Py_XDECREF(first); - Py_XDECREF(second); - return NULL; - } - if (secondalign) - align4(reader); - PyList_SetItem(value, i, PyTuple_Pack(2, first, second)); - Py_XDECREF(first); - Py_XDECREF(second); - } - Py_XDECREF(first_nodes); - Py_XDECREF(second_nodes); - } - else - { - if (*index+1 >= PyList_Size(nodes)) - { - // end of nodes reached, - // so an empty class is returned - return PyDict_New(); - } - TypeTreeNodeObject *node2 = (TypeTreeNodeObject *)PyList_GetItem(nodes, *index + 1); - if (strcmp(node2->m_Type, "Array") == 0) - { - if (node2->m_MetaFlag & kAlignBytesFlag) - align = 1; - *index += 3; // skip self, Array, size - PyObject *vector_nodes = NULL; - read_type vector_func = NULL; - char subalign = 0; - initReadFuncOrNodes(nodes, index, &vector_nodes, &vector_func, &subalign); - - int size = read_length(reader); - value = PyList_New(size); - for (int i = 0; i < size; i++) - { - int sub_index = 0; - PyObject *vector_value = (vector_func) ? vector_func(reader) : TypeTreeHelper_ReadValue(vector_nodes, reader, &sub_index); - if (vector_value == NULL) - { - Py_XDECREF(value); - return NULL; - } - if (subalign) - align4(reader); - PyList_SetItem_Safe(value, i, vector_value); - } - Py_XDECREF(vector_nodes); - } - else // Class - { - PyObject *cls_nodes = getSubNodes(nodes, index); - int j = 1; - value = PyDict_New(); - char *j_name = NULL; - while (j < PyList_Size(cls_nodes)) - { - j_name = ((TypeTreeNodeObject *)PyList_GetItem(cls_nodes, j))->m_Name; - PyObject *j_value = TypeTreeHelper_ReadValue(cls_nodes, reader, &j); - if (j_value == NULL) - { - Py_XDECREF(value); - return NULL; - } - PyDict_SetItemString_Safe(value, j_name, j_value); - j++; - } - Py_XDECREF(cls_nodes); - } - } - } - - if (align) - align4(reader); - return value; -} - -static PyObject *TypeTreeHelper_ReadTypeTree(PyObject *nodes, PyObject *buf, char swap) -{ - Py_buffer view; - if (Py_TYPE(buf)->tp_as_buffer && Py_TYPE(buf)->tp_as_buffer->bf_releasebuffer) - { - buf = PyMemoryView_FromObject(buf); - if (buf == NULL) - { - return NULL; - } - } - else - { - Py_INCREF(buf); - } - - if (PyObject_GetBuffer(buf, &view, PyBUF_WRITABLE | PyBUF_SIMPLE) < 0) - { - PyErr_Clear(); - if (PyObject_GetBuffer(buf, &view, PyBUF_SIMPLE) < 0) - { - Py_DECREF(buf); - return NULL; - } - } - - Reader reader = { - .data = (char *)view.buf, - .dataStart = (char *)view.buf, - .dataEnd = (char *)view.buf + view.len, - .swap = swap, - .obj = buf}; - - PyBuffer_Release(&view); - - int index = 0; - PyObject *result = TypeTreeHelper_ReadValue(nodes, &reader, &index); - - Py_DECREF(buf); - - return result; -} - -PyObject *read_typetree(PyObject *self, PyObject *args) -{ - PyObject *nodes = PyTuple_GetItem(args, 0); - PyObject *buf = PyTuple_GetItem(args, 1); - PyObject *swap_obj = PyTuple_GetItem(args, 2); - char swap = 0; - - if (!PyUnicode_Check(swap_obj)) - { - PyErr_SetString(PyExc_TypeError, - "The endian attribute value must be a string"); - return NULL; - } - if (PyUnicode_GET_LENGTH(swap_obj) != 1) - { - PyErr_SetString(PyExc_TypeError, - "The endian attribute value must be a string of size 1"); - return NULL; - } - char endian = *(char *)PyUnicode_DATA(swap_obj); - switch (endian) - { - case '<': - if (IS_LITTLE_ENDIAN == 0) - swap = 1; - break; - case '>': - if (IS_LITTLE_ENDIAN == 1) - swap = 1; - break; - case '=': - case '|': - break; - default: - { - PyErr_SetString(PyExc_TypeError, - "The endian attribute value must be one of '>', '<', '=', '|'"); - return NULL; - } - } - return TypeTreeHelper_ReadTypeTree(nodes, buf, swap); -} - -static void -TypeTreeNode_dealloc(TypeTreeNodeObject *self) -{ - PyMem_Free(self->m_Name); - PyMem_Free(self->m_Type); - // for (unsigned short i = 0; i < self->children_count; i++) - // { - // Py_DECREF((PyObject*)self->children[i]); - // } - // PyMem_Free(self->children); - Py_TYPE(self)->tp_free((PyObject *)self); -} - -PyObject * -TypeTreeNode_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - TypeTreeNodeObject *self; - self = (TypeTreeNodeObject *)type->tp_alloc(type, 0); - if (self != NULL) - { - self->m_Version = 0; - self->m_Level = 0; - self->m_TypeFlags = 0; - self->m_ByteSize = 0; - self->m_Index = 0; - self->m_MetaFlag = 0; - self->m_Type = PyMem_Malloc(sizeof(char)); - self->m_Name = PyMem_Malloc(sizeof(char)); - self->m_Type[0] = '\0'; - self->m_Name[0] = '\0'; - // self->children_count = 0; - // self->children = NULL; - self->m_TypeStrOffset = 0; - self->m_NameStrOffset = 0; - self->m_RefTypeHash = 0; - self->m_VariableCount = 0; - } - return (PyObject *)self; -} - -static int -TypeTreeNode_init(TypeTreeNodeObject *self, PyObject *args, PyObject *kwds) -{ - static char *kwlist[] = { - "m_Name", // char* - "m_Type", // char* - "m_Level", // uint8 - "m_MetaFlag", // int32 - "m_Version", // int16 - "m_TypeFlags", // int32 - "m_ByteSize", // int - "m_Index", // int - "m_TypeStrOffset", // unsigned int - "m_NameStrOffset", // unsigned int - "m_RefTypeHash", // unsigned long long - "m_VariableCount", // int - NULL}; - const char *type = NULL; - const char *name = NULL; - if (!PyArg_ParseTupleAndKeywords( - args, - kwds, - "|zzbihiiiIIKi", - kwlist, - &name, - &type, - &self->m_Level, - &self->m_MetaFlag, - &self->m_Version, - &self->m_TypeFlags, - &self->m_ByteSize, - &self->m_Index, - &self->m_TypeStrOffset, - &self->m_NameStrOffset, - &self->m_RefTypeHash, - &self->m_VariableCount)) - return -1; - if (type != NULL) - { - PyMem_Free(self->m_Type); - self->m_Type = PyMem_Malloc(strlen(type) + 1); - strcpy(self->m_Type, type); - self->typehash = hash_str(type); - } - if (name != NULL) - { - PyMem_Free(self->m_Name); - self->m_Name = PyMem_Malloc(strlen(name) + 1); - strcpy(self->m_Name, name); - } - return 0; -}; - -static PyObject * -TypeTreeNode_getType(TypeTreeNodeObject *self, void *closure) -{ - return PyUnicode_DecodeUTF8(self->m_Type, strlen(self->m_Type), SURROGATEESCAPE); -} - -static int -TypeTreeNode_setType(TypeTreeNodeObject *self, PyObject *value, void *closure) -{ - if (!PyUnicode_Check(value)) - { - PyErr_SetString(PyExc_TypeError, "The type attribute value must be a string"); - return -1; - } - PyMem_Free(self->m_Type); - char *type = PyUnicode_AsUTF8(value); - self->m_Type = PyMem_Malloc(strlen(type) + 1); - strcpy(self->m_Type, type); - self->typehash = hash_str(type); - return 0; -} - -static PyObject * -TypeTreeNode_getName(TypeTreeNodeObject *self, void *closure) -{ - return PyUnicode_DecodeUTF8(self->m_Name, strlen(self->m_Name), SURROGATEESCAPE); -} - -static int -TypeTreeNode_setName(TypeTreeNodeObject *self, PyObject *value, void *closure) -{ - if (!PyUnicode_Check(value)) - { - PyErr_SetString(PyExc_TypeError, "The name attribute value must be a string"); - return -1; - } - PyMem_Free(self->m_Name); - char *name = PyUnicode_AsUTF8(value); - self->m_Name = PyMem_Malloc(strlen(name) + 1); - strcpy(self->m_Name, name); - return 0; -} - -static PyMemberDef TypeTreeNode_members[] = { - //{"m_Type", T_STRING, offsetof(TypeTreeNodeObject, m_Type), 0, ""}, - //{"m_Name", T_STRING, offsetof(TypeTreeNodeObject, m_Name), 0, ""}, - {"m_ByteSize", T_INT, offsetof(TypeTreeNodeObject, m_ByteSize), 0, ""}, - {"m_Index", T_INT, offsetof(TypeTreeNodeObject, m_Index), 0, ""}, - {"m_TypeFlags", T_INT, offsetof(TypeTreeNodeObject, m_TypeFlags), 0, ""}, - {"m_Version", T_SHORT, offsetof(TypeTreeNodeObject, m_Version), 0, ""}, - {"m_MetaFlag", T_INT, offsetof(TypeTreeNodeObject, m_MetaFlag), 0, ""}, - {"m_Level", T_UBYTE, offsetof(TypeTreeNodeObject, m_Level), 0, ""}, - {"m_TypeStrOffset", T_UINT, offsetof(TypeTreeNodeObject, m_TypeStrOffset), 0, ""}, - {"m_NameStrOffset", T_UINT, offsetof(TypeTreeNodeObject, m_NameStrOffset), 0, ""}, - {"m_RefTypeHash", T_ULONGLONG, offsetof(TypeTreeNodeObject, m_RefTypeHash), 0, ""}, - {"m_VariableCount", T_INT, offsetof(TypeTreeNodeObject, m_VariableCount), 0, ""}, - {NULL} /* Sentinel */ -}; - -static PyGetSetDef TypeTreeNode_getsetters[] = { - {"m_Type", (getter)TypeTreeNode_getType, (setter)TypeTreeNode_setType, - "", NULL}, - {"m_Name", (getter)TypeTreeNode_getName, (setter)TypeTreeNode_setName, - "", NULL}, - {NULL} /* Sentinel */ -}; - -static PyObject * -TypeTreeNode_repr(PyObject *self) -{ - TypeTreeNodeObject *node = (TypeTreeNodeObject *)self; - return PyUnicode_FromFormat( - "", - node->m_Level, - node->m_Type, - node->m_Name); -} - -// PyTypeObject TypeTreeNodeType; - -// static int -// TypeTreeNode_set_children(TypeTreeNodeObject *self, PyObject *value, void *closure) -// { -// PyObject *tmp; -// if (value == NULL) -// { -// PyErr_SetString(PyExc_TypeError, "Cannot delete the children attribute"); -// return -1; -// } -// if (!PyList_Check(value)) -// { -// PyErr_SetString(PyExc_TypeError, "The children attribute value must be a list"); -// return -1; -// } -// for (unsigned short i = 0; i < self->children_count; i++) -// { -// Py_DECREF((PyObject*)self->children[i]); -// } -// PyMem_Free(self->children); -// self->children_count = PyList_Size(value); -// self->children = PyMem_Malloc(sizeof(TypeTreeNodeObject *) * self->children_count); -// for (unsigned short i = 0; i < self->children_count; i++) -// { -// tmp = PyList_GetItem(value, i); -// if (!PyObject_TypeCheck(tmp, &TypeTreeNodeType)) -// { -// PyErr_SetString(PyExc_TypeError, "The children attribute value must be a list of TypeTreeNode objects"); -// return -1; -// } -// self->children[i] = (TypeTreeNodeObject *)tmp; -// Py_INCREF(tmp); -// } -// return 0; -// } - -// static PyGetSetDef TypeTreeNode_getsetters[] = { -// {"children", (getter) TypeTreeNode_get_children, (setter) TypeTreeNode_set_children, -// "", NULL}, -// {NULL} /* Sentinel */ -// }; - -PyTypeObject TypeTreeNodeType = { - PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "UnityPyBoost.TypeTreeNode", - .tp_doc = PyDoc_STR("TypeTreeNode objects"), - .tp_basicsize = sizeof(TypeTreeNodeObject), - .tp_itemsize = 0, - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .tp_new = TypeTreeNode_new, - .tp_init = (initproc)TypeTreeNode_init, - .tp_dealloc = (destructor)TypeTreeNode_dealloc, - .tp_members = TypeTreeNode_members, - .tp_getset = TypeTreeNode_getsetters, - .tp_repr = (reprfunc)TypeTreeNode_repr, -}; - -int add_typetreenode_to_module(PyObject *m) -{ - if (PyType_Ready(&TypeTreeNodeType) < 0) - return -1; - Py_INCREF(&TypeTreeNodeType); - PyModule_AddObject(m, "TypeTreeNode", (PyObject *)&TypeTreeNodeType); - return 0; -} diff --git a/UnityPyBoost/TypeTreeHelper.h b/UnityPyBoost/TypeTreeHelper.h deleted file mode 100644 index 8de8fea6..00000000 --- a/UnityPyBoost/TypeTreeHelper.h +++ /dev/null @@ -1,31 +0,0 @@ -#define PY_SSIZE_T_CLEAN -#pragma once -#include -#include "structmember.h" - -typedef struct TypeTreeNodeObject{ - PyObject_HEAD - short m_Version; - unsigned char m_Level; - int m_TypeFlags; - int m_ByteSize; - int m_Index; - int m_MetaFlag; - char *m_Type; - char *m_Name; - //unsigned short children_count; - //struct TypeTreeNodeObject **children; - // UnityFS - unsigned int m_TypeStrOffset; - unsigned int m_NameStrOffset; - // UnityFS - version >= 19 - unsigned long long m_RefTypeHash; - // UnityRaw - versin = 2 - int m_VariableCount; - // helper fields - unsigned int typehash; -} TypeTreeNodeObject; - -int add_typetreenode_to_module(PyObject *m); - -PyObject* read_typetree(PyObject *self, PyObject *args); \ No newline at end of file diff --git a/UnityPyBoost/UnityPyBoost.c b/UnityPyBoost/UnityPyBoost.c deleted file mode 100644 index 48745d35..00000000 --- a/UnityPyBoost/UnityPyBoost.c +++ /dev/null @@ -1,52 +0,0 @@ -#define PY_SSIZE_T_CLEAN -#pragma once -#include -#include "AnimationClip.h" -#include "Mesh.h" -#include "TypeTreeHelper.h" - -/* Mesh.py */ - -static struct PyMethodDef method_table[] = { - {"unpack_floats", - (PyCFunction)unpack_floats, - METH_VARARGS, - "replacement for PackedFloatVector.unpack_floats"}, - {"unpack_ints", - (PyCFunction)unpack_ints, - METH_VARARGS, - "replacement for PackedIntVector.unpack_ints"}, - {"unpack_vertexdata", - (PyCFunction)unpack_vertexdata, - METH_VARARGS, - "replacement for VertexData to ComponentData in Mesh.ReadVertexData"}, - {"read_typetree", - (PyCFunction)read_typetree, - METH_VARARGS, - "replacement for TypeTreeHelper.read_typetree"}, - {NULL, - NULL, - 0, - NULL} // Sentinel value ending the table -}; - -// A struct contains the definition of a module -static PyModuleDef UnityPyBoost_module = { - PyModuleDef_HEAD_INIT, - "UnityPyBoost", // Module name - "TODO", - -1, // Optional size of the module state memory - method_table, - NULL, // Optional slot definitions - NULL, // Optional traversal function - NULL, // Optional clear function - NULL // Optional module deallocation function -}; - -// The module init function -PyMODINIT_FUNC PyInit_UnityPyBoost(void) -{ - PyObject* module = PyModule_Create(&UnityPyBoost_module); - add_typetreenode_to_module(module); - return module; -} \ No newline at end of file diff --git a/UnityPyBoost/setup.py b/UnityPyBoost/setup.py deleted file mode 100644 index fc197c1f..00000000 --- a/UnityPyBoost/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -from setuptools import setup, Extension, find_packages -import os -import platform - -local = os.path.dirname(os.path.abspath(__file__)) - -setup( - name="UnityPyBoost", - description="TODO", - author="K0lb3", - version="0.0.3", - ext_modules=[ - Extension( - "UnityPyBoost", - [ - os.path.join(local, f) - for f in os.listdir(local) if f.endswith(".c") - ], - language="c", - include_dirs=[local], - ) - ], -) diff --git a/UnityPyBoost/swap.h b/UnityPyBoost/swap.h deleted file mode 100644 index 5597a232..00000000 --- a/UnityPyBoost/swap.h +++ /dev/null @@ -1,35 +0,0 @@ -// check if the system is little endian -#define IS_LITTLE_ENDIAN (*(unsigned char *)&(unsigned short){1}) - -// set swap funcions (source: old version of nodejs/src/node_buffer.cc) -#if defined(__GNUC__) || defined(__clang__) -#define bswap16(x) __builtin_bswap16(x) -#define bswap32(x) __builtin_bswap32(x) -#define bswap64(x) __builtin_bswap64(x) -#elif defined(__linux__) -#include -#define bswap16(x) bswap_16(x) -#define bswap32(x) bswap_32(x) -#define bswap64(x) bswap_64(x) -#elif defined(_MSC_VER) -#include -#define bswap16(x) _byteswap_ushort(x) -#define bswap32(x) _byteswap_ulong(x) -#define bswap64(x) _byteswap_uint64(x) -#else -#define bswap16 ((x) << 8) | ((x) >> 8) -#define bswap32 \ - (((x)&0xFF) << 24) | \ - (((x)&0xFF00) << 8) | \ - (((x) >> 8) & 0xFF00) | \ - (((x) >> 24) & 0xFF) -#define bswap64 \ - (((x)&0xFF00000000000000ull) >> 56) | \ - (((x)&0x00FF000000000000ull) >> 40) | \ - (((x)&0x0000FF0000000000ull) >> 24) | \ - (((x)&0x000000FF00000000ull) >> 8) | \ - (((x)&0x00000000FF000000ull) << 8) | \ - (((x)&0x0000000000FF0000ull) << 24) | \ - (((x)&0x000000000000FF00ull) << 40) | \ - (((x)&0x00000000000000FFull) << 56) -#endif \ No newline at end of file diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 9da9a029..00000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-dinky \ No newline at end of file diff --git a/examples/AssetPatch/522608825 b/examples/AssetPatch/522608825 deleted file mode 100644 index 4938f2f9..00000000 Binary files a/examples/AssetPatch/522608825 and /dev/null differ diff --git a/examples/AssetPatch/patch.py b/examples/AssetPatch/patch.py deleted file mode 100644 index 7a4d199b..00000000 --- a/examples/AssetPatch/patch.py +++ /dev/null @@ -1,65 +0,0 @@ -import json -import os -import time -import UnityPy -from googletrans import Translator - -root = os.path.dirname(os.path.realpath(__file__)) - -def main(): - # load the original japanese localisation - src = os.path.join(root, "522608825") - e = UnityPy.load(src) - # iterate over all localisation assets - for cont, obj in e.container.items(): - # read the asset data - data = obj.read() - # get the localisation data - script = json.loads(bytes(data.script)) # bytes wrapper to handle memoryview - if hasattr(script, "infos"): - continue - print(data.name) - # translate the localisation - for entry in script["infos"]: - entry["value"] = translate(entry["value"]) - # overwrite the original - data.script = json.dumps( - script, - ensure_ascii=False, indent=4 - ).encode("utf8") - # apply the changes - data.save() - - # save the modified Bundle as file - with open(os.path.join(root, "522608825_patched"), "wb") as f: - f.write(e.file.save()) - - -# simple cache for already translated strings -# prevents requesting the same string multiple times -# therefore saving time and reducing the chance of being IP-blocked -TL_CACHE = {} -def translate(text): - # check if the text is valid - if not text.strip(" "): - return text - # check if the text was already translated once - if text in TL_CACHE: - return TL_CACHE[text] - - # actual google translation - translator = Translator(service_urls=['translate.googleapis.com']) - try: - ret = translator.translate(text, "en", "ja").text - TL_CACHE[text] = ret - return ret - except json.JSONDecodeError: - input("DecodeError") - return translate(text) - except (ConnectionError, AttributeError) as e: - print(e) - time.sleep(1) - return translate(text) - -if __name__ == "__main__": - main() diff --git a/examples/CustomMonoBehaviour/data.unity3d b/examples/CustomMonoBehaviour/data.unity3d deleted file mode 100644 index 85abee47..00000000 Binary files a/examples/CustomMonoBehaviour/data.unity3d and /dev/null differ diff --git a/examples/CustomMonoBehaviour/get_scriptable_texture.py b/examples/CustomMonoBehaviour/get_scriptable_texture.py deleted file mode 100644 index 06ba8e73..00000000 --- a/examples/CustomMonoBehaviour/get_scriptable_texture.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -This example shows how to write a custom MonoBehaviour class. - - -The script is for a game that generates its encryption key based on a specific image. -This image is linked to by a MonoBheaviour called "ScriptableTexture2D". -It is linked to by a pointer after the default MonoBehaviour structure. -This intel was acquired from the assembly of the game. - -Unfortunately, the asset doesn't have a type-tree, so the default MonoBehaviour class can't find the pointer. -So a custom MonoBehaviour class is required to get the pointer comfortably. -Doing so is simple, as seen in the following code. -""" - -import os -import UnityPy -from UnityPy.classes import MonoBehaviour, PPtr - -class ScriptableTexture2D(MonoBehaviour): - def __init__(self, reader): - # calls the default MonoBehaviour init - super().__init__(reader=reader) - # here goes the implementation of the extra data - self.texture = PPtr(reader) - -# set the path for the target file -root = os.path.dirname(os.path.realpath(__file__)) -fp = os.path.join(root,"data.unity3d") - -env = UnityPy.load(fp) - -# find the correct asset -for obj in env.objects: - if obj.type == "MonoBehaviour": - data = obj.read() - if data.name == "ScriptableTexture2D": - # correct obj found - # lets read it with the custom class - st = ScriptableTexture2D(obj) - # read the linked image and save it - tex = st.texture.read() - print(st.texture.read().name) - #tex.image.save(os.path.join(root, f"{tex.name}.png")) \ No newline at end of file diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb b/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb deleted file mode 100644 index 527ffa1a..00000000 Binary files a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb and /dev/null differ diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.dev.json b/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.dev.json deleted file mode 100644 index 4a6bcc1f..00000000 --- a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.dev.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "runtimeOptions": { - "additionalProbingPaths": [ - "C:\\Users\\W0lf\\.dotnet\\store\\|arch|\\|tfm|", - "C:\\Users\\W0lf\\.nuget\\packages" - ] - } -} \ No newline at end of file diff --git a/examples/MonoBehaviourFromAssembly/monobehaviour_from_assembly.py b/examples/MonoBehaviourFromAssembly/monobehaviour_from_assembly.py deleted file mode 100644 index 93c98753..00000000 --- a/examples/MonoBehaviourFromAssembly/monobehaviour_from_assembly.py +++ /dev/null @@ -1,170 +0,0 @@ -# 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 - - -############################ -# -# Warning: This example isn't for beginners -# -############################ - -import os -import UnityPy -from typing import Dict -import json - -ROOT = os.path.dirname(os.path.realpath(__file__)) -TYPETREE_GENERATOR_PATH = os.path.join(ROOT, "TypeTreeGenerator") - -def main(): - # dump the trees for all classes in the assembly - dll_folder = os.path.join(ROOT, "DummyDll") - 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. - - export_monobehaviours(asset_path, trees) - -def export_monobehaviours(asset_path: str, trees: dict): - for r, d, fs in os.walk(asset_path): - for f in fs: - try: - env = UnityPy.load(os.path.join(r, f)) - except: - continue - for obj in env.objects: - if obj.type == "MonoBehaviour": - d = obj.read() - if obj.serialized_type and obj.serialized_type.nodes: - tree = obj.read_typetree() - else: - if not d.m_Script: - continue - # RIP, no referenced script - # can only dump raw - script = d.m_Script.read() - # on-demand solution without already dumped tree - #nodes = generate_tree( - # g, script.m_AssemblyName, script.m_ClassName, script.m_Namespace - #) - if script.m_ClassName not in trees: - # class not found in known trees, - # might have to add the classes of the other dlls - continue - nodes = FakeNode(**trees[script.m_ClassName]) - tree = obj.read_typetree(nodes) - - # save tree as json whereever you like - - - -def dump_assembly_trees(dll_folder: str, out_path: str): - # 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 = generate_tree(g, "Assembly-CSharp.dll", "", "") - - if out_path: - with open("typetrees.json", "wt", encoding="utf8") as f: - json.dump(trees, f, ensure_ascii=False) - 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( - os.path.join(TYPETREE_GENERATOR_PATH, "TypeTreeGenerator.runtimeconfig.json") - ) - 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 - - -class FakeNode: - """A fake/minimal Node class for use in UnityPy.""" - - def __init__(self, **kwargs): - self.__dict__.update(**kwargs) - - -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/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json b/examples/TypeTreeGenerator/TypeTreeGenerator.deps.json similarity index 100% rename from examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json rename to examples/TypeTreeGenerator/TypeTreeGenerator.deps.json diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.dll b/examples/TypeTreeGenerator/TypeTreeGenerator.dll similarity index 100% rename from examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.dll rename to examples/TypeTreeGenerator/TypeTreeGenerator.dll diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json b/examples/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json similarity index 100% rename from examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json rename to examples/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json diff --git a/examples/dump_assets.py b/examples/dump_assets.py new file mode 100644 index 00000000..3802373b --- /dev/null +++ b/examples/dump_assets.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +import os, json, re +from glob import glob +from UnityPy import Environment +from UnityPy.enums import ClassIDType as CID +from UnityPy.helpers.GameObjectNode import GameObjectNode +from UnityPy.streams import EndianBinaryReader +from UnityPy.helpers.TypeTreeHelper import TypeTreeNode +from tqdm import tqdm +import json + +TYPES = [CID.Texture2D, CID.TextAsset, CID.MonoBehaviour, CID.Shader, CID.VideoClip] + +BUILD_SCENE_TREE = True + +ROOT = os.path.abspath(os.getcwd()) # base directory +DST = os.path.join(ROOT, "output") # destination folder + +ASSETS = glob( + os.path.join(ROOT,"globalmanagers")) + glob( + os.path.join(ROOT,"*.assets")) + glob( + os.path.join(ROOT,"level*")) + glob( + os.path.join(ROOT,"StreamingAssets\\aa\\StandaloneWindows64\\**\\*.bundle"), recursive=True) + glob( + os.path.join(ROOT,"StreamingAssets\\aa\\Windows\\StandaloneWindows64\\**\\*.bundle"), recursive=True) + 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()) + +from numpy import frombuffer, uint8, bitwise_and, bitwise_xor, right_shift, concatenate, dtype, fromstring, uint64, uint32, uint16, ubyte +def get_decryption_func(key): + def decryption_func(data, pos): + data = frombuffer(data, dtype=ubyte) + # some cypher algo here + return bytes(bitwise_xor(data, key)) + return decryption_func + +def allowed_path(path_name): + return re.sub(r'[^\w(){}[]\-_\. ]|[\*\?\!]', '_', path_name) + +component_dict = {} +gobj_leafs_dict = {} +parent_root = GameObjectNode() + +def main(): + global component_dict + global gobj_leafs_dict + global parent_root + + os.makedirs(DST, exist_ok=True) + GLOBAL_MAP = Environment.prepare_global_map(ASSETS, tqdm) + + for file_name in ASSETS: + parent_root = GameObjectNode() + am = Environment(globalmap=GLOBAL_MAP) + component_dict = {} + src = os.path.realpath(os.path.join(ROOT, file_name)) + print("Parsing file:", src) + if "StreamingAssets" in src: + key = 0 + asset = am.load_file( + EndianBinaryReader( + open(file_name, 'rb'), crypto_func=get_decryption_func(key) + ), name=file_name) + else: + asset = am.load_file(file_name, name=file_name)#, dump=True) + if asset is None: + continue + + am.ignore_dir_lvls = 2 + if BUILD_SCENE_TREE: + am.process(export_obj, ['GameObject']) + lines = [line for line in parent_root.print_tree() if line] + if lines: + with open(file_name+"_scenetree.txt", "w", encoding="utf-8") as st: + st.write('\n'.join(lines)) + + if hasattr(asset, "get_filtered_assets"): + for item in tqdm(list(asset.get_filtered_assets(TYPES))): + export_obj(item, item.assets_file.name) + + asset.close() + am.close() + +def make_path(*args): + fp = os.path.join(*args) + os.makedirs(allowed_path(os.path.dirname(fp)), exist_ok=True) + return fp + + +def export_obj(obj, asset: str, **kwargs) -> list: + global component_dict + global gobj_leafs_dict + global parent_root + + objfmt = obj.type + + data = obj.read() + + name = "unnamed asset" + fbase = os.path.basename(asset) + try: + if objfmt == CID.MonoBehaviour: + if BUILD_SCENE_TREE: + name = component_dict[fbase + ',' + str(obj.path_id)].find_path_up() + elif (objfmt != CID.GameObject and hasattr(data, "name") and data.name != ''): + name = data.name + except: + pass + if objfmt != CID.MonoBehaviour and objfmt != CID.GameObject: + name = re.sub(r'[^\w(){}\-_\. ]', '_', name) + fname, extension = os.path.splitext(name) + objname = "%s-%s-%d" % (fname, asset, obj.path_id) + + if BUILD_SCENE_TREE and objfmt == CID.GameObject: + if data not in gobj_leafs_dict: + current_node = GameObjectNode(data.m_Name, data) + gobj_leafs_dict[data] = current_node + else: + current_node = gobj_leafs_dict[data] + parent = parent_root + for component in data.m_Components: + name = None + try: + name = component.m_Name + except: + pass + if name is None: name = data.m_Name + if component.type in [CID.Transform, CID.RectTransform]: + if tmp := component.read(): + if tmp.m_Father: + if tmp := tmp.m_Father.read(): + parent_go = tmp.m_GameObject.read() + if not parent_go: + parent = parent_root + continue + if parent_go not in gobj_leafs_dict: + parent = GameObjectNode(parent_go.m_Name, parent_go) + gobj_leafs_dict[parent_go] = parent + else: + parent = gobj_leafs_dict[parent_go] + component_dict[fbase + ',' + str(component.path_id)] = current_node + for component in data.m_Components: + current_node.add_attachment(component) + + parent.add_child(current_node) + + elif objfmt == CID.VideoClip: + fp = f"{make_path(DST, 'VideoClip', os.path.split(fname)[0], objname).strip()}.mp4" + if not os.path.isfile(fp): + try: + video = data.video + if len(video): + with open(fp, "wb") as f: + f.write(video) + except Exception as e: + if data.name is not None: + objfmt = data.name + print(repr(e), "in file:", objname, "object type:", objfmt) + return [obj.path_id] + + elif objfmt == CID.TextAsset: + if data.script: + fp = f"{make_path(DST, 'TextAsset', 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 == CID.Texture2D: + fp = f"{make_path(DST, 'Texture2D', fbase, os.path.split(fname)[0], objname).strip()}.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 [obj.path_id] + + elif objfmt == CID.Sprite: + fp = f"{make_path(DST, 'Sprite', fbase, fname)}.png" + if not os.path.isfile(fp): + data.image.save(fp) + + elif objfmt == CID.PlayerSettings: + fp = f"{make_path(DST, objname)}.dat" + if not os.path.isfile(fp): + with open(fp, "wb") as f: + f.write(obj.get_raw_data()) + + elif objfmt == CID.Shader: + extension = "txt" + fp = f"{make_path(DST, 'Shader', objname)}" + if not os.path.isfile(fp): + with open(f"{fp}.txt", "w", encoding="utf-8") as f: + f.write(data.export()) + if not os.path.isfile(fp): + with open(f"{fp}.dat", "wb") as f: + f.write(data.get_raw_data()) + + elif objfmt == CID.MonoBehaviour: + is_raw = True + script = None + if not data.m_Script: + # RIP, no referenced script, can only dump raw + pass + else: + script = data.m_Script.read() + if script: + cname = script.m_ClassName + #if "TextMeshProUGUI" not in cname: + #return [obj.path_id] + if not script: + pass + elif ASSEMBLY_TREES and cname and cname in ASSEMBLY_TREES: + nodes = ASSEMBLY_TREES[cname] + try: + tree = obj.read_typetree(nodes) + is_raw = False + except Exception as e: + #print("Error #1", str(e), "in", objname) + #is_raw = False + #tree = e.nodes + #objname += "-broken" + pass + if (is_raw or not cname or (ASSEMBLY_TREES and cname not in ASSEMBLY_TREES)) and obj.serialized_type.nodes: + # only try embedded nodes as the last resort since they have unknown quality + try: + tree = obj.read_typetree() + is_raw = False + except Exception as e: + print("Error #2", str(e), "in", objname) + pass + + def default(instance): + if not isinstance(instance, TypeTreeNode): + return instance + values = [v for v in dir(instance) if v[0] != "_"] + return dict(zip(values, [instance.__getattribute__(v) for v in values])) + + if is_raw: + extension = "dat" + export = data.get_raw_data() + else: + extension = "json" + export = json.dumps(tree, indent=4, ensure_ascii=False, default=default).encode("utf-8-sig") + + if not BUILD_SCENE_TREE and script and hasattr(script, "m_Namespace") and hasattr(script, "m_ClassName"): + fp = f"{make_path(DST, 'MonoBehaviours', script.m_Namespace, script.m_ClassName, objname)}.{extension}" + else: + fp = f"{make_path(DST, 'MonoBehaviours', objname)}.{extension}" + + if not is_raw and 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/dump_dll_typetrees.py b/examples/dump_dll_typetrees.py new file mode 100644 index 00000000..41105e35 --- /dev/null +++ b/examples/dump_dll_typetrees.py @@ -0,0 +1,136 @@ +# 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) + + from pythonnet import load + + load("coreclr") + import clr + + +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) + if not def_iter: return {} + + # 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/rebundle.py b/examples/rebundle.py deleted file mode 100644 index 9e223aa8..00000000 --- a/examples/rebundle.py +++ /dev/null @@ -1,138 +0,0 @@ -""" -This script shows how to create a bundle from dumped assets from the memory. - -The dumped assets consist of SerializedFiles and their resources(cabs). -A sample file of the original game is required for this script. -This example uses the globalgamemanager as this asset should exist in all Unity games. -""" - -import os -import uuid -import random -from copy import copy -import re - -import UnityPy -from UnityPy.enums import ClassIDType -from UnityPy.files import BundleFile -from UnityPy.files.SerializedFile import FileIdentifier, ObjectReader, SerializedType - - -SERIALIZED_PATH = r"globalgamemanagers" -DATA_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") - - -def main(): - bf = Fake( - signature="UnityFS", - version=6, - format=6, - version_engine="2017.4.30f1", - version_player="5.x.x", - _class=BundleFile, - files={}, - ) - # load default serialized file and prepare some variables for easier access to key objects - env = UnityPy.load(SERIALIZED_PATH) - sf = env.file # serialized file - or_bp = list(sf.objects.values())[0].__dict__ # object data - - bf.files["serialized_file"] = sf - sf.flags = 4 - - # remove all unnesessary stuff - for key in list(sf.objects.keys()): - del sf.objects[key] - sf.externals = [] - - # add all files from DATA_PATH - for root, dirs, files in os.walk(DATA_PATH): - for f in files: - fp = os.path.join(root, f) - if f[:3] == "CAB": - add_cab(bf, sf, root, f) - else: - add_object(sf, fp, or_bp) - - # save edited bundle - open("bundle_edited.unity3d", "wb").write(bf.save()) - - -def add_cab(bf, sf, root, f): - fp = os.path.join(root, f) - bf.files[f] = Fake(data=open(fp, "rb").read(), flags=4) - sf.externals.append( - Fake( - temp_empty="", - guid=generate_16_byte_uid(), - path=f"archive:/{os.path.basename(root)}/{f}", - type=0, - _class=FileIdentifier, - ) - ) - - -def add_object(sf, fp, or_bp): - # get correct type id - path_id, class_name = os.path.splitext(os.path.basename(fp)) - path_id = int(path_id) if re.match( - r"^\d+$", path_id) else generate_path_id(sf.objects) - class_id = getattr( - ClassIDType, class_name[1:], ClassIDType.UnknownType).value - type_id = -1 - for i, styp in enumerate(sf.types): - if styp.class_id == class_id: - type_id = i - if type_id == -1: # not found, add type - type_id = len(sf.types) - sf.types.append( - Fake( - class_id=class_id, - is_stripped_type=False, - node=[], - script_type_index=-1, - old_type_hash=generate_16_byte_uid(), - _class=SerializedType, - ) - ) - - # add new object - odata = copy(or_bp) - odata.update( - { - "data": open(fp, "rb").read(), - "path_id": generate_path_id(sf.objects), - "class_id": class_id, - "type_id": type_id, - } - ) - sf.objects[path_id] = Fake(**odata, _class=ObjectReader) - - -def generate_path_id(objects): - while True: - uid = random.randint(-(2 ** 16), 2 ** 16 - 1) - if uid not in objects: - return uid - - -def generate_16_byte_uid(): - return uuid.uuid1().urn[-16:].encode("ascii") - - -class Fake(object): - """ - fake class for easy class creation without init call - """ - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - if "_class" in kwargs: - self.__class__ = kwargs["_class"] - - def save(self): - return self.data - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 00000000..e824936f --- /dev/null +++ b/examples/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/tpk_class_generator.py b/examples/tpk_class_generator.py new file mode 100644 index 00000000..aeea5f29 --- /dev/null +++ b/examples/tpk_class_generator.py @@ -0,0 +1,591 @@ +from __future__ import annotations +from dataclasses import dataclass, field +import re +from typing import Dict, Set, Optional, Tuple, List + +from UnityPy.helpers.Tpk import TPKTYPETREE, TpkUnityNode +from UnityPy.helpers.UnityVersion import UnityVersion +from UnityPy.helpers.TypeTreeNode import kAlignBytes + +NODES = TPKTYPETREE.NodeBuffer.Nodes +STRINGS = TPKTYPETREE.StringBuffer.Strings + +""" + HACK: TPK class generator: + - Uses .tpk to generate separate(sic!) files each primary Unity class with its subclasses + - Auto-generates __init__ reader and save methods for them + - Auto-adds conditional branching for version-dependent attributes (since it's all in the .tpk already) + - __slots__ are unnecessary since we read classes one by one without storing them as arrays + - TODO: Add field comments from the official Unity docs +""" + +UNITYPY_TYPE_MAP = { + "char": "byte", + "short": "short", + "int": "int", + "long long": "long", + "unsigned short": "u_short", + "unsigned int": "u_int", + "unsigned long long": "u_long", + "UInt8": "u_byte", + "UInt16": "u_short", + "UInt32": "u_int", + "UInt64": "u_long", + "SInt8": "byte", + "SInt16": "short", + "SInt32": "int", + "SInt64": "long", + "Type*": "int", + "FileSize": "int", + "float": "half", + "double": "double", + "bool": "bool", + "string": "aligned_string", + "TypelessData": "bytes", + "Quaternion": "quaternion", + "Color3": "color3", + "Color": "color4", + "Rectangle": "rectangle_f", + "Matrix4x4": "matrix", + "Vector2": "vector2", + "Vector3": "vector3", + "Vector4": "vector4", + "vector": "vector", + "Quaternionf": "quaternion", + "map": "map" +} + +UNINY_TO_PY_TYPE_MAP = { + "char": "str", + "short": "int", + "int": "int", + "long long": "int", + "unsigned short": "int", + "unsigned int": "int", + "unsigned long long": "int", + "UInt8": "int", + "UInt16": "int", + "UInt32": "int", + "UInt64": "int", + "SInt8": "int", + "SInt16": "int", + "SInt32": "int", + "SInt64": "int", + "Type*": "int", + "FileSize": "int", + "float": "float", + "double": "float", + "bool": "bool", + "string": "str", + "TypelessData": "bytes" +} +FORBIDDEN_NAMES = ["pass", "from"] +FORBIDDEN_CLASSES = {"bool", "float", "int", "void"} + +CLASS_CACHE_ID: Dict[Tuple[int, str], NodeClass] = {} +CLASS_CACHE_NAME: Dict[str, NodeClass] = {} +TYPE_CACHE: Dict[int, str] = {} +UNITY_TYPE_CACHE: Dict[int, str] = {} + +@dataclass +class NodeClassField: + ids: Set[int] + name: str + py_types: Set[str] = field(default_factory=set) + type: str = None, + parameters: str = None, + aligned: bool = False + min_version: UnityVersion = None + max_version: UnityVersion = None + + +class IndentedTextGenerator: + def __init__(self, start_depth=0, indent_size=4, indent_char=" "): + self.indent_size = indent_size + self.start_depth = min(start_depth, 0) + self.indent_char = indent_char + self.text_array = [] + self.set_depth() + + def set_depth(self, value = None): + if value is None: + self.depth = self.start_depth + else: + self.depth = value + + def is_depth(self, depth): + return self.depth == depth + + def indent(self): + self.depth += 1 + + def unindent(self): + self.depth = max(self.depth - 1, self.start_depth) + + def add(self, text=""): + self.text_array.append(f"{' ' * (self.depth * self.indent_size)}{text}") + + def generate(self) -> str: + return "\n".join(self.text_array) + +inner_classes = dict() + +@dataclass +class NodeClass: + ids: Set[int] + name: str + aliases: Set[str] = field(default_factory=set) + fields: Dict[str, NodeClassField] = field(default_factory=dict) + field_ids: Set[int] = field(default_factory=set) + key_fields: Set[str] = field(default_factory=set) + abstract: bool = False + base: Optional[str] = None + versions_min_max: Tuple[UnityVersion] = None + is_subclass: bool = False + + def needs_versioning(self): + return any(not self.is_same_as_base(i) for i in self.fields.values()) + + def add_to_import(self, item: NodeClassField, sorted_classes: dict, parameter: int=None): + global inner_classes + item_type = None + if parameter is None: + item_type = item.type + else: + item_type = item.parameters.split(',')[parameter] + if item_type not in inner_classes and item_type in sorted_classes and ( + sorted_classes[item_type] and sorted_classes[item_type].name not in UNITYPY_TYPE_MAP): + inner_classes[item_type] = True + inner_classes[item_type] = sorted_classes[item_type].generate_str(sorted_classes, False) + + def gen_class_reader(self, item, sorted_classes): + unity_type = item.type + code = f"{unity_type}(reader)" + if unity_type == "pair": + parameters = item.parameters.split(',') + code = "(" + if parameters[0] in UNINY_TO_PY_TYPE_MAP: + code += f"reader.read_{UNITYPY_TYPE_MAP[parameters[0]]}" + else: + code += f"{parameters[0]}(reader)" + self.add_to_import(item, sorted_classes, 0) + code += "," + if parameters[1] in UNINY_TO_PY_TYPE_MAP: + code += f"reader.read_{UNITYPY_TYPE_MAP[parameters[1]]}" + else: + code += f"{parameters[1]}(reader)" + self.add_to_import(item, sorted_classes, 1) + code += ")" + elif unity_type == "map": + parameters = item.parameters.split(',') + code = "reader.read_map(" + if parameters[0] in UNITYPY_TYPE_MAP: + code += f"reader.read_{UNITYPY_TYPE_MAP[parameters[0]]}" + else: + code += f"partial({parameters[0]}, reader)" + self.add_to_import(item, sorted_classes, 0) + code += ", " + if parameters[1] in UNITYPY_TYPE_MAP: + code += f"reader.read_{UNITYPY_TYPE_MAP[parameters[1]]}" + else: + code += f"partial({parameters[1]}, reader)" + self.add_to_import(item, sorted_classes, 1) + code += ")" + elif unity_type == "vector" or unity_type == "set": + if item.parameters in UNITYPY_TYPE_MAP: + code = f"reader.read_array(reader.read_{UNITYPY_TYPE_MAP[item.parameters]})" + else: + code = f"reader.read_array(partial({item.parameters}, reader))" + self.add_to_import(item, sorted_classes, 0) + elif unity_type == "Array": + code = f"reader.read_array(partial({item.parameters}, reader))" + self.add_to_import(item, sorted_classes, 0) + elif unity_type.startswith("PPtr<"): + code = f"PPtr(reader)" + else: + pass + self.add_to_import(item, sorted_classes) + return code + + def gen_class_writer(self, item): + unity_type = item.type + item_name = item.name + code = f"self.{item_name}.save(writer)" + if unity_type == "pair": + parameters = item.parameters.split(',') + code = "" + if parameters[0] in UNINY_TO_PY_TYPE_MAP: + code += f"writer.write_{UNITYPY_TYPE_MAP[parameters[0]]}({item_name[0]})" + else: + code += f"self.{item_name[0]}.save(writer)" + code += "," + if parameters[1] in UNINY_TO_PY_TYPE_MAP: + code += f"writer.write_{UNITYPY_TYPE_MAP[parameters[1]]}({item_name[1]})" + else: + code += f"self.{item_name[1]}.save(writer)" + elif unity_type == "map": + parameters = item.parameters.split(',') + code = f"writer.write_u_int(len(self.{item_name}.keys())), [(" + if parameters[0] in UNINY_TO_PY_TYPE_MAP: + code += f"writer.write_{UNITYPY_TYPE_MAP[parameters[0]]}(k)" + else: + code += "k.save(writer)" + code += ", " + if parameters[1] in UNINY_TO_PY_TYPE_MAP: + code += f"writer.write_{UNITYPY_TYPE_MAP[parameters[1]]}(v)" + else: + code += "v.save(writer)" + code += f") for k, v in self.{item_name}.items()]" + elif unity_type == "vector" or unity_type == "Array" or unity_type == "set": + code = "" + if item.parameters in UNINY_TO_PY_TYPE_MAP: + code += f"writer.write_array(writer.write_{UNITYPY_TYPE_MAP[item.parameters]}, self.{item_name})" + else: + code = f"writer.write_u_int(len(self.{item_name}.keys())), " + code += f"[item.save(writer) for item in self.{item_name})]" + return code + + def generate_str(self, sorted_classes, global_func=True) -> str: + parents = [] + if self.base: + parents.append(self.base) + if self.abstract: + parents.append("ABC") + parents = f"({', '.join(parents)})" if parents else "" + + code = IndentedTextGenerator() + if len(self.fields) == 0: + code.add(f"class {self.name}{parents}:") + code.indent() + code.add("pass") + else: + code.add(f"class {self.name}{parents}:") + code.indent() + code.add('def __init__(self, reader: "ObjectReader"):') + code.indent() + if self.needs_versioning(): + code.add("version = self._version = reader.version") + if not self.is_subclass: + code.add("super().__init__(reader=reader)") + last_max_version = last_min_version = None + last_bool = inside_check = False + for item_name, item in self.fields.items(): + if item_name == "m_Name" and "NamedObject" in parents: + continue + if last_max_version is None: + last_max_version, last_min_version = item.max_version, item.min_version + self.unindent_before(code, last_max_version, last_min_version, item, item_name) + if item.type in UNINY_TO_PY_TYPE_MAP: + inside_check = self.generate_version_check(code, item, f"self.{item_name} = reader.read_{UNITYPY_TYPE_MAP[item.type]}()", last_max_version, last_min_version) + else: + inside_check = self.generate_version_check(code, item, f"self.{item_name} = {self.gen_class_reader(item, sorted_classes)}", last_max_version, last_min_version) + self.add_to_import(item, sorted_classes) + last_max_version, last_min_version = item.max_version, item.min_version + next_after_bool = last_bool and item.type != "bool" + if item.aligned: + code.add(f"reader.align_stream()") + last_bool = item.type == "bool" # TODO: only last conseqent bool should be alinged along the all applicable versions + code.add("") + code.set_depth() + code.indent() + code.add('def save(self, writer: "EndianBinaryWriter" = None):') + code.indent() + if self.needs_versioning(): + code.add("version = self._version") + if not self.is_subclass: + code.add(f"super().save(writer)") + code.add(f"if writer is None:") + code.indent() + code.add(f"writer = EndianBinaryWriter(endian=self.reader.endian)") + code.unindent() + last_max_version = last_min_version = None + inside_check = False + for item_name, item in self.fields.items(): + if last_max_version is None: + last_max_version, last_min_version = item.max_version, item.min_version + self.unindent_before(code, last_max_version, last_min_version, item, item_name) + if item.type in UNINY_TO_PY_TYPE_MAP: + inside_check = self.generate_version_check(code, item, f"writer.write_{UNITYPY_TYPE_MAP[item.type]}(self.{item_name})", last_max_version, last_min_version) + else: + inside_check = self.generate_version_check(code, item, self.gen_class_writer(item), last_max_version, last_min_version) + last_max_version, last_min_version = item.max_version, item.min_version + if item.aligned: + code.add(f"writer.align_stream()") + code.add("\n") + #return ("\n".join(f"from .{c} import {c}" for c in inner_classes.keys())+ "\n\n") + code.generate() + return (("\n".join(v for k, v in inner_classes.items())+ "\n\n") if global_func else "") + code.generate() + + def unindent_before(self, code, last_max_version, last_min_version, item, item_name=None): + if last_max_version > item.max_version: + pass + if last_max_version < item.max_version: + if not self.is_version_unchanged(last_max_version, last_min_version, item): + code.set_depth(2) + else: + code.unindent() + if last_min_version > item.min_version: + if not self.is_version_unchanged(last_max_version, last_min_version, item): + code.set_depth(2) + else: + code.unindent() + if last_min_version < item.min_version: + pass + + def is_version_unchanged(self, last_max_version, last_min_version, item): + return last_max_version == item.max_version and last_min_version == item.min_version + + def is_same_as_base(self, item): + if not self.versions_min_max: + return True + return item.min_version == self.versions_min_max[0] and item.max_version == self.versions_min_max[1] + + def generate_version_check(self, code, item, text, last_max_version, last_min_version) -> bool: + min_version = item.min_version + max_version = item.max_version + inside_check = False + version_unchanged = self.is_version_unchanged(last_max_version, last_min_version, item) + if self.is_same_as_base(item): + pass + elif version_unchanged: + pass + elif max_version == self.versions_min_max[1]: + code.add(f"if version > {min_version.as_tuple()}:") + code.indent() + inside_check = True + elif min_version == self.versions_min_max[0]: + code.add(f"if version <= {max_version.as_tuple()}") + code.indent() + inside_check = True + else: + code.set_depth(2) + code.add(f"if {min_version.as_tuple()} <= version <= {max_version.as_tuple()}:") + code.indent() + inside_check = True + code.add(text) + return inside_check + +def clean_name(name: str) -> str: + if name.startswith("(int&)"): + name = name[6:] + if name.endswith("?"): + name = name[:-1] + name = re.sub("[ \.:\-\[\]]", "_", name) + if name in FORBIDDEN_NAMES: + name += "_" + if name[0].isdigit(): + # only used by display res + # attrs.define has a problem with _num, + # so some other value has to be used + name = f"x{name}" + return name + + +def implement_node_class( + node_id: int, + node: Optional[TpkUnityNode] = None, + override_name: Optional[str] = None, + versions_min_max: Tuple[UnityVersion] = None, + version=None, + is_subclass=False, + needs_versioning=False +) -> NodeClass: + if node is None: + node = NODES[node_id] + cls_name = override_name or STRINGS[node.TypeName] + + cls = CLASS_CACHE_ID.get((node_id, cls_name)) + if not cls is None: + return cls + + cls = CLASS_CACHE_NAME.get(cls_name) + first_impl = False + if cls is None: + cls = NodeClass(ids={node_id}, name=cls_name, versions_min_max=versions_min_max, is_subclass=is_subclass) + CLASS_CACHE_NAME[cls_name] = cls + first_impl = True + else: + cls.ids.add(node_id) + + CLASS_CACHE_ID[(node_id, cls_name)] = cls + if override_name and override_name != STRINGS[node.TypeName]: + cls.aliases.add(STRINGS[node.TypeName]) + + field_names = set() + for subnode_id in node.SubNodes: + subnode = NODES[subnode_id] + subnode_name = clean_name(STRINGS[subnode.Name]) + + field_names.add(subnode_name) + + if subnode_id in cls.field_ids: + continue + + field = cls.fields.get(subnode_name) + if field is None: + field = NodeClassField({subnode_id}, subnode_name, min_version=version, max_version=versions_min_max[1], aligned=subnode.MetaFlag & kAlignBytes) + cls.fields[subnode_name] = field + else: + field.ids.add(subnode_id) + + field_type = generate_field_type(subnode_id, subnode, versions_min_max=versions_min_max, version=version) + field.py_types.add(field_type) + unity_type = STRINGS[subnode.TypeName] + field.parameters = generate_unity_field_type(subnode_id, unity_type) + field.type = unity_type + + cls.field_ids |= set(node.SubNodes) + + if first_impl: + cls.key_fields = field_names + else: + deprecated_field_names = cls.key_fields - field_names + added_field_names = field_names - cls.key_fields + cls.key_fields -= deprecated_field_names + for deprecated_field in deprecated_field_names: + cls.fields[deprecated_field].max_version = version + for added_field in added_field_names: + cls.fields[added_field].min_version = version + return cls + +def generate_unity_field_type(node_id: int, unity_type: str): + if unity_type in UNINY_TO_PY_TYPE_MAP: + return unity_type + result = UNITY_TYPE_CACHE.get(node_id) + if result is not None: + return result + node = NODES[node_id] + parameter_type = "" + if unity_type == "pair": + typ1 = generate_unity_field_type(node.SubNodes[0], STRINGS[NODES[node.SubNodes[0]].TypeName]) + typ2 = generate_unity_field_type(node.SubNodes[1], STRINGS[NODES[node.SubNodes[1]].TypeName]) + parameter_type = f"{typ1},{typ2}" + elif unity_type == "map": + subnode = NODES[node.SubNodes[0]].SubNodes[1] + parameter_type = generate_unity_field_type(subnode, STRINGS[NODES[subnode].TypeName]) + elif unity_type == "vector" or unity_type == "set": + subnode = NODES[node.SubNodes[0]].SubNodes[1] + parameter_type = generate_unity_field_type(subnode, STRINGS[NODES[subnode].TypeName]) + elif unity_type == "Array": + subnode = node.SubNodes[1] + parameter_type = generate_unity_field_type(subnode, STRINGS[NODES[subnode].TypeName]) + elif unity_type.startswith("PPtr"): + parameter_type = "PPtr" + else: + parameter_type = unity_type + UNITY_TYPE_CACHE[node_id] = parameter_type + return parameter_type + +def generate_field_type(node_id: int, node: Optional[TpkUnityNode] = None, versions_min_max=None, version=None) -> str: + result = TYPE_CACHE.get(node_id) + if result is not None: + return result + if node is None: + node = NODES[node_id] + unity_type = STRINGS[node.TypeName] + native_py_type = UNINY_TO_PY_TYPE_MAP.get(unity_type) + if native_py_type: + result = native_py_type + elif unity_type == "pair": + typ1 = generate_field_type(node.SubNodes[0], versions_min_max=versions_min_max, version=version) + typ2 = generate_field_type(node.SubNodes[1], versions_min_max=versions_min_max, version=version) + result = f"Tuple[{typ1}, {typ2}]" + elif unity_type.startswith("PPtr<"): + result = "PPtr" + else: + # map & vector + subnode0 = NODES[node.SubNodes[0]] if len(node.SubNodes) > 0 else None + if subnode0 and STRINGS[subnode0.TypeName] == "Array": + result = f"List[{generate_field_type(subnode0.SubNodes[1], versions_min_max=versions_min_max, version=version)}]" + else: + # custom class + implement_node_class(node_id, node, versions_min_max=versions_min_max, version=version, is_subclass=True) + result = unity_type + + TYPE_CACHE[node_id] = result + return result + + +def main(): + import os + global inner_classes + + main_classes = set() + deps: Dict[str, List[str]] = {} + + for class_id, class_info in TPKTYPETREE.ClassInformation.items(): + abstract = True + base = None + cls_name = None + + for version, unity_class in class_info.Classes: + if unity_class is None: + continue + cls_name = STRINGS[unity_class.Name] + base = STRINGS[unity_class.Base] + if unity_class.ReleaseRootNode != None: + abstract = False + cls = implement_node_class( + unity_class.ReleaseRootNode, + override_name=cls_name, + versions_min_max=( + class_info.Classes[0][0], + class_info.Classes[-1][0] + ), + version = version + ) + cls.base = base + + if abstract: + CLASS_CACHE_NAME[cls_name] = NodeClass( + {0}, name=cls_name, base=base, abstract=True + ) + + if cls_name: + main_classes.add(cls_name) + if base: + if base in deps: + deps[base].append(cls_name) + else: + deps[base] = [cls_name] + + CLASS_CACHE_NAME.pop("Object") + sorted_classes = [] + + stack = [*sorted(deps.pop("Object"))] + while stack: + cls_name = stack.pop(0) + sorted_classes.append(cls_name) + if cls_name in deps: + stack = sorted(deps.pop(cls_name)) + stack + + sorted_classes += sorted( + set(CLASS_CACHE_NAME.keys()) - set(sorted_classes) - FORBIDDEN_CLASSES + ) + + INTRO = """from __future__ import annotations +from functools import partial, partialmethod +from typing import List, Union, Optional, Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from ..files import ObjectReader + from ..streams import EndianBinaryWriter +from .Object import Object +from .PPtr import PPtr\n\n""" + + all_classes = CLASS_CACHE_NAME + os.makedirs("classes", exist_ok=True) + for cls in all_classes.values(): + print(f'Processing {cls.name}...') + if not cls or len(cls.fields) == 0 or cls.is_subclass: continue + inner_classes = {} + class_text = (INTRO + cls.generate_str(all_classes)) + with open(f"classes\\{cls.name}.py", "w", encoding="utf-8") as f: + f.write(class_text) + + +if __name__ == "__main__": + import time + + t1 = time.time_ns() + main() + print(f"Elapsed {str((time.time_ns() - t1) / 10**9)[:4]}s") diff --git a/examples/undump_assets.py b/examples/undump_assets.py new file mode 100644 index 00000000..5c38f27d --- /dev/null +++ b/examples/undump_assets.py @@ -0,0 +1,209 @@ +import os, sys, struct, json +from glob import glob +from UnityPy import Environment +from UnityPy.enums import ClassIDType as CID +from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter +from UnityPy.enums import TextureFormat +from UnityPy.helpers import TypeTreeHelper +from functools import partial +from UnityPy.math import Vector2, Rectangle +from PIL import Image +from tqdm import tqdm + +ROOT = os.path.abspath(os.getcwd()) # base directory +TYPES = [CID.MonoBehaviour, CID.Texture2D, CID.TextAsset, CID.Sprite, CID.Shader] +#DST = os.path.join(ROOT, "output") # destination folder + +ASSETS = glob( + os.path.join(ROOT, "original", "data.unity3d"), recursive=True) +) # source folder or files +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()) + +from numpy import frombuffer, uint8, bitwise_and, bitwise_xor, right_shift, concatenate, dtype, fromstring, uint64, uint32, uint16, ubyte +def get_encryption_func(key): + def encryption_func(data, pos): + data = frombuffer(data, dtype=ubyte) + return bytes(bitwise_xor(data, key)) + return encryption_func + +def get_reader_func(key): + def reader_generator(fn): + with open(fn, "rb") as f: + return EndianBinaryReader(f, encrypt_func=get_encryption_func(key)) + return reader_generator + +def get_writer_func(key): + def writer_generator(fn): + with open(fn, "w+b") as f: + return EndianBinaryWriter(f, encrypt_func=get_encryption_func(key)) + return writer_generator + +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) + jmbehavs = glob(IN_JSON_MONO_BEHAVIOURS, recursive=True) + + def obj_modify(obj, asset_name, **kwargs): + objfmt = obj.type + data = obj.read() + name = f"{asset_name}-{obj.path_id}." + if objfmt == CID.RectTransform: + if obj.path_id == 1234: # or load from somewhere + #print(data.m_AnchoredPosition.X,data.m_AnchoredPosition.Y,"->", end='') + data.m_AnchoredPosition.X = 123 + data.m_AnchoredPosition.Y = -45 + data.m_SizeDelta.X = 678 + data.m_SizeDelta.Y = 90 + data.m_Pivot.X = 0.4 + data.m_AnchorMax.X = 0.6 + #print(data.m_AnchoredPosition.X,data.m_AnchoredPosition.Y) + data.save() + elif objfmt == CID.SpriteRenderer: + data.m_DrawMode = 1 + obj.save_typetree(data) # data is of type NodeHelper here + elif objfmt == CID.Shader: + if "Mosaic" in data.name: + print(f"Modding Shader {obj.path_id} in {asset_name}...") + for i, rtb in enumerate(data.m_ParsedForm.m_SubShaders[0].m_Passes[0].m_State.rtBlend): + rtb.colMask.val = 0.0 + #rtb.blendOp.val = D3D_BLEND.D3D_BLEND_SRC_COLOR + #rtb.srcBlend.val = D3D_BLEND.D3D_BLEND_SRC_ALPHA + #rtb.destBlend.val = D3D_BLEND.D3D_BLEND_INV_SRC_ALPHA + data.save() + pass + return [obj.path_id] + elif objfmt == CID.Material: + if "Mosaic" in data.name: + print(f"Modding Material {obj.path_id} in {asset_name}...") + #data.m_SavedProperties.m_Colors["_Color"].G = 1.17549435e-38 + #data.m_SavedProperties.m_Colors["_Color"].B = 1.17549435e-38 + #data.m_SavedProperties.m_Colors["_Color"].A = 1e-5 + #data.m_Shader.path_id = 0 + #data.m_CustomRenderQueue = 0 + if objfmt == CID.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.settingsRaw = 2 + data.m_RD.textureRect = data.m_Rect + data.m_RD.textureRectOffset = Vector2(0, 0) + #data.m_RD.m_SubMeshes = [] + #data.m_RD.m_IndexBuffer = b'' + #data.m_RD.m_VertexData.m_VertexCount = 0 + #data.m_RD.m_VertexData.m_Channels = [] + #data.m_RD.m_VertexData.m_Streams = [] + #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 == CID.Texture2D: + fname = next((path for path in images if f"{asset_name}-{obj.path_id}." in path), None) + by_name = False + if not fname: + fname = next((path for path in images if f"{data.name + '-'}" in path), None) + by_name = True + if not fname: return [] + with Image.open(fname) as img: + _img = img + if _img.height != data.m_Height or _img.width != data.m_Width: + # it's not the same image even if their names are the same + return [obj.path_id] + if data.set_image(img): + data.save() + elif objfmt == CID.PlayerSettings: + data.companyName = "Company" + data.productName = "Game" + data.save() + if objfmt == CID.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 == CID.MonoBehaviour or objfmt == CID.Shader: + fname = next((path for path in mbehavs if name in path), None) + if not fname: + fname = next((path for path in jmbehavs if name in path), None) + if not fname: return [obj.path_id] + if ".json" == os.path.splitext(fname)[1]: + script = data.m_Script.read() + if script: + nodes = ASSEMBLY_TREES[script.m_ClassName] + with open(fname, "r", encoding="utf-8-sig") as dat: + sjson = None + try: + sjson = json.load(dat) + except Exception as e: + print(f"---- ERROR READING JSON ----\n\n{e} from {fname}") + if not sjson: + sys.exit(2) + try: + obj.save_typetree( + sjson, nodes, + all_trees=ASSEMBLY_TREES|obj.assets_file.get_ref_typetrees() + ) + obj.mark_changed() + except Exception as e: + try: + obj.save_typetree( + sjson, obj.serialized_type.nodes, + all_trees=ASSEMBLY_TREES|obj.assets_file.get_ref_typetrees() + ) + obj.mark_changed() + except Exception as e1: + pass + raise Exception(f"Error {e}/{e1} in {fname}") + else: + with open(fname, "rb") as dat: + obj.set_raw_data(dat.read()) + return [obj.path_id] + + print(f"Output folder is {os.path.abspath(OUT_PATH)}") + GLOBAL_MAP = Environment.prepare_global_map(ASSETS, tqdm) + for file_name in ASSETS: + am = Environment(globalmap=GLOBAL_MAP, ignore_dependencies=True, extended_search=True) + am.out_path = os.path.dirname(OUT_PATH + file_name.replace(ROOT, '').replace("\\original\\", '')) + enc_folder = "StreamingAssets" + is_encrypted = enc_folder in os.path.abspath(file_name) + if is_encrypted: + key = 0 + asset = am.load_file(EndianBinaryReader(f, encrypt_func=get_encryption_func(key)), name=file_name) + else: + asset = am.load_file(file_name, name=file_name) #, dump=True) + if asset is not None: + print(f"Processing {file_name.replace(ROOT, '')}...") + for item in tqdm(list(asset.get_filtered_assets(TYPES)), desc=asset.name): + obj_modify(item, item.assets_file.name) + gen = get_writer_func(key) if is_encrypted else None + opath = os.path.join(am.out_path, enc_folder) if is_encrypted else None + am.save(pack="none", writer_generator=gen, out_path=opath) + #am.save(pack="lz4", writer_generator=get_writer_func(key)) + asset.close() + am.close() # to save memory + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/manifest.in b/manifest.in deleted file mode 100644 index 04762de9..00000000 --- a/manifest.in +++ /dev/null @@ -1,2 +0,0 @@ -recursive-include UnityPyBoost *.c -recursive-include UnityPyBoost *.h diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 6e27f3a5..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,60 +0,0 @@ -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[project] -name = "UnityPy" -authors = [{ name = "Rudolf Kolbe", email = "rkolbe96@gmail.com" }] -description = "A Unity extraction and patching package" -readme = "README.md" -license = { file = "LICENSE" } -requires-python = ">=3.6" -keywords = [ - "python", - "unity", - "unity-asset", - "python3", - "data-minig", - "unitypack", - "assetstudio", - "unity-asset-extractor", -] -classifiers = [ - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Intended Audience :: Developers", - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Games/Entertainment", - "Topic :: Multimedia :: Graphics", -] -dependencies = [ - # block compression/decompression - "lz4", # BundleFile block compression - "brotli", # WebFile compression - # Texture & Sprite handling - "Pillow", - "texture2ddecoder", # texture decompression - "etcpak", # ETC & DXT compression - # raw typetree dumping - "tabulate", - # audio extraction - "pyfmodex", - # filesystem handling - "fsspec", -] -dynamic = ["version"] - -[project.urls] -"Homepage" = "https://github.com/K0lb3/UnityPy" -"Bug Tracker" = "https://github.com/K0lb3/UnityPy/issues" - -[tool.setuptools.dynamic] -version = { attr = "UnityPy.__version__" } diff --git a/setup.py b/setup.py index 23d54c66..883aa170 100644 --- a/setup.py +++ b/setup.py @@ -1,74 +1,46 @@ -import os -import platform -from setuptools import setup, Extension, find_packages - - -INSTALL_DIR = os.path.dirname(os.path.realpath(__file__)) -UNITYPYBOOST_DIR = os.path.join(INSTALL_DIR, "UnityPyBoost") - - -def get_fmod_library(): - # 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] - - lib_name = "" - if system in ["Windows", "Darwin"]: - lib_name = "fmod.dll" if system == "Windows" else "libfmod.dylib" - if arch == "32bit": - arch = "x86" - elif arch == "64bit": - arch = "x64" - elif system == "Linux": - lib_name = "libfmod.so" - # 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": - return None - elif arch == "32bit": - arch = "x86" - elif arch == "64bit": - arch = "x86_64" - else: - return None - - return f"lib/FMOD/{system}/{arch}/{lib_name}" - - -unitypy_package_data = ["resources/uncompressed.tpk"] -fmod_lib = get_fmod_library() -if fmod_lib is not None: - unitypy_package_data.append(fmod_lib) - - -# These packages are missing __init__.py so setuptools will warn about unspecified packages -extra_packages = [ - "UnityPy.resources", - "UnityPy.tools", - "UnityPy.tools.libil2cpp_helper", -] - - -setup( - name="UnityPy", - packages=find_packages() + extra_packages, - package_data={"UnityPy": unitypy_package_data}, - ext_modules=[ - Extension( - "UnityPy.UnityPyBoost", - [ - f"UnityPyBoost/{f}" - for f in os.listdir(UNITYPYBOOST_DIR) - if f.endswith(".c") - ], - language="c", - include_dirs=[UNITYPYBOOST_DIR], - ) - ], +import setuptools +import re +with open("UnityPy/__init__.py") as f: + version = re.search(r'__version__\s*=\s*"(.*)"', f.read()).group(1) + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="UnityPy", + packages=setuptools.find_packages(), + include_package_data = True, + version=version, + author="K0lb3", + description="A pythonic port of AssetStudio by Perfare", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/K0lb3/UnityPy", + download_url="https://github.com/K0lb3/UnityPy/tarball/master", + 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", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Games/Entertainment", + "Topic :: Multimedia :: Graphics", + ], + install_requires=[ + # block compression/decompression + "etcpak", + "lz4", # BundleFile block compression + "brotli", # WebFile compression + # Texture & Sprite handling + "Pillow", + "texture2ddecoder", # texture decompression + # raw typetree dumping + #"tabulate" + ] ) diff --git a/tests/samples/atlas_test b/tests/samples/atlas_test deleted file mode 100644 index faa7e996..00000000 Binary files a/tests/samples/atlas_test and /dev/null differ diff --git a/tests/samples/banner_1 b/tests/samples/banner_1 deleted file mode 100644 index 132d0e3b..00000000 Binary files a/tests/samples/banner_1 and /dev/null differ diff --git a/tests/samples/char_118_yuki.ab b/tests/samples/char_118_yuki.ab deleted file mode 100644 index dda9c41a..00000000 Binary files a/tests/samples/char_118_yuki.ab and /dev/null differ diff --git a/tests/samples/xinzexi_2_n_tex b/tests/samples/xinzexi_2_n_tex deleted file mode 100644 index e80f3c81..00000000 Binary files a/tests/samples/xinzexi_2_n_tex and /dev/null differ diff --git a/tests/samples/xinzexi_2_n_tex_mesh b/tests/samples/xinzexi_2_n_tex_mesh deleted file mode 100644 index 0a57a2ec..00000000 --- a/tests/samples/xinzexi_2_n_tex_mesh +++ /dev/null @@ -1,1812 +0,0 @@ -g xinzexi_2_n-mesh -v -1152 671 0 -v -1152 1343 0 -v -1440 1343 0 -v -1440 671 0 -v -1152 31 0 -v -1152 671 0 -v -1440 671 0 -v -1440 31 0 -v -832 607 0 -v -832 927 0 -v -1152 927 0 -v -1152 607 0 -v -512 607 0 -v -512 927 0 -v -832 927 0 -v -832 607 0 -v -64 319 0 -v -64 607 0 -v -384 607 0 -v -384 319 0 -v -384 319 0 -v -384 607 0 -v -672 607 0 -v -672 319 0 -v -960 927 0 -v -960 1215 0 -v -1152 1215 0 -v -1152 927 0 -v -960 1215 0 -v -960 1503 0 -v -1152 1503 0 -v -1152 1215 0 -v -1440 735 0 -v -1440 991 0 -v -1632 991 0 -v -1632 735 0 -v -1440 991 0 -v -1440 1247 0 -v -1632 1247 0 -v -1632 991 0 -v -704 223 0 -v -704 479 0 -v -832 479 0 -v -832 223 0 -v -1440 1247 0 -v -1440 1407 0 -v -1632 1407 0 -v -1632 1247 0 -v -1152 1343 0 -v -1152 1503 0 -v -1312 1503 0 -v -1312 1343 0 -v -1632 1663 0 -v -1632 2047 0 -v -1696 2047 0 -v -1696 1663 0 -v -1632 1311 0 -v -1632 1663 0 -v -1696 1663 0 -v -1696 1311 0 -v -992 1503 0 -v -992 1599 0 -v -1216 1599 0 -v -1216 1503 0 -v -1440 223 0 -v -1440 447 0 -v -1536 447 0 -v -1536 223 0 -v -800 479 0 -v -800 607 0 -v -960 607 0 -v -960 479 0 -v -800 927 0 -v -800 1055 0 -v -960 1055 0 -v -960 927 0 -v -1024 95 0 -v -1024 255 0 -v -1152 255 0 -v -1152 95 0 -v -1440 31 0 -v -1440 223 0 -v -1536 223 0 -v -1536 31 0 -v -192 191 0 -v -192 255 0 -v -448 255 0 -v -448 191 0 -v -448 191 0 -v -448 255 0 -v -704 255 0 -v -704 191 0 -v -832 1311 0 -v -832 1439 0 -v -928 1439 0 -v -928 1311 0 -v -800 1599 0 -v -800 1727 0 -v -896 1727 0 -v -896 1599 0 -v -1632 1055 0 -v -1632 1183 0 -v -1728 1183 0 -v -1728 1055 0 -v -256 607 0 -v -256 703 0 -v -384 703 0 -v -384 607 0 -v -1632 927 0 -v -1632 1055 0 -v -1728 1055 0 -v -1728 927 0 -v -384 607 0 -v -384 703 0 -v -512 703 0 -v -512 607 0 -v -1600 31 0 -v -1600 159 0 -v -1696 159 0 -v -1696 31 0 -v -1600 159 0 -v -1600 287 0 -v -1696 287 0 -v -1696 159 0 -v -1888 735 0 -v -1888 895 0 -v -1937 895 0 -v -1937 735 0 -v -1760 959 0 -v -1760 1055 0 -v -1856 1055 0 -v -1856 959 0 -v -896 1535 0 -v -896 1631 0 -v -992 1631 0 -v -992 1535 0 -v -128 287 0 -v -128 319 0 -v -416 319 0 -v -416 287 0 -v -416 287 0 -v -416 319 0 -v -704 319 0 -v -704 287 0 -v -1888 607 0 -v -1888 735 0 -v -1937 735 0 -v -1937 607 0 -v -1408 1503 0 -v -1408 1567 0 -v -1536 1567 0 -v -1536 1503 0 -v -1344 1407 0 -v -1344 1535 0 -v -1408 1535 0 -v -1408 1407 0 -v -0 415 0 -v -0 543 0 -v -64 543 0 -v -64 415 0 -v -1088 479 0 -v -1088 607 0 -v -1152 607 0 -v -1152 479 0 -v -1536 31 0 -v -1536 223 0 -v -1568 223 0 -v -1568 31 0 -v -1536 1503 0 -v -1536 1567 0 -v -1632 1567 0 -v -1632 1503 0 -v -1536 223 0 -v -1536 415 0 -v -1568 415 0 -v -1568 223 0 -v -1696 159 0 -v -1696 255 0 -v -1760 255 0 -v -1760 159 0 -v -768 1343 0 -v -768 1407 0 -v -832 1407 0 -v -832 1343 0 -v -448 831 0 -v -448 895 0 -v -512 895 0 -v -512 831 0 -v -1280 0 0 -v -1280 31 0 -v -1408 31 0 -v -1408 0 0 -v -1440 1439 0 -v -1440 1503 0 -v -1504 1503 0 -v -1504 1439 0 -v -1472 671 0 -v -1472 735 0 -v -1536 735 0 -v -1536 671 0 -v -1792 479 0 -v -1792 543 0 -v -1856 543 0 -v -1856 479 0 -v -320 703 0 -v -320 767 0 -v -384 767 0 -v -384 703 0 -v -448 703 0 -v -448 767 0 -v -512 767 0 -v -512 703 0 -v -1568 319 0 -v -1568 447 0 -v -1600 447 0 -v -1600 319 0 -v -1856 863 0 -v -1856 991 0 -v -1888 991 0 -v -1888 863 0 -v -1696 95 0 -v -1696 159 0 -v -1760 159 0 -v -1760 95 0 -v -1568 191 0 -v -1568 319 0 -v -1600 319 0 -v -1600 191 0 -v -160 255 0 -v -160 287 0 -v -288 287 0 -v -288 255 0 -v -832 351 0 -v -832 479 0 -v -864 479 0 -v -864 351 0 -v -288 255 0 -v -288 287 0 -v -416 287 0 -v -416 255 0 -v -672 383 0 -v -672 479 0 -v -704 479 0 -v -704 383 0 -v -1440 639 0 -v -1440 735 0 -v -1472 735 0 -v -1472 639 0 -v -928 1119 0 -v -928 1215 0 -v -960 1215 0 -v -960 1119 0 -v -1856 767 0 -v -1856 863 0 -v -1888 863 0 -v -1888 767 0 -v -1568 63 0 -v -1568 159 0 -v -1600 159 0 -v -1600 63 0 -v -832 255 0 -v -832 351 0 -v -864 351 0 -v -864 255 0 -v -1696 415 0 -v -1696 447 0 -v -1792 447 0 -v -1792 415 0 -v -512 255 0 -v -512 287 0 -v -608 287 0 -v -608 255 0 -v -704 927 0 -v -704 959 0 -v -800 959 0 -v -800 927 0 -v -1600 415 0 -v -1600 447 0 -v -1696 447 0 -v -1696 415 0 -v -608 927 0 -v -608 959 0 -v -704 959 0 -v -704 927 0 -v -1408 0 0 -v -1408 31 0 -v -1504 31 0 -v -1504 0 0 -v -1696 1791 0 -v -1696 1887 0 -v -1728 1887 0 -v -1728 1791 0 -v -1408 1407 0 -v -1408 1503 0 -v -1440 1503 0 -v -1440 1407 0 -v -608 255 0 -v -608 287 0 -v -704 287 0 -v -704 255 0 -v -1696 1887 0 -v -1696 1983 0 -v -1728 1983 0 -v -1728 1887 0 -v -1600 1599 0 -v -1600 1663 0 -v -1632 1663 0 -v -1632 1599 0 -v -768 511 0 -v -768 575 0 -v -800 575 0 -v -800 511 0 -v -1504 1471 0 -v -1504 1503 0 -v -1568 1503 0 -v -1568 1471 0 -v -1216 1567 0 -v -1216 1599 0 -v -1280 1599 0 -v -1280 1567 0 -v -832 1567 0 -v -832 1599 0 -v -896 1599 0 -v -896 1567 0 -v -672 543 0 -v -672 607 0 -v -704 607 0 -v -704 543 0 -v -1440 479 0 -v -1440 543 0 -v -1472 543 0 -v -1472 479 0 -v -416 799 0 -v -416 863 0 -v -448 863 0 -v -448 799 0 -v -384 767 0 -v -384 831 0 -v -416 831 0 -v -416 767 0 -v -1632 383 0 -v -1632 415 0 -v -1696 415 0 -v -1696 383 0 -v -768 1663 0 -v -768 1727 0 -v -800 1727 0 -v -800 1663 0 -v -896 1631 0 -v -896 1695 0 -v -928 1695 0 -v -928 1631 0 -v -1760 223 0 -v -1760 287 0 -v -1792 287 0 -v -1792 223 0 -v -1728 31 0 -v -1728 95 0 -v -1760 95 0 -v -1760 31 0 -v -1120 255 0 -v -1120 319 0 -v -1152 319 0 -v -1152 255 0 -v -1792 223 0 -v -1792 287 0 -v -1824 287 0 -v -1824 223 0 -v -864 415 0 -v -864 479 0 -v -896 479 0 -v -896 415 0 -v -32 351 0 -v -32 415 0 -v -64 415 0 -v -64 351 0 -v -1088 63 0 -v -1088 95 0 -v -1152 95 0 -v -1152 63 0 -v -1600 0 0 -v -1600 31 0 -v -1664 31 0 -v -1664 0 0 -v -960 191 0 -v -960 255 0 -v -992 255 0 -v -992 191 0 -v -480 159 0 -v -480 191 0 -v -544 191 0 -v -544 159 0 -v -1888 543 0 -v -1888 607 0 -v -1920 607 0 -v -1920 543 0 -v -512 1087 0 -v -512 1151 0 -v -544 1151 0 -v -544 1087 0 -v -896 1055 0 -v -896 1119 0 -v -928 1119 0 -v -928 1055 0 -v -1600 1439 0 -v -1600 1503 0 -v -1632 1503 0 -v -1632 1439 0 -v -1632 1247 0 -v -1632 1311 0 -v -1664 1311 0 -v -1664 1247 0 -v -1888 895 0 -v -1888 959 0 -v -1920 959 0 -v -1920 895 0 -v -960 543 0 -v -960 607 0 -v -992 607 0 -v -992 543 0 -v -1760 1055 0 -v -1760 1119 0 -v -1792 1119 0 -v -1792 1055 0 -v -768 959 0 -v -768 1023 0 -v -800 1023 0 -v -800 959 0 -v -1824 895 0 -v -1824 959 0 -v -1856 959 0 -v -1856 895 0 -v -928 1343 0 -v -928 1407 0 -v -960 1407 0 -v -960 1343 0 -v -1504 1407 0 -v -1504 1439 0 -v -1568 1439 0 -v -1568 1407 0 -v -1312 1375 0 -v -1312 1439 0 -v -1344 1439 0 -v -1344 1375 0 -v -928 1407 0 -v -928 1471 0 -v -960 1471 0 -v -960 1407 0 -v -1728 1087 0 -v -1728 1151 0 -v -1760 1151 0 -v -1760 1087 0 -v -192 607 0 -v -192 639 0 -v -256 639 0 -v -256 607 0 -v -1856 575 0 -v -1856 639 0 -v -1888 639 0 -v -1888 575 0 -v -1728 1023 0 -v -1728 1087 0 -v -1760 1087 0 -v -1760 1023 0 -v -128 607 0 -v -128 639 0 -v -192 639 0 -v -192 607 0 -v -1312 1439 0 -v -1312 1503 0 -v -1344 1503 0 -v -1344 1439 0 -v -1440 575 0 -v -1440 639 0 -v -1472 639 0 -v -1472 575 0 -v -672 319 0 -v -672 383 0 -v -704 383 0 -v -704 319 0 -v -1408 1343 0 -v -1408 1407 0 -v -1440 1407 0 -v -1440 1343 0 -v -928 1055 0 -v -928 1119 0 -v -960 1119 0 -v -960 1055 0 -v -1568 0 0 -v -1568 63 0 -v -1600 63 0 -v -1600 0 0 -v -1504 1567 0 -v -1504 1599 0 -v -1568 1599 0 -v -1568 1567 0 -v -1568 1567 0 -v -1568 1599 0 -v -1632 1599 0 -v -1632 1567 0 -v -1696 1631 0 -v -1696 1695 0 -v -1728 1695 0 -v -1728 1631 0 -v -1696 1567 0 -v -1696 1631 0 -v -1728 1631 0 -v -1728 1567 0 -v -1856 511 0 -v -1856 575 0 -v -1888 575 0 -v -1888 511 0 -v -1600 287 0 -v -1600 351 0 -v -1632 351 0 -v -1632 287 0 -v -1568 1407 0 -v -1568 1439 0 -v -1632 1439 0 -v -1632 1407 0 -v -1600 351 0 -v -1600 415 0 -v -1632 415 0 -v -1632 351 0 -v -1152 1599 0 -v -1152 1631 0 -v -1216 1631 0 -v -1216 1599 0 -v -992 159 0 -v -992 223 0 -v -1024 223 0 -v -1024 159 0 -v -992 223 0 -v -992 287 0 -v -1024 287 0 -v -1024 223 0 -v -1760 447 0 -v -1760 479 0 -v -1824 479 0 -v -1824 447 0 -v -1120 351 0 -v -1120 415 0 -v -1152 415 0 -v -1152 351 0 -v -1120 415 0 -v -1120 479 0 -v -1152 479 0 -v -1152 415 0 -v -1696 447 0 -v -1696 479 0 -v -1760 479 0 -v -1760 447 0 -v -448 799 0 -v -448 831 0 -v -480 831 0 -v -480 799 0 -v -288 703 0 -v -288 735 0 -v -320 735 0 -v -320 703 0 -v -736 959 0 -v -736 991 0 -v -768 991 0 -v -768 959 0 -v -1792 927 0 -v -1792 959 0 -v -1824 959 0 -v -1824 927 0 -v -1536 703 0 -v -1536 735 0 -v -1568 735 0 -v -1568 703 0 -v -704 575 0 -v -704 607 0 -v -736 607 0 -v -736 575 0 -v -416 703 0 -v -416 735 0 -v -448 735 0 -v -448 703 0 -v -352 767 0 -v -352 799 0 -v -384 799 0 -v -384 767 0 -v -224 639 0 -v -224 671 0 -v -256 671 0 -v -256 639 0 -v -480 767 0 -v -480 799 0 -v -512 799 0 -v -512 767 0 -v -32 543 0 -v -32 575 0 -v -64 575 0 -v -64 543 0 -v -896 351 0 -v -896 383 0 -v -928 383 0 -v -928 351 0 -v -1472 447 0 -v -1472 479 0 -v -1504 479 0 -v -1504 447 0 -v -1632 287 0 -v -1632 319 0 -v -1664 319 0 -v -1664 287 0 -v -1760 0 0 -v -1760 31 0 -v -1792 31 0 -v -1792 0 0 -v -1024 255 0 -v -1024 287 0 -v -1056 287 0 -v -1056 255 0 -v -1824 543 0 -v -1824 575 0 -v -1856 575 0 -v -1856 543 0 -v -1824 255 0 -v -1824 287 0 -v -1856 287 0 -v -1856 255 0 -v -736 479 0 -v -736 511 0 -v -768 511 0 -v -768 479 0 -v -896 447 0 -v -896 479 0 -v -928 479 0 -v -928 447 0 -v -1760 479 0 -v -1760 511 0 -v -1792 511 0 -v -1792 479 0 -v -1792 1055 0 -v -1792 1087 0 -v -1824 1087 0 -v -1824 1055 0 -v -1440 447 0 -v -1440 479 0 -v -1472 479 0 -v -1472 447 0 -v -768 479 0 -v -768 511 0 -v -800 511 0 -v -800 479 0 -v -1696 383 0 -v -1696 415 0 -v -1728 415 0 -v -1728 383 0 -v -1728 0 0 -v -1728 31 0 -v -1760 31 0 -v -1760 0 0 -v -1760 191 0 -v -1760 223 0 -v -1792 223 0 -v -1792 191 0 -v -1824 863 0 -v -1824 895 0 -v -1856 895 0 -v -1856 863 0 -v -1216 1599 0 -v -1216 1631 0 -v -1248 1631 0 -v -1248 1599 0 -v -416 767 0 -v -416 799 0 -v -448 799 0 -v -448 767 0 -v -672 511 0 -v -672 543 0 -v -704 543 0 -v -704 511 0 -v -384 735 0 -v -384 767 0 -v -416 767 0 -v -416 735 0 -v -928 1631 0 -v -928 1663 0 -v -960 1663 0 -v -960 1631 0 -v -800 1311 0 -v -800 1343 0 -v -832 1343 0 -v -832 1311 0 -v -1376 1343 0 -v -1376 1375 0 -v -1408 1375 0 -v -1408 1343 0 -v -1632 1183 0 -v -1632 1215 0 -v -1664 1215 0 -v -1664 1183 0 -v -864 1055 0 -v -864 1087 0 -v -896 1087 0 -v -896 1055 0 -v -640 1087 0 -v -640 1119 0 -v -672 1119 0 -v -672 1087 0 -v -1248 1535 0 -v -1248 1567 0 -v -1280 1567 0 -v -1280 1535 0 -v -992 1599 0 -v -992 1631 0 -v -1024 1631 0 -v -1024 1599 0 -v -1216 1503 0 -v -1216 1535 0 -v -1248 1535 0 -v -1248 1503 0 -v -1344 1375 0 -v -1344 1407 0 -v -1376 1407 0 -v -1376 1375 0 -v -896 1439 0 -v -896 1471 0 -v -928 1471 0 -v -928 1439 0 -vt 0.0004882812 0.001024604 -vt 0.0004882812 0.6895492 -vt 0.1411133 0.6895492 -vt 0.1411133 0.001024604 -vt 0.1420898 0.001024604 -vt 0.1420898 0.6567623 -vt 0.2827148 0.6567623 -vt 0.2827148 0.001024604 -vt 0.2836914 0.001024604 -vt 0.2836914 0.3288934 -vt 0.4399414 0.3288934 -vt 0.4399414 0.001024604 -vt 0.440918 0.001024604 -vt 0.440918 0.3288934 -vt 0.597168 0.3288934 -vt 0.597168 0.001024604 -vt 0.5981445 0.001024604 -vt 0.5981445 0.2961066 -vt 0.7543945 0.2961066 -vt 0.7543945 0.001024604 -vt 0.7553711 0.001024604 -vt 0.7553711 0.2961066 -vt 0.8959961 0.2961066 -vt 0.8959961 0.001024604 -vt 0.8969727 0.001024604 -vt 0.8969727 0.2961066 -vt 0.9907227 0.2961066 -vt 0.9907227 0.001024604 -vt 0.5981445 0.2981557 -vt 0.5981445 0.5932377 -vt 0.6918945 0.5932377 -vt 0.6918945 0.2981557 -vt 0.6928711 0.2981557 -vt 0.6928711 0.5604508 -vt 0.7866211 0.5604508 -vt 0.7866211 0.2981557 -vt 0.7875977 0.2981557 -vt 0.7875977 0.5604508 -vt 0.8813477 0.5604508 -vt 0.8813477 0.2981557 -vt 0.8823242 0.2981557 -vt 0.8823242 0.5604508 -vt 0.9448242 0.5604508 -vt 0.9448242 0.2981557 -vt 0.2836914 0.3309426 -vt 0.2836914 0.494877 -vt 0.3774414 0.494877 -vt 0.3774414 0.3309426 -vt 0.378418 0.3309426 -vt 0.378418 0.494877 -vt 0.456543 0.494877 -vt 0.456543 0.3309426 -vt 0.9458008 0.2981557 -vt 0.9458008 0.6915984 -vt 0.9770508 0.6915984 -vt 0.9770508 0.2981557 -vt 0.4575195 0.3309426 -vt 0.4575195 0.6915984 -vt 0.4887695 0.6915984 -vt 0.4887695 0.3309426 -vt 0.2836914 0.4969262 -vt 0.2836914 0.5952868 -vt 0.3930664 0.5952868 -vt 0.3930664 0.4969262 -vt 0.4897461 0.3309426 -vt 0.4897461 0.5604508 -vt 0.5366211 0.5604508 -vt 0.5366211 0.3309426 -vt 0.4897461 0.5625 -vt 0.4897461 0.6936475 -vt 0.5678711 0.6936475 -vt 0.5678711 0.5625 -vt 0.6928711 0.5625 -vt 0.6928711 0.6936475 -vt 0.7709961 0.6936475 -vt 0.7709961 0.5625 -vt 0.394043 0.4969262 -vt 0.394043 0.6608607 -vt 0.456543 0.6608607 -vt 0.456543 0.4969262 -vt 0.7719727 0.5625 -vt 0.7719727 0.7592213 -vt 0.8188477 0.7592213 -vt 0.8188477 0.5625 -vt 0.8198242 0.5625 -vt 0.8198242 0.6280738 -vt 0.9448242 0.6280738 -vt 0.9448242 0.5625 -vt 0.8198242 0.630123 -vt 0.8198242 0.6956967 -vt 0.9448242 0.6956967 -vt 0.9448242 0.630123 -vt 0.5981445 0.5952868 -vt 0.5981445 0.7264345 -vt 0.6450195 0.7264345 -vt 0.6450195 0.5952868 -vt 0.2836914 0.5973361 -vt 0.2836914 0.7284836 -vt 0.3305664 0.7284836 -vt 0.3305664 0.5973361 -vt 0.331543 0.5973361 -vt 0.331543 0.7284836 -vt 0.378418 0.7284836 -vt 0.378418 0.5973361 -vt 0.1420898 0.6588115 -vt 0.1420898 0.7571721 -vt 0.2045898 0.7571721 -vt 0.2045898 0.6588115 -vt 0.2055664 0.6588115 -vt 0.2055664 0.789959 -vt 0.2524414 0.789959 -vt 0.2524414 0.6588115 -vt 0.394043 0.6629099 -vt 0.394043 0.7612705 -vt 0.456543 0.7612705 -vt 0.456543 0.6629099 -vt 0.0004882812 0.6915984 -vt 0.0004882812 0.8227459 -vt 0.04736328 0.8227459 -vt 0.04736328 0.6915984 -vt 0.04833984 0.6915984 -vt 0.04833984 0.8227459 -vt 0.09521484 0.8227459 -vt 0.09521484 0.6915984 -vt 0.5688477 0.3309426 -vt 0.5688477 0.494877 -vt 0.5932617 0.494877 -vt 0.5932617 0.3309426 -vt 0.9458008 0.6936475 -vt 0.9458008 0.7920082 -vt 0.9926758 0.7920082 -vt 0.9926758 0.6936475 -vt 0.4897461 0.6956967 -vt 0.4897461 0.7940574 -vt 0.5366211 0.7940574 -vt 0.5366211 0.6956967 -vt 0.5981445 0.7284836 -vt 0.5981445 0.7612705 -vt 0.7387695 0.7612705 -vt 0.7387695 0.7284836 -vt 0.7719727 0.7612705 -vt 0.7719727 0.7940574 -vt 0.9125977 0.7940574 -vt 0.9125977 0.7612705 -vt 0.5688477 0.4969262 -vt 0.5688477 0.6280738 -vt 0.5932617 0.6280738 -vt 0.5932617 0.4969262 -vt 0.2836914 0.7305328 -vt 0.2836914 0.7961066 -vt 0.3461914 0.7961066 -vt 0.3461914 0.7305328 -vt 0.09619141 0.6915984 -vt 0.09619141 0.8227459 -vt 0.1274414 0.8227459 -vt 0.1274414 0.6915984 -vt 0.4575195 0.6936475 -vt 0.4575195 0.8247951 -vt 0.4887695 0.8247951 -vt 0.4887695 0.6936475 -vt 0.5375977 0.6956967 -vt 0.5375977 0.8268443 -vt 0.5688477 0.8268443 -vt 0.5688477 0.6956967 -vt 0.5698242 0.630123 -vt 0.5698242 0.8268443 -vt 0.5854492 0.8268443 -vt 0.5854492 0.630123 -vt 0.1420898 0.7592213 -vt 0.1420898 0.8247951 -vt 0.1889648 0.8247951 -vt 0.1889648 0.7592213 -vt 0.253418 0.6588115 -vt 0.253418 0.8555328 -vt 0.269043 0.8555328 -vt 0.269043 0.6588115 -vt 0.7397461 0.6956967 -vt 0.7397461 0.7940574 -vt 0.7709961 0.7940574 -vt 0.7709961 0.6956967 -vt 0.9135742 0.6977459 -vt 0.9135742 0.7633197 -vt 0.9448242 0.7633197 -vt 0.9448242 0.6977459 -vt 0.347168 0.7305328 -vt 0.347168 0.7961066 -vt 0.378418 0.7961066 -vt 0.378418 0.7305328 -vt 0.394043 0.7633197 -vt 0.394043 0.7961066 -vt 0.456543 0.7961066 -vt 0.456543 0.7633197 -vt 0.5981445 0.7633197 -vt 0.5981445 0.8288934 -vt 0.6293945 0.8288934 -vt 0.6293945 0.7633197 -vt 0.6303711 0.7633197 -vt 0.6303711 0.8288934 -vt 0.6616211 0.8288934 -vt 0.6616211 0.7633197 -vt 0.6625977 0.7633197 -vt 0.6625977 0.8288934 -vt 0.6938477 0.8288934 -vt 0.6938477 0.7633197 -vt 0.6948242 0.7633197 -vt 0.6948242 0.8288934 -vt 0.7260742 0.8288934 -vt 0.7260742 0.7633197 -vt 0.9135742 0.7653688 -vt 0.9135742 0.8309426 -vt 0.9448242 0.8309426 -vt 0.9448242 0.7653688 -vt 0.2055664 0.7920082 -vt 0.2055664 0.9231557 -vt 0.2211914 0.9231557 -vt 0.2211914 0.7920082 -vt 0.222168 0.7920082 -vt 0.222168 0.9231557 -vt 0.237793 0.9231557 -vt 0.237793 0.7920082 -vt 0.9458008 0.7940574 -vt 0.9458008 0.8596312 -vt 0.9770508 0.8596312 -vt 0.9770508 0.7940574 -vt 0.9780273 0.7940574 -vt 0.9780273 0.9252049 -vt 0.9936523 0.9252049 -vt 0.9936523 0.7940574 -vt 0.7397461 0.7961066 -vt 0.7397461 0.8288934 -vt 0.8022461 0.8288934 -vt 0.8022461 0.7961066 -vt 0.4897461 0.7961066 -vt 0.4897461 0.9272541 -vt 0.5053711 0.9272541 -vt 0.5053711 0.7961066 -vt 0.8032227 0.7961066 -vt 0.8032227 0.8288934 -vt 0.8657227 0.8288934 -vt 0.8657227 0.7961066 -vt 0.5063477 0.7961066 -vt 0.5063477 0.8944672 -vt 0.5219727 0.8944672 -vt 0.5219727 0.7961066 -vt 0.8666992 0.7961066 -vt 0.8666992 0.8944672 -vt 0.8823242 0.8944672 -vt 0.8823242 0.7961066 -vt 0.8833008 0.7961066 -vt 0.8833008 0.8944672 -vt 0.8989258 0.8944672 -vt 0.8989258 0.7961066 -vt 0.2836914 0.7981557 -vt 0.2836914 0.8965164 -vt 0.2993164 0.8965164 -vt 0.2993164 0.7981557 -vt 0.300293 0.7981557 -vt 0.300293 0.8965164 -vt 0.315918 0.8965164 -vt 0.315918 0.7981557 -vt 0.3168945 0.7981557 -vt 0.3168945 0.8965164 -vt 0.3325195 0.8965164 -vt 0.3325195 0.7981557 -vt 0.3334961 0.7981557 -vt 0.3334961 0.8309426 -vt 0.3803711 0.8309426 -vt 0.3803711 0.7981557 -vt 0.394043 0.7981557 -vt 0.394043 0.8309426 -vt 0.440918 0.8309426 -vt 0.440918 0.7981557 -vt 0.0004882812 0.8247951 -vt 0.0004882812 0.857582 -vt 0.04736328 0.857582 -vt 0.04736328 0.8247951 -vt 0.04833984 0.8247951 -vt 0.04833984 0.857582 -vt 0.09521484 0.857582 -vt 0.09521484 0.8247951 -vt 0.1420898 0.8268443 -vt 0.1420898 0.8596312 -vt 0.1889648 0.8596312 -vt 0.1889648 0.8268443 -vt 0.5375977 0.8288934 -vt 0.5375977 0.8616803 -vt 0.5844727 0.8616803 -vt 0.5844727 0.8288934 -vt 0.09619141 0.8247951 -vt 0.09619141 0.9231557 -vt 0.1118164 0.9231557 -vt 0.1118164 0.8247951 -vt 0.112793 0.8247951 -vt 0.112793 0.9231557 -vt 0.128418 0.9231557 -vt 0.128418 0.8247951 -vt 0.5981445 0.8309426 -vt 0.5981445 0.8637295 -vt 0.6450195 0.8637295 -vt 0.6450195 0.8309426 -vt 0.4575195 0.8268443 -vt 0.4575195 0.9252049 -vt 0.4731445 0.9252049 -vt 0.4731445 0.8268443 -vt 0.6459961 0.8309426 -vt 0.6459961 0.8965164 -vt 0.6616211 0.8965164 -vt 0.6616211 0.8309426 -vt 0.6625977 0.8309426 -vt 0.6625977 0.8965164 -vt 0.6782227 0.8965164 -vt 0.6782227 0.8309426 -vt 0.6791992 0.8309426 -vt 0.6791992 0.8637295 -vt 0.7104492 0.8637295 -vt 0.7104492 0.8309426 -vt 0.7114258 0.8309426 -vt 0.7114258 0.8637295 -vt 0.7426758 0.8637295 -vt 0.7426758 0.8309426 -vt 0.7436523 0.8309426 -vt 0.7436523 0.8637295 -vt 0.7749023 0.8637295 -vt 0.7749023 0.8309426 -vt 0.7758789 0.8309426 -vt 0.7758789 0.8965164 -vt 0.7915039 0.8965164 -vt 0.7915039 0.8309426 -vt 0.7924805 0.8309426 -vt 0.7924805 0.8965164 -vt 0.8081055 0.8965164 -vt 0.8081055 0.8309426 -vt 0.809082 0.8309426 -vt 0.809082 0.8965164 -vt 0.824707 0.8965164 -vt 0.824707 0.8309426 -vt 0.8256836 0.8309426 -vt 0.8256836 0.8965164 -vt 0.8413086 0.8965164 -vt 0.8413086 0.8309426 -vt 0.3334961 0.8329918 -vt 0.3334961 0.8657787 -vt 0.3647461 0.8657787 -vt 0.3647461 0.8329918 -vt 0.8422852 0.8309426 -vt 0.8422852 0.8965164 -vt 0.8579102 0.8965164 -vt 0.8579102 0.8309426 -vt 0.3657227 0.8329918 -vt 0.3657227 0.8985656 -vt 0.3813477 0.8985656 -vt 0.3813477 0.8329918 -vt 0.394043 0.8329918 -vt 0.394043 0.8985656 -vt 0.409668 0.8985656 -vt 0.409668 0.8329918 -vt 0.4106445 0.8329918 -vt 0.4106445 0.8985656 -vt 0.4262695 0.8985656 -vt 0.4262695 0.8329918 -vt 0.4272461 0.8329918 -vt 0.4272461 0.8985656 -vt 0.4428711 0.8985656 -vt 0.4428711 0.8329918 -vt 0.9135742 0.8329918 -vt 0.9135742 0.8985656 -vt 0.9291992 0.8985656 -vt 0.9291992 0.8329918 -vt 0.253418 0.857582 -vt 0.253418 0.9231557 -vt 0.269043 0.9231557 -vt 0.269043 0.857582 -vt 0.0004882812 0.8596312 -vt 0.0004882812 0.9252049 -vt 0.01611328 0.9252049 -vt 0.01611328 0.8596312 -vt 0.01708984 0.8596312 -vt 0.01708984 0.892418 -vt 0.04833984 0.892418 -vt 0.04833984 0.8596312 -vt 0.04931641 0.8596312 -vt 0.04931641 0.892418 -vt 0.08056641 0.892418 -vt 0.08056641 0.8596312 -vt 0.1420898 0.8616803 -vt 0.1420898 0.9272541 -vt 0.1577148 0.9272541 -vt 0.1577148 0.8616803 -vt 0.1586914 0.8616803 -vt 0.1586914 0.8944672 -vt 0.1899414 0.8944672 -vt 0.1899414 0.8616803 -vt 0.9458008 0.8616803 -vt 0.9458008 0.9272541 -vt 0.9614258 0.9272541 -vt 0.9614258 0.8616803 -vt 0.5375977 0.8637295 -vt 0.5375977 0.9293033 -vt 0.5532227 0.9293033 -vt 0.5532227 0.8637295 -vt 0.5541992 0.8637295 -vt 0.5541992 0.9293033 -vt 0.5698242 0.9293033 -vt 0.5698242 0.8637295 -vt 0.5708008 0.8637295 -vt 0.5708008 0.9293033 -vt 0.5864258 0.9293033 -vt 0.5864258 0.8637295 -vt 0.5981445 0.8657787 -vt 0.5981445 0.9313524 -vt 0.6137695 0.9313524 -vt 0.6137695 0.8657787 -vt 0.6147461 0.8657787 -vt 0.6147461 0.9313524 -vt 0.6303711 0.9313524 -vt 0.6303711 0.8657787 -vt 0.6791992 0.8657787 -vt 0.6791992 0.9313524 -vt 0.6948242 0.9313524 -vt 0.6948242 0.8657787 -vt 0.6958008 0.8657787 -vt 0.6958008 0.9313524 -vt 0.7114258 0.9313524 -vt 0.7114258 0.8657787 -vt 0.7124023 0.8657787 -vt 0.7124023 0.9313524 -vt 0.7280273 0.9313524 -vt 0.7280273 0.8657787 -vt 0.7290039 0.8657787 -vt 0.7290039 0.9313524 -vt 0.7446289 0.9313524 -vt 0.7446289 0.8657787 -vt 0.7456055 0.8657787 -vt 0.7456055 0.9313524 -vt 0.7612305 0.9313524 -vt 0.7612305 0.8657787 -vt 0.3334961 0.8678279 -vt 0.3334961 0.9006147 -vt 0.3647461 0.9006147 -vt 0.3647461 0.8678279 -vt 0.01708984 0.8944672 -vt 0.01708984 0.960041 -vt 0.03271484 0.960041 -vt 0.03271484 0.8944672 -vt 0.03369141 0.8944672 -vt 0.03369141 0.960041 -vt 0.04931641 0.960041 -vt 0.04931641 0.8944672 -vt 0.05029297 0.8944672 -vt 0.05029297 0.960041 -vt 0.06591797 0.960041 -vt 0.06591797 0.8944672 -vt 0.1586914 0.8965164 -vt 0.1586914 0.9293033 -vt 0.1899414 0.9293033 -vt 0.1899414 0.8965164 -vt 0.06689453 0.8944672 -vt 0.06689453 0.960041 -vt 0.08251953 0.960041 -vt 0.08251953 0.8944672 -vt 0.5063477 0.8965164 -vt 0.5063477 0.9620901 -vt 0.5219727 0.9620901 -vt 0.5219727 0.8965164 -vt 0.8666992 0.8965164 -vt 0.8666992 0.9293033 -vt 0.8979492 0.9293033 -vt 0.8979492 0.8965164 -vt 0.2836914 0.8985656 -vt 0.2836914 0.9641393 -vt 0.2993164 0.9641393 -vt 0.2993164 0.8985656 -vt 0.300293 0.8985656 -vt 0.300293 0.9641393 -vt 0.315918 0.9641393 -vt 0.315918 0.8985656 -vt 0.3168945 0.8985656 -vt 0.3168945 0.9641393 -vt 0.3325195 0.9641393 -vt 0.3325195 0.8985656 -vt 0.6459961 0.8985656 -vt 0.6459961 0.9641393 -vt 0.6616211 0.9641393 -vt 0.6616211 0.8985656 -vt 0.6625977 0.8985656 -vt 0.6625977 0.9641393 -vt 0.6782227 0.9641393 -vt 0.6782227 0.8985656 -vt 0.7758789 0.8985656 -vt 0.7758789 0.9641393 -vt 0.7915039 0.9641393 -vt 0.7915039 0.8985656 -vt 0.7924805 0.8985656 -vt 0.7924805 0.9313524 -vt 0.8237305 0.9313524 -vt 0.8237305 0.8985656 -vt 0.824707 0.8985656 -vt 0.824707 0.9313524 -vt 0.855957 0.9313524 -vt 0.855957 0.8985656 -vt 0.3657227 0.9006147 -vt 0.3657227 0.9661885 -vt 0.3813477 0.9661885 -vt 0.3813477 0.9006147 -vt 0.394043 0.9006147 -vt 0.394043 0.9661885 -vt 0.409668 0.9661885 -vt 0.409668 0.9006147 -vt 0.4106445 0.9006147 -vt 0.4106445 0.9661885 -vt 0.4262695 0.9661885 -vt 0.4262695 0.9006147 -vt 0.4272461 0.9006147 -vt 0.4272461 0.9661885 -vt 0.4428711 0.9661885 -vt 0.4428711 0.9006147 -vt 0.9135742 0.9006147 -vt 0.9135742 0.9334016 -vt 0.9448242 0.9334016 -vt 0.9448242 0.9006147 -vt 0.3334961 0.9026639 -vt 0.3334961 0.9682377 -vt 0.3491211 0.9682377 -vt 0.3491211 0.9026639 -vt 0.09619141 0.9252049 -vt 0.09619141 0.9579918 -vt 0.1274414 0.9579918 -vt 0.1274414 0.9252049 -vt 0.2055664 0.9252049 -vt 0.2055664 0.9907787 -vt 0.2211914 0.9907787 -vt 0.2211914 0.9252049 -vt 0.222168 0.9252049 -vt 0.222168 0.9907787 -vt 0.237793 0.9907787 -vt 0.237793 0.9252049 -vt 0.4575195 0.9272541 -vt 0.4575195 0.960041 -vt 0.4887695 0.960041 -vt 0.4887695 0.9272541 -vt 0.253418 0.9252049 -vt 0.253418 0.9907787 -vt 0.269043 0.9907787 -vt 0.269043 0.9252049 -vt 0.0004882812 0.9272541 -vt 0.0004882812 0.9928279 -vt 0.01611328 0.9928279 -vt 0.01611328 0.9272541 -vt 0.9458008 0.9293033 -vt 0.9458008 0.9620901 -vt 0.9770508 0.9620901 -vt 0.9770508 0.9293033 -vt 0.9780273 0.9272541 -vt 0.9780273 0.960041 -vt 0.9936523 0.960041 -vt 0.9936523 0.9272541 -vt 0.1420898 0.9293033 -vt 0.1420898 0.9620901 -vt 0.1577148 0.9620901 -vt 0.1577148 0.9293033 -vt 0.4897461 0.9293033 -vt 0.4897461 0.9620901 -vt 0.5053711 0.9620901 -vt 0.5053711 0.9293033 -vt 0.1586914 0.9313524 -vt 0.1586914 0.9641393 -vt 0.1743164 0.9641393 -vt 0.1743164 0.9313524 -vt 0.175293 0.9313524 -vt 0.175293 0.9641393 -vt 0.190918 0.9641393 -vt 0.190918 0.9313524 -vt 0.5375977 0.9313524 -vt 0.5375977 0.9641393 -vt 0.5532227 0.9641393 -vt 0.5532227 0.9313524 -vt 0.5541992 0.9313524 -vt 0.5541992 0.9641393 -vt 0.5698242 0.9641393 -vt 0.5698242 0.9313524 -vt 0.5708008 0.9313524 -vt 0.5708008 0.9641393 -vt 0.5864258 0.9641393 -vt 0.5864258 0.9313524 -vt 0.8666992 0.9313524 -vt 0.8666992 0.9641393 -vt 0.8823242 0.9641393 -vt 0.8823242 0.9313524 -vt 0.8833008 0.9313524 -vt 0.8833008 0.9641393 -vt 0.8989258 0.9641393 -vt 0.8989258 0.9313524 -vt 0.5981445 0.9334016 -vt 0.5981445 0.9661885 -vt 0.6137695 0.9661885 -vt 0.6137695 0.9334016 -vt 0.6147461 0.9334016 -vt 0.6147461 0.9661885 -vt 0.6303711 0.9661885 -vt 0.6303711 0.9334016 -vt 0.6791992 0.9334016 -vt 0.6791992 0.9661885 -vt 0.6948242 0.9661885 -vt 0.6948242 0.9334016 -vt 0.6958008 0.9334016 -vt 0.6958008 0.9661885 -vt 0.7114258 0.9661885 -vt 0.7114258 0.9334016 -vt 0.7124023 0.9334016 -vt 0.7124023 0.9661885 -vt 0.7280273 0.9661885 -vt 0.7280273 0.9334016 -vt 0.7290039 0.9334016 -vt 0.7290039 0.9661885 -vt 0.7446289 0.9661885 -vt 0.7446289 0.9334016 -vt 0.7456055 0.9334016 -vt 0.7456055 0.9661885 -vt 0.7612305 0.9661885 -vt 0.7612305 0.9334016 -vt 0.7924805 0.9334016 -vt 0.7924805 0.9661885 -vt 0.8081055 0.9661885 -vt 0.8081055 0.9334016 -vt 0.809082 0.9334016 -vt 0.809082 0.9661885 -vt 0.824707 0.9661885 -vt 0.824707 0.9334016 -vt 0.8256836 0.9334016 -vt 0.8256836 0.9661885 -vt 0.8413086 0.9661885 -vt 0.8413086 0.9334016 -vt 0.8422852 0.9334016 -vt 0.8422852 0.9661885 -vt 0.8579102 0.9661885 -vt 0.8579102 0.9334016 -vt 0.9135742 0.9354508 -vt 0.9135742 0.9682377 -vt 0.9291992 0.9682377 -vt 0.9291992 0.9354508 -vt 0.09619141 0.960041 -vt 0.09619141 0.9928279 -vt 0.1118164 0.9928279 -vt 0.1118164 0.960041 -vt 0.112793 0.960041 -vt 0.112793 0.9928279 -vt 0.128418 0.9928279 -vt 0.128418 0.960041 -vt 0.01708984 0.9620901 -vt 0.01708984 0.994877 -vt 0.03271484 0.994877 -vt 0.03271484 0.9620901 -vt 0.03369141 0.9620901 -vt 0.03369141 0.994877 -vt 0.04931641 0.994877 -vt 0.04931641 0.9620901 -vt 0.05029297 0.9620901 -vt 0.05029297 0.994877 -vt 0.06591797 0.994877 -vt 0.06591797 0.9620901 -vt 0.06689453 0.9620901 -vt 0.06689453 0.994877 -vt 0.08251953 0.994877 -vt 0.08251953 0.9620901 -vt 0.4575195 0.9620901 -vt 0.4575195 0.994877 -vt 0.4731445 0.994877 -vt 0.4731445 0.9620901 -vt 0.9780273 0.9620901 -vt 0.9780273 0.994877 -vt 0.9936523 0.994877 -vt 0.9936523 0.9620901 -vt 0.1420898 0.9641393 -vt 0.1420898 0.9969262 -vt 0.1577148 0.9969262 -vt 0.1577148 0.9641393 -vt 0.4897461 0.9641393 -vt 0.4897461 0.9969262 -vt 0.5053711 0.9969262 -vt 0.5053711 0.9641393 -vt 0.5063477 0.9641393 -vt 0.5063477 0.9969262 -vt 0.5219727 0.9969262 -vt 0.5219727 0.9641393 -vt 0.9458008 0.9641393 -vt 0.9458008 0.9969262 -vt 0.9614258 0.9969262 -vt 0.9614258 0.9641393 -vt 0.1586914 0.9661885 -vt 0.1586914 0.9989754 -vt 0.1743164 0.9989754 -vt 0.1743164 0.9661885 -vt 0.175293 0.9661885 -vt 0.175293 0.9989754 -vt 0.190918 0.9989754 -vt 0.190918 0.9661885 -vt 0.2836914 0.9661885 -vt 0.2836914 0.9989754 -vt 0.2993164 0.9989754 -vt 0.2993164 0.9661885 -vt 0.300293 0.9661885 -vt 0.300293 0.9989754 -vt 0.315918 0.9989754 -vt 0.315918 0.9661885 -vt 0.3168945 0.9661885 -vt 0.3168945 0.9989754 -vt 0.3325195 0.9989754 -vt 0.3325195 0.9661885 -vt 0.5375977 0.9661885 -vt 0.5375977 0.9989754 -vt 0.5532227 0.9989754 -vt 0.5532227 0.9661885 -vt 0.5541992 0.9661885 -vt 0.5541992 0.9989754 -vt 0.5698242 0.9989754 -vt 0.5698242 0.9661885 -vt 0.5708008 0.9661885 -vt 0.5708008 0.9989754 -vt 0.5864258 0.9989754 -vt 0.5864258 0.9661885 -vt 0.6459961 0.9661885 -vt 0.6459961 0.9989754 -vt 0.6616211 0.9989754 -vt 0.6616211 0.9661885 -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 deleted file mode 100644 index ec8cbea9..00000000 --- a/tests/test_main.py +++ /dev/null @@ -1,99 +0,0 @@ -import os - -import UnityPy -from PIL import Image - -SAMPLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples") - - -def test_read_single(): - for f in os.listdir(SAMPLES): - 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(): - for f in os.listdir(SAMPLES): - env = UnityPy.load(os.path.join(SAMPLES, f)) - for obj in env.objects: - if obj.type.name == "Texture2D": - data = obj.read() - data.image.save("test.png") - data.image = data.image.transpose(Image.ROTATE_90) - data.save() - - -def test_sprite(): - for f in os.listdir(SAMPLES): - env = UnityPy.load(os.path.join(SAMPLES, f)) - for obj in env.objects: - if obj.type.name == "Sprite": - obj.read().image.save("test.png") - - -def test_audioclip(): - # as not platforms are supported by FMOD - # we have to check if the platform is supported first - try: - UnityPy.export.AudioClipConverter.import_pyfmodex() - except NotImplementedError: - return - except OSError: - # cibuildwheel doesn't copy the .so files - # so we have to skip the test on it - print("Failed to load the fmod lib for your system.") - print("Skipping the audioclip test.") - return - if UnityPy.export.AudioClipConverter.pyfmodex is False: - return - env = UnityPy.load(os.path.join(SAMPLES, "char_118_yuki.ab")) - for obj in env.objects: - if obj.type.name == "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.name == "Mesh": - mesh = obj.read() - data = mesh.export() - if isinstance(data, str): - data = data.encode("utf8").replace(b"\r", b"") - assert data == wanted - - -def test_read_typetree(): - env = UnityPy.load(SAMPLES) - for obj in env.objects: - obj.read_typetree() - - -def test_save(): - env = UnityPy.load(SAMPLES) - # TODO - check against original - # this only makes sure - # that the save function still produces a readable file - for name, file in env.files.items(): - if isinstance(file, UnityPy.streams.EndianBinaryReader): - continue - save1 = file.save() - save2 = UnityPy.load(save1).file.save() - assert save1 == save2 - - -if __name__ == "__main__": - for x in list(locals()): - if str(x)[:4] == "test": - locals()[x]() - input("All Tests Passed")