├── UnityPy ├── py.typed ├── lib │ ├── __init__.py │ ├── FMOD │ │ ├── __init__.py │ │ ├── Darwin │ │ │ ├── __init__.py │ │ │ └── libfmod.dylib │ │ ├── Linux │ │ │ ├── __init__.py │ │ │ ├── arm │ │ │ │ ├── __init__.py │ │ │ │ └── libfmod.so │ │ │ ├── x86 │ │ │ │ ├── __init__.py │ │ │ │ └── libfmod.so │ │ │ ├── arm64 │ │ │ │ ├── __init__.py │ │ │ │ └── libfmod.so │ │ │ └── x86_64 │ │ │ │ ├── __init__.py │ │ │ │ └── libfmod.so │ │ └── Windows │ │ │ ├── __init__.py │ │ │ ├── arm │ │ │ ├── __init__.py │ │ │ └── fmod.dll │ │ │ ├── x64 │ │ │ ├── __init__.py │ │ │ └── fmod.dll │ │ │ └── x86 │ │ │ ├── __init__.py │ │ │ └── fmod.dll │ └── README.MD ├── tools │ ├── __init__.py │ └── libil2cpp_helper │ │ ├── il2cpp.py │ │ ├── __init__.py │ │ ├── helper.py │ │ └── il2cpp_class.py ├── resources │ ├── __init__.py │ └── uncompressed.tpk ├── helpers │ ├── __init__.py │ ├── ResourceReader.py │ ├── TypeTreeGenerator.py │ ├── PackedBitVector.py │ ├── ArchiveStorageManager.py │ ├── ImportHelper.py │ ├── TextureSwizzler.py │ └── CompressionHelper.py ├── export │ ├── __init__.py │ ├── MeshExporter.py │ ├── MeshRendererExporter.py │ └── AudioClipConverter.py ├── streams │ ├── __init__.py │ └── EndianBinaryWriter.py ├── enums │ ├── SpritePackingMode.py │ ├── PassType.py │ ├── SpriteMeshType.py │ ├── FileType.py │ ├── SerializedPropertyType.py │ ├── SpritePackingRotation.py │ ├── MeshTopology.py │ ├── GfxPrimitiveType.py │ ├── TextureDimension.py │ ├── BundleFile.py │ ├── ExtendableEnum.py │ ├── __init__.py │ ├── ShaderCompilerPlatform.py │ ├── BuildTarget.py │ ├── ShaderGpuProgramType.py │ ├── Audio.py │ ├── TextureFormat.py │ ├── VertexFormat.py │ ├── CommonString.py │ └── GraphicsFormat.py ├── classes │ ├── __init__.py │ ├── legacy_patch │ │ ├── Renderer.pyi │ │ ├── Shader.py │ │ ├── Sprite.py │ │ ├── Mesh.py │ │ ├── Renderer.py │ │ ├── __init__.py │ │ ├── AudioClip.py │ │ ├── Texture2DArray.pyi │ │ ├── GameObject.pyi │ │ ├── Sprite.pyi │ │ ├── AudioClip.pyi │ │ ├── Shader.pyi │ │ ├── GameObject.py │ │ ├── Texture2D.pyi │ │ ├── Texture2DArray.py │ │ ├── Mesh.pyi │ │ └── Texture2D.py │ ├── UnknownObject.py │ ├── Object.py │ ├── math.py │ └── PPtr.py ├── files │ ├── __init__.py │ ├── WebFile.py │ └── File.py ├── math │ ├── __init__.py │ ├── Quaternion.py │ ├── Rectangle.py │ ├── Color.py │ ├── Vector2.py │ ├── Vector3.py │ ├── Vector4.py │ └── Matrix4x4.py ├── __init__.py ├── exceptions.py ├── UnityPyBoost.pyi └── config.py ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug---error-report.md └── workflows │ ├── test.yml │ ├── release.yml │ └── codeql-analysis.yml ├── _config.yml ├── tests ├── samples │ ├── banner_1 │ ├── atlas_test │ ├── char_118_yuki.ab │ ├── xinzexi_2_n_tex │ └── a1dd7f06fc870d5df6ccb483c7cd5686.bin ├── test_extractor.py ├── test_ak.py ├── test_main.py └── test_typetree.py ├── UnityPyBoost ├── Mesh.hpp ├── ArchiveStorageDecryptor.hpp ├── ReadMe.md ├── UnityPyBoost.cpp ├── TypeTreeHelper.hpp ├── swap.hpp ├── ArchiveStorageDecryptor.cpp └── Mesh.cpp ├── .editorconfig ├── CHANGELOG.md ├── LICENSE ├── .gitignore ├── pyproject.toml └── setup.py /UnityPy/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: K0lb3 2 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Darwin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/arm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/x86/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Windows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-dinky -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/arm64/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/x86_64/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Windows/arm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Windows/x64/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Windows/x86/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/tools/libil2cpp_helper/il2cpp.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /UnityPy/tools/libil2cpp_helper/__init__.py: -------------------------------------------------------------------------------- 1 | from .metadata import Metadata -------------------------------------------------------------------------------- /UnityPy/lib/README.MD: -------------------------------------------------------------------------------- 1 | ## FMOD 2 | Source: [FMOD Engine 2.00.10](https://fmod.com/download) 3 | -------------------------------------------------------------------------------- /tests/samples/banner_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/tests/samples/banner_1 -------------------------------------------------------------------------------- /tests/samples/atlas_test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/tests/samples/atlas_test -------------------------------------------------------------------------------- /UnityPy/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ArchiveStorageManager, CompressionHelper, ImportHelper, TypeTreeHelper -------------------------------------------------------------------------------- /tests/samples/char_118_yuki.ab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/tests/samples/char_118_yuki.ab -------------------------------------------------------------------------------- /tests/samples/xinzexi_2_n_tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/tests/samples/xinzexi_2_n_tex -------------------------------------------------------------------------------- /UnityPy/resources/uncompressed.tpk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/resources/uncompressed.tpk -------------------------------------------------------------------------------- /UnityPy/export/__init__.py: -------------------------------------------------------------------------------- 1 | from . import MeshRendererExporter, SpriteHelper, Texture2DConverter, AudioClipConverter, MeshExporter 2 | -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Darwin/libfmod.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Darwin/libfmod.dylib -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/arm/libfmod.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Linux/arm/libfmod.so -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/x86/libfmod.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Linux/x86/libfmod.so -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Windows/arm/fmod.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Windows/arm/fmod.dll -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Windows/x64/fmod.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Windows/x64/fmod.dll -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Windows/x86/fmod.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Windows/x86/fmod.dll -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/arm64/libfmod.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Linux/arm64/libfmod.so -------------------------------------------------------------------------------- /UnityPy/lib/FMOD/Linux/x86_64/libfmod.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/UnityPy/lib/FMOD/Linux/x86_64/libfmod.so -------------------------------------------------------------------------------- /UnityPy/streams/__init__.py: -------------------------------------------------------------------------------- 1 | from .EndianBinaryReader import EndianBinaryReader 2 | from .EndianBinaryWriter import EndianBinaryWriter 3 | -------------------------------------------------------------------------------- /UnityPy/enums/SpritePackingMode.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | class SpritePackingMode(IntEnum): 4 | kSPMTight = 0 5 | kSPMRectangle = 1 -------------------------------------------------------------------------------- /UnityPyBoost/Mesh.hpp: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #pragma once 3 | #include 4 | 5 | PyObject *unpack_vertexdata(PyObject *self, PyObject *args); -------------------------------------------------------------------------------- /tests/samples/a1dd7f06fc870d5df6ccb483c7cd5686.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MooncellWiki/UnityPy/HEAD/tests/samples/a1dd7f06fc870d5df6ccb483c7cd5686.bin -------------------------------------------------------------------------------- /UnityPy/enums/PassType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class PassType(IntEnum): 5 | kPassTypeNormal = 0 6 | kPassTypeUse = 1 7 | kPassTypeGrab = 2 8 | -------------------------------------------------------------------------------- /UnityPy/enums/SpriteMeshType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class SpriteMeshType(IntEnum): 5 | kSpriteMeshTypeFullRect = 0 6 | kSpriteMeshTypeTight = 1 7 | -------------------------------------------------------------------------------- /UnityPyBoost/ArchiveStorageDecryptor.hpp: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #pragma once 3 | #include 4 | 5 | PyObject *decrypt_block(PyObject *self, PyObject *args); 6 | -------------------------------------------------------------------------------- /UnityPy/enums/FileType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class FileType(IntEnum): 5 | AssetsFile = 0 6 | BundleFile = 1 7 | WebFile = 2 8 | ResourceFile = 9 9 | ZIP = 10 10 | -------------------------------------------------------------------------------- /UnityPy/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from .generated import * 2 | from .legacy_patch import * 3 | from .Object import Object as Object 4 | from .PPtr import PPtr as PPtr 5 | from .UnknownObject import UnknownObject as UnknownObject 6 | -------------------------------------------------------------------------------- /UnityPy/enums/SerializedPropertyType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class SerializedPropertyType(IntEnum): 5 | kColor = 0 6 | kVector = 1 7 | kFloat = 2 8 | kRange = 3 9 | kTexture = 4 10 | -------------------------------------------------------------------------------- /UnityPy/files/__init__.py: -------------------------------------------------------------------------------- 1 | from .File import File, DirectoryInfo 2 | from .SerializedFile import SerializedFile 3 | from .BundleFile import BundleFile 4 | from .WebFile import WebFile 5 | from .ObjectReader import ObjectReader 6 | -------------------------------------------------------------------------------- /UnityPyBoost/ReadMe.md: -------------------------------------------------------------------------------- 1 | # UnityPyBoost 2 | 3 | A C-extension for UnityPy that accelerates various parts of UnityPy (by a lot). 4 | 5 | ## Structure 6 | 7 | The filename corresponds to where the original python functionality is situated. 8 | -------------------------------------------------------------------------------- /UnityPy/enums/SpritePackingRotation.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | class SpritePackingRotation(IntEnum): 4 | kSPRNone = 0 5 | kSPRFlipHorizontal = 1 6 | kSPRFlipVertical = 2 7 | kSPRRotate180 = 3 8 | kSPRRotate90 = 4 9 | -------------------------------------------------------------------------------- /UnityPy/enums/MeshTopology.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class MeshTopology(IntEnum): 5 | Triangles = 0 6 | TriangleStrip = 1 # deprecated 7 | Quads = 2 8 | Lines = 3 9 | LineStrip = 4 10 | Points = 5 11 | -------------------------------------------------------------------------------- /UnityPy/math/__init__.py: -------------------------------------------------------------------------------- 1 | from .Color import Color 2 | from .Vector2 import Vector2 3 | from .Vector3 import Vector3 4 | from .Vector4 import Vector4 5 | from .Matrix4x4 import Matrix4x4 6 | from .Quaternion import Quaternion 7 | from .Rectangle import Rectangle 8 | -------------------------------------------------------------------------------- /UnityPy/enums/GfxPrimitiveType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class GfxPrimitiveType(IntEnum): 5 | kPrimitiveTriangles = 0 6 | kPrimitiveTriangleStrip = 1 7 | kPrimitiveQuads = 2 8 | kPrimitiveLines = 3 9 | kPrimitiveLineStrip = 4 10 | kPrimitivePoints = 5 -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Renderer.pyi: -------------------------------------------------------------------------------- 1 | from UnityPy.classes import PPtr 2 | from UnityPy.classes.generated import Component 3 | from UnityPy.classes.legacy_patch import GameObject 4 | 5 | class Renderer(Component): 6 | m_GameObject: PPtr[GameObject] 7 | 8 | def export(self, export_dir: str) -> None: ... 9 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Shader.py: -------------------------------------------------------------------------------- 1 | from ..generated import Shader 2 | 3 | 4 | def _Shader_export(self: Shader) -> str: 5 | from ...export.ShaderConverter import export_shader 6 | 7 | return export_shader(self) 8 | 9 | 10 | Shader.export = _Shader_export 11 | 12 | __all__ = ("Shader",) 13 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Sprite.py: -------------------------------------------------------------------------------- 1 | from ..generated import Sprite 2 | 3 | 4 | def _Sprite_image(self: Sprite): 5 | from ...export import SpriteHelper 6 | 7 | return SpriteHelper.get_image_from_sprite(self) 8 | 9 | 10 | Sprite.image = property(_Sprite_image) 11 | 12 | __all__ = ("Sprite",) 13 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Mesh.py: -------------------------------------------------------------------------------- 1 | from ..generated import Mesh 2 | 3 | 4 | def _Mesh_export(self: Mesh, format: str = "obj"): 5 | from ...export.MeshExporter import export_mesh 6 | 7 | return export_mesh(self, format) 8 | 9 | 10 | Mesh.export = _Mesh_export 11 | 12 | 13 | __all__ = ("Mesh",) 14 | -------------------------------------------------------------------------------- /UnityPy/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.21.3" 2 | 3 | from .environment import Environment as Environment 4 | from .helpers.ArchiveStorageManager import ( 5 | set_assetbundle_decrypt_key as set_assetbundle_decrypt_key, 6 | ) 7 | 8 | load = Environment 9 | 10 | # backward compatibility 11 | AssetsManager = Environment 12 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Renderer.py: -------------------------------------------------------------------------------- 1 | from ..generated import Renderer 2 | 3 | 4 | def export(self, export_dir: str) -> None: 5 | from ...export import MeshRendererExporter 6 | 7 | MeshRendererExporter.export_mesh_renderer(self, export_dir) 8 | 9 | 10 | Renderer.export = export 11 | 12 | __all__ = ("Renderer",) 13 | -------------------------------------------------------------------------------- /UnityPy/enums/TextureDimension.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class TextureDimension(IntEnum): 5 | kTexDimUnknown = -1 6 | kTexDimNone = 0 7 | kTexDimAny = 1 8 | kTexDim2D = 2 9 | kTexDim3D = 3 10 | kTexDimCUBE = 4 11 | kTexDim2DArray = 5 12 | kTexDimCubeArray = 6 13 | kTexDimForce32Bit = 2147483647 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | quote_type = double 10 | insert_final_newline = true 11 | tab_width = 4 12 | trim_trailing_whitespace = true 13 | 14 | [*.py] 15 | spaces_around_brackets = none 16 | spaces_around_operators = true 17 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/__init__.py: -------------------------------------------------------------------------------- 1 | from .AudioClip import AudioClip as AudioClip 2 | from .GameObject import GameObject as GameObject 3 | from .Mesh import Mesh as Mesh 4 | from .Renderer import Renderer as Renderer 5 | from .Shader import Shader as Shader 6 | from .Sprite import Sprite as Sprite 7 | from .Texture2D import Texture2D as Texture2D 8 | from .Texture2DArray import Texture2DArray as Texture2DArray 9 | -------------------------------------------------------------------------------- /UnityPy/exceptions.py: -------------------------------------------------------------------------------- 1 | class TypeTreeError(Exception): 2 | def __init__(self, message, nodes): 3 | super().__init__(message) 4 | self.nodes = nodes 5 | 6 | 7 | class UnityVersionFallbackError(Exception): 8 | def __init__(self, message): 9 | super().__init__(message) 10 | 11 | 12 | class UnityVersionFallbackWarning(UserWarning): 13 | def __init__(self, message): 14 | super().__init__(message) 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug---error-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug & Error report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Code** 11 | A snippet of the code section that cases the bug or error. 12 | 13 | **Error** 14 | The error message that is produced by python. 15 | 16 | **Bug** 17 | A description of what you expect to happen/see and what actually happens. 18 | 19 | **To Reproduce** 20 | - a copy of the file that causes the problem 21 | - following data: 22 | - Python version 23 | - UnityPy version 24 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/AudioClip.py: -------------------------------------------------------------------------------- 1 | from ..generated import AudioClip 2 | from ...enums import AUDIO_TYPE_EXTEMSION 3 | 4 | 5 | def _AudioClip_extension(self: AudioClip) -> str: 6 | return AUDIO_TYPE_EXTEMSION.get(self.m_CompressionFormat, ".audioclip") 7 | 8 | 9 | def _AudioClip_samples(self: AudioClip) -> dict: 10 | from ...export import AudioClipConverter 11 | 12 | return AudioClipConverter.extract_audioclip_samples(self) 13 | 14 | 15 | AudioClip.extension = property(_AudioClip_extension) 16 | AudioClip.samples = property(_AudioClip_samples) 17 | 18 | __all__ = ("AudioClip",) 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.20 4 | 5 | - overall type-hint improvements 6 | - **UnityPy.classes** 7 |   - replace hard-coded UnityPy.classes with generated class stubs 8 |     - UnityPy.classes.legacy_patch to provide backward compatibility 9 |     - classes are all parsed and dumped/stored using typetrees now 10 | - **TypeTree** 11 |   - use a hierarchical instead of a flat structure 12 |     - list to Node.m_Children 13 |   - rewrite the typetree read and write functions to reflect this 14 |   - **remove map typetree type type due to its multi-dict nature** 15 | - **Exporter** 16 |   - extended Sprite-mesh support (solves some dicings automatically) 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: [ "master" ] 5 | pull_request: 6 | branches: [ "master" ] 7 | 8 | jobs: 9 | build_wheels: 10 | name: Build wheels on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-13] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.9' 24 | 25 | - name: Install 26 | run: pip install .[tests] 27 | 28 | - name: Run tests 29 | run: python -m pytest -vs ./tests 30 | -------------------------------------------------------------------------------- /tests/test_extractor.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tempfile import TemporaryDirectory 3 | 4 | from UnityPy.tools.extractor import extract_assets 5 | 6 | SAMPLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples") 7 | 8 | 9 | def test_extractor(): 10 | temp_dir = TemporaryDirectory(prefix="unitypy_test") 11 | extract_assets( 12 | SAMPLES, 13 | temp_dir.name, 14 | True, 15 | ) 16 | files = [ 17 | os.path.relpath(os.path.join(root, f), temp_dir.name) 18 | for root, dirs, files in os.walk(temp_dir.name) 19 | for f in files 20 | ] 21 | temp_dir.cleanup() 22 | assert len(files) == 45 23 | 24 | 25 | if __name__ == "__main__": 26 | test_extractor() 27 | -------------------------------------------------------------------------------- /UnityPy/enums/BundleFile.py: -------------------------------------------------------------------------------- 1 | from enum import IntFlag 2 | 3 | 4 | class CompressionFlags(IntFlag): 5 | NONE = 0 6 | LZMA = 1 7 | LZ4 = 2 8 | LZ4HC = 3 9 | COMPRESSION_4 = 4 10 | COMPRESSION_5 = 5 11 | 12 | 13 | class ArchiveFlagsOld(IntFlag): 14 | CompressionTypeMask = 0x3F 15 | BlocksAndDirectoryInfoCombined = 0x40 16 | BlocksInfoAtTheEnd = 0x80 17 | OldWebPluginCompatibility = 0x100 18 | UsesAssetBundleEncryption = 0x200 19 | 20 | 21 | class ArchiveFlags(IntFlag): 22 | CompressionTypeMask = 0x3F 23 | BlocksAndDirectoryInfoCombined = 0x40 24 | BlocksInfoAtTheEnd = 0x80 25 | OldWebPluginCompatibility = 0x100 26 | BlockInfoNeedPaddingAtStart = 0x200 27 | UsesAssetBundleEncryption = 0x400 28 | -------------------------------------------------------------------------------- /tests/test_ak.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tempfile import TemporaryDirectory 3 | 4 | from UnityPy.tools.extractor import extract_assets 5 | 6 | SAMPLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples") 7 | 8 | 9 | def test_ak(): 10 | os.environ['UNITYPY_AK'] = '1' 11 | 12 | temp_dir = TemporaryDirectory(prefix="unitypy_test_ak") 13 | extract_assets( 14 | SAMPLES, 15 | temp_dir.name, 16 | True, 17 | ) 18 | files = [ 19 | os.path.relpath(os.path.join(root, f), temp_dir.name) 20 | for root, dirs, files in os.walk(temp_dir.name) 21 | for f in files 22 | ] 23 | print(files) 24 | temp_dir.cleanup() 25 | assert len(files) == 46 26 | 27 | 28 | if __name__ == "__main__": 29 | test_ak() 30 | -------------------------------------------------------------------------------- /UnityPy/enums/ExtendableEnum.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | # based on https://stackoverflow.com/a/57179436 4 | 5 | 6 | class ExtendableEnum(IntEnum): 7 | @classmethod 8 | def _missing_(cls, value): 9 | if isinstance(value, int): 10 | pseudo_member = cls._value2member_map_.get(value, None) 11 | if pseudo_member is None: 12 | new_member = int.__new__(cls, value) 13 | # I expect a name attribute to hold a string, hence str(value) 14 | # However, new_member._name_ = value works, too 15 | new_member._name_ = f"Unknown ({value})" 16 | new_member._value_ = value 17 | pseudo_member = cls._value2member_map_.setdefault(value, new_member) 18 | return pseudo_member 19 | return None # will raise the ValueError in Enum.__new__ 20 | -------------------------------------------------------------------------------- /UnityPy/enums/__init__.py: -------------------------------------------------------------------------------- 1 | from .Audio import AudioType, AudioCompressionFormat, AUDIO_TYPE_EXTEMSION 2 | from .BuildTarget import BuildTarget 3 | from .BundleFile import ArchiveFlags, ArchiveFlagsOld, CompressionFlags 4 | from .ClassIDType import ClassIDType 5 | from .FileType import FileType 6 | from .TextureFormat import TextureFormat 7 | from .SpriteMeshType import SpriteMeshType 8 | from .GfxPrimitiveType import GfxPrimitiveType 9 | from .CommonString import CommonString 10 | from .ShaderCompilerPlatform import ShaderCompilerPlatform 11 | from .ShaderGpuProgramType import ShaderGpuProgramType 12 | from .SerializedPropertyType import SerializedPropertyType 13 | from .SpritePackingMode import SpritePackingMode 14 | from .SpritePackingRotation import SpritePackingRotation 15 | from .TextureDimension import TextureDimension 16 | from .PassType import PassType 17 | from .GraphicsFormat import GraphicsFormat 18 | -------------------------------------------------------------------------------- /UnityPy/enums/ShaderCompilerPlatform.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class ShaderCompilerPlatform(IntEnum): 5 | kShaderCompPlatformNone = -1 6 | kShaderCompPlatformGL = 0 7 | kShaderCompPlatformD3D9 = 1 8 | kShaderCompPlatformXbox360 = 2 9 | kShaderCompPlatformPS3 = 3 10 | kShaderCompPlatformD3D11 = 4 11 | kShaderCompPlatformGLES20 = 5 12 | kShaderCompPlatformNaCl = 6 13 | kShaderCompPlatformFlash = 7 14 | kShaderCompPlatformD3D11_9x = 8 15 | kShaderCompPlatformGLES3Plus = 9 16 | kShaderCompPlatformPSP2 = 10 17 | kShaderCompPlatformPS4 = 11 18 | kShaderCompPlatformXboxOne = 12 19 | kShaderCompPlatformPSM = 13 20 | kShaderCompPlatformMetal = 14 21 | kShaderCompPlatformOpenGLCore = 15 22 | kShaderCompPlatformN3DS = 16 23 | kShaderCompPlatformWiiU = 17 24 | kShaderCompPlatformVulkan = 18 25 | kShaderCompPlatformSwitch = 19 26 | kShaderCompPlatformXboxOneD3D12 = 20 27 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Texture2DArray.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from PIL.Image import Image 4 | 5 | from UnityPy.classes.generated import GLTextureSettings, StreamingInfo, Texture 6 | 7 | class Texture2DArray(Texture): 8 | image_data: bytes 9 | m_ColorSpace: int 10 | m_DataSize: int 11 | m_Depth: int 12 | m_Format: int 13 | m_Height: int 14 | m_IsReadable: bool 15 | m_MipCount: int 16 | m_Name: str 17 | m_TextureSettings: GLTextureSettings 18 | m_Width: int 19 | m_DownscaleFallback: Optional[bool] = None 20 | m_ForcedFallbackFormat: Optional[int] = None 21 | m_IgnoreMipmapLimit: Optional[bool] = None 22 | m_IsAlphaChannelOptional: Optional[bool] = None 23 | m_MipmapLimitGroupName: Optional[str] = None 24 | m_MipsStripped: Optional[int] = None 25 | m_StreamData: Optional[StreamingInfo] = None 26 | m_UsageMode: Optional[int] = None 27 | 28 | @property 29 | def images(self) -> List[Image]: ... 30 | -------------------------------------------------------------------------------- /UnityPy/enums/BuildTarget.py: -------------------------------------------------------------------------------- 1 | from .ExtendableEnum import ExtendableEnum 2 | 3 | 4 | class BuildTarget(ExtendableEnum): 5 | UnknownPlatform = 3716 6 | DashboardWidget = 1 7 | StandaloneOSX = 2 8 | StandaloneOSXPPC = 3 9 | StandaloneOSXIntel = 4 10 | StandaloneWindows = 5 11 | WebPlayer = 6 12 | WebPlayerStreamed = 7 13 | Wii = 8 14 | iOS = 9 15 | PS3 = 10 16 | XBOX360 = 11 17 | Android = 13 18 | StandaloneGLESEmu = 14 19 | NaCl = 16 20 | StandaloneLinux = 17 21 | FlashPlayer = 18 22 | StandaloneWindows64 = 19 23 | WebGL = 20 24 | WSAPlayer = 21 25 | StandaloneLinux64 = 24 26 | StandaloneLinuxUniversal = 25 27 | WP8Player = 26 28 | StandaloneOSXIntel64 = 27 29 | BlackBerry = 28 30 | Tizen = 29 31 | PSP2 = 30 32 | PS4 = 31 33 | PSM = 32 34 | XboxOne = 33 35 | SamsungTV = 34 36 | N3DS = 35 37 | WiiU = 36 38 | tvOS = 37 39 | Switch = 38 40 | NoTarget = -2 41 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/GameObject.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Union 2 | 3 | from UnityPy.classes import Component, PPtr 4 | from UnityPy.classes.generated import ComponentPair, EditorExtension 5 | 6 | class GameObject(EditorExtension): 7 | m_Component: Union[List[ComponentPair], List[Tuple[int, PPtr[Component]]]] 8 | m_IsActive: Union[bool, int] 9 | m_Layer: int 10 | m_Name: str 11 | m_Tag: int 12 | 13 | @property 14 | def m_Components(self) -> List[PPtr[Component]]: ... 15 | @property 16 | def m_Animator(self) -> Union[PPtr[Component], None]: ... 17 | @property 18 | def m_Animation(self) -> Union[PPtr[Component], None]: ... 19 | @property 20 | def m_Transform(self) -> Union[PPtr[Component], None]: ... 21 | @property 22 | def m_SkinnedMeshRenderer(self) -> Union[PPtr[Component], None]: ... 23 | @property 24 | def m_MeshRenderer(self) -> Union[PPtr[Component], None]: ... 25 | @property 26 | def m_MeshFilter(self) -> Union[PPtr[Component], None]: ... 27 | -------------------------------------------------------------------------------- /UnityPy/math/Quaternion.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Quaternion: 6 | X: float 7 | Y: float 8 | Z: float 9 | W: float 10 | 11 | def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 1.0): 12 | if not all(isinstance(v, (int, float)) for v in (x, y, z, w)): 13 | raise TypeError("All components must be numeric.") 14 | self.X = float(x) 15 | self.Y = float(y) 16 | self.Z = float(z) 17 | self.W = float(w) 18 | 19 | def __getitem__(self, index): 20 | return (self.X, self.Y, self.Z, self.W)[index] 21 | 22 | def __setitem__(self, index, value): 23 | if index == 0: 24 | self.X = value 25 | elif index == 1: 26 | self.Y = value 27 | elif index == 2: 28 | self.Z = value 29 | elif index == 3: 30 | self.W = value 31 | else: 32 | raise IndexError("Index out of range") 33 | -------------------------------------------------------------------------------- /UnityPy/classes/UnknownObject.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from ..helpers.TypeTreeNode import TypeTreeNode 4 | from .Object import Object 5 | 6 | 7 | class UnknownObject(Object): 8 | """An object of unknown type that showed up during typetree parsing.""" 9 | 10 | __node__: Optional[TypeTreeNode] 11 | 12 | def __init__(self, __node__: TypeTreeNode = None, **kwargs): 13 | self.__node__ = __node__ 14 | self.__dict__.update(**kwargs) 15 | 16 | def get_type(self): 17 | return self.__node__.m_Type if self.__node__ else None 18 | 19 | def __repr__(self) -> str: 20 | def format_value(v): 21 | vstr = repr(v) 22 | if len(vstr) > 100: 23 | return vstr[:97] + "..." 24 | return vstr 25 | 26 | inner_str = ", ".join( 27 | f"{k}={format_value(v)}" 28 | for k, v in self.__dict__.items() 29 | if k != "__node__" 30 | ) 31 | 32 | return f" {inner_str}>" 33 | -------------------------------------------------------------------------------- /UnityPy/classes/Object.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from abc import ABC, ABCMeta 3 | from typing import TYPE_CHECKING, Dict, Any, Optional 4 | 5 | if TYPE_CHECKING: 6 | from ..files.ObjectReader import ObjectReader 7 | from ..files.SerializedFile import SerializedFile 8 | 9 | 10 | class Object(ABC, metaclass=ABCMeta): 11 | object_reader: Optional[ObjectReader] = None 12 | 13 | def __init__(self, **kwargs: Dict[str, Any]) -> None: 14 | self.__dict__.update(**kwargs) 15 | 16 | def set_object_reader(self, object_info: ObjectReader[Any]): 17 | self.object_reader = object_info 18 | 19 | def __repr__(self) -> str: 20 | return f"<{self.__class__.__name__}>" 21 | 22 | @property 23 | def assets_file(self) -> Optional[SerializedFile]: 24 | if self.object_reader: 25 | return self.object_reader.assets_file 26 | return None 27 | 28 | def save(self) -> None: 29 | if self.object_reader is None: 30 | raise ValueError("ObjectReader not set") 31 | 32 | self.object_reader.save_typetree(self) 33 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Sprite.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | from PIL.Image import Image 4 | 5 | from UnityPy.classes import PPtr 6 | from UnityPy.classes.generated import (GUID, MonoBehaviour, NamedObject, Rectf, 7 | SpriteAtlas, SpriteBone, 8 | SpriteRenderData) 9 | from UnityPy.classes.math import Vector2f, Vector4f 10 | 11 | class Sprite(NamedObject): 12 | m_Extrude: int 13 | m_Name: str 14 | m_Offset: Vector2f 15 | m_PixelsToUnits: float 16 | m_RD: SpriteRenderData 17 | m_Rect: Rectf 18 | m_AtlasTags: Optional[List[str]] = None 19 | m_Bones: Optional[List[SpriteBone]] = None 20 | m_Border: Optional[Vector4f] = None 21 | m_IsPolygon: Optional[bool] = None 22 | m_PhysicsShape: Optional[List[List[Vector2f]]] = None 23 | m_Pivot: Optional[Vector2f] = None 24 | m_RenderDataKey: Optional[Tuple[GUID, int]] = None 25 | m_ScriptableObjects: Optional[List[PPtr[MonoBehaviour]]] = None 26 | m_SpriteAtlas: Optional[PPtr[SpriteAtlas]] = None 27 | 28 | @property 29 | def image(self) -> Image: ... 30 | -------------------------------------------------------------------------------- /UnityPy/math/Rectangle.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Rectangle: 6 | height: int 7 | width: int 8 | x: int 9 | y: int 10 | 11 | def __init__(self, *args, **kwargs): 12 | if args: 13 | # Rectangle(Point, Size) 14 | if len(args) == 4: 15 | self.x, self.y, self.width, self.height = args 16 | elif kwargs: 17 | self.__dict__.update(kwargs) 18 | 19 | def round(self): 20 | return Rectangle( 21 | round(self.x), round(self.y), round(self.width), round(self.height) 22 | ) 23 | 24 | @property 25 | def left(self): 26 | return self.x 27 | 28 | @property 29 | def top(self): 30 | return self.y 31 | 32 | @property 33 | def right(self): 34 | return self.x + self.width 35 | 36 | @property 37 | def bottom(self): 38 | return self.y + self.height 39 | 40 | @property 41 | def size(self): 42 | return self.width, self.height 43 | 44 | @property 45 | def location(self): 46 | return self.x, self.y 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 K0lb3 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/AudioClip.pyi: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | from UnityPy.classes.generated import SampleClip, StreamedResource 4 | 5 | class AudioClip(SampleClip): 6 | m_Name: str 7 | m_3D: Optional[bool] = None 8 | m_Ambisonic: Optional[bool] = None 9 | m_AudioData: Optional[List[int]] = None 10 | m_BitsPerSample: Optional[int] = None 11 | m_Channels: Optional[int] = None 12 | m_CompressionFormat: Optional[int] = None 13 | m_Format: Optional[int] = None 14 | m_Frequency: Optional[int] = None 15 | m_IsTrackerFormat: Optional[bool] = None 16 | m_Legacy3D: Optional[bool] = None 17 | m_Length: Optional[float] = None 18 | m_LoadInBackground: Optional[bool] = None 19 | m_LoadType: Optional[int] = None 20 | m_PreloadAudioData: Optional[bool] = None 21 | m_Resource: Optional[StreamedResource] = None 22 | m_Stream: Optional[int] = None 23 | m_SubsoundIndex: Optional[int] = None 24 | m_Type: Optional[int] = None 25 | m_UseHardware: Optional[bool] = None 26 | 27 | @property 28 | def extension(self) -> str: ... 29 | 30 | @property 31 | def samples(self) -> Dict[str, bytes]: ... 32 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Shader.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple, Union 2 | 3 | from UnityPy.classes import PPtr 4 | from UnityPy.classes.generated import (GUID, NamedObject, SerializedShader, 5 | Texture) 6 | 7 | class Shader(NamedObject): 8 | m_Name: str 9 | compressedBlob: Optional[List[int]] = None 10 | compressedLengths: Optional[Union[List[int], List[List[int]]]] = None 11 | decompressedLengths: Optional[Union[List[int], List[List[int]]]] = None 12 | decompressedSize: Optional[int] = None 13 | m_AssetGUID: Optional[GUID] = None 14 | m_Dependencies: Optional[List[PPtr[Shader]]] = None 15 | m_NonModifiableTextures: Optional[List[Tuple[str, PPtr[Texture]]]] = None 16 | m_ParsedForm: Optional[SerializedShader] = None 17 | m_PathName: Optional[str] = None 18 | m_Script: Optional[str] = None 19 | m_ShaderIsBaked: Optional[bool] = None 20 | m_SubProgramBlob: Optional[List[int]] = None 21 | offsets: Optional[Union[List[int], List[List[int]]]] = None 22 | platforms: Optional[List[int]] = None 23 | stageCounts: Optional[List[int]] = None 24 | 25 | def export(self) -> str: ... 26 | -------------------------------------------------------------------------------- /UnityPy/helpers/ResourceReader.py: -------------------------------------------------------------------------------- 1 | import ntpath 2 | from ..streams import EndianBinaryReader 3 | 4 | from typing import TYPE_CHECKING 5 | if TYPE_CHECKING: 6 | from ..files.SerializedFile import SerializedFile 7 | 8 | 9 | def get_resource_data(res_path: str, assets_file: "SerializedFile", offset: int, size: int): 10 | basename = ntpath.basename(res_path) 11 | name, ext = ntpath.splitext(basename) 12 | possible_names = [ 13 | basename, 14 | f"{name}.resource", 15 | f"{name}.assets.resS", 16 | f"{name}.resS", 17 | ] 18 | environment = assets_file.environment 19 | reader = None 20 | for possible_name in possible_names: 21 | reader = environment.get_cab(possible_name) 22 | if reader: 23 | break 24 | if not reader: 25 | assets_file.load_dependencies(possible_names) 26 | for possible_name in possible_names: 27 | reader = environment.get_cab(possible_name) 28 | if reader: 29 | break 30 | if not reader: 31 | raise FileNotFoundError(f"Resource file {basename} not found") 32 | return _get_resource_data(reader, offset, size) 33 | 34 | 35 | def _get_resource_data(reader: EndianBinaryReader, offset: int, size: int): 36 | reader.Position = offset 37 | return reader.read_bytes(size) 38 | -------------------------------------------------------------------------------- /UnityPy/UnityPyBoost.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, List, Optional, Union 4 | 5 | if TYPE_CHECKING: 6 | from .classes import Object 7 | from .files.SerializedFile import SerializedFile 8 | 9 | def unpack_vertexdata( 10 | data: Union[bytes, bytearray], 11 | component_byte_size: int, 12 | vertex_count: int, 13 | stream_offset: int, 14 | stream_stride: int, 15 | channel_offset: int, 16 | channel_dimension: int, 17 | swap: bool, 18 | ) -> bytes: ... 19 | def read_typetree( 20 | data: Union[bytes, bytearray], 21 | node: TypeTreeNode, 22 | endian: Union["<", ">"], 23 | as_dict: bool, 24 | assetsfile: SerializedFile, 25 | classes: dict, 26 | ) -> Union[dict[str, Any], Object]: ... 27 | 28 | class TypeTreeNode: 29 | m_Level: int 30 | m_Type: str 31 | m_Name: str 32 | m_ByteSize: int 33 | m_Version: int 34 | m_Children: List[TypeTreeNode] 35 | m_TypeFlags: Optional[int] = None 36 | m_VariableCount: Optional[int] = None 37 | m_Index: Optional[int] = None 38 | m_MetaFlag: Optional[int] = None 39 | m_RefTypeHash: Optional[int] = None 40 | _clean_name: str 41 | 42 | def decrypt_block( 43 | index_bytes: Union[bytes, bytearray], 44 | substitute_bytes: Union[bytes, bytearray], 45 | data: Union[bytes, bytearray], 46 | index: int, 47 | ) -> bytes: ... 48 | -------------------------------------------------------------------------------- /UnityPy/enums/ShaderGpuProgramType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class ShaderGpuProgramType(IntEnum): 5 | kShaderGpuProgramUnknown = 0 6 | kShaderGpuProgramGLLegacy = 1 7 | kShaderGpuProgramGLES31AEP = 2 8 | kShaderGpuProgramGLES31 = 3 9 | kShaderGpuProgramGLES3 = 4 10 | kShaderGpuProgramGLES = 5 11 | kShaderGpuProgramGLCore32 = 6 12 | kShaderGpuProgramGLCore41 = 7 13 | kShaderGpuProgramGLCore43 = 8 14 | kShaderGpuProgramDX9VertexSM20 = 9 15 | kShaderGpuProgramDX9VertexSM30 = 10 16 | kShaderGpuProgramDX9PixelSM20 = 11 17 | kShaderGpuProgramDX9PixelSM30 = 12 18 | kShaderGpuProgramDX10Level9Vertex = 13 19 | kShaderGpuProgramDX10Level9Pixel = 14 20 | kShaderGpuProgramDX11VertexSM40 = 15 21 | kShaderGpuProgramDX11VertexSM50 = 16 22 | kShaderGpuProgramDX11PixelSM40 = 17 23 | kShaderGpuProgramDX11PixelSM50 = 18 24 | kShaderGpuProgramDX11GeometrySM40 = 19 25 | kShaderGpuProgramDX11GeometrySM50 = 20 26 | kShaderGpuProgramDX11HullSM50 = 21 27 | kShaderGpuProgramDX11DomainSM50 = 22 28 | kShaderGpuProgramMetalVS = 23 29 | kShaderGpuProgramMetalFS = 24 30 | kShaderGpuProgramSPIRV = 25 31 | kShaderGpuProgramConsoleVS = 26 32 | kShaderGpuProgramConsoleFS = 27 33 | kShaderGpuProgramConsoleHS = 28 34 | kShaderGpuProgramConsoleDS = 29 35 | kShaderGpuProgramConsoleGS = 30 36 | kShaderGpuProgramRayTracing = 31 37 | -------------------------------------------------------------------------------- /UnityPy/enums/Audio.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class AudioType(IntEnum): 5 | UNKNOWN = 0 6 | ACC = 1 7 | AIFF = 2 8 | IT = 10 9 | MOD = 12 10 | MPEG = 13 11 | OGGVORBIS = 14 12 | S3M = 17 13 | WAV = 20 14 | XM = 21 15 | XMA = 22 16 | VAG = 23 17 | AUDIOQUEUE = 24 18 | 19 | 20 | class AudioCompressionFormat(IntEnum): 21 | PCM = 0 22 | Vorbis = 1 23 | ADPCM = 2 24 | MP3 = 3 25 | VAG = 4 26 | HEVAG = 5 27 | XMA = 6 28 | AAC = 7 29 | GCADPCM = 8 30 | ATRAC9 = 9 31 | 32 | 33 | AUDIO_TYPE_EXTEMSION = { 34 | AudioType.ACC: ".m4a", 35 | AudioType.AIFF: ".aif", 36 | AudioType.IT: ".it", 37 | AudioType.MOD: ".mod", 38 | AudioType.MPEG: ".mp3", 39 | AudioType.OGGVORBIS: ".ogg", 40 | AudioType.S3M: ".s3m", 41 | AudioType.WAV: ".wav", 42 | AudioType.XM: ".xm", 43 | AudioType.XMA: ".wav", 44 | AudioType.VAG: ".vag", 45 | AudioType.AUDIOQUEUE: ".fsb", 46 | AudioCompressionFormat.PCM: ".fsb", 47 | AudioCompressionFormat.Vorbis: ".fsb", 48 | AudioCompressionFormat.ADPCM: ".fsb", 49 | AudioCompressionFormat.MP3: ".fsb", 50 | AudioCompressionFormat.VAG: ".vag", 51 | AudioCompressionFormat.HEVAG: ".vag", 52 | AudioCompressionFormat.XMA: ".wav", 53 | AudioCompressionFormat.AAC: ".m4a", 54 | AudioCompressionFormat.GCADPCM: ".fsb", 55 | AudioCompressionFormat.ATRAC9: ".at9", 56 | } 57 | -------------------------------------------------------------------------------- /UnityPy/config.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from .exceptions import UnityVersionFallbackError, UnityVersionFallbackWarning 4 | 5 | 6 | FALLBACK_UNITY_VERSION = None 7 | """The Unity version to use when no version is defined 8 | by the SerializedFile or its BundleFile. 9 | 10 | You may manually configure this value to a version string, e.g. `2.5.0f5`. 11 | """ 12 | 13 | SERIALIZED_FILE_PARSE_TYPETREE = True 14 | """Determines if the typetree structures for the Object types will be parsed. 15 | 16 | Disabling this will reduce the load time by a lot (half of the time is spend on parsing the typetrees), 17 | but it will also prevent saving an edited file. 18 | """ 19 | 20 | 21 | # WARNINGS CONTROL 22 | warnings.simplefilter("once", UnityVersionFallbackWarning) 23 | 24 | 25 | # GET FUNCTIONS 26 | def get_fallback_version(): 27 | global FALLBACK_UNITY_VERSION 28 | 29 | if not isinstance(FALLBACK_UNITY_VERSION, str): 30 | raise UnityVersionFallbackError( 31 | "No valid Unity version found, and the fallback version is not correctly configured. " 32 | + "Please explicitly set the value of UnityPy.config.FALLBACK_UNITY_VERSION." 33 | ) 34 | 35 | warnings.warn( 36 | f"No valid Unity version found, defaulting to UnityPy.config.FALLBACK_UNITY_VERSION ({FALLBACK_UNITY_VERSION})", # noqa: E501 37 | category=UnityVersionFallbackWarning, 38 | stacklevel=2 39 | ) 40 | 41 | return FALLBACK_UNITY_VERSION 42 | -------------------------------------------------------------------------------- /UnityPyBoost/UnityPyBoost.cpp: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | #include "Mesh.hpp" 4 | #include "TypeTreeHelper.hpp" 5 | #include "ArchiveStorageDecryptor.hpp" 6 | 7 | /* Mesh.py */ 8 | 9 | static struct PyMethodDef method_table[] = { 10 | {"unpack_vertexdata", 11 | (PyCFunction)unpack_vertexdata, 12 | METH_VARARGS, 13 | "replacement for VertexData to ComponentData in Mesh.ReadVertexData"}, 14 | {"read_typetree", 15 | (PyCFunction)read_typetree, 16 | METH_VARARGS | METH_KEYWORDS, 17 | "replacement for TypeTreeHelper.read_typetree"}, 18 | {"decrypt_block", 19 | (PyCFunction)decrypt_block, 20 | METH_VARARGS, 21 | "replacement for ArchiveStorageDecryptor.decrypt_block"}, 22 | {NULL, 23 | NULL, 24 | 0, 25 | NULL} // Sentinel value ending the table 26 | }; 27 | 28 | // A struct contains the definition of a module 29 | static PyModuleDef UnityPyBoost_module = { 30 | PyModuleDef_HEAD_INIT, 31 | "UnityPyBoost", // Module name 32 | "TODO", 33 | -1, // Optional size of the module state memory 34 | method_table, 35 | NULL, // Optional slot definitions 36 | NULL, // Optional traversal function 37 | NULL, // Optional clear function 38 | NULL // Optional module deallocation function 39 | }; 40 | 41 | // The module init function 42 | PyMODINIT_FUNC PyInit_UnityPyBoost(void) 43 | { 44 | PyObject *module = PyModule_Create(&UnityPyBoost_module); 45 | add_typetreenode_to_module(module); 46 | return module; 47 | } 48 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/GameObject.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List, Union 4 | 5 | from ...enums import ClassIDType 6 | from ..generated import Component, GameObject, PPtr 7 | 8 | 9 | def _GameObject_Components(self) -> List[PPtr[Component]]: 10 | if self.m_Component is None: 11 | return [] 12 | if isinstance(self.m_Component[0], tuple): 13 | return [c.m_GameObject for i, c in self.m_Component] 14 | else: 15 | return [c.component for c in self.m_Component] 16 | 17 | 18 | def _GameObject_GetComponent(self, type: ClassIDType) -> Union[PPtr[Component], None]: 19 | for component in self.m_Components: 20 | if component.type == type: 21 | return component 22 | return None 23 | 24 | 25 | GameObject.m_Components = property(_GameObject_Components) 26 | GameObject.m_Animator = property( 27 | lambda self: _GameObject_GetComponent(self, ClassIDType.Animator) 28 | ) 29 | GameObject.m_Animation = property( 30 | lambda self: _GameObject_GetComponent(self, ClassIDType.Animation) 31 | ) 32 | GameObject.m_Transform = property( 33 | lambda self: _GameObject_GetComponent(self, ClassIDType.Transform) 34 | ) 35 | GameObject.m_MeshRenderer = property( 36 | lambda self: _GameObject_GetComponent(self, ClassIDType.MeshRenderer) 37 | ) 38 | GameObject.m_SkinnedMeshRenderer = property( 39 | lambda self: _GameObject_GetComponent(self, ClassIDType.SkinnedMeshRenderer) 40 | ) 41 | GameObject.m_MeshFilter = property( 42 | lambda self: _GameObject_GetComponent(self, ClassIDType.MeshFilter) 43 | ) 44 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Texture2D.pyi: -------------------------------------------------------------------------------- 1 | from typing import BinaryIO, List, Optional, Union 2 | 3 | from PIL.Image import Image 4 | 5 | from UnityPy.classes.generated import GLTextureSettings, StreamingInfo, Texture 6 | 7 | class Texture2D(Texture): 8 | image_data: bytes 9 | m_CompleteImageSize: int 10 | m_Height: int 11 | m_ImageCount: int 12 | m_IsReadable: bool 13 | m_LightmapFormat: int 14 | m_Name: str 15 | m_TextureDimension: int 16 | m_TextureFormat: int 17 | m_TextureSettings: GLTextureSettings 18 | m_Width: int 19 | m_ColorSpace: Optional[int] = None 20 | m_DownscaleFallback: Optional[bool] = None 21 | m_ForcedFallbackFormat: Optional[int] = None 22 | m_IgnoreMasterTextureLimit: Optional[bool] = None 23 | m_IgnoreMipmapLimit: Optional[bool] = None 24 | m_IsAlphaChannelOptional: Optional[bool] = None 25 | m_IsPreProcessed: Optional[bool] = None 26 | m_MipCount: Optional[int] = None 27 | m_MipMap: Optional[bool] = None 28 | m_MipmapLimitGroupName: Optional[str] = None 29 | m_MipsStripped: Optional[int] = None 30 | m_PlatformBlob: Optional[List[int]] = None 31 | m_ReadAllowed: Optional[bool] = None 32 | m_StreamData: Optional[StreamingInfo] = None 33 | m_StreamingMipmaps: Optional[bool] = None 34 | m_StreamingMipmapsPriority: Optional[int] = None 35 | 36 | @property 37 | def image(self) -> Image: ... 38 | def set_image( 39 | self, 40 | img: Union[Image, str, BinaryIO], 41 | target_format: Optional[int] = None, 42 | mipmap_count: int = 1, 43 | ) -> None: ... 44 | def get_image_data(self) -> bytes: ... 45 | 46 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Texture2DArray.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import TYPE_CHECKING, List 3 | 4 | from ...enums.GraphicsFormat import GRAPHICS_TO_TEXTURE_MAP, GraphicsFormat 5 | from ..generated import Texture2DArray 6 | 7 | if TYPE_CHECKING: 8 | from PIL import Image 9 | 10 | 11 | def _Texture2DArray_get_images(self: Texture2DArray) -> List[Image.Image]: 12 | from ...export import Texture2DConverter 13 | from ...helpers.ResourceReader import get_resource_data 14 | 15 | texture_format = GRAPHICS_TO_TEXTURE_MAP.get(GraphicsFormat(self.m_Format)) 16 | if not texture_format: 17 | raise NotImplementedError(f"GraphicsFormat {self.m_Format} not supported yet") 18 | 19 | image_data = self.image_data 20 | if image_data is None: 21 | image_data = get_resource_data( 22 | self.m_StreamData.path, 23 | self.object_reader.assets_file, 24 | self.m_StreamData.offset, 25 | self.m_StreamData.size, 26 | ) 27 | 28 | # calculate the number of textures in the array 29 | texture_size = self.m_DataSize // self.m_Depth 30 | 31 | return [ 32 | Texture2DConverter.parse_image_data( 33 | image_data[offset : offset + texture_size], 34 | self.m_Width, 35 | self.m_Height, 36 | texture_format, 37 | self.object_reader.version, 38 | 0, 39 | None, 40 | ) 41 | for offset in range(0, self.m_DataSize, texture_size) 42 | ] 43 | 44 | 45 | Texture2DArray.images = property(_Texture2DArray_get_images) 46 | 47 | __all__ = ("Texture2DArray",) 48 | -------------------------------------------------------------------------------- /UnityPyBoost/TypeTreeHelper.hpp: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #pragma once 3 | #include 4 | #include "structmember.h" 5 | #include 6 | 7 | enum NodeDataType 8 | { 9 | u8 = 0, 10 | u16 = 1, 11 | u32 = 2, 12 | u64 = 3, 13 | s8 = 4, 14 | s16 = 5, 15 | s32 = 6, 16 | s64 = 7, 17 | f32 = 8, 18 | f64 = 9, 19 | boolean = 10, 20 | str = 11, 21 | bytes = 12, 22 | pair = 13, 23 | Array = 14, 24 | PPtr = 15, 25 | ReferencedObject = 16, 26 | ReferencedObjectData = 17, 27 | ManagedReferencesRegistry = 18, 28 | unk = 255 29 | }; 30 | 31 | typedef struct TypeTreeNodeObject 32 | { 33 | PyObject_HEAD 34 | // helper field - simple hash of type for faster comparison 35 | NodeDataType _data_type; 36 | bool _align; 37 | PyObject *_clean_name; // str 38 | // used filds for fast access 39 | PyObject *m_Children; // list of TypeTreeNodes 40 | PyObject *m_Name; // str 41 | PyObject *m_Type; // str 42 | // fields not used in C 43 | PyObject *m_Level; // legacy: /, blob: u8 44 | PyObject *m_ByteSize; // legacy: i32, blob: i32 45 | PyObject *m_Version; // legacy: i32, blob: i16 46 | PyObject *m_TypeFlags; // legacy: i32, blob: u8 47 | PyObject *m_VariableCount; // legacy: i32, blob: / 48 | PyObject *m_Index; // legacy: i32, blob: i32 49 | PyObject *m_MetaFlag; // legacy: i32, blob: i32 50 | PyObject *m_RefTypeHash; // legacy: /, blob: u64 51 | } TypeTreeNodeObject; 52 | 53 | int add_typetreenode_to_module(PyObject *m); 54 | 55 | PyObject *read_typetree(PyObject *self, PyObject *args, PyObject *kwargs); 56 | -------------------------------------------------------------------------------- /UnityPy/math/Color.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from .Vector4 import Vector4 3 | 4 | 5 | @dataclass 6 | class Color: 7 | R: float 8 | G: float 9 | B: float 10 | A: float 11 | 12 | def __init__(self, r: float = 0.0, g: float = 0.0, b: float = 0.0, a: float = 0.0): 13 | if not all(isinstance(v, (int, float)) for v in (r, g, b, a)): 14 | raise TypeError("All components must be numeric.") 15 | self.R = r 16 | self.G = g 17 | self.B = b 18 | self.A = a 19 | 20 | def __add__(self, other): 21 | return Color( 22 | self.R + other.R, self.G + other.G, self.B + other.B, self.A + other.A 23 | ) 24 | 25 | def __sub__(self, other): 26 | return Color( 27 | self.R - other.R, self.G - other.G, self.B - other.B, self.A - other.A 28 | ) 29 | 30 | def __mul__(self, other): 31 | if isinstance(other, Color): 32 | return Color( 33 | self.R * other.R, self.G * other.G, self.B * other.B, self.A * other.A 34 | ) 35 | else: 36 | return Color(self.R * other, self.G * other, self.B * other, self.A * other) 37 | 38 | def __truediv__(self, other): 39 | if isinstance(other, Color): 40 | return Color( 41 | self.R / other.R, self.G / other.G, self.B / other.B, self.A / other.A 42 | ) 43 | else: 44 | return Color(self.R / other, self.G / other, self.B / other, self.A / other) 45 | 46 | def __eq__(self, other): 47 | if isinstance(other, Color): 48 | return self.__dict__ == other.__dict__ 49 | else: 50 | return False 51 | 52 | def __ne__(self, other): 53 | return not (self == other) 54 | 55 | def Vector4(self): 56 | return Vector4(self.R, self.G, self.B, self.A) 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # test files and others 2 | test.py 3 | AssetStudio/ 4 | .vscode/ 5 | 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | .dump 10 | .idea/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # precompiled C extensions 15 | UnityPy/*.so 16 | UnityPy/*.pyd 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | -------------------------------------------------------------------------------- /UnityPy/enums/TextureFormat.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class TextureFormat(IntEnum): 5 | Alpha8 = 1 6 | ARGB4444 = 2 7 | RGB24 = 3 8 | RGBA32 = 4 9 | ARGB32 = 5 10 | ARGBFloat = 6 11 | RGB565 = 7 12 | BGR24 = 8 13 | R16 = 9 14 | DXT1 = 10 15 | DXT3 = 11 16 | DXT5 = 12 17 | RGBA4444 = 13 18 | BGRA32 = 14 19 | RHalf = 15 20 | RGHalf = 16 21 | RGBAHalf = 17 22 | RFloat = 18 23 | RGFloat = 19 24 | RGBAFloat = 20 25 | YUY2 = 21 26 | RGB9e5Float = 22 27 | RGBFloat = 23 28 | BC6H = 24 29 | BC7 = 25 30 | BC4 = 26 31 | BC5 = 27 32 | DXT1Crunched = 28 33 | DXT5Crunched = 29 34 | PVRTC_RGB2 = 30 35 | PVRTC_RGBA2 = 31 36 | PVRTC_RGB4 = 32 37 | PVRTC_RGBA4 = 33 38 | ETC_RGB4 = 34 39 | ATC_RGB4 = 35 40 | ATC_RGBA8 = 36 41 | EAC_R = 41 42 | EAC_R_SIGNED = 42 43 | EAC_RG = 43 44 | EAC_RG_SIGNED = 44 45 | ETC2_RGB = 45 46 | ETC2_RGBA1 = 46 47 | ETC2_RGBA8 = 47 48 | ASTC_RGB_4x4 = 48 49 | ASTC_RGB_5x5 = 49 50 | ASTC_RGB_6x6 = 50 51 | ASTC_RGB_8x8 = 51 52 | ASTC_RGB_10x10 = 52 53 | ASTC_RGB_12x12 = 53 54 | ASTC_RGBA_4x4 = 54 55 | ASTC_RGBA_5x5 = 55 56 | ASTC_RGBA_6x6 = 56 57 | ASTC_RGBA_8x8 = 57 58 | ASTC_RGBA_10x10 = 58 59 | ASTC_RGBA_12x12 = 59 60 | ETC_RGB4_3DS = 60 61 | ETC_RGBA8_3DS = 61 62 | RG16 = 62 63 | R8 = 63 64 | ETC_RGB4Crunched = 64 65 | ETC2_RGBA8Crunched = 65 66 | ASTC_HDR_4x4 = 66 67 | ASTC_HDR_5x5 = 67 68 | ASTC_HDR_6x6 = 68 69 | ASTC_HDR_8x8 = 69 70 | ASTC_HDR_10x10 = 70 71 | ASTC_HDR_12x12 = 71 72 | RG32 = 72 73 | RGB48 = 73 74 | RGBA64 = 74 75 | R8_SIGNED = 75 76 | RG16_SIGNED = 76 77 | RGB24_SIGNED = 77 78 | RGBA32_SIGNED = 78 79 | R16_SIGNED = 79 80 | RG32_SIGNED = 80 81 | RGB48_SIGNED = 81 82 | RGBA64_SIGNED = 82 83 | -------------------------------------------------------------------------------- /UnityPy/export/MeshExporter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, List, Optional 4 | 5 | from ..helpers.MeshHelper import MeshHandler 6 | 7 | if TYPE_CHECKING: 8 | from ..classes.generated import Mesh 9 | 10 | 11 | def export_mesh(m_Mesh: Mesh, format: str = "obj") -> str: 12 | if format == "obj": 13 | return export_mesh_obj(m_Mesh) 14 | raise NotImplementedError(f"Export format {format} not implemented") 15 | 16 | 17 | def export_mesh_obj(mesh: Mesh, material_names: Optional[List[str]] = None) -> str: 18 | handler = MeshHandler(mesh) 19 | handler.process() 20 | 21 | m_Mesh = handler 22 | if m_Mesh.m_VertexCount <= 0: 23 | return False 24 | 25 | sb = [f"g {mesh.m_Name}\n"] 26 | if material_names: 27 | sb.append(f"mtllib {mesh.m_Name}.mtl\n") 28 | # region Vertices 29 | if not m_Mesh.m_Vertices: 30 | return False 31 | 32 | sb.extend( 33 | "v {0:.9G} {1:.9G} {2:.9G}\n".format(-pos[0], pos[1], pos[2]).replace( 34 | "nan", "0" 35 | ) 36 | for pos in m_Mesh.m_Vertices 37 | ) 38 | # endregion 39 | 40 | # region UV 41 | if m_Mesh.m_UV0: 42 | sb.extend( 43 | "vt {0:.9G} {1:.9G}\n".format(uv[0], uv[1]).replace("nan", "0") 44 | for uv in m_Mesh.m_UV0 45 | ) 46 | # endregion 47 | 48 | # region Normals 49 | if m_Mesh.m_Normals: 50 | sb.extend( 51 | "vn {0:.9G} {1:.9G} {2:.9G}\n".format(-n[0], n[1], n[2]).replace("nan", "0") 52 | for n in m_Mesh.m_Normals 53 | ) 54 | # endregion 55 | 56 | # region Face 57 | for i, triangles in enumerate(m_Mesh.get_triangles()): 58 | sb.append(f"g {mesh.m_Name}_{i}\n") 59 | if material_names and i < len(material_names) and material_names[i]: 60 | sb.append(f"usemtl {material_names[i]}\n") 61 | sb.extend( 62 | "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n".format(c + 1, b + 1, a + 1) 63 | for a, b, c in triangles 64 | ) 65 | # endregion 66 | return "".join(sb) 67 | -------------------------------------------------------------------------------- /UnityPy/math/Vector2.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from math import sqrt 3 | 4 | 5 | kEpsilon = 0.00001 6 | 7 | 8 | @dataclass 9 | class Vector2: 10 | '''https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Vector2.cs''' 11 | 12 | X: float = 0.0 13 | Y: float = 0.0 14 | 15 | def __init__(self, x: float = 0.0, y: float = 0.0): 16 | if not all(isinstance(v, (int, float)) for v in (x, y)): 17 | raise TypeError("All components must be numeric.") 18 | self.X = float(x) 19 | self.Y = float(y) 20 | 21 | def __getitem__(self, index): 22 | return (self.X, self.Y)[index] 23 | 24 | def __setitem__(self, index, value): 25 | if index == 0: 26 | self.X = value 27 | elif index == 1: 28 | self.Y = value 29 | else: 30 | raise IndexError("Index out of range") 31 | 32 | def __hash__(self): 33 | return self.X.__hash__() ^ (self.Y.__hash__() << 2) 34 | 35 | def normalize(self): 36 | length = self.length() 37 | if length > kEpsilon: 38 | invNorm = 1.0 / length 39 | self.X *= invNorm 40 | self.Y *= invNorm 41 | else: 42 | self.X = self.Y = 0.0 43 | 44 | Normalize = normalize 45 | 46 | def length(self): 47 | return sqrt(self.lengthSquared()) 48 | 49 | Length = length 50 | 51 | def lengthSquared(self): 52 | return self.X ** 2 + self.Y ** 2 53 | 54 | LengthSquared = lengthSquared 55 | 56 | @staticmethod 57 | def Zero(): 58 | return Vector2(0, 0) 59 | 60 | @staticmethod 61 | def One(): 62 | return Vector2(1, 1) 63 | 64 | def __add__(a, b): 65 | return Vector2(a.X + b.X, a.Y + b.Y) 66 | 67 | def __sub__(a, b): 68 | return Vector2(a.X - b.X, a.Y - b.Y) 69 | 70 | def __mul__(a, d): 71 | return Vector2(a.X * d, a.Y * d) 72 | 73 | def __truediv__(a, d): 74 | return Vector2(a.X / d, a.Y / d) 75 | 76 | def __eq__(lhs, rhs): 77 | if isinstance(rhs, Vector2): 78 | diff = lhs - rhs 79 | return diff.lengthSquared() < kEpsilon * kEpsilon 80 | return False 81 | 82 | def __ne__(lhs, rhs): 83 | return not (lhs == rhs) 84 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Mesh.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | 3 | from UnityPy.classes.generated import (AABB, BlendShapeData, BoneInfluence, 4 | BoneWeights4, CompressedMesh, 5 | MeshBlendShape, MeshBlendShapeVertex, 6 | MinMaxAABB, NamedObject, StreamingInfo, 7 | SubMesh, VariableBoneCountWeights, 8 | VertexData) 9 | from UnityPy.classes.math import (ColorRGBA, Matrix4x4f, Vector2f, Vector3f, 10 | Vector4f) 11 | 12 | class Mesh(NamedObject): 13 | m_BindPose: List[Matrix4x4f] 14 | m_CompressedMesh: CompressedMesh 15 | m_IndexBuffer: List[int] 16 | m_LocalAABB: AABB 17 | m_MeshCompression: int 18 | m_MeshUsageFlags: int 19 | m_Name: str 20 | m_SubMeshes: List[SubMesh] 21 | m_BakedConvexCollisionMesh: Optional[List[int]] = None 22 | m_BakedTriangleCollisionMesh: Optional[List[int]] = None 23 | m_BoneNameHashes: Optional[List[int]] = None 24 | m_BonesAABB: Optional[List[MinMaxAABB]] = None 25 | m_CollisionTriangles: Optional[List[int]] = None 26 | m_CollisionVertexCount: Optional[int] = None 27 | m_Colors: Optional[List[ColorRGBA]] = None 28 | m_CookingOptions: Optional[int] = None 29 | m_IndexFormat: Optional[int] = None 30 | m_IsReadable: Optional[bool] = None 31 | m_KeepIndices: Optional[bool] = None 32 | m_KeepVertices: Optional[bool] = None 33 | m_MeshMetrics_0_: Optional[float] = None 34 | m_MeshMetrics_1_: Optional[float] = None 35 | m_Normals: Optional[List[Vector3f]] = None 36 | m_RootBoneNameHash: Optional[int] = None 37 | m_ShapeVertices: Optional[List[MeshBlendShapeVertex]] = None 38 | m_Shapes: Optional[Union[BlendShapeData, List[MeshBlendShape]]] = None 39 | m_Skin: Optional[Union[List[BoneInfluence], List[BoneWeights4]]] = None 40 | m_StreamCompression: Optional[int] = None 41 | m_StreamData: Optional[StreamingInfo] = None 42 | m_Tangents: Optional[List[Vector4f]] = None 43 | m_UV: Optional[List[Vector2f]] = None 44 | m_UV1: Optional[List[Vector2f]] = None 45 | m_Use16BitIndices: Optional[int] = None 46 | m_VariableBoneCountWeights: Optional[VariableBoneCountWeights] = None 47 | m_VertexData: Optional[VertexData] = None 48 | m_Vertices: Optional[List[Vector3f]] = None 49 | 50 | def export(self, format: str = "obj") -> str: ... 51 | -------------------------------------------------------------------------------- /UnityPyBoost/swap.hpp: -------------------------------------------------------------------------------- 1 | #if __cplusplus >= 202101L // "C++23"; 2 | #include 3 | #define bswap16(x) std::byteswap(x) 4 | #define bswap32(x) std::byteswap(x) 5 | #define bswap64(x) std::byteswap(x) 6 | #else 7 | // set swap funcions (source: old version of nodejs/src/node_buffer.cc) 8 | #if defined(__GNUC__) || defined(__clang__) 9 | #define bswap16(x) __builtin_bswap16(x) 10 | #define bswap32(x) __builtin_bswap32(x) 11 | #define bswap64(x) __builtin_bswap64(x) 12 | #elif defined(__linux__) 13 | #include 14 | #define bswap16(x) bswap_16(x) 15 | #define bswap32(x) bswap_32(x) 16 | #define bswap64(x) bswap_64(x) 17 | #elif defined(_MSC_VER) 18 | #include 19 | #define bswap16(x) _byteswap_ushort(x) 20 | #define bswap32(x) _byteswap_ulong(x) 21 | #define bswap64(x) _byteswap_uint64(x) 22 | #else 23 | #ifdef __builtin_bswap16 24 | #define bswap16(x) __builtin_bswap16(x) 25 | #else 26 | #define bswap16 ((x) << 8) | ((x) >> 8) 27 | #endif 28 | #ifdef __builtin_bswap32 29 | #define bswap32(x) __builtin_bswap32(x) 30 | #else 31 | #define bswap32 \ 32 | (((x) & 0xFF) << 24) | \ 33 | (((x) & 0xFF00) << 8) | \ 34 | (((x) >> 8) & 0xFF00) | \ 35 | (((x) >> 24) & 0xFF) 36 | #endif 37 | #ifdef __builtin_bswap64 38 | #define bswap64 __builtin_bswap64(x) 39 | #else 40 | #define bswap64 \ 41 | (((x) & 0xFF00000000000000ull) >> 56) | \ 42 | (((x) & 0x00FF000000000000ull) >> 40) | \ 43 | (((x) & 0x0000FF0000000000ull) >> 24) | \ 44 | (((x) & 0x000000FF00000000ull) >> 8) | \ 45 | (((x) & 0x00000000FF000000ull) << 8) | \ 46 | (((x) & 0x0000000000FF0000ull) << 24) | \ 47 | (((x) & 0x000000000000FF00ull) << 40) | \ 48 | (((x) & 0x00000000000000FFull) << 56) 49 | #endif 50 | #endif 51 | #endif 52 | 53 | template 54 | inline void swap_any_inplace(T *x) 55 | { 56 | if constexpr (sizeof(T) == 1) 57 | { 58 | // do nothing 59 | } 60 | else if constexpr (sizeof(T) == 2) 61 | { 62 | 63 | *(uint16_t *)x = bswap16(*(uint16_t *)x); 64 | } 65 | else if constexpr (sizeof(T) == 4) 66 | { 67 | *(uint32_t *)x = bswap32(*(uint32_t *)x); 68 | } 69 | else if constexpr (sizeof(T) == 8) 70 | { 71 | *(uint64_t *)x = bswap64(*(uint64_t *)x); 72 | } 73 | else 74 | { 75 | // gcc is tripping and somehow reaching this at compile time 76 | // static_assert(false, "Swap not implemented for this size"); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "UnityPy" 7 | authors = [{ name = "Rudolf Kolbe", email = "rkolbe96@gmail.com" }] 8 | description = "A Unity extraction and patching package" 9 | readme = "README.md" 10 | license = { file = "LICENSE" } 11 | requires-python = ">=3.7" 12 | keywords = [ 13 | "python", 14 | "unity", 15 | "unity-asset", 16 | "python3", 17 | "data-minig", 18 | "unitypack", 19 | "assetstudio", 20 | "unity-asset-extractor", 21 | ] 22 | classifiers = [ 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | "Intended Audience :: Developers", 26 | "Development Status :: 5 - Production/Stable", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3.12", 35 | "Topic :: Software Development :: Libraries :: Python Modules", 36 | "Topic :: Games/Entertainment", 37 | "Topic :: Multimedia :: Graphics", 38 | ] 39 | dependencies = [ 40 | # block compression/decompression 41 | "lz4", # BundleFile block compression 42 | "brotli", # WebFile compression 43 | # Texture & Sprite handling 44 | "Pillow", 45 | "texture2ddecoder >= 1.0.5", # texture decompression 46 | "etcpak", # ETC & DXT compression 47 | "astc-encoder-py >= 0.1.8", # ASTC compression 48 | # audio extraction 49 | "pyfmodex >= 0.7.1", 50 | # filesystem handling 51 | "fsspec", 52 | # better classes 53 | "attrs", 54 | ] 55 | dynamic = ["version"] 56 | 57 | [project.optional-dependencies] 58 | # optional dependencies must be lowercase/normalized 59 | ttgen = ["typetreegeneratorapi>=0.0.5"] 60 | full = ["unitypy[ttgen]"] 61 | tests = ["pytest", "pillow", "psutil", "unitypy[full]"] 62 | 63 | [project.urls] 64 | "Homepage" = "https://github.com/K0lb3/UnityPy" 65 | "Bug Tracker" = "https://github.com/K0lb3/UnityPy/issues" 66 | 67 | [tool.setuptools.dynamic] 68 | version = { attr = "UnityPy.__version__" } 69 | 70 | [tool.pytest.ini_options] 71 | testpaths = ["tests"] 72 | 73 | [tool.cibuildwheel.linux] 74 | archs = ["x86_64", "i686"] 75 | # auditwheel issues with fmod on: "aarch64", "armv7l" 76 | 77 | [tool.cibuildwheel.macos] 78 | archs = ["x86_64", "arm64"] 79 | 80 | [tool.cibuildwheel.windows] 81 | archs = ["AMD64", "x86", "ARM64"] 82 | -------------------------------------------------------------------------------- /UnityPy/enums/VertexFormat.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class VertexChannelFormat(IntEnum): 5 | kChannelFormatFloat = 0 6 | kChannelFormatFloat16 = 1 7 | kChannelFormatColor = 2 8 | kChannelFormatByte = 3 9 | kChannelFormatUInt32 = 4 10 | 11 | 12 | class VertexFormat2017(IntEnum): 13 | kVertexFormatFloat = 0 14 | kVertexFormatFloat16 = 1 15 | kVertexFormatColor = 2 16 | kVertexFormatUNorm8 = 3 17 | kVertexFormatSNorm8 = 4 18 | kVertexFormatUNorm16 = 5 19 | kVertexFormatSNorm16 = 6 20 | kVertexFormatUInt8 = 7 21 | kVertexFormatSInt8 = 8 22 | kVertexFormatUInt16 = 9 23 | kVertexFormatSInt16 = 10 24 | kVertexFormatUInt32 = 11 25 | kVertexFormatSInt32 = 12 26 | 27 | 28 | class VertexFormat(IntEnum): 29 | kVertexFormatFloat = 0 30 | kVertexFormatFloat16 = 1 31 | kVertexFormatUNorm8 = 2 32 | kVertexFormatSNorm8 = 3 33 | kVertexFormatUNorm16 = 4 34 | kVertexFormatSNorm16 = 5 35 | kVertexFormatUInt8 = 6 36 | kVertexFormatSInt8 = 7 37 | kVertexFormatUInt16 = 8 38 | kVertexFormatSInt16 = 9 39 | kVertexFormatUInt32 = 10 40 | kVertexFormatSInt32 = 11 41 | 42 | 43 | VERTEX_CHANNEL_FORMAT_STRUCT_TYPE_MAP = { 44 | VertexChannelFormat.kChannelFormatFloat: "f", 45 | VertexChannelFormat.kChannelFormatFloat16: "e", 46 | VertexChannelFormat.kChannelFormatColor: "B", 47 | VertexChannelFormat.kChannelFormatByte: "B", 48 | VertexChannelFormat.kChannelFormatUInt32: "I", 49 | } 50 | 51 | VERTEX_FORMAT_2017_STRUCT_TYPE_MAP = { 52 | VertexFormat2017.kVertexFormatFloat: "f", 53 | VertexFormat2017.kVertexFormatFloat16: "e", 54 | VertexFormat2017.kVertexFormatColor: "B", 55 | VertexFormat2017.kVertexFormatUNorm8: "B", 56 | VertexFormat2017.kVertexFormatSNorm8: "b", 57 | VertexFormat2017.kVertexFormatUNorm16: "H", 58 | VertexFormat2017.kVertexFormatSNorm16: "h", 59 | VertexFormat2017.kVertexFormatUInt8: "B", 60 | VertexFormat2017.kVertexFormatSInt8: "b", 61 | VertexFormat2017.kVertexFormatUInt16: "H", 62 | VertexFormat2017.kVertexFormatSInt16: "h", 63 | VertexFormat2017.kVertexFormatUInt32: "I", 64 | VertexFormat2017.kVertexFormatSInt32: "i", 65 | } 66 | 67 | VERTEX_FORMAT_STRUCT_TYPE_MAP = { 68 | VertexFormat.kVertexFormatFloat: "f", 69 | VertexFormat.kVertexFormatFloat16: "e", 70 | VertexFormat.kVertexFormatUNorm8: "B", 71 | VertexFormat.kVertexFormatSNorm8: "b", 72 | VertexFormat.kVertexFormatUNorm16: "H", 73 | VertexFormat.kVertexFormatSNorm16: "h", 74 | VertexFormat.kVertexFormatUInt8: "B", 75 | VertexFormat.kVertexFormatSInt8: "b", 76 | VertexFormat.kVertexFormatUInt16: "H", 77 | VertexFormat.kVertexFormatSInt16: "h", 78 | VertexFormat.kVertexFormatUInt32: "I", 79 | VertexFormat.kVertexFormatSInt32: "i", 80 | } 81 | -------------------------------------------------------------------------------- /UnityPy/classes/legacy_patch/Texture2D.py: -------------------------------------------------------------------------------- 1 | from typing import Union, BinaryIO, Optional 2 | 3 | from PIL import Image 4 | 5 | from ..generated import Texture2D 6 | 7 | 8 | def _Texture2d_get_image(self: Texture2D): 9 | from ...export import Texture2DConverter 10 | 11 | return Texture2DConverter.get_image_from_texture2d(self) 12 | 13 | 14 | def _Texture2d_set_image( 15 | self: Texture2D, 16 | img: Union["Image.Image", str, BinaryIO], 17 | target_format: Optional[int] = None, 18 | mipmap_count: int = 1, 19 | ): 20 | from ...export import Texture2DConverter 21 | 22 | if not target_format: 23 | target_format = self.m_TextureFormat 24 | 25 | if not isinstance(img, Image.Image): 26 | img = Image.open(img) 27 | 28 | platform = self.object_reader.platform if self.object_reader is not None else 0 29 | img_data, tex_format = Texture2DConverter.image_to_texture2d( 30 | img, target_format, platform, self.m_PlatformBlob 31 | ) 32 | self.m_Width = img.width 33 | self.m_Height = img.height 34 | 35 | if mipmap_count > 1: 36 | width = self.m_Width 37 | height = self.m_Height 38 | re_img = img 39 | for i in range(mipmap_count - 1): 40 | width //= 2 41 | height //= 2 42 | if width < 4 or height < 4: 43 | mipmap_count = i + 1 44 | break 45 | re_img = re_img.resize((width, height), Image.BICUBIC) 46 | img_data += Texture2DConverter.image_to_texture2d(re_img, target_format)[0] 47 | 48 | # disable mipmaps as we don't store them ourselves by default 49 | if self.m_MipMap is not None: 50 | self.m_MipMap = mipmap_count > 1 51 | if self.m_MipCount is not None: 52 | self.m_MipCount = mipmap_count 53 | 54 | self.image_data = img_data 55 | # width * height * channel count 56 | self.m_CompleteImageSize = len( 57 | img_data 58 | ) # img.width * img.height * len(img.getbands()) 59 | self.m_TextureFormat = tex_format 60 | 61 | if self.m_StreamData is not None: 62 | self.m_StreamData.path = "" 63 | self.m_StreamData.offset = 0 64 | self.m_StreamData.size = 0 65 | 66 | 67 | def _Texture2D_get_image_data(self: Texture2D): 68 | if self.image_data: 69 | return self.image_data 70 | if self.m_StreamData: 71 | from ...helpers.ResourceReader import get_resource_data 72 | 73 | return get_resource_data( 74 | self.m_StreamData.path, 75 | self.object_reader.assets_file, 76 | self.m_StreamData.offset, 77 | self.m_StreamData.size, 78 | ) 79 | raise ValueError("No image data found") 80 | 81 | 82 | Texture2D.image = property(_Texture2d_get_image, _Texture2d_set_image) 83 | Texture2D.set_image = _Texture2d_set_image 84 | Texture2D.get_image_data = _Texture2D_get_image_data 85 | 86 | 87 | __all__ = ("Texture2D",) 88 | -------------------------------------------------------------------------------- /UnityPy/enums/CommonString.py: -------------------------------------------------------------------------------- 1 | CommonString = { 2 | 0: "AABB", 3 | 5: "AnimationClip", 4 | 19: "AnimationCurve", 5 | 34: "AnimationState", 6 | 49: "Array", 7 | 55: "Base", 8 | 60: "BitField", 9 | 69: "bitset", 10 | 76: "bool", 11 | 81: "char", 12 | 86: "ColorRGBA", 13 | 96: "Component", 14 | 106: "data", 15 | 111: "deque", 16 | 117: "double", 17 | 124: "dynamic_array", 18 | 138: "FastPropertyName", 19 | 155: "first", 20 | 161: "float", 21 | 167: "Font", 22 | 172: "GameObject", 23 | 183: "Generic Mono", 24 | 196: "GradientNEW", 25 | 208: "GUID", 26 | 213: "GUIStyle", 27 | 222: "int", 28 | 226: "list", 29 | 231: "long long", 30 | 241: "map", 31 | 245: "Matrix4x4f", 32 | 256: "MdFour", 33 | 263: "MonoBehaviour", 34 | 277: "MonoScript", 35 | 288: "m_ByteSize", 36 | 299: "m_Curve", 37 | 307: "m_EditorClassIdentifier", 38 | 331: "m_EditorHideFlags", 39 | 349: "m_Enabled", 40 | 359: "m_ExtensionPtr", 41 | 374: "m_GameObject", 42 | 387: "m_Index", 43 | 395: "m_IsArray", 44 | 405: "m_IsStatic", 45 | 416: "m_MetaFlag", 46 | 427: "m_Name", 47 | 434: "m_ObjectHideFlags", 48 | 452: "m_PrefabInternal", 49 | 469: "m_PrefabParentObject", 50 | 490: "m_Script", 51 | 499: "m_StaticEditorFlags", 52 | 519: "m_Type", 53 | 526: "m_Version", 54 | 536: "Object", 55 | 543: "pair", 56 | 548: "PPtr", 57 | 564: "PPtr", 58 | 581: "PPtr", 59 | 596: "PPtr", 60 | 616: "PPtr", 61 | 633: "PPtr", 62 | 646: "PPtr", 63 | 659: "PPtr", 64 | 672: "PPtr", 65 | 688: "PPtr", 66 | 702: "PPtr", 67 | 718: "PPtr", 68 | 734: "Prefab", 69 | 741: "Quaternionf", 70 | 753: "Rectf", 71 | 759: "RectInt", 72 | 767: "RectOffset", 73 | 778: "second", 74 | 785: "set", 75 | 789: "short", 76 | 795: "size", 77 | 800: "SInt16", 78 | 807: "SInt32", 79 | 814: "SInt64", 80 | 821: "SInt8", 81 | 827: "staticvector", 82 | 840: "string", 83 | 847: "TextAsset", 84 | 857: "TextMesh", 85 | 866: "Texture", 86 | 874: "Texture2D", 87 | 884: "Transform", 88 | 894: "TypelessData", 89 | 907: "UInt16", 90 | 914: "UInt32", 91 | 921: "UInt64", 92 | 928: "UInt8", 93 | 934: "unsigned int", 94 | 947: "unsigned long long", 95 | 966: "unsigned short", 96 | 981: "vector", 97 | 988: "Vector2f", 98 | 997: "Vector3f", 99 | 1006: "Vector4f", 100 | 1015: "m_ScriptingClassIdentifier", 101 | 1042: "Gradient", 102 | 1051: "Type*", 103 | 1057: "int2_storage", 104 | 1070: "int3_storage", 105 | 1083: "BoundsInt", 106 | 1093: "m_CorrespondingSourceObject", 107 | 1121: "m_PrefabInstance", 108 | 1138: "m_PrefabAsset", 109 | 1152: "FileSize" 110 | } 111 | -------------------------------------------------------------------------------- /UnityPy/classes/math.py: -------------------------------------------------------------------------------- 1 | """ 2 | Definitions for math related classes. 3 | As most calculations involving them are done in numpy, 4 | we define them here as subtypes of np.ndarray, so that casting won't be necessary. 5 | """ 6 | 7 | from attrs import define 8 | 9 | 10 | @define(slots=True) 11 | class Vector2f: 12 | x: float = 0 13 | y: float = 0 14 | 15 | def __repr__(self) -> str: 16 | return f"Vector2f({self.x}, {self.y})" 17 | 18 | 19 | @define(slots=True) 20 | class Vector3f: 21 | x: float = 0 22 | y: float = 0 23 | z: float = 0 24 | 25 | def __repr__(self) -> str: 26 | return f"Vector3f({self.x}, {self.y}, {self.z})" 27 | 28 | 29 | @define(slots=True) 30 | class Vector4f: 31 | x: float = 0 32 | y: float = 0 33 | z: float = 0 34 | w: float = 0 35 | 36 | def __repr__(self) -> str: 37 | return f"Vector4f({self.x}, {self.y}, {self.z}, {self.w})" 38 | 39 | 40 | float3 = Vector3f 41 | float4 = Vector4f 42 | 43 | 44 | class Quaternionf(Vector4f): 45 | # TODO: Implement quaternion operations 46 | def __repr__(self) -> str: 47 | return f"Quaternion({self.x}, {self.y}, {self.z}, {self.w})" 48 | 49 | 50 | @define(slots=True) 51 | class Matrix3x4f: 52 | e00: float 53 | e01: float 54 | e02: float 55 | e03: float 56 | e10: float 57 | e11: float 58 | e12: float 59 | e13: float 60 | e20: float 61 | e21: float 62 | e22: float 63 | e23: float 64 | 65 | 66 | @define(slots=True) 67 | class Matrix4x4f: 68 | e00: float 69 | e01: float 70 | e02: float 71 | e03: float 72 | e10: float 73 | e11: float 74 | e12: float 75 | e13: float 76 | e20: float 77 | e21: float 78 | e22: float 79 | e23: float 80 | e30: float 81 | e31: float 82 | e32: float 83 | e33: float 84 | 85 | 86 | @define(slots=True) 87 | class ColorRGBA: 88 | r: float = 0 89 | g: float = 0 90 | b: float = 0 91 | a: float = 1 92 | 93 | def __new__( 94 | cls, r: float = 0, g: float = 0, b: float = 0, a: float = 1, rgba: int = -1 95 | ) -> "ColorRGBA": 96 | obj = super().__new__(cls) 97 | if rgba != -1: 98 | r = ((rgba >> 24) & 0xFF) / 255 99 | g = ((rgba >> 16) & 0xFF) / 255 100 | b = ((rgba >> 8) & 0xFF) / 255 101 | a = (rgba & 0xFF) / 255 102 | obj.__init__(r, g, b, a) 103 | return obj 104 | 105 | @property 106 | def rgba(self) -> int: 107 | return ( 108 | int(self.r * 255) << 24 109 | | int(self.g * 255) << 16 110 | | int(self.b * 255) << 8 111 | | int(self.a * 255) 112 | ) 113 | 114 | @rgba.setter 115 | def rgba(self, value: int): 116 | self.r = ((value >> 24) & 0xFF) / 255 117 | self.g = ((value >> 16) & 0xFF) / 255 118 | self.b = ((value >> 8) & 0xFF) / 255 119 | self.a = (value & 0xFF) / 255 120 | 121 | 122 | __all__ = ( 123 | "Vector2f", 124 | "Vector3f", 125 | "Vector4f", 126 | "Quaternionf", 127 | "Matrix3x4f", 128 | "Matrix4x4f", 129 | "ColorRGBA", 130 | "float3", 131 | "float4", 132 | ) 133 | -------------------------------------------------------------------------------- /UnityPy/helpers/TypeTreeGenerator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List, Tuple 3 | from .TypeTreeNode import TypeTreeNode 4 | 5 | try: 6 | from TypeTreeGeneratorAPI import TypeTreeGenerator as TypeTreeGeneratorBase 7 | except ImportError: 8 | 9 | class TypeTreeGeneratorBase: 10 | def __init__(self, unity_version: str): 11 | raise ImportError("TypeTreeGeneratorAPI isn't installed!") 12 | 13 | def load_dll(self, dll: bytes): ... 14 | def load_il2cpp(self, il2cpp: bytes, metadata: bytes): ... 15 | def get_nodes_as_json(self, assembly: str, fullname: str) -> str: ... 16 | def get_nodes(self, assembly: str, fullname: str) -> List[TypeTreeNode]: ... 17 | 18 | 19 | class TypeTreeGenerator(TypeTreeGeneratorBase): 20 | cache: Dict[Tuple[str, str], TypeTreeNode] 21 | 22 | def __init__(self, unity_version: str): 23 | super().__init__(unity_version) 24 | self.cache = {} 25 | 26 | def load_local_game(self, root_dir: str): 27 | root_files = os.listdir(root_dir) 28 | data_dir = os.path.join( 29 | root_dir, next(f for f in root_files if f.endswith("_Data")) 30 | ) 31 | if "GameAssembly.dll" in root_files: 32 | ga_fp = os.path.join(root_dir, "GameAssembly.dll") 33 | gm_fp = os.path.join( 34 | data_dir, "il2cpp_data", "Metadata", "global-metadata.dat" 35 | ) 36 | ga_raw = open(ga_fp, "rb").read() 37 | gm_raw = open(gm_fp, "rb").read() 38 | self.load_il2cpp(ga_raw, gm_raw) 39 | else: 40 | self.load_local_dll_folder(os.path.join(data_dir, "Managed")) 41 | 42 | def load_local_dll_folder(self, dll_dir: str): 43 | for f in os.listdir(dll_dir): 44 | fp = os.path.join(dll_dir, f) 45 | with open(fp, "rb") as f: 46 | data = f.read() 47 | self.load_dll(data) 48 | 49 | def get_nodes_up(self, assembly: str, fullname: str) -> TypeTreeNode: 50 | root = self.cache.get((assembly, fullname)) 51 | if root is not None: 52 | return root 53 | 54 | base_nodes = self.get_nodes(f"{assembly}.dll", fullname) 55 | 56 | base_root = base_nodes[0] 57 | root = TypeTreeNode( 58 | base_root.m_Level, 59 | base_root.m_Type, 60 | base_root.m_Name, 61 | 0, 62 | 0, 63 | m_MetaFlag=base_root.m_MetaFlag, 64 | ) 65 | stack: List[TypeTreeNode] = [] 66 | parent = root 67 | prev = root 68 | 69 | for base_node in base_nodes[1:]: 70 | node = TypeTreeNode( 71 | base_node.m_Level, 72 | base_node.m_Type, 73 | base_node.m_Name, 74 | 0, 75 | 0, 76 | m_MetaFlag=base_node.m_MetaFlag, 77 | ) 78 | if node.m_Level > prev.m_Level: 79 | stack.append(parent) 80 | parent = prev 81 | elif node.m_Level < prev.m_Level: 82 | while node.m_Level <= parent.m_Level: 83 | parent = stack.pop() 84 | 85 | parent.m_Children.append(node) 86 | prev = node 87 | 88 | self.cache[(assembly, fullname)] = root 89 | return root 90 | 91 | 92 | __all__ = ("TypeTreeGenerator",) 93 | -------------------------------------------------------------------------------- /UnityPy/math/Vector3.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from math import sqrt 3 | from typing import Sequence 4 | 5 | 6 | kEpsilon = 0.00001 7 | 8 | 9 | @dataclass 10 | class Vector3: 11 | '''https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Vector3.cs''' 12 | 13 | X: float = 0.0 14 | Y: float = 0.0 15 | Z: float = 0.0 16 | 17 | def __init__(self, *args): 18 | from .Vector4 import Vector4 19 | if len(args) == 1: 20 | args = args[0] 21 | 22 | if isinstance(args, Sequence): 23 | if len(args) == 3: # args=(x, y, z) 24 | self.X, self.Y, self.Z = args 25 | elif len(args) == 0: # args=() 26 | self.X = self.Y = self.Z = 0.0 27 | else: 28 | raise TypeError("Invalid argument length for Vector3") 29 | elif isinstance(args, Vector4): 30 | # dirty patch for Vector4 31 | self.X, self.Y, self.Z = args.X, args.Y, args.Z 32 | else: 33 | raise TypeError("If only 1 argument passed, it must be a sequence or Vector4") 34 | 35 | def __getitem__(self, index): 36 | return (self.X, self.Y, self.Z)[index] 37 | 38 | def __setitem__(self, index, value): 39 | if index == 0: 40 | self.X = value 41 | elif index == 1: 42 | self.Y = value 43 | elif index == 2: 44 | self.Z = value 45 | else: 46 | raise IndexError("Index out of range") 47 | 48 | def __hash__(self): 49 | return ( 50 | self.X.__hash__() ^ 51 | (self.Y.__hash__() << 2) ^ 52 | (self.Z.__hash__() >> 2) 53 | ) 54 | 55 | def normalize(self): 56 | length = self.length() 57 | if length > kEpsilon: 58 | invNorm = 1.0 / length 59 | self.X *= invNorm 60 | self.Y *= invNorm 61 | self.Z *= invNorm 62 | else: 63 | self.X = self.Y = self.Z = 0.0 64 | 65 | Normalize = normalize 66 | 67 | def length(self): 68 | return sqrt(self.lengthSquared()) 69 | 70 | Length = length 71 | 72 | def lengthSquared(self): 73 | return self.X ** 2 + self.Y ** 2 + self.Z ** 2 74 | 75 | LengthSquared = lengthSquared 76 | 77 | @staticmethod 78 | def Zero(): 79 | return Vector3(0, 0, 0) 80 | 81 | @staticmethod 82 | def One(): 83 | return Vector3(1, 1, 1) 84 | 85 | def __add__(a, b): 86 | return Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z) 87 | 88 | def __sub__(a, b): 89 | return Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z) 90 | 91 | def __mul__(a, d): 92 | return Vector3(a.X * d, a.Y * d, a.Z * d) 93 | 94 | def __truediv__(a, d): 95 | return Vector3(a.X / d, a.Y / d, a.Z / d) 96 | 97 | def __eq__(lhs, rhs): 98 | if isinstance(rhs, Vector3): 99 | diff = lhs - rhs 100 | return diff.lengthSquared() < kEpsilon * kEpsilon 101 | return False 102 | 103 | def __ne__(lhs, rhs): 104 | return not (lhs == rhs) 105 | 106 | def Vector2(self): 107 | from .Vector2 import Vector2 108 | return Vector2(self.X, self.Y) 109 | 110 | def Vector4(self): 111 | from .Vector4 import Vector4 112 | return Vector4(self.X, self.Y, self.Z, 0.0) 113 | -------------------------------------------------------------------------------- /UnityPyBoost/ArchiveStorageDecryptor.cpp: -------------------------------------------------------------------------------- 1 | // based on https://github.com/RazTools/Studio/blob/main/AssetStudio/Crypto/UnityCN.cs 2 | 3 | #include "ArchiveStorageDecryptor.hpp" 4 | #include 5 | 6 | inline unsigned char decrypt_byte(unsigned char *bytes, uint64_t& offset, uint64_t& index, const unsigned char *index_data, const unsigned char *substitute_data) 7 | { 8 | unsigned char count_byte = substitute_data[((index >> 2) & 3) + 4] 9 | + substitute_data[index & 3] 10 | + substitute_data[((index >> 4) & 3) + 8] 11 | + substitute_data[((unsigned char)index >> 6) + 12]; 12 | bytes[offset] = (unsigned char)((index_data[bytes[offset] & 0xF] - count_byte) & 0xF | 0x10 * (index_data[bytes[offset] >> 4] - count_byte)); 13 | count_byte = bytes[offset++]; 14 | index++; 15 | return count_byte; 16 | } 17 | 18 | inline uint64_t decrypt(unsigned char *bytes, uint64_t index, uint64_t remaining, const unsigned char *index_data, const unsigned char *substitute_data) 19 | { 20 | uint64_t offset = 0; 21 | 22 | unsigned char current_byte = decrypt_byte(bytes, offset, index, index_data, substitute_data); 23 | uint64_t current_byte_high = current_byte >> 4; 24 | uint64_t current_byte_low = current_byte & 0xF; 25 | 26 | if (current_byte_high == 0xF) 27 | { 28 | unsigned char count_byte; 29 | do 30 | { 31 | count_byte = decrypt_byte(bytes, offset, index, index_data, substitute_data); 32 | current_byte_high += count_byte; 33 | } while (count_byte == 0xFF); 34 | } 35 | 36 | offset += current_byte_high; 37 | 38 | if (offset < remaining) 39 | { 40 | decrypt_byte(bytes, offset, index, index_data, substitute_data); 41 | decrypt_byte(bytes, offset, index, index_data, substitute_data); 42 | if (current_byte_low == 0xF) 43 | { 44 | unsigned char count_byte; 45 | do 46 | { 47 | count_byte = decrypt_byte(bytes, offset, index, index_data, substitute_data); 48 | } while (count_byte == 0xFF); 49 | } 50 | } 51 | 52 | return offset; 53 | } 54 | 55 | PyObject *decrypt_block(PyObject *self, PyObject *args) { 56 | Py_buffer index_data; 57 | Py_buffer substitute_data; 58 | Py_buffer data; 59 | uint64_t index; 60 | 61 | if (!PyArg_ParseTuple(args, "y*y*y*K", &index_data, &substitute_data, &data, &index)) { 62 | if (index_data.buf) PyBuffer_Release(&index_data); 63 | if (substitute_data.buf) PyBuffer_Release(&substitute_data); 64 | if (data.buf) PyBuffer_Release(&data); 65 | return NULL; 66 | } 67 | 68 | PyObject *result = PyBytes_FromStringAndSize(NULL, data.len); 69 | if (result == NULL) { 70 | PyBuffer_Release(&index_data); 71 | PyBuffer_Release(&substitute_data); 72 | PyBuffer_Release(&data); 73 | return NULL; 74 | } 75 | 76 | unsigned char *result_raw = (unsigned char *)PyBytes_AS_STRING(result); 77 | memcpy(result_raw, data.buf, data.len); 78 | 79 | uint64_t offset = 0; 80 | while (offset < data.len) { 81 | offset += decrypt(result_raw + offset, index++, data.len - offset, (unsigned char *)index_data.buf, (unsigned char *)substitute_data.buf); 82 | } 83 | 84 | PyBuffer_Release(&index_data); 85 | PyBuffer_Release(&substitute_data); 86 | PyBuffer_Release(&data); 87 | 88 | return result; 89 | } 90 | 91 | -------------------------------------------------------------------------------- /UnityPy/math/Vector4.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from math import sqrt 3 | from typing import Sequence 4 | 5 | 6 | kEpsilon = 0.00001 7 | 8 | 9 | @dataclass 10 | class Vector4: 11 | '''https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Vector4.cs''' 12 | 13 | X: float = 0.0 14 | Y: float = 0.0 15 | Z: float = 0.0 16 | W: float = 0.0 17 | 18 | def __init__(self, *args): 19 | if len(args) == 1: 20 | args = args[0] 21 | 22 | if isinstance(args, Sequence): 23 | if len(args) == 4: # args=(x, y, z, w) 24 | self.X, self.Y, self.Z, self.W = args 25 | elif len(args) == 2: # args=(Vector3, w) 26 | self.X, self.Y, self.Z = args[0] 27 | self.W = args[1] 28 | elif len(args) == 0: # args=() 29 | self.X = self.Y = self.Z = self.W = 0.0 30 | else: 31 | raise TypeError("Invalid argument length for Vector4") 32 | else: 33 | raise TypeError("If only 1 argument passed, it must be a sequence") 34 | 35 | def __getitem__(self, index): 36 | return (self.X, self.Y, self.Z, self.W)[index] 37 | 38 | def __setitem__(self, index, value): 39 | if index == 0: 40 | self.X = value 41 | elif index == 1: 42 | self.Y = value 43 | elif index == 2: 44 | self.Z = value 45 | elif index == 3: 46 | self.W = value 47 | else: 48 | raise IndexError("Index out of range") 49 | 50 | def __hash__(self): 51 | return ( 52 | self.X.__hash__() ^ 53 | (self.Y.__hash__() << 2) ^ 54 | (self.Z.__hash__() >> 2) ^ 55 | (self.W.__hash__() >> 1) 56 | ) 57 | 58 | def normalize(self): 59 | length = self.length() 60 | if length > kEpsilon: 61 | invNorm = 1.0 / length 62 | self.X *= invNorm 63 | self.Y *= invNorm 64 | self.Z *= invNorm 65 | self.W *= invNorm 66 | else: 67 | self.X = self.Y = self.Z = self.W = 0.0 68 | 69 | Normalize = normalize 70 | 71 | def length(self): 72 | return sqrt(self.lengthSquared()) 73 | 74 | Length = length 75 | 76 | def lengthSquared(self): 77 | return self.X ** 2 + self.Y ** 2 + self.Z ** 2 + self.W ** 2 78 | 79 | LengthSquared = lengthSquared 80 | 81 | @staticmethod 82 | def Zero(): 83 | return Vector4(0, 0, 0, 0) 84 | 85 | @staticmethod 86 | def One(): 87 | return Vector4(1, 1, 1, 1) 88 | 89 | def __add__(a, b): 90 | return Vector4(a.X + b.X, a.Y + b.Y, a.Z + b.Z, a.W + b.W) 91 | 92 | def __sub__(a, b): 93 | return Vector4(a.X - b.X, a.Y - b.Y, a.Z - b.Z, a.W - b.W) 94 | 95 | def __mul__(a, d): 96 | return Vector4(a.X * d, a.Y * d, a.Z * d, a.W * d) 97 | 98 | def __truediv__(a, d): 99 | return Vector4(a.X / d, a.Y / d, a.Z / d, a.W / d) 100 | 101 | def __eq__(lhs, rhs): 102 | if isinstance(rhs, Vector4): 103 | diff = lhs - rhs 104 | return diff.lengthSquared() < kEpsilon * kEpsilon 105 | return False 106 | 107 | def __ne__(lhs, rhs): 108 | return not (lhs == rhs) 109 | 110 | def Vector3(self): 111 | from .Vector3 import Vector3 112 | return Vector3(self.X, self.Y, self.Z) 113 | -------------------------------------------------------------------------------- /UnityPy/tools/libil2cpp_helper/helper.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from io import BytesIO 3 | from struct import pack, unpack 4 | from typing import List, Iterator, Tuple, Union, BinaryIO, get_origin, get_args 5 | 6 | # save original int class 7 | _int = int 8 | 9 | 10 | class CustomIntWrapper(int): 11 | __size: int 12 | __format: str 13 | 14 | @classmethod 15 | def read_from(cls, f: BytesIO): 16 | return cls( 17 | unpack("<" + getattr(cls, "__format"), f.read(getattr(cls, "__size")))[0] 18 | ) 19 | 20 | 21 | def CustomIntWrapperFactory(name: str, __size: int, __format: str) -> CustomIntWrapper: 22 | return type(name, (CustomIntWrapper,), {"__size": __size, "__format": __format}) 23 | 24 | 25 | byte = CustomIntWrapperFactory("byte", 1, "B") 26 | short = CustomIntWrapperFactory("short", 2, "h") 27 | ushort = CustomIntWrapperFactory("ushort", 2, "H") 28 | int = CustomIntWrapperFactory("int", 4, "i") 29 | uint = CustomIntWrapperFactory("uint", 4, "I") 30 | long = CustomIntWrapperFactory("long", 8, "q") 31 | ulong = CustomIntWrapperFactory("ulong", 8, "Q") 32 | 33 | 34 | class Version: 35 | Min: float 36 | Max: float 37 | 38 | def __new__(cls, Min: float = 0, Max: float = 99): 39 | spec = [] 40 | if Min: 41 | spec.append(f"Min={Min}") 42 | if Max != 99: 43 | spec.append(f"Max={Max}") 44 | newclass = type( 45 | f"Version ({', '.join(spec)})", (Version,), {"Min": Min, "Max": Max} 46 | ) 47 | return newclass 48 | 49 | @classmethod 50 | def check_compatiblity(cls, version): 51 | return cls.Min <= version <= cls.Max 52 | 53 | 54 | class MetaDataClass: 55 | version: float 56 | size: int 57 | parseString: str 58 | 59 | def __init__(self, reader: BinaryIO = None) -> None: 60 | if not (self.version): 61 | raise NotImplementedError( 62 | "Using an unversioned MetaDataClass isn't possible." 63 | ) 64 | if reader: 65 | self.read_from(reader) 66 | 67 | def read_from(self, reader: BytesIO): 68 | self.__dict__.update( 69 | zip( 70 | self.__annotations__.keys(), 71 | unpack(self.parseString, reader.read(self.size)), 72 | ) 73 | ) 74 | 75 | def write_to(self, writer: BytesIO): 76 | writer.write( 77 | pack( 78 | "<" + self.parseString, 79 | (self.get(key) for key in self.__annotations__.keys()), 80 | ) 81 | ) 82 | 83 | @classmethod 84 | def generate_versioned_subclass(cls, version: float): 85 | # fetch fields & calculate size 86 | compatible_fields = {} 87 | size = 0 88 | parseString = [] 89 | for key, clz in cls.__annotations__.items(): 90 | if get_origin(clz) == Union: 91 | clz, *version_checks = get_args(clz) 92 | if not any( 93 | version_check.check_compatiblity(version) 94 | for version_check in version_checks 95 | ): 96 | continue 97 | compatible_fields[key] = clz 98 | size += getattr(clz, "__size") 99 | parseString.append(getattr(clz, "__format")) 100 | 101 | newclass = type( 102 | f"{cls.__name__} - V{version:.1f}", 103 | (MetaDataClass,), 104 | { 105 | "__annotations__": compatible_fields, 106 | "size": size, 107 | "version": version, 108 | "parseString": "".join(parseString), 109 | }, 110 | ) 111 | return newclass 112 | -------------------------------------------------------------------------------- /UnityPy/files/WebFile.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from . import File 4 | from ..helpers import CompressionHelper 5 | from ..streams import EndianBinaryReader, EndianBinaryWriter 6 | 7 | 8 | class WebFile(File.File): 9 | """A package which can hold other WebFiles, Bundles and SerialiedFiles. 10 | It may be compressed via gzip or brotli. 11 | 12 | files -- list of all files in the WebFile 13 | """ 14 | 15 | def __init__(self, reader: EndianBinaryReader, parent: File, name=None, **kwargs): 16 | """Constructor Method""" 17 | super().__init__(parent=parent, name=name, **kwargs) 18 | 19 | # check compression 20 | magic = reader.read_bytes(2) 21 | reader.Position = 0 22 | 23 | if magic == CompressionHelper.GZIP_MAGIC: 24 | self.packer = "gzip" 25 | data = CompressionHelper.decompress_gzip(reader.bytes) 26 | reader = EndianBinaryReader(data, endian="<") 27 | else: 28 | reader.Position = 0x20 29 | magic = reader.read_bytes(6) 30 | reader.Position = 0 31 | if CompressionHelper.BROTLI_MAGIC == magic: 32 | self.packer = "brotli" 33 | data = CompressionHelper.decompress_brotli(reader.bytes) 34 | reader = EndianBinaryReader(data, endian="<") 35 | else: 36 | self.packer = "none" 37 | reader.endian = "<" 38 | 39 | # signature check 40 | signature = reader.read_string_to_null() 41 | if not signature.startswith(("UnityWebData", "TuanjieWebData")): 42 | return 43 | self.signature = signature 44 | 45 | # read header -> contains file headers 46 | head_length = reader.read_int() 47 | 48 | files = [] 49 | while reader.Position < head_length: 50 | offset = reader.read_int() 51 | length = reader.read_int() 52 | path_length = reader.read_int() 53 | name = bytes(reader.read_bytes(path_length)).decode("utf-8") 54 | files.append(File.DirectoryInfo(name, offset, length)) 55 | 56 | self.read_files(reader, files) 57 | 58 | def save( 59 | self, 60 | files: Optional[dict] = None, 61 | packer: str = "none", 62 | signature: str = "UnityWebData1.0", 63 | ) -> bytes: 64 | # solve defaults 65 | if not files: 66 | files = self.files 67 | if not packer: 68 | packer = self.packer 69 | 70 | # get raw data 71 | files = { 72 | name: f.bytes if isinstance(f, EndianBinaryReader) else f.save() 73 | for name, f in files.items() 74 | } 75 | 76 | # create writer 77 | writer = EndianBinaryWriter(endian="<") 78 | # signature 79 | writer.write_string_to_null(signature) 80 | 81 | # data offset 82 | offset = sum([ 83 | writer.Position, # signature 84 | sum( 85 | len(path.encode("utf-8")) for path in files.keys() 86 | ), # path of each file 87 | 4 * 3 * len(files), # 3 ints per file 88 | 4, # offset int 89 | ]) 90 | 91 | writer.write_int(offset) 92 | 93 | # 1. file headers 94 | for name, data in files.items(): 95 | # offset 96 | writer.write_int(offset) 97 | # length 98 | length = len(data) 99 | writer.write_int(length) 100 | offset += length 101 | # path 102 | enc_path = name.encode("utf-8") 103 | writer.write_int(len(enc_path)) 104 | writer.write(enc_path) 105 | 106 | # 2. file data 107 | for data in files.values(): 108 | writer.write(data) 109 | 110 | if packer == "gzip": 111 | return CompressionHelper.compress_gzip(writer.bytes) 112 | elif packer == "brotli": 113 | return CompressionHelper.compress_brotli(writer.bytes) 114 | else: 115 | return writer.bytes 116 | -------------------------------------------------------------------------------- /UnityPy/classes/PPtr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, cast 4 | 5 | from attr import define 6 | 7 | if TYPE_CHECKING: 8 | from ..enums.ClassIDType import ClassIDType 9 | from ..files.ObjectReader import ObjectReader 10 | from ..files.SerializedFile import SerializedFile 11 | 12 | T = TypeVar("T") 13 | 14 | 15 | @define(slots=True, kw_only=True) 16 | class PPtr(Generic[T]): 17 | m_FileID: int 18 | m_PathID: int 19 | assetsfile: Optional[SerializedFile] = None 20 | 21 | @property 22 | def file_id(self) -> int: 23 | # backwards compatibility 24 | return self.m_FileID 25 | 26 | @property 27 | def path_id(self) -> int: 28 | # backwards compatibility 29 | return self.m_PathID 30 | 31 | @property 32 | def type(self) -> ClassIDType: 33 | return self.deref().type 34 | 35 | # backwards compatibility - to be removed in UnityPy 2 36 | def read(self): 37 | return self.deref_parse_as_object() 38 | 39 | # backwards compatibility - to be removed in UnityPy 2 40 | def read_typetree(self): 41 | return self.deref_parse_as_dict() 42 | 43 | def deref(self, assetsfile: Optional[SerializedFile] = None) -> ObjectReader[T]: 44 | assetsfile = assetsfile or self.assetsfile 45 | if assetsfile is None: 46 | raise ValueError("PPtr can't deref without an assetsfile!") 47 | 48 | if self.m_PathID == 0: 49 | raise ValueError("PPtr can't deref with m_PathID == 0!") 50 | 51 | if self.m_FileID == 0: 52 | pass 53 | else: 54 | # resolve file id to external name 55 | external_id = self.m_FileID - 1 56 | if external_id >= len(assetsfile.externals): 57 | raise FileNotFoundError("Failed to resolve pointer - invalid m_FileID!") 58 | external = assetsfile.externals[external_id] 59 | 60 | # resolve external name to assetsfile 61 | container = assetsfile.parent 62 | if container is None: 63 | # TODO - use default fs 64 | raise FileNotFoundError( 65 | f"PPtr points to {external.path} but no container is set!" 66 | ) 67 | 68 | external_clean_path = external.path 69 | if external_clean_path.startswith("archive:/"): 70 | external_clean_path = external_clean_path[9:] 71 | if external_clean_path.startswith("assets/"): 72 | external_clean_path = external_clean_path[7:] 73 | external_clean_path = external_clean_path.rsplit("/")[-1].lower() 74 | 75 | for key, file in container.files.items(): 76 | if key.lower() == external_clean_path: 77 | assetsfile = file 78 | break 79 | else: 80 | env = assetsfile.environment 81 | cab = env.find_file(external_clean_path) 82 | if cab: 83 | assetsfile = cab 84 | else: 85 | raise FileNotFoundError( 86 | f"Failed to resolve pointer - {external.path} not found!" 87 | ) 88 | 89 | return cast("ObjectReader[T]", assetsfile.objects[self.m_PathID]) 90 | 91 | def deref_parse_as_object(self, assetsfile: Optional[SerializedFile] = None) -> T: 92 | return self.deref(assetsfile).parse_as_object() 93 | 94 | def deref_parse_as_dict( 95 | self, assetsfile: Optional[SerializedFile] = None 96 | ) -> dict[str, Any]: 97 | return self.deref(assetsfile).parse_as_dict() 98 | 99 | def __bool__(self): 100 | return self.m_PathID != 0 101 | 102 | def __hash__(self) -> int: 103 | return hash((self.m_FileID, self.m_PathID)) 104 | 105 | def __eq__(self, other: object) -> bool: 106 | if not isinstance(other, PPtr): 107 | return False 108 | return self.m_FileID == other.m_FileID and self.m_PathID == other.m_PathID 109 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import platform 4 | 5 | from PIL import Image 6 | 7 | import UnityPy 8 | 9 | SAMPLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples") 10 | 11 | 12 | def test_read_single(): 13 | for f in os.listdir(SAMPLES): 14 | env = UnityPy.load(os.path.join(SAMPLES, f)) 15 | for obj in env.objects: 16 | obj.read() 17 | 18 | 19 | def test_read_batch(): 20 | env = UnityPy.load(SAMPLES) 21 | for obj in env.objects: 22 | obj.read() 23 | 24 | 25 | def test_save_dict(): 26 | env = UnityPy.load(SAMPLES) 27 | for obj in env.objects: 28 | data = obj.get_raw_data() 29 | item = obj.read_typetree(wrap=False) 30 | assert isinstance(item, dict) 31 | re_data = obj.save_typetree(item) 32 | assert data == re_data 33 | 34 | 35 | def test_save_wrap(): 36 | env = UnityPy.load(SAMPLES) 37 | for obj in env.objects: 38 | data = obj.get_raw_data() 39 | item = obj.read_typetree(wrap=True) 40 | assert not isinstance(item, dict) 41 | re_data = obj.save_typetree(item) 42 | assert data == re_data 43 | 44 | 45 | def test_texture2d(): 46 | for f in os.listdir(SAMPLES): 47 | env = UnityPy.load(os.path.join(SAMPLES, f)) 48 | for obj in env.objects: 49 | if obj.type.name == "Texture2D": 50 | data = obj.read() 51 | data.image.save(io.BytesIO(), format="PNG") 52 | data.image = data.image.transpose(Image.ROTATE_90) 53 | data.save() 54 | 55 | 56 | def test_sprite(): 57 | for f in os.listdir(SAMPLES): 58 | env = UnityPy.load(os.path.join(SAMPLES, f)) 59 | for obj in env.objects: 60 | if obj.type.name == "Sprite": 61 | obj.read().image.save(io.BytesIO(), format="PNG") 62 | 63 | 64 | if platform.system() == "Darwin": 65 | # crunch issue on macos leading to segfault 66 | del test_texture2d 67 | del test_sprite 68 | 69 | def test_audioclip(): 70 | # as not platforms are supported by FMOD 71 | # we have to check if the platform is supported first 72 | try: 73 | from UnityPy.export import AudioClipConverter 74 | 75 | AudioClipConverter.import_pyfmodex() 76 | except NotImplementedError: 77 | return 78 | except OSError: 79 | # cibuildwheel doesn't copy the .so files 80 | # so we have to skip the test on it 81 | print("Failed to load the fmod lib for your system.") 82 | print("Skipping the audioclip test.") 83 | return 84 | if AudioClipConverter.pyfmodex is False: 85 | return 86 | env = UnityPy.load(os.path.join(SAMPLES, "char_118_yuki.ab")) 87 | for obj in env.objects: 88 | if obj.type.name == "AudioClip": 89 | clip = obj.read() 90 | assert len(clip.samples) == 1 91 | 92 | 93 | def test_mesh(): 94 | env = UnityPy.load(os.path.join(SAMPLES, "xinzexi_2_n_tex")) 95 | with open(os.path.join(SAMPLES, "xinzexi_2_n_tex_mesh"), "rb") as f: 96 | wanted = f.read().replace(b"\r", b"") 97 | for obj in env.objects: 98 | if obj.type.name == "Mesh": 99 | mesh = obj.read() 100 | data = mesh.export() 101 | if isinstance(data, str): 102 | data = data.encode("utf8").replace(b"\r", b"") 103 | assert data == wanted 104 | 105 | 106 | def test_read_typetree(): 107 | env = UnityPy.load(SAMPLES) 108 | for obj in env.objects: 109 | obj.read_typetree() 110 | 111 | 112 | def test_save(): 113 | env = UnityPy.load(SAMPLES) 114 | # TODO - check against original 115 | # this only makes sure 116 | # that the save function still produces a readable file 117 | for name, file in env.files.items(): 118 | if isinstance(file, UnityPy.streams.EndianBinaryReader): 119 | continue 120 | save1 = file.save() 121 | save2 = UnityPy.load(save1).file.save() 122 | assert save1 == save2 123 | 124 | 125 | if __name__ == "__main__": 126 | for x in list(locals()): 127 | if str(x)[:4] == "test": 128 | locals()[x]() 129 | input("All Tests Passed") 130 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish wheels 2 | on: 3 | workflow_dispatch 4 | 5 | 6 | jobs: 7 | build_sdist: 8 | name: Build source distribution 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | submodules: recursive 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.x' 19 | 20 | - name: Build sdist 21 | run: pipx run build --sdist 22 | 23 | - name: Install sdist 24 | run: pip install dist/*.tar.gz 25 | 26 | - uses: actions/upload-artifact@v4 27 | with: 28 | name: "sdist" 29 | path: dist/*.tar.gz 30 | 31 | build_wheels: 32 | name: Build wheels on ${{ matrix.os }} 33 | runs-on: ${{ matrix.os }} 34 | needs: [build_sdist] 35 | strategy: 36 | fail-fast: true 37 | matrix: 38 | os: [windows-latest, macos-latest] 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | with: 43 | submodules: recursive 44 | 45 | - name: Build wheels 46 | uses: joerick/cibuildwheel@v2.21.2 47 | env: 48 | CIBW_TEST_SKIP: "*" 49 | CIBW_SKIP: "pp*" 50 | 51 | - uses: actions/upload-artifact@v4 52 | with: 53 | name: "${{ matrix.os }}" 54 | path: ./wheelhouse/*.whl 55 | retention-days: 1 56 | 57 | build_manylinux_wheels_ubuntu: 58 | name: Build manylinux wheels on ubuntu-latest 59 | runs-on: ubuntu-latest 60 | needs: [build_sdist] 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | with: 65 | submodules: recursive 66 | 67 | - name: Set up QEMU 68 | uses: docker/setup-qemu-action@v3 69 | with: 70 | platforms: all 71 | 72 | - name: Build wheels 73 | uses: joerick/cibuildwheel@v2.21.2 74 | env: 75 | CIBW_TEST_SKIP: "*" 76 | CIBW_SKIP: "pp* *-musllinux*" 77 | 78 | - uses: actions/upload-artifact@v4 79 | with: 80 | name: "manylinux" 81 | path: ./wheelhouse/*.whl 82 | retention-days: 1 83 | 84 | build_musllinux_wheels_ubuntu: 85 | name: Build musllinux wheels on ubuntu-latest 86 | runs-on: ubuntu-latest 87 | needs: [build_sdist] 88 | 89 | steps: 90 | - uses: actions/checkout@v4 91 | with: 92 | submodules: recursive 93 | 94 | - name: Set up QEMU 95 | uses: docker/setup-qemu-action@v3 96 | with: 97 | platforms: all 98 | 99 | - name: Build wheels 100 | uses: joerick/cibuildwheel@v2.21.2 101 | env: 102 | CIBW_TEST_SKIP: "*" 103 | CIBW_SKIP: "pp* *-manylinux*" 104 | # fmod requires: 105 | # default via musl: -exclude flag 106 | # libdl.so.2 => /lib/ld-musl-x86_64.so.1 (0x7faeb127d000) 107 | # librt.so.1 => /lib/ld-musl-x86_64.so.1 (0x7faeb127d000) 108 | # libm.so.6 => /lib/ld-musl-x86_64.so.1 (0x7faeb127d000) 109 | # libpthread.so.0 => /lib/ld-musl-x86_64.so.1 (0x7faeb127d000) 110 | # libc.so.6 => /lib/ld-musl-x86_64.so.1 (0x7faeb127d000) 111 | # deps: 112 | # libgcc 113 | # libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7faeb1253000) 114 | # libstdc++ 115 | # libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x7faeb0a00000) 116 | CIBW_BEFORE_ALL: "apk add libgcc libstdc++" 117 | CIBW_REPAIR_WHEEL_COMMAND: "auditwheel repair -w {dest_dir} {wheel} --exclude libdl.so.2 --exclude librt.so.1 --exclude libm.so.6 --exclude libpthread.so.0 --exclude libc.so.6" 118 | 119 | 120 | - uses: actions/upload-artifact@v4 121 | with: 122 | name: "musllinux" 123 | path: ./wheelhouse/*.whl 124 | retention-days: 1 125 | 126 | 127 | upload_pypi: 128 | name: Publish to PyPI 129 | needs: [build_sdist, build_wheels, build_manylinux_wheels_ubuntu, build_musllinux_wheels_ubuntu] 130 | runs-on: ubuntu-latest 131 | 132 | permissions: 133 | id-token: write 134 | steps: 135 | - uses: actions/download-artifact@v4 136 | with: 137 | path: dist 138 | merge-multiple: true 139 | 140 | - name: Publish package distributions to PyPI 141 | uses: pypa/gh-action-pypi-publish@release/v1 142 | with: 143 | skip-existing: true 144 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | schedule: 20 | - cron: '19 5 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: c-cpp 47 | build-mode: autobuild 48 | - language: python 49 | build-mode: none 50 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 51 | # Use `c-cpp` to analyze code written in C, C++ or both 52 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 53 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 54 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 55 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 56 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 57 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@v4 61 | 62 | # Initializes the CodeQL tools for scanning. 63 | - name: Initialize CodeQL 64 | uses: github/codeql-action/init@v3 65 | with: 66 | languages: ${{ matrix.language }} 67 | build-mode: ${{ matrix.build-mode }} 68 | # If you wish to specify custom queries, you can do so here or in a config file. 69 | # By default, queries listed here will override any specified in a config file. 70 | # Prefix the list here with "+" to use these queries and those in the config file. 71 | 72 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 73 | # queries: security-extended,security-and-quality 74 | 75 | # If the analyze step fails for one of the languages you are analyzing with 76 | # "We were unable to automatically build your code", modify the matrix above 77 | # to set the build mode to "manual" for that language. Then modify this step 78 | # to build your code. 79 | # ℹ️ Command-line programs to run using the OS shell. 80 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 81 | - if: matrix.build-mode == 'manual' 82 | shell: bash 83 | run: | 84 | echo 'If you are using a "manual" build mode for one or more of the' \ 85 | 'languages you are analyzing, replace this with the commands to build' \ 86 | 'your code, for example:' 87 | echo ' make bootstrap' 88 | echo ' make release' 89 | exit 1 90 | 91 | - name: Perform CodeQL Analysis 92 | uses: github/codeql-action/analyze@v3 93 | with: 94 | category: "/language:${{matrix.language}}" 95 | -------------------------------------------------------------------------------- /UnityPy/helpers/PackedBitVector.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, TYPE_CHECKING, Tuple 2 | 3 | if TYPE_CHECKING: 4 | from ..classes.generated import PackedBitVector 5 | 6 | 7 | def reshape(data: list, shape: Optional[Tuple[int, ...]] = None) -> list: 8 | if shape is None: 9 | return data 10 | if len(shape) == 1: 11 | m = shape[0] 12 | return [data[i : i + m] for i in range(0, len(data), m)] 13 | elif len(shape) == 2: 14 | m, n = shape 15 | return [ 16 | [[data[i + j : i + j + n] for j in range(0, m * n, n)]] 17 | for i in range(0, len(data), m * n) 18 | ] 19 | else: 20 | raise ValueError("Invalid shape") 21 | 22 | 23 | def unpack_ints( 24 | packed: "PackedBitVector", 25 | start: int = 0, 26 | count: Optional[int] = None, 27 | shape: Optional[Tuple[int, ...]] = None, 28 | ) -> List[int]: 29 | assert packed.m_BitSize is not None 30 | 31 | m_BitSize = packed.m_BitSize 32 | m_Data = packed.m_Data 33 | 34 | bitPos = m_BitSize * start 35 | indexPos = bitPos // 8 36 | bitPos %= 8 37 | 38 | if count is None: 39 | count = packed.m_NumItems 40 | 41 | # if m_BitSize <= 8: 42 | # dtype = np.uint8 43 | # elif m_BitSize <= 16: 44 | # dtype = np.uint16 45 | # elif m_BitSize <= 32: 46 | # dtype = np.uint32 47 | # elif m_BitSize <= 64: 48 | # dtype = np.uint64 49 | # else: 50 | # raise ValueError("Invalid bit size") 51 | 52 | # data = np.zeros(packed.m_NumItems, dtype=dtype) 53 | data = [0] * packed.m_NumItems 54 | 55 | for i in range(packed.m_NumItems): 56 | bits = 0 57 | value = 0 58 | while bits < m_BitSize: 59 | value |= (m_Data[indexPos] >> bitPos) << bits 60 | num = min(m_BitSize - bits, 8 - bitPos) 61 | bitPos += num 62 | bits += num 63 | if bitPos == 8: 64 | indexPos += 1 65 | bitPos = 0 66 | data[i] = value & ((1 << m_BitSize) - 1) 67 | 68 | return reshape(data, shape) 69 | 70 | 71 | def unpack_floats( 72 | packed: "PackedBitVector", 73 | start: int = 0, 74 | count: Optional[int] = None, 75 | shape: Optional[Tuple[int, ...]] = None, 76 | ) -> List[float]: 77 | assert ( 78 | packed.m_BitSize is not None 79 | and packed.m_Range is not None 80 | and packed.m_Start is not None 81 | ) 82 | 83 | # read as int and cast up to double to prevent loss of precision 84 | quantized_f64 = unpack_ints(packed, start, count) 85 | quantized = [x * packed.m_Range + packed.m_Start for x in quantized_f64] 86 | return reshape(quantized, shape) 87 | 88 | 89 | # def pack_ints( 90 | # data: npt.NDArray[np.uint], bitsize: Optional[int] = 0 91 | # ) -> PackedBitVector: 92 | # # ensure that the data type is unsigned 93 | # assert "uint" in data.dtype.name 94 | 95 | # m_NumItems = data.size 96 | 97 | # maxi = data.max() 98 | # # Prevent overflow 99 | # if bitsize: 100 | # m_BitSize = bitsize 101 | # else: 102 | # m_BitSize = (32 if maxi == 0xFFFFFFFF else np.ceil(np.log2(maxi + 1))) % 256 103 | # m_Data = np.zeros((m_NumItems * m_BitSize + 7) // 8, dtype=np.uint8) 104 | 105 | # indexPos = 0 106 | # bitPos = 0 107 | # for x in data: 108 | # bits = 0 109 | # while bits < m_BitSize: 110 | # m_Data[indexPos] |= (x >> bits) << bitPos 111 | # num = min(m_BitSize - bits, 8 - bitPos) 112 | # bitPos += num 113 | # bits += num 114 | # if bitPos == 8: 115 | # indexPos += 1 116 | # bitPos = 0 117 | 118 | # return PackedBitVector(m_NumItems=m_NumItems, m_BitSize=m_BitSize, m_Data=m_Data) 119 | 120 | 121 | # def pack_floats( 122 | # data: npt.NDArray[np.floating[Any]], 123 | # bitsize: Optional[int] = None, 124 | # ) -> PackedBitVector: 125 | # min = data.min() 126 | # max = data.max() 127 | # range = max - min 128 | # data_f64 = data.astype(np.float64) 129 | # # rebase to 0 130 | # data_f64 -= min 131 | # # scale to [0, 1] 132 | # data_f64 /= range 133 | # # quantize to [0, 2^bit_size - 1] 134 | # bitsize = bitsize or max(data.itemsize, 32) 135 | # assert bitsize is not None 136 | 137 | # data_f64 *= (1 << bitsize) - 1 138 | # # pack the data 139 | # packed = pack_ints(data_f64.astype(np.uint32), bitsize) 140 | # packed.m_Start = min 141 | # packed.m_Range = range 142 | # return packed 143 | 144 | __all__ = ("unpack_ints", "unpack_floats") 145 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | from typing import Union 5 | 6 | from setuptools import Extension, find_packages, setup 7 | from setuptools.command.build_ext import build_ext 8 | from setuptools.command.sdist import sdist 9 | 10 | try: 11 | from setuptools.command.bdist_wheel import bdist_wheel 12 | except ImportError: 13 | from wheel.bdist_wheel import bdist_wheel 14 | 15 | 16 | INSTALL_DIR = os.path.dirname(os.path.realpath(__file__)) 17 | UNITYPYBOOST_DIR = os.path.join(INSTALL_DIR, "UnityPyBoost") 18 | 19 | 20 | class BuildExt(build_ext): 21 | def build_extensions(self): 22 | cpp_version_flag: str 23 | compiler = self.compiler 24 | # msvc - only ever used c++20, never c++2a 25 | if compiler.compiler_type == "msvc": 26 | cpp_version_flag = "/std:c++20" 27 | # gnu & clang 28 | elif compiler.compiler_type == "unix": 29 | res = subprocess.run( 30 | [compiler.compiler[0], "-v"], 31 | capture_output=True, 32 | ) 33 | # for some reason g++ and clang++ return this as error 34 | text = (res.stdout or res.stderr).decode("utf-8") 35 | version = re.search(r"version\s+(\d+)\.", text) 36 | if version is None: 37 | raise Exception("Failed to determine compiler version") 38 | version = int(version.group(1)) 39 | if version < 10: 40 | cpp_version_flag = "-std=c++2a" 41 | else: 42 | cpp_version_flag = "-std=c++20" 43 | else: 44 | cpp_version_flag = "-std=c++20" 45 | 46 | for ext in self.extensions: 47 | ext.extra_compile_args = [cpp_version_flag] 48 | 49 | build_ext.build_extensions(self) 50 | 51 | 52 | class SDist(sdist): 53 | def make_distribution(self) -> None: 54 | # add all fmod libraries to the distribution 55 | for root, dirs, files in os.walk("UnityPy/lib/FMOD"): 56 | for file in files: 57 | fp = f"{root}/{file}" 58 | if fp not in self.filelist.files: 59 | self.filelist.files.append(fp) 60 | return super().make_distribution() 61 | 62 | 63 | BDIST_TAG_FMOD_MAP = { 64 | # Windows 65 | "win32": "x86", 66 | "win_amd64": "x64", 67 | "win_arm64": "arm", 68 | # Linux and Mac endings 69 | "arm64": "arm64", # Mac 70 | "x86_64": "x64", 71 | "aarch64": "arm64", # Linux 72 | "i686": "x86", 73 | "armv7l": "arm", # armhf 74 | } 75 | 76 | 77 | def get_fmod_path( 78 | system: Union["Windows", "Linux", "Darwin"], arch: ["x64", "x86", "arm", "arm64"] 79 | ) -> str: 80 | if system == "Darwin": 81 | # universal dylib 82 | return "lib/FMOD/Darwin/libfmod.dylib" 83 | 84 | if system == "Windows": 85 | return f"lib/FMOD/Windows/{arch}/fmod.dll" 86 | 87 | if system == "Linux": 88 | if arch == "x64": 89 | arch = "x86_64" 90 | return f"lib/FMOD/Linux/{arch}/libfmod.so" 91 | 92 | raise NotImplementedError(f"Unsupported system: {system}") 93 | 94 | 95 | class BDistWheel(bdist_wheel): 96 | def run(self): 97 | platform_tag = self.get_tag()[2] 98 | if platform_tag.startswith("win"): 99 | system = "Windows" 100 | arch = BDIST_TAG_FMOD_MAP[platform_tag] 101 | else: 102 | arch = next( 103 | (v for k, v in BDIST_TAG_FMOD_MAP.items() if platform_tag.endswith(k)), 104 | None, 105 | ) 106 | if platform_tag.startswith("macosx"): 107 | system = "Darwin" 108 | else: 109 | system = "Linux" 110 | 111 | try: 112 | self.distribution.package_data["UnityPy"].append( 113 | get_fmod_path(system, arch) 114 | ) 115 | except NotImplementedError: 116 | pass 117 | super().run() 118 | 119 | 120 | setup( 121 | name="UnityPy", 122 | packages=find_packages(), 123 | package_data={"UnityPy": ["resources/uncompressed.tpk"]}, 124 | ext_modules=[ 125 | Extension( 126 | "UnityPy.UnityPyBoost", 127 | [ 128 | f"UnityPyBoost/{f}" 129 | for f in os.listdir(UNITYPYBOOST_DIR) 130 | if f.endswith(".cpp") 131 | ], 132 | depends=[ 133 | f"UnityPyBoost/{f}" 134 | for f in os.listdir(UNITYPYBOOST_DIR) 135 | if f.endswith(".hpp") 136 | ], 137 | language="c++", 138 | include_dirs=[UNITYPYBOOST_DIR], 139 | ) 140 | ], 141 | cmdclass={"build_ext": BuildExt, "sdist": SDist, "bdist_wheel": BDistWheel}, 142 | ) 143 | -------------------------------------------------------------------------------- /UnityPy/helpers/ArchiveStorageManager.py: -------------------------------------------------------------------------------- 1 | # based on: https://github.com/Razmoth/PGRStudio/blob/master/AssetStudio/PGR/PGR.cs 2 | import re 3 | from typing import Optional, Tuple, Union 4 | 5 | from ..streams import EndianBinaryReader 6 | 7 | try: 8 | from UnityPy import UnityPyBoost 9 | except ImportError: 10 | UnityPyBoost = None 11 | 12 | UNITY3D_SIGNATURE = b"#$unity3dchina!@" 13 | DECRYPT_KEY: Optional[bytes] = None 14 | 15 | 16 | def set_assetbundle_decrypt_key(key: Union[bytes, str]): 17 | if isinstance(key, str): 18 | key = key.encode("utf-8", "surrogateescape") 19 | if len(key) != 16: 20 | raise ValueError( 21 | f"AssetBundle Key length is wrong. It should be 16 bytes and now is {len(key)} bytes." 22 | ) 23 | global DECRYPT_KEY 24 | DECRYPT_KEY = key 25 | 26 | 27 | def read_vector(reader: EndianBinaryReader) -> Tuple[bytes, bytes]: 28 | data = reader.read_bytes(0x10) 29 | key = reader.read_bytes(0x10) 30 | reader.Position += 1 31 | 32 | return data, key 33 | 34 | 35 | def decrypt_key(key: bytes, data: bytes, keybytes: bytes): 36 | from Crypto.Cipher import AES 37 | 38 | key = AES.new(keybytes, AES.MODE_ECB).encrypt(key) 39 | return bytes(x ^ y for x, y in zip(data, key)) 40 | 41 | 42 | def brute_force_key( 43 | fp: str, 44 | key_sig: bytes, 45 | data_sig: bytes, 46 | pattern: re.Pattern = re.compile(rb"(?=(\w{16}))"), 47 | verbose: bool = False, 48 | ): 49 | with open(fp, "rb") as f: 50 | data = f.read() 51 | 52 | matches = pattern.findall(data) 53 | for i, key in enumerate(matches): 54 | if verbose: 55 | print(f"Trying {i + 1}/{len(matches)} - {key}") 56 | signature = decrypt_key(key_sig, data_sig, key) 57 | if signature == UNITY3D_SIGNATURE: 58 | if verbose: 59 | print(f"Found key: {key}") 60 | return key 61 | return None 62 | 63 | 64 | class ArchiveStorageDecryptor: 65 | unknown_1: int 66 | index: bytes 67 | substitute: bytes = bytes(0x10) 68 | 69 | def __init__(self, reader: EndianBinaryReader): 70 | self.unknown_1 = reader.read_u_int() 71 | 72 | # read vector data/key vectors 73 | self.data, self.key = read_vector(reader) 74 | self.data_sig, self.key_sig = read_vector(reader) 75 | 76 | if DECRYPT_KEY is None: 77 | raise LookupError( 78 | "\n".join( 79 | [ 80 | "The BundleFile is encrypted, but no key was provided!", 81 | "You can set the key via UnityPy.set_assetbundle_decrypt_key(key).", 82 | "To try brute-forcing the key, use UnityPy.helpers.ArchiveStorageManager.brute_force_key(fp, key_sig, data_sig)", 83 | f"with key_sig = {self.key_sig}, data_sig = {self.data_sig}," 84 | "and fp being the path to global-metadata.dat or a memory dump.", 85 | ] 86 | ) 87 | ) 88 | 89 | signature = decrypt_key(self.key_sig, self.data_sig, DECRYPT_KEY) 90 | if signature != UNITY3D_SIGNATURE: 91 | raise Exception(f"Invalid signature {signature} != {UNITY3D_SIGNATURE}") 92 | 93 | data = decrypt_key(self.key, self.data, DECRYPT_KEY) 94 | data = bytes( 95 | nibble for byte in data for nibble in (byte >> 4, byte & 0xF) 96 | ) 97 | self.index = data[:0x10] 98 | self.substitute = bytes( 99 | data[0x10 + i * 4 + j] for j in range(4) for i in range(4) 100 | ) 101 | 102 | def decrypt_block(self, data: bytes, index: int): 103 | if UnityPyBoost: 104 | return UnityPyBoost.decrypt_block(self.index, self.substitute, data, index) 105 | 106 | offset = 0 107 | size = len(data) 108 | data = bytearray(data) 109 | view = memoryview(data) 110 | while offset < len(data): 111 | offset += self.decrypt(view[offset:], index, size - offset) 112 | index += 1 113 | return data 114 | 115 | def decrypt_byte(self, view: Union[bytearray, memoryview], offset: int, index: int): 116 | b = ( 117 | self.substitute[((index >> 2) & 3) + 4] 118 | + self.substitute[index & 3] 119 | + self.substitute[((index >> 4) & 3) + 8] 120 | + self.substitute[(index % 256 >> 6) + 12] 121 | ) 122 | view[offset] = ( 123 | (self.index[view[offset] & 0xF] - b) & 0xF 124 | | 0x10 * (self.index[view[offset] >> 4] - b) 125 | ) % 256 126 | b = view[offset] 127 | return b, offset + 1, index + 1 128 | 129 | def decrypt(self, data: Union[bytearray, memoryview], index: int, remaining: int): 130 | offset = 0 131 | 132 | curByte, offset, index = self.decrypt_byte(data, offset, index) 133 | byteHigh = curByte >> 4 134 | byteLow = curByte & 0xF 135 | 136 | if byteHigh == 0xF: 137 | b = 0xFF 138 | while b == 0xFF: 139 | b, offset, index = self.decrypt_byte(data, offset, index) 140 | byteHigh += b 141 | 142 | offset += byteHigh 143 | 144 | if offset < remaining: 145 | _, offset, index = self.decrypt_byte(data, offset, index) 146 | _, offset, index = self.decrypt_byte(data, offset, index) 147 | if byteLow == 0xF: 148 | b = 0xFF 149 | while b == 0xFF: 150 | b, offset, index = self.decrypt_byte(data, offset, index) 151 | 152 | return offset 153 | -------------------------------------------------------------------------------- /UnityPy/helpers/ImportHelper.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import os 5 | from typing import Union, List, Optional, Tuple 6 | from .CompressionHelper import BROTLI_MAGIC, GZIP_MAGIC 7 | from ..enums import FileType 8 | from ..streams import EndianBinaryReader 9 | from .. import files 10 | 11 | 12 | FileSourceType = Union[str, bytes, bytearray, io.IOBase] 13 | 14 | 15 | def file_name_without_extension(file_name: str) -> str: 16 | return os.path.join( 17 | os.path.dirname(file_name), os.path.splitext(os.path.basename(file_name))[0] 18 | ) 19 | 20 | 21 | def list_all_files(directory: str) -> List[str]: 22 | return [ 23 | val 24 | for sublist in [ 25 | [os.path.join(dir_path, filename) for filename in filenames] 26 | for (dir_path, dirnames, filenames) in os.walk(directory) 27 | if ".git" not in dir_path 28 | ] 29 | for val in sublist 30 | ] 31 | 32 | 33 | def find_all_files(directory: str, search_str: str) -> List[str]: 34 | return [ 35 | val 36 | for sublist in [ 37 | [ 38 | os.path.join(dir_path, filename) 39 | for filename in filenames 40 | if search_str in filename 41 | ] 42 | for (dir_path, dirnames, filenames) in os.walk(directory) 43 | if ".git" not in dir_path 44 | ] 45 | for val in sublist 46 | ] 47 | 48 | 49 | def check_file_type( 50 | input_: FileSourceType, 51 | ) -> Tuple[Optional[FileType], Optional[EndianBinaryReader]]: 52 | if isinstance(input_, str) and os.path.isfile(input_): 53 | reader = EndianBinaryReader(open(input_, "rb")) 54 | elif isinstance(input_, EndianBinaryReader): 55 | reader = input_ 56 | else: 57 | try: 58 | reader = EndianBinaryReader(input_) 59 | except: 60 | return None, None 61 | 62 | if reader.Length < 20: 63 | return FileType.ResourceFile, reader 64 | 65 | signature = reader.read_string_to_null(20) 66 | 67 | reader.Position = 0 68 | if signature in [ 69 | "UnityWeb", 70 | "UnityRaw", 71 | "\xfa\xfa\xfa\xfa\xfa\xfa\xfa\xfa", 72 | "UnityFS", 73 | ]: 74 | return FileType.BundleFile, reader 75 | elif signature.startswith(("UnityWebData", "TuanjieWebData")): 76 | return FileType.WebFile, reader 77 | elif signature == "PK\x03\x04": 78 | return FileType.ZIP, reader 79 | else: 80 | if reader.Length < 128: 81 | return FileType.ResourceFile, reader 82 | 83 | magic = bytes(reader.read_bytes(2)) 84 | reader.Position = 0 85 | if GZIP_MAGIC == magic: 86 | return FileType.WebFile, reader 87 | reader.Position = 0x20 88 | magic = bytes(reader.read_bytes(6)) 89 | reader.Position = 0 90 | if BROTLI_MAGIC == magic: 91 | return FileType.WebFile, reader 92 | 93 | # check if AssetsFile 94 | old_endian = reader.endian 95 | # read as if assetsfile and check version 96 | # ReadHeader 97 | reader.Position = 0 98 | metadata_size = reader.read_u_int() 99 | file_size = reader.read_u_int() 100 | version = reader.read_u_int() 101 | data_offset = reader.read_u_int() 102 | 103 | if version >= 22: 104 | endian = ">" if reader.read_boolean() else "<" 105 | reserved = reader.read_bytes(3) 106 | metadata_size = reader.read_u_int() 107 | file_size = reader.read_long() 108 | data_offset = reader.read_long() 109 | unknown = reader.read_long() # unknown 110 | 111 | # reset 112 | reader.endian = old_endian 113 | reader.Position = 0 114 | # check info 115 | if any(( 116 | version < 0, 117 | version > 100, 118 | *[ 119 | x < 0 or x > reader.Length 120 | for x in [file_size, metadata_size, version, data_offset] 121 | ], 122 | file_size < metadata_size, 123 | file_size < data_offset, 124 | )): 125 | return FileType.ResourceFile, reader 126 | else: 127 | return FileType.AssetsFile, reader 128 | 129 | 130 | def parse_file( 131 | reader: EndianBinaryReader, 132 | parent: files.File, 133 | name: str, 134 | typ: Optional[FileType] = None, 135 | is_dependency: bool = False, 136 | ) -> Union[files.File, EndianBinaryReader]: 137 | if typ is None: 138 | typ, _ = check_file_type(reader) 139 | if typ == FileType.AssetsFile and not name.endswith(( 140 | ".resS", 141 | ".resource", 142 | ".config", 143 | ".xml", 144 | ".dat", 145 | )): 146 | f = files.SerializedFile(reader, parent, name=name, is_dependency=is_dependency) 147 | elif typ == FileType.BundleFile: 148 | f = files.BundleFile(reader, parent, name=name, is_dependency=is_dependency) 149 | elif typ == FileType.WebFile: 150 | f = files.WebFile(reader, parent, name=name, is_dependency=is_dependency) 151 | else: 152 | f = reader 153 | return f 154 | 155 | 156 | def find_sensitive_path(dir: str, insensitive_path: str) -> Union[str, None]: 157 | parts = os.path.split(insensitive_path.strip(os.path.sep)) 158 | 159 | sensitive_path = dir 160 | for part in parts: 161 | part_lower = part.lower() 162 | part = next( 163 | (name for name in os.listdir(sensitive_path) if name.lower() == part_lower), 164 | None, 165 | ) 166 | if part is None: 167 | return None 168 | sensitive_path = os.path.join(sensitive_path, part) 169 | 170 | return sensitive_path 171 | -------------------------------------------------------------------------------- /UnityPy/files/File.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import namedtuple 4 | from os.path import basename 5 | from typing import TYPE_CHECKING, Dict, Optional 6 | 7 | from ..helpers import ImportHelper 8 | from ..streams import EndianBinaryReader, EndianBinaryWriter 9 | 10 | if TYPE_CHECKING: 11 | from ..environment import Environment 12 | 13 | DirectoryInfo = namedtuple("DirectoryInfo", "path offset size") 14 | 15 | 16 | class File: 17 | name: str 18 | files: Dict[str, File] 19 | environment: Environment 20 | cab_file: str 21 | is_changed: bool 22 | signature: str 23 | packer: str 24 | is_dependency: bool 25 | parent: Optional[File] 26 | 27 | def __init__( 28 | self, 29 | parent: Optional[File] = None, 30 | name: Optional[str] = None, 31 | is_dependency: bool = False, 32 | ): 33 | self.files = {} 34 | self.is_changed = False 35 | self.cab_file = "CAB-UnityPy_Mod.resS" 36 | self.parent = parent 37 | self.environment = ( 38 | getattr(parent, "environment", parent) if parent else None 39 | ) 40 | self.name = basename(name) if isinstance(name, str) else "" 41 | self.is_dependency = is_dependency 42 | 43 | def get_assets(self): 44 | if isinstance(self, SerializedFile.SerializedFile): 45 | return self 46 | 47 | for f in self.files.values(): 48 | if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): 49 | for asset in f.get_assets(): 50 | yield asset 51 | elif isinstance(f, SerializedFile.SerializedFile): 52 | yield f 53 | 54 | def get_filtered_objects(self, obj_types=[]): 55 | if len(obj_types) == 0: 56 | return self.get_objects() 57 | for f in self.files.values(): 58 | if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): 59 | for obj in f.objects: 60 | if obj.type in obj_types: 61 | yield obj 62 | elif isinstance(f, SerializedFile.SerializedFile): 63 | for obj in f.objects.values(): 64 | if obj.type in obj_types: 65 | yield obj 66 | 67 | def get_objects(self): 68 | for f in self.files.values(): 69 | if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): 70 | for obj in f.objects: 71 | yield obj 72 | elif isinstance(f, SerializedFile.SerializedFile): 73 | for obj in f.objects.values(): 74 | yield obj 75 | elif isinstance(f, ObjectReader.ObjectReader): 76 | yield f 77 | 78 | def read_files(self, reader: EndianBinaryReader, files: list): 79 | # read file data and convert it 80 | for node in files: 81 | reader.Position = node.offset 82 | name = node.path 83 | node_reader = EndianBinaryReader( 84 | reader.read(node.size), offset=(reader.BaseOffset + node.offset) 85 | ) 86 | f = ImportHelper.parse_file( 87 | node_reader, self, name, is_dependency=self.is_dependency 88 | ) 89 | 90 | if isinstance(f, (EndianBinaryReader, SerializedFile.SerializedFile)): 91 | if self.environment: 92 | self.environment.register_cab(name, f) 93 | 94 | # required for BundleFiles 95 | f.flags = getattr(node, "flags", 0) 96 | self.files[name] = f 97 | 98 | def get_writeable_cab(self, name: Optional[str] = None): 99 | """ 100 | Creates a new cab file in the bundle that contains the given data. 101 | This is useful for asset types that use resource files. 102 | """ 103 | 104 | if not name: 105 | name = self.cab_file 106 | 107 | if not name: 108 | return None 109 | 110 | if name in self.files: 111 | if isinstance(self.files[name], EndianBinaryWriter): 112 | return self.files[name] 113 | else: 114 | raise ValueError( 115 | "This cab already exists and isn't an EndianBinaryWriter" 116 | ) 117 | 118 | writer = EndianBinaryWriter() 119 | # try to find another resource file to copy the flags from 120 | for fname, f in self.files.items(): 121 | if fname.endswith(".resS"): 122 | writer.flags = f.flags 123 | writer.endian = f.endian 124 | break 125 | else: 126 | writer.flags = 0 127 | writer.name = name 128 | self.files[name] = writer 129 | return writer 130 | 131 | @property 132 | def container(self): 133 | return { 134 | path: obj 135 | for f in self.files.values() 136 | if isinstance(f, File) 137 | for path, obj in f.container.items() 138 | } 139 | 140 | def get(self, key, default=None): 141 | return getattr(self, key, default) 142 | 143 | def keys(self): 144 | return self.files.keys() 145 | 146 | def items(self): 147 | return self.files.items() 148 | 149 | def values(self): 150 | return self.files.values() 151 | 152 | def __getitem__(self, item): 153 | return self.files[item] 154 | 155 | def __repr__(self): 156 | return f"<{self.__class__.__name__}>" 157 | 158 | def mark_changed(self): 159 | if isinstance(self.parent, File): 160 | self.parent.mark_changed() 161 | self.is_changed = True 162 | 163 | 164 | # recursive import requires the import down here 165 | from . import BundleFile, ObjectReader, SerializedFile, WebFile 166 | -------------------------------------------------------------------------------- /UnityPy/helpers/TextureSwizzler.py: -------------------------------------------------------------------------------- 1 | # based on https://github.com/nesrak1/UABEA/blob/master/TexturePlugin/Texture2DSwitchDeswizzler.cs 2 | from typing import Dict, Tuple 3 | 4 | from ..enums import TextureFormat 5 | 6 | GOB_X_TEXEL_COUNT = 4 7 | GOB_Y_TEXEL_COUNT = 8 8 | TEXEL_BYTE_SIZE = 16 9 | TEXELS_IN_GOB = GOB_X_TEXEL_COUNT * GOB_Y_TEXEL_COUNT 10 | GOB_MAP = [ 11 | (((l >> 3) & 0b10) | ((l >> 1) & 0b1), ((l >> 1) & 0b110) | (l & 0b1)) 12 | for l in range(TEXELS_IN_GOB) 13 | ] 14 | 15 | 16 | def ceil_divide(a: int, b: int) -> int: 17 | return (a + b - 1) // b 18 | 19 | 20 | def deswizzle( 21 | data: bytes, 22 | width: int, 23 | height: int, 24 | block_width: int, 25 | block_height: int, 26 | texels_per_block: int, 27 | ) -> bytearray: 28 | block_count_x = ceil_divide(width, block_width) 29 | block_count_y = ceil_divide(height, block_height) 30 | gob_count_x = block_count_x // GOB_X_TEXEL_COUNT 31 | gob_count_y = block_count_y // GOB_Y_TEXEL_COUNT 32 | new_data = bytearray(len(data)) 33 | data_view = memoryview(data) 34 | 35 | for i in range(gob_count_y // texels_per_block): 36 | for j in range(gob_count_x): 37 | base_gob_dst_x = j * 4 38 | for k in range(texels_per_block): 39 | base_gob_dst_y = (i * texels_per_block + k) * GOB_Y_TEXEL_COUNT 40 | for gob_x, gob_y in GOB_MAP: 41 | dst_offset = ( 42 | (base_gob_dst_y + gob_y) * block_count_x 43 | + (base_gob_dst_x + gob_x) 44 | ) * TEXEL_BYTE_SIZE 45 | new_data[dst_offset : dst_offset + TEXEL_BYTE_SIZE] = data_view[ 46 | :TEXEL_BYTE_SIZE 47 | ] 48 | data_view = data_view[TEXEL_BYTE_SIZE:] 49 | return new_data 50 | 51 | 52 | def swizzle( 53 | data: bytes, 54 | width: int, 55 | height: int, 56 | block_width: int, 57 | block_height: int, 58 | texels_per_block: int, 59 | ) -> bytearray: 60 | block_count_x = ceil_divide(width, block_width) 61 | block_count_y = ceil_divide(height, block_height) 62 | gob_count_x = block_count_x // GOB_X_TEXEL_COUNT 63 | gob_count_y = block_count_y // GOB_Y_TEXEL_COUNT 64 | new_data = bytearray(len(data)) 65 | data_view = memoryview(new_data) 66 | 67 | for i in range(gob_count_y // texels_per_block): 68 | for j in range(gob_count_x): 69 | base_gob_dst_x = j * 4 70 | for k in range(texels_per_block): 71 | base_gob_dst_y = (i * texels_per_block + k) * GOB_Y_TEXEL_COUNT 72 | for gob_x, gob_y in GOB_MAP: 73 | src_offset = ( 74 | (base_gob_dst_y + gob_y) * block_count_x 75 | + (base_gob_dst_x + gob_x) 76 | ) * TEXEL_BYTE_SIZE 77 | data_view[:TEXEL_BYTE_SIZE] = data[ 78 | src_offset : src_offset + TEXEL_BYTE_SIZE 79 | ] 80 | data_view = data_view[TEXEL_BYTE_SIZE:] 81 | 82 | return new_data 83 | 84 | 85 | # this should be the amount of pixels that can fit 16 bytes 86 | TEXTUREFORMAT_BLOCK_SIZE_MAP: Dict[TextureFormat, Tuple[int, int]] = { 87 | TextureFormat.Alpha8: (16, 1), # 1 byte per pixel 88 | TextureFormat.ARGB4444: (8, 1), # 2 bytes per pixel 89 | TextureFormat.RGBA32: (4, 1), # 4 bytes per pixel 90 | TextureFormat.ARGB32: (4, 1), # 4 bytes per pixel 91 | TextureFormat.ARGBFloat: (1, 1), # 16 bytes per pixel (?) 92 | TextureFormat.RGB565: (8, 1), # 2 bytes per pixel 93 | TextureFormat.R16: (8, 1), # 2 bytes per pixel 94 | TextureFormat.DXT1: (8, 4), # 8 bytes per 4x4=16 pixels 95 | TextureFormat.DXT5: (4, 4), # 16 bytes per 4x4=16 pixels 96 | TextureFormat.RGBA4444: (8, 1), # 2 bytes per pixel 97 | TextureFormat.BGRA32: (4, 1), # 4 bytes per pixel 98 | TextureFormat.BC6H: (4, 4), # 16 bytes per 4x4=16 pixels 99 | TextureFormat.BC7: (4, 4), # 16 bytes per 4x4=16 pixels 100 | TextureFormat.BC4: (8, 4), # 8 bytes per 4x4=16 pixels 101 | TextureFormat.BC5: (4, 4), # 16 bytes per 4x4=16 pixels 102 | TextureFormat.ASTC_RGB_4x4: (4, 4), # 16 bytes per 4x4=16 pixels 103 | TextureFormat.ASTC_RGB_5x5: (5, 5), # 16 bytes per 5x5=25 pixels 104 | TextureFormat.ASTC_RGB_6x6: (6, 6), # 16 bytes per 6x6=36 pixels 105 | TextureFormat.ASTC_RGB_8x8: (8, 8), # 16 bytes per 8x8=64 pixels 106 | TextureFormat.ASTC_RGB_10x10: (10, 10), # 16 bytes per 10x10=100 pixels 107 | TextureFormat.ASTC_RGB_12x12: (12, 12), # 16 bytes per 12x12=144 pixels 108 | TextureFormat.ASTC_RGBA_4x4: (4, 4), # 16 bytes per 4x4=16 pixels 109 | TextureFormat.ASTC_RGBA_5x5: (5, 5), # 16 bytes per 5x5=25 pixels 110 | TextureFormat.ASTC_RGBA_6x6: (6, 6), # 16 bytes per 6x6=36 pixels 111 | TextureFormat.ASTC_RGBA_8x8: (8, 8), # 16 bytes per 8x8=64 pixels 112 | TextureFormat.ASTC_RGBA_10x10: (10, 10), # 16 bytes per 10x10=100 pixels 113 | TextureFormat.ASTC_RGBA_12x12: (12, 12), # 16 bytes per 12x12=144 pixels 114 | TextureFormat.RG16: (8, 1), # 2 bytes per pixel 115 | TextureFormat.R8: (16, 1), # 1 byte per pixel 116 | } 117 | 118 | 119 | def get_padded_texture_size( 120 | width: int, height: int, block_width: int, block_height: int, texels_per_block: int 121 | ): 122 | width = ( 123 | ceil_divide(width, block_width * GOB_X_TEXEL_COUNT) 124 | * block_width 125 | * GOB_X_TEXEL_COUNT 126 | ) 127 | height = ( 128 | ceil_divide(height, block_height * GOB_Y_TEXEL_COUNT * texels_per_block) 129 | * block_height 130 | * GOB_Y_TEXEL_COUNT 131 | * texels_per_block 132 | ) 133 | return width, height 134 | 135 | 136 | def get_switch_gobs_per_block(platform_blob: bytes) -> int: 137 | return 1 << int.from_bytes(platform_blob[8:12], "little") 138 | -------------------------------------------------------------------------------- /UnityPy/streams/EndianBinaryWriter.py: -------------------------------------------------------------------------------- 1 | from struct import pack 2 | from io import BytesIO, IOBase 3 | 4 | import builtins 5 | from typing import Callable, Sequence, TypeVar, Union 6 | 7 | from ..math import Color, Matrix4x4, Quaternion, Vector2, Vector3, Vector4, Rectangle 8 | 9 | T = TypeVar("T") 10 | 11 | 12 | class EndianBinaryWriter: 13 | endian: str 14 | Position: int 15 | stream: IOBase 16 | 17 | def __init__( 18 | self, 19 | input_: Union[bytes, bytearray, IOBase] = b"", 20 | endian: str = ">" 21 | ): 22 | if isinstance(input_, (bytes, bytearray)): 23 | self.stream = BytesIO(input_) 24 | self.stream.seek(0, 2) 25 | elif isinstance(input_, IOBase): 26 | self.stream = input_ 27 | else: 28 | raise ValueError("Invalid input type - %s." % type(input_)) 29 | self.endian = endian 30 | self.Position = self.stream.tell() 31 | 32 | @property 33 | def bytes(self): 34 | self.stream.seek(0) 35 | return self.stream.read() 36 | 37 | @property 38 | def Length(self) -> int: 39 | pos = self.stream.tell() 40 | self.stream.seek(0, 2) 41 | l = self.stream.tell() 42 | self.stream.seek(pos) 43 | return l 44 | 45 | def dispose(self): 46 | self.stream.close() 47 | 48 | def write(self, *args): 49 | if self.Position != self.stream.tell(): 50 | self.stream.seek(self.Position) 51 | ret = self.stream.write(*args) 52 | self.Position = self.stream.tell() 53 | return ret 54 | 55 | def write_byte(self, value: int): 56 | self.write(pack(self.endian + "b", value)) 57 | 58 | def write_u_byte(self, value: int): 59 | self.write(pack(self.endian + "B", value)) 60 | 61 | def write_bytes(self, value: builtins.bytes): 62 | return self.write(value) 63 | 64 | def write_short(self, value: int): 65 | self.write(pack(self.endian + "h", value)) 66 | 67 | def write_int(self, value: int): 68 | self.write(pack(self.endian + "i", value)) 69 | 70 | def write_long(self, value: int): 71 | self.write(pack(self.endian + "q", value)) 72 | 73 | def write_u_short(self, value: int): 74 | self.write(pack(self.endian + "H", value)) 75 | 76 | def write_u_int(self, value: int): 77 | self.write(pack(self.endian + "I", value)) 78 | 79 | def write_u_long(self, value: int): 80 | self.write(pack(self.endian + "Q", value)) 81 | 82 | def write_float(self, value: float): 83 | self.write(pack(self.endian + "f", value)) 84 | 85 | def write_double(self, value: float): 86 | self.write(pack(self.endian + "d", value)) 87 | 88 | def write_boolean(self, value: bool): 89 | self.write(pack(self.endian + "?", value)) 90 | 91 | def write_string_to_null(self, value: str): 92 | self.write(value.encode("utf8", "surrogateescape")) 93 | self.write(b"\0") 94 | 95 | def write_aligned_string(self, value: str): 96 | bstring = value.encode("utf8", "surrogateescape") 97 | self.write_int(len(bstring)) 98 | self.write(bstring) 99 | self.align_stream(4) 100 | 101 | def align_stream(self, alignment:int = 4): 102 | pos = self.stream.tell() 103 | align = (alignment - pos % alignment) % alignment 104 | self.write(b"\0" * align) 105 | 106 | def write_quaternion(self, value: Quaternion): 107 | self.write_float(value.X) 108 | self.write_float(value.Y) 109 | self.write_float(value.Z) 110 | self.write_float(value.W) 111 | 112 | def write_vector2(self, value: Vector2): 113 | self.write_float(value.X) 114 | self.write_float(value.Y) 115 | 116 | def write_vector3(self, value: Vector3): 117 | self.write_float(value.X) 118 | self.write_float(value.Y) 119 | self.write_float(value.Z) 120 | 121 | def write_vector4(self, value: Vector4): 122 | self.write_float(value.X) 123 | self.write_float(value.Y) 124 | self.write_float(value.Z) 125 | self.write_float(value.W) 126 | 127 | def write_rectangle_f(self, value: Rectangle): 128 | self.write_float(value.x) 129 | self.write_float(value.y) 130 | self.write_float(value.width) 131 | self.write_float(value.height) 132 | 133 | def write_color_uint(self, value: Color): 134 | self.write_u_byte(int(value.R * 255)) 135 | self.write_u_byte(int(value.G * 255)) 136 | self.write_u_byte(int(value.B * 255)) 137 | self.write_u_byte(int(value.A * 255)) 138 | 139 | def write_color4(self, value: Color): 140 | self.write_float(value.R) 141 | self.write_float(value.G) 142 | self.write_float(value.B) 143 | self.write_float(value.A) 144 | 145 | def write_matrix(self, value: Matrix4x4): 146 | for val in value.M: 147 | self.write_float(val) 148 | 149 | def write_array(self, command: Callable[[T], None], value: Sequence[T], write_length: bool = True): 150 | if write_length: 151 | self.write_int(len(value)) 152 | for val in value: 153 | command(val) 154 | 155 | def write_byte_array(self, value: builtins.bytes): 156 | self.write_int(len(value)) 157 | self.write(value) 158 | 159 | def write_boolean_array(self, value: Sequence[bool]): 160 | self.write_array(self.write_boolean, value) 161 | 162 | def write_u_short_array(self, value: Sequence[int]): 163 | self.write_array(self.write_u_short, value) 164 | 165 | def write_int_array(self, value: Sequence[int], write_length: bool = False): 166 | return self.write_array(self.write_int, value, write_length) 167 | 168 | def write_u_int_array(self, value: Sequence[int], write_length: bool = False): 169 | return self.write_array(self.write_u_int, value, write_length) 170 | 171 | def write_float_array(self, value: Sequence[float], write_length: bool = False): 172 | return self.write_array(self.write_float, value, write_length) 173 | 174 | def write_string_array(self, value: Sequence[str]): 175 | self.write_array(self.write_aligned_string, value) 176 | 177 | def write_vector2_array(self, value: Sequence[Vector2]): 178 | self.write_array(self.write_vector2, value) 179 | 180 | def write_vector4_array(self, value: Sequence[Vector4]): 181 | self.write_array(self.write_vector4, value) 182 | 183 | def write_matrix_array(self, value: Sequence[Matrix4x4]): 184 | self.write_array(self.write_matrix, value) 185 | -------------------------------------------------------------------------------- /tests/test_typetree.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import math 3 | import os 4 | import random 5 | from typing import List, Tuple, TypeVar, Union, Type 6 | 7 | import psutil 8 | 9 | from UnityPy.classes.generated import GameObject 10 | from UnityPy.helpers.Tpk import get_typetree_node 11 | from UnityPy.helpers.TypeTreeHelper import read_typetree, write_typetree 12 | from UnityPy.helpers.TypeTreeNode import TypeTreeNode 13 | from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter 14 | 15 | PROCESS = psutil.Process(os.getpid()) 16 | 17 | 18 | def get_memory(): 19 | gc.collect() 20 | return PROCESS.memory_info().rss 21 | 22 | 23 | def check_leak(func): 24 | def wrapper(*args, **kwargs): 25 | mem_0 = get_memory() 26 | func(*args, **kwargs) 27 | mem_1 = get_memory() 28 | diff = mem_1 - mem_0 29 | if diff != 0: 30 | diff %= 4096 31 | assert diff == 0, f"Memory leak in {func.__name__}" 32 | 33 | return wrapper 34 | 35 | 36 | TEST_NODE_STR = "TestNode" 37 | 38 | 39 | @check_leak 40 | def test_typetreenode(): 41 | TypeTreeNode( 42 | m_Level=0, m_Type=TEST_NODE_STR, m_Name=TEST_NODE_STR, m_ByteSize=0, m_Version=0 43 | ) 44 | 45 | 46 | def generate_dummy_node(typ: str, name: str = ""): 47 | return TypeTreeNode(m_Level=0, m_Type=typ, m_Name=name, m_ByteSize=0, m_Version=0) 48 | 49 | 50 | SIMPLE_NODE_SAMPLES = [ 51 | (["SInt8"], int, (-(2**7), 2**7)), 52 | (["SInt16", "short"], int, (-(2**15), 2**15)), 53 | (["SInt32", "int"], int, (-(2**31), 2**31)), 54 | (["SInt64", "long long"], int, (-(2**63), 2**63)), 55 | (["UInt8", "char"], int, (0, 2**8)), 56 | (["UInt16", "unsigned short"], int, (0, 2**16)), 57 | (["UInt32", "unsigned int", "Type*"], int, (0, 2**32)), 58 | (["UInt64", "unsigned long long", "FileSize"], int, (0, 2**64)), 59 | (["float"], float, (-1, 1)), 60 | (["double"], float, (-1, 1)), 61 | (["bool"], bool, (False, True)), 62 | ] 63 | 64 | T = TypeVar("T") 65 | 66 | INT_BYTESIZE_MAP = { 67 | 1: "b", 68 | 2: "h", 69 | 4: "i", 70 | 8: "q", 71 | } 72 | 73 | 74 | def generate_sample_data( 75 | u_type: List[str], 76 | py_typ: Type[Union[int, float, str]], 77 | bounds: Tuple[T, T], 78 | count: int = 10, 79 | ) -> List[T]: 80 | if py_typ is int: 81 | if bounds[0] < 0: 82 | # signed 83 | byte_size = math.log2(bounds[1]) + 1 84 | signed = True 85 | elif bounds[0] == 0: 86 | # unsigned 87 | byte_size = math.log2(bounds[1]) 88 | signed = False 89 | 90 | byte_size = round(byte_size / 8) 91 | char = INT_BYTESIZE_MAP[byte_size] 92 | if not signed: 93 | char = char.upper() 94 | 95 | sample_values = [ 96 | bounds[0], 97 | *[random.randint(bounds[0], bounds[1] - 1) for _ in range(count)], 98 | bounds[1] - 1, 99 | ] 100 | # sample_data = pack(f"<{count+2}{char}", *sample_values) 101 | 102 | elif py_typ is float: 103 | sample_values = [ 104 | bounds[0], 105 | *[random.uniform(bounds[0], bounds[1]) for _ in range(count)], 106 | bounds[1], 107 | ] 108 | char = "f" if u_type == "float" else "d" 109 | # sample_data = pack(f"<{count+2}f", *sample_values) 110 | 111 | elif py_typ is bool: 112 | sample_values = [ 113 | bounds[0], 114 | *[random.choice([True, False]) for _ in range(count)], 115 | bounds[1], 116 | ] 117 | # sample_data = pack(f"<{count+2}?", *sample_values) 118 | 119 | elif py_typ is str: 120 | raise NotImplementedError("String generation not implemented") 121 | 122 | elif py_typ is bytes: 123 | raise NotImplementedError("Bytes generation not implemented") 124 | 125 | return sample_values 126 | 127 | 128 | def _test_read_typetree(node: TypeTreeNode, data: bytes, as_dict: bool): 129 | reader = EndianBinaryReader(data, "<") 130 | py_values = read_typetree(node, reader, as_dict=as_dict, check_read=False) 131 | reader.Position = 0 132 | cpp_values = read_typetree(node, reader, as_dict=as_dict, byte_size=len(data)) 133 | assert py_values == cpp_values 134 | return py_values 135 | 136 | 137 | @check_leak 138 | def test_simple_nodes(): 139 | for typs, py_typ, bounds in SIMPLE_NODE_SAMPLES: 140 | values = generate_sample_data(typs, py_typ, bounds) 141 | for typ in typs: 142 | node = generate_dummy_node(typ) 143 | for value in values: 144 | writer = EndianBinaryWriter(b"", "<") 145 | write_typetree(value, node, writer) 146 | raw = writer.bytes 147 | re_value = _test_read_typetree(node, raw, as_dict=True) 148 | assert ( 149 | abs(value - re_value) < 1e-5 150 | ), f"Failed on {typ}: {value} != {re_value}" 151 | 152 | 153 | @check_leak 154 | def test_simple_nodes_array(): 155 | def generate_list_node(item_node: TypeTreeNode): 156 | root = generate_dummy_node("root", "root") 157 | array = generate_dummy_node("Array", "Array") 158 | array.m_Children = [None, item_node] 159 | root.m_Children = [array] 160 | return root 161 | 162 | for typs, py_typ, bounds in SIMPLE_NODE_SAMPLES: 163 | values = generate_sample_data(typs, py_typ, bounds) 164 | for typ in typs: 165 | node = generate_dummy_node(typ) 166 | array_node = generate_list_node(node) 167 | writer = EndianBinaryWriter(b"", "<") 168 | write_typetree(values, array_node, writer) 169 | raw = writer.bytes 170 | re_values = _test_read_typetree(array_node, raw, as_dict=True) 171 | assert all( 172 | (abs(value - re_value) < 1e-5) 173 | for value, re_value in zip(values, re_values) 174 | ), f"Failed on {typ}: {values} != {re_values}" 175 | 176 | 177 | TEST_CLASS_NODE = get_typetree_node(1, (5, 0, 0, 0)) 178 | TEST_CLASS_NODE_OBJ = GameObject( 179 | m_Component=[], m_IsActive=True, m_Layer=0, m_Name="TestObject", m_Tag=0 180 | ) 181 | TEST_CLASS_NODE_DICT = TEST_CLASS_NODE_OBJ.__dict__ 182 | 183 | 184 | def test_class_node_dict(): 185 | writer = EndianBinaryWriter(b"", "<") 186 | write_typetree(TEST_CLASS_NODE_DICT, TEST_CLASS_NODE, writer) 187 | raw = writer.bytes 188 | re_value = _test_read_typetree(TEST_CLASS_NODE, raw, as_dict=True) 189 | assert re_value == TEST_CLASS_NODE_DICT 190 | 191 | 192 | def test_class_node_clz(): 193 | writer = EndianBinaryWriter(b"", "<") 194 | write_typetree(TEST_CLASS_NODE_OBJ, TEST_CLASS_NODE, writer) 195 | raw = writer.bytes 196 | re_value = _test_read_typetree(TEST_CLASS_NODE, raw, as_dict=False) 197 | assert re_value == TEST_CLASS_NODE_OBJ 198 | 199 | 200 | def test_node_from_list_clz(): 201 | node = TypeTreeNode.from_list(list(TEST_CLASS_NODE.traverse())) 202 | assert node == TEST_CLASS_NODE 203 | 204 | 205 | def test_node_from_list_dict(): 206 | node = TypeTreeNode.from_list(TEST_CLASS_NODE.to_dict_list()) 207 | assert node == TEST_CLASS_NODE 208 | 209 | 210 | if __name__ == "__main__": 211 | for x in list(locals()): 212 | if str(x)[:4] == "test": 213 | locals()[x]() 214 | input("All Tests Passed") 215 | -------------------------------------------------------------------------------- /UnityPy/helpers/CompressionHelper.py: -------------------------------------------------------------------------------- 1 | import brotli 2 | import gzip 3 | import lzma 4 | import lz4.block 5 | import struct 6 | from typing import Tuple 7 | 8 | GZIP_MAGIC: bytes = b"\x1f\x8b" 9 | BROTLI_MAGIC: bytes = b"brotli" 10 | 11 | 12 | # LZMA 13 | def decompress_lzma(data: bytes, read_decompressed_size: bool = False) -> bytes: 14 | """decompresses lzma-compressed data 15 | 16 | :param data: compressed data 17 | :type data: bytes 18 | :raises _lzma.LZMAError: Compressed data ended before the end-of-stream marker was reached 19 | :return: uncompressed data 20 | :rtype: bytes 21 | """ 22 | props, dict_size = struct.unpack(" bytes: 44 | """compresses data via lzma (unity specific) 45 | The current static settings may not be the best solution, 46 | but they are the most commonly used values and should therefore be enough for the time being. 47 | 48 | :param data: uncompressed data 49 | :type data: bytes 50 | :return: compressed data 51 | :rtype: bytes 52 | """ 53 | dict_size = 0x800000 # 1 << 23 54 | compressor = lzma.LZMACompressor( 55 | format=lzma.FORMAT_RAW, 56 | filters=[ 57 | { 58 | "id": lzma.FILTER_LZMA1, 59 | "dict_size": dict_size, 60 | "lc": 3, 61 | "lp": 0, 62 | "pb": 2, 63 | "mode": lzma.MODE_NORMAL, 64 | "mf": lzma.MF_BT4, 65 | "nice_len": 123, 66 | } 67 | ], 68 | ) 69 | 70 | compressed_data = compressor.compress(data) + compressor.flush() 71 | cdl = len(compressed_data) 72 | if write_decompressed_size: 73 | return struct.pack(f" bytes: # LZ4M/LZ4HC 80 | """decompresses lz4-compressed data 81 | 82 | :param data: compressed data 83 | :type data: bytes 84 | :param uncompressed_size: size of the uncompressed data 85 | :type uncompressed_size: int 86 | :raises _block.LZ4BlockError: Decompression failed: corrupt input or insufficient space in destination buffer. 87 | :return: uncompressed data 88 | :rtype: bytes 89 | """ 90 | return lz4.block.decompress(data, uncompressed_size) 91 | 92 | 93 | def compress_lz4(data: bytes) -> bytes: # LZ4M/LZ4HC 94 | """compresses data via lz4.block 95 | 96 | :param data: uncompressed data 97 | :type data: bytes 98 | :return: compressed data 99 | :rtype: bytes 100 | """ 101 | return lz4.block.compress( 102 | data, mode="high_compression", compression=9, store_size=False 103 | ) 104 | 105 | 106 | # Brotli 107 | def decompress_brotli(data: bytes) -> bytes: 108 | """decompresses brotli-compressed data 109 | 110 | :param data: compressed data 111 | :type data: bytes 112 | :raises brotli.error: BrotliDecompress failed 113 | :return: uncompressed data 114 | :rtype: bytes 115 | """ 116 | return brotli.decompress(data) 117 | 118 | 119 | def compress_brotli(data: bytes) -> bytes: 120 | """compresses data via brotli 121 | 122 | :param data: uncompressed data 123 | :type data: bytes 124 | :return: compressed data 125 | :rtype: bytes 126 | """ 127 | return brotli.compress(data) 128 | 129 | 130 | # GZIP 131 | def decompress_gzip(data: bytes) -> bytes: 132 | """decompresses gzip-compressed data 133 | 134 | :param data: compressed data 135 | :type data: bytes 136 | :raises OSError: Not a gzipped file 137 | :return: uncompressed data 138 | :rtype: bytes 139 | """ 140 | return gzip.decompress(data) 141 | 142 | 143 | def compress_gzip(data: bytes) -> bytes: 144 | """compresses data via gzip 145 | The current static settings may not be the best solution, 146 | but they are the most commonly used values and should therefore be enough for the time being. 147 | 148 | :param data: uncompressed data 149 | :type data: bytes 150 | :return: compressed data 151 | :rtype: bytes 152 | """ 153 | return gzip.compress(data) 154 | 155 | 156 | def chunk_based_compress(data: bytes, block_info_flag: int) -> Tuple[bytes, list]: 157 | """compresses AssetBundle data based on the block_info_flag 158 | LZ4/LZ4HC will be chunk-based compression 159 | 160 | :param data: uncompressed data 161 | :type data: bytes 162 | :param block_info_flag: block info flag 163 | :type block_info_flag: int 164 | :return: compressed data and block info 165 | :rtype: tuple 166 | """ 167 | switch = block_info_flag & 0x3F 168 | if switch == 0: # NONE 169 | return data, [(len(data), len(data), block_info_flag)] 170 | elif switch == 1: # LZMA 171 | chunk_size = 0xFFFFFFFF 172 | compress_func = compress_lzma 173 | elif switch in [2, 3]: # LZ4 174 | chunk_size = 0x00020000 175 | compress_func = compress_lz4 176 | elif switch == 4: # LZHAM 177 | raise NotImplementedError 178 | block_info = [] 179 | uncompressed_data_size = len(data) 180 | compressed_file_data = bytearray() 181 | p = 0 182 | while uncompressed_data_size > chunk_size: 183 | compressed_data = compress_func(data[p : p + chunk_size]) 184 | if len(compressed_data) > chunk_size: 185 | compressed_file_data.extend(data[p : p + chunk_size]) 186 | block_info.append( 187 | ( 188 | chunk_size, 189 | chunk_size, 190 | block_info_flag ^ switch, 191 | ) 192 | ) 193 | else: 194 | compressed_file_data.extend(compressed_data) 195 | block_info.append( 196 | ( 197 | chunk_size, 198 | len(compressed_data), 199 | block_info_flag, 200 | ) 201 | ) 202 | p += chunk_size 203 | uncompressed_data_size -= chunk_size 204 | if uncompressed_data_size > 0: 205 | compressed_data = compress_func(data[p:]) 206 | if len(compressed_data) > uncompressed_data_size: 207 | compressed_file_data.extend(data[p:]) 208 | block_info.append( 209 | ( 210 | uncompressed_data_size, 211 | uncompressed_data_size, 212 | block_info_flag ^ switch, 213 | ) 214 | ) 215 | else: 216 | compressed_file_data.extend(compressed_data) 217 | block_info.append( 218 | ( 219 | uncompressed_data_size, 220 | len(compressed_data), 221 | block_info_flag, 222 | ) 223 | ) 224 | return bytes(compressed_file_data), block_info 225 | -------------------------------------------------------------------------------- /UnityPy/export/MeshRendererExporter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, List, Optional 4 | 5 | from ..classes.generated import SkinnedMeshRenderer 6 | from .MeshExporter import export_mesh_obj 7 | 8 | if TYPE_CHECKING: 9 | from ..classes import ( 10 | Material, 11 | Mesh, 12 | MeshFilter, 13 | PPtr, 14 | Renderer, 15 | StaticBatchInfo, 16 | Texture2D, 17 | ) 18 | 19 | class Renderer(Renderer): 20 | m_Materials: Optional[list[PPtr[Material]]] = None 21 | m_StaticBatchInfo: Optional[StaticBatchInfo] = None 22 | m_SubsetIndices: Optional[List[int]] = None 23 | 24 | 25 | def get_mesh(meshR: Renderer) -> Optional[Mesh]: 26 | if isinstance(meshR, SkinnedMeshRenderer): 27 | if meshR.m_Mesh: 28 | return meshR.m_Mesh.read() 29 | else: 30 | m_GameObject = meshR.m_GameObject.read() 31 | for comp in m_GameObject.m_Component: 32 | if isinstance(comp, tuple): 33 | pptr = comp[1] 34 | else: 35 | pptr = comp.component 36 | if not pptr: 37 | continue 38 | obj = pptr.deref() 39 | if not obj: 40 | continue 41 | if obj.type.name == "MeshFilter": 42 | filter: MeshFilter = pptr.read() 43 | if filter.m_Mesh: 44 | return filter.m_Mesh.read() 45 | return None 46 | 47 | 48 | def export_mesh_renderer(renderer: Renderer, export_dir: str) -> None: 49 | env = renderer.object_reader.assets_file.environment 50 | env.fs.makedirs(export_dir, exist_ok=True) 51 | mesh = get_mesh(renderer) 52 | if not mesh: 53 | return 54 | 55 | firstSubMesh = 0 56 | if ( 57 | hasattr(renderer, "m_StaticBatchInfo") 58 | and renderer.m_StaticBatchInfo.subMeshCount > 0 59 | ): 60 | firstSubMesh = renderer.m_StaticBatchInfo.firstSubMesh 61 | elif hasattr(renderer, "m_SubsetIndices"): 62 | firstSubMesh = min(renderer.m_SubsetIndices) 63 | 64 | materials = [] 65 | material_names = [] 66 | for i, submesh in enumerate(mesh.m_SubMeshes): 67 | mat_index = i - firstSubMesh 68 | if mat_index < 0 or mat_index >= len(renderer.m_Materials): 69 | continue 70 | matPtr: Optional[PPtr[Material]] = renderer.m_Materials[i - firstSubMesh] 71 | if matPtr: 72 | mat: Material = matPtr.read() 73 | else: 74 | material_names.append(None) 75 | continue 76 | materials.append(export_material(mat)) 77 | material_names.append(mat.m_Name) 78 | # save material textures 79 | for key, texEnv in mat.m_SavedProperties.m_TexEnvs: 80 | if not texEnv.m_Texture: 81 | continue 82 | if not isinstance(key, str): 83 | # FastPropertyName 84 | key = key.name 85 | tex: Texture2D = texEnv.m_Texture.read() 86 | texName = f"{tex.m_Name if tex.m_Name else key}.png" 87 | with env.fs.open(env.fs.sep.join([export_dir, texName]), "wb") as f: 88 | tex.read().image.save(f) 89 | 90 | # save .obj 91 | with env.fs.open( 92 | env.fs.sep.join([export_dir, f"{mesh.m_Name}.obj"]), 93 | "wt", 94 | encoding="utf8", 95 | newline="", 96 | ) as f: 97 | f.write(export_mesh_obj(mesh, material_names)) 98 | 99 | # save .mtl 100 | with env.fs.open( 101 | env.fs.sep.join([export_dir, f"{mesh.m_Name}.mtl"]), 102 | "wt", 103 | encoding="utf8", 104 | newline="", 105 | ) as f: 106 | f.write("\n".join(materials)) 107 | 108 | 109 | def export_material(mat: Material) -> str: 110 | """Creates a material file (.mtl) for the given material.""" 111 | 112 | def clt(color): # color to tuple 113 | return ( 114 | color if isinstance(color, tuple) else (color.R, color.G, color.B, color.A) 115 | ) 116 | 117 | def properties_to_dict(properties): 118 | return { 119 | k if isinstance(k, str) else k.name: v 120 | for k, v in properties 121 | if v is not None 122 | } 123 | 124 | colors = properties_to_dict(mat.m_SavedProperties.m_Colors) 125 | floats = properties_to_dict(mat.m_SavedProperties.m_Floats) 126 | texEnvs = properties_to_dict(mat.m_SavedProperties.m_TexEnvs) 127 | 128 | diffuse = clt(colors.get("_Color", (0.8, 0.8, 0.8, 1))) 129 | ambient = clt(colors.get("_SColor", (0.2, 0.2, 0.2, 1))) 130 | # emissive = clt(colors.get("_EmissionColor", (0, 0, 0, 1))) 131 | specular = clt(colors.get("_SpecularColor", (0.2, 0.2, 0.2, 1))) 132 | # reflection = clt(colors.get("_ReflectColor", (0, 0, 0, 1))) 133 | shininess = floats.get("_Shininess", 20.0) 134 | transparency = floats.get("_Transparency", 0.0) 135 | 136 | sb: List[str] = [] 137 | sb.append(f"newmtl {mat.m_Name}") 138 | # Ka r g b 139 | # defines the ambient color of the material to be (r,g,b). The default is (0.2,0.2,0.2); 140 | sb.append(f"Ka {ambient[0]:.4f} {ambient[1]:.4f} {ambient[2]:.4f}") 141 | # Kd r g b 142 | # defines the diffuse color of the material to be (r,g,b). The default is (0.8,0.8,0.8); 143 | sb.append(f"Kd {diffuse[0]:.4f} {diffuse[1]:.4f} {diffuse[2]:.4f}") 144 | # Ks r g b 145 | # defines the specular color of the material to be (r,g,b). This color shows up in highlights. The default is (1.0,1.0,1.0); 146 | sb.append(f"Ks {specular[0]:.4f} {specular[1]:.4f} {specular[2]:.4f}") 147 | # d alpha 148 | # defines the non-transparency of the material to be alpha. The default is 1.0 (not transparent at all). The quantities d and Tr are the opposites of each other, and specifying transparency or nontransparency is simply a matter of user convenience. 149 | # Tr alpha 150 | # defines the transparency of the material to be alpha. The default is 0.0 (not transparent at all). The quantities d and Tr are the opposites of each other, and specifying transparency or nontransparency is simply a matter of user convenience. 151 | sb.append(f"Tr {transparency:.4f}") 152 | # Ns s 153 | # defines the shininess of the material to be s. The default is 0.0; 154 | sb.append(f"Ns {shininess:.4f}") 155 | # illum n 156 | # denotes the illumination model used by the material. illum = 1 indicates a flat material with no specular highlights, so the value of Ks is not used. illum = 2 denotes the presence of specular highlights, and so a specification for Ks is required. 157 | # map_Ka filename 158 | # names a file containing a texture map, which should just be an ASCII dump of RGB values; 159 | texName = None 160 | tex = None 161 | for key, texEnv in texEnvs: 162 | if not texEnv.m_Texture: 163 | continue 164 | if not isinstance(key, str): 165 | # FastPropertyName 166 | key = key.name 167 | 168 | tex: Texture2D = texEnv.m_Texture.read() 169 | texName = f"{tex.m_Name if tex.m_Name else key}.png" 170 | if key == "_MainTex": 171 | sb.append(f"map_Kd {texName}") 172 | elif key == "_BumpMap": 173 | # TODO: bump is default, some use map_bump 174 | sb.append(f"map_bump {texName}") 175 | sb.append(f"bump {texName}") 176 | elif "Specular" in key: 177 | sb.append(f"map_Ks {texName}") 178 | elif "Normal" in key: 179 | # TODO: figure out the key 180 | pass 181 | ret = "\n".join(sb) 182 | return ret 183 | -------------------------------------------------------------------------------- /UnityPyBoost/Mesh.cpp: -------------------------------------------------------------------------------- 1 | #include "Mesh.hpp" 2 | #include 3 | #include 4 | 5 | #define MAX(x, y) (((x) > (y)) ? (x) : (y)) 6 | 7 | enum VertexFormat 8 | { 9 | kVertexFormatFloat, 10 | kVertexFormatFloat16, 11 | kVertexFormatUNorm8, 12 | kVertexFormatSNorm8, 13 | kVertexFormatUNorm16, 14 | kVertexFormatSNorm16, 15 | kVertexFormatUInt8, 16 | kVertexFormatSInt8, 17 | kVertexFormatUInt16, 18 | kVertexFormatSInt16, 19 | kVertexFormatUInt32, 20 | kVertexFormatSInt32 21 | }; 22 | 23 | PyObject *unpack_vertexdata(PyObject *self, PyObject *args) 24 | { 25 | // define vars 26 | int componentByteSize; 27 | uint32_t m_VertexCount; 28 | uint8_t swap; 29 | // char format; 30 | Py_buffer vertexDataView; 31 | uint32_t m_StreamOffset; 32 | uint32_t m_StreamStride; 33 | uint32_t m_ChannelOffset; 34 | uint32_t m_ChannelDimension; 35 | 36 | if (!PyArg_ParseTuple(args, "y*iIIIIIb", &vertexDataView, &componentByteSize, &m_VertexCount, &m_StreamOffset, &m_StreamStride, &m_ChannelOffset, &m_ChannelDimension, &swap)) 37 | { 38 | if (vertexDataView.buf) 39 | { 40 | PyBuffer_Release(&vertexDataView); 41 | } 42 | return NULL; 43 | } 44 | 45 | uint8_t *vertexData = (uint8_t *)vertexDataView.buf; 46 | 47 | Py_ssize_t componentBytesLength = m_VertexCount * m_ChannelDimension * componentByteSize; 48 | 49 | // check if max values are ok 50 | uint32_t maxVertexDataAccess = (m_VertexCount - 1) * m_StreamStride + m_ChannelOffset + m_StreamOffset + componentByteSize * (m_ChannelDimension - 1) + componentByteSize; 51 | if (maxVertexDataAccess > vertexDataView.len) 52 | { 53 | PyBuffer_Release(&vertexDataView); 54 | PyErr_SetString(PyExc_ValueError, "Vertex data access out of bounds"); 55 | return NULL; 56 | } 57 | 58 | PyObject *res = PyBytes_FromStringAndSize(nullptr, componentBytesLength); 59 | if (!res) 60 | { 61 | PyBuffer_Release(&vertexDataView); 62 | return NULL; 63 | } 64 | uint8_t *componentBytes = (uint8_t *)PyBytes_AS_STRING(res); 65 | 66 | for (uint32_t v = 0; v < m_VertexCount; v++) 67 | { 68 | uint32_t vertexOffset = m_StreamOffset + m_ChannelOffset + m_StreamStride * v; 69 | for (uint32_t d = 0; d < m_ChannelDimension; d++) 70 | { 71 | uint32_t vertexDataOffset = vertexOffset + componentByteSize * d; 72 | uint32_t componentOffset = componentByteSize * (v * m_ChannelDimension + d); 73 | memcpy(componentBytes + componentOffset, vertexData + vertexDataOffset, componentByteSize); 74 | } 75 | } 76 | 77 | if (swap) // swap bytes 78 | { 79 | if (componentByteSize == 2) 80 | { 81 | uint16_t *componentUints = (uint16_t *)componentBytes; 82 | for (uint32_t i = 0; i < componentBytesLength; i += 2) 83 | { 84 | swap_any_inplace(componentUints++); 85 | } 86 | } 87 | else if (componentByteSize == 4) 88 | { 89 | 90 | uint32_t *componentUints = (uint32_t *)componentBytes; 91 | for (uint32_t i = 0; i < componentBytesLength; i += 4) 92 | { 93 | swap_any_inplace(componentUints++); 94 | } 95 | } 96 | } 97 | 98 | PyBuffer_Release(&vertexDataView); 99 | return res; 100 | 101 | // fast enough in Python 102 | // uint32_t itemCount = componentBytesLength / componentByteSize; 103 | // PyObject *lst = PyList_New(itemCount); 104 | // if (!lst) 105 | // return NULL; 106 | 107 | // switch (format) 108 | // { 109 | // case kVertexFormatFloat: 110 | // { 111 | // float *items = (float *)componentBytes; 112 | // for (uint32_t i = 0; i < itemCount; i++) 113 | // { 114 | // PyList_SetItem(lst, i, PyFloat_FromDouble((double)*items++)); 115 | // } 116 | // // result[i] = BitConverter.ToSingle(inputBytes, i * 4); 117 | // break; 118 | // } 119 | // case kVertexFormatFloat16: 120 | // { 121 | // uint16_t *items = (uint16_t *)componentBytes; 122 | // for (uint32_t i = 0; i < itemCount; i++) 123 | // { 124 | // double x = _PyFloat_Unpack2(items++, 0); 125 | // if (x == -1.0 && PyErr_Occurred()) 126 | // { 127 | // return NULL; 128 | // } 129 | // PyList_SetItem(lst, i, PyFloat_FromDouble(x)); 130 | // } 131 | // // result[i] = Half.ToHalf(inputBytes, i * 2); 132 | // break; 133 | // } 134 | // case kVertexFormatUNorm8: 135 | // { 136 | // uint8_t *items = componentBytes; 137 | // for (uint32_t i = 0; i < itemCount; i++) 138 | // { 139 | // PyList_SetItem(lst, i, PyFloat_FromDouble((double)(*items++ / 255.0f))); 140 | // } 141 | // // result[i] = inputBytes[i] / 255f; 142 | // break; 143 | // } 144 | // case kVertexFormatSNorm8: 145 | // { 146 | // int8_t *items = (int8_t *)componentBytes; 147 | // for (uint32_t i = 0; i < itemCount; i++) 148 | // { 149 | // PyList_SetItem(lst, i, PyFloat_FromDouble((double)MAX((*items++ / 127.0f), -1.0f))); 150 | // } 151 | // // result[i] = Math.Max((sbyte)inputBytes[i] / 127f, -1f); 152 | // break; 153 | // } 154 | // case kVertexFormatUNorm16: 155 | // { 156 | // uint16_t *items = (uint16_t *)componentBytes; 157 | // for (uint32_t i = 0; i < itemCount; i++) 158 | // { 159 | // PyList_SetItem(lst, i, PyFloat_FromDouble((double)(*items++ / 65535.0f))); 160 | // } 161 | // // result[i] = BitConverter.ToUInt16(inputBytes, i * 2) / 65535f; 162 | // break; 163 | // } 164 | // case kVertexFormatSNorm16: 165 | // { 166 | // int16_t *items = (int16_t *)componentBytes; 167 | // for (uint32_t i = 0; i < itemCount; i++) 168 | // { 169 | // PyList_SetItem(lst, i, PyFloat_FromDouble((double)MAX((*items++ / 32767.0f), -1.0f))); 170 | // } 171 | // // result[i] = Math.Max(BitConverter.ToInt16(inputBytes, i * 2) / 32767f, -1f); 172 | // break; 173 | // } 174 | // case kVertexFormatUInt8: 175 | // case kVertexFormatSInt8: 176 | // { 177 | // uint8_t *items = componentBytes; 178 | // for (uint32_t i = 0; i < itemCount; i++) 179 | // { 180 | // PyList_SetItem(lst, i, PyLong_FromUnsignedLong((uint32_t)*items++)); 181 | // } 182 | // // result[i] = inputBytes[i]; 183 | // break; 184 | // } 185 | // case kVertexFormatUInt16: 186 | // case kVertexFormatSInt16: 187 | // { 188 | // uint16_t *items = (uint16_t *)componentBytes; 189 | // for (uint32_t i = 0; i < itemCount; i++) 190 | // { 191 | // PyList_SetItem(lst, i, PyLong_FromUnsignedLong((uint32_t)*items++)); 192 | // } 193 | // // result[i] = BitConverter.ToInt16(inputBytes, i * 2); 194 | // break; 195 | // } 196 | // case kVertexFormatUInt32: 197 | // case kVertexFormatSInt32: 198 | // { 199 | // uint32_t *items = (uint32_t *)componentBytes; 200 | // for (uint32_t i = 0; i < itemCount; i++) 201 | // { 202 | // PyList_SetItem(lst, i, PyLong_FromUnsignedLong(*items++)); 203 | // } 204 | // // result[i] = BitConverter.ToInt32(inputBytes, i * 4); 205 | // break; 206 | // } 207 | // } 208 | // free(componentBytes); 209 | // return lst; 210 | } 211 | -------------------------------------------------------------------------------- /UnityPy/math/Matrix4x4.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import MutableSequence, Sequence, Union 3 | from .Vector3 import Vector3 4 | 5 | 6 | @dataclass 7 | class Matrix4x4: 8 | M: MutableSequence[float] 9 | 10 | def __init__(self, values: Sequence[Union[int, float]]): 11 | if not isinstance(values, Sequence) or len(values) != 16: 12 | raise ValueError("Values must be a sequence with 16 elements.") 13 | if not all(isinstance(v, (int, float)) for v in values): 14 | raise TypeError("All values must be numeric.") 15 | self.M = [float(v) for v in values] 16 | 17 | def __getitem__(self, index): 18 | if isinstance(index, tuple): 19 | row, col = index 20 | if not (0 <= row < 4 and 0 <= col < 4): 21 | raise IndexError("Row and column indices must in range [0, 3].") 22 | index = row + col * 4 23 | if not (0 <= index < 16): 24 | raise IndexError("Index out of range for Matrix4x4.") 25 | return self.M[index] 26 | 27 | def __setitem__(self, index, value): 28 | if isinstance(index, tuple): 29 | row, col = index 30 | if not (0 <= row < 4 and 0 <= col < 4): 31 | raise IndexError("Row and column indices must in range [0, 3].") 32 | index = row + col * 4 33 | if not (0 <= index < 16): 34 | raise IndexError("Index out of range for Matrix4x4.") 35 | self.M[index] = value 36 | 37 | def __eq__(self, other): 38 | if not isinstance(other, Matrix4x4): 39 | return False 40 | return all(abs(a - b) < 1e-6 for a, b in zip(self.M, other.M)) 41 | 42 | def __mul__(lhs, rhs): 43 | res = Matrix4x4([0] * 16) 44 | res.M00 = ( 45 | lhs.M00 * rhs.M00 46 | + lhs.M01 * rhs.M10 47 | + lhs.M02 * rhs.M20 48 | + lhs.M03 * rhs.M30 49 | ) 50 | res.M01 = ( 51 | lhs.M00 * rhs.M01 52 | + lhs.M01 * rhs.M11 53 | + lhs.M02 * rhs.M21 54 | + lhs.M03 * rhs.M31 55 | ) 56 | res.M02 = ( 57 | lhs.M00 * rhs.M02 58 | + lhs.M01 * rhs.M12 59 | + lhs.M02 * rhs.M22 60 | + lhs.M03 * rhs.M32 61 | ) 62 | res.M03 = ( 63 | lhs.M00 * rhs.M03 64 | + lhs.M01 * rhs.M13 65 | + lhs.M02 * rhs.M23 66 | + lhs.M03 * rhs.M33 67 | ) 68 | 69 | res.M10 = ( 70 | lhs.M10 * rhs.M00 71 | + lhs.M11 * rhs.M10 72 | + lhs.M12 * rhs.M20 73 | + lhs.M13 * rhs.M30 74 | ) 75 | res.M11 = ( 76 | lhs.M10 * rhs.M01 77 | + lhs.M11 * rhs.M11 78 | + lhs.M12 * rhs.M21 79 | + lhs.M13 * rhs.M31 80 | ) 81 | res.M12 = ( 82 | lhs.M10 * rhs.M02 83 | + lhs.M11 * rhs.M12 84 | + lhs.M12 * rhs.M22 85 | + lhs.M13 * rhs.M32 86 | ) 87 | res.M13 = ( 88 | lhs.M10 * rhs.M03 89 | + lhs.M11 * rhs.M13 90 | + lhs.M12 * rhs.M23 91 | + lhs.M13 * rhs.M33 92 | ) 93 | 94 | res.M20 = ( 95 | lhs.M20 * rhs.M00 96 | + lhs.M21 * rhs.M10 97 | + lhs.M22 * rhs.M20 98 | + lhs.M23 * rhs.M30 99 | ) 100 | res.M21 = ( 101 | lhs.M20 * rhs.M01 102 | + lhs.M21 * rhs.M11 103 | + lhs.M22 * rhs.M21 104 | + lhs.M23 * rhs.M31 105 | ) 106 | res.M22 = ( 107 | lhs.M20 * rhs.M02 108 | + lhs.M21 * rhs.M12 109 | + lhs.M22 * rhs.M22 110 | + lhs.M23 * rhs.M32 111 | ) 112 | res.M23 = ( 113 | lhs.M20 * rhs.M03 114 | + lhs.M21 * rhs.M13 115 | + lhs.M22 * rhs.M23 116 | + lhs.M23 * rhs.M33 117 | ) 118 | 119 | res.M30 = ( 120 | lhs.M30 * rhs.M00 121 | + lhs.M31 * rhs.M10 122 | + lhs.M32 * rhs.M20 123 | + lhs.M33 * rhs.M30 124 | ) 125 | res.M31 = ( 126 | lhs.M30 * rhs.M01 127 | + lhs.M31 * rhs.M11 128 | + lhs.M32 * rhs.M21 129 | + lhs.M33 * rhs.M31 130 | ) 131 | res.M32 = ( 132 | lhs.M30 * rhs.M02 133 | + lhs.M31 * rhs.M12 134 | + lhs.M32 * rhs.M22 135 | + lhs.M33 * rhs.M32 136 | ) 137 | res.M33 = ( 138 | lhs.M30 * rhs.M03 139 | + lhs.M31 * rhs.M13 140 | + lhs.M32 * rhs.M23 141 | + lhs.M33 * rhs.M33 142 | ) 143 | 144 | return res 145 | 146 | @staticmethod 147 | def Scale(vector: Vector3): 148 | return Matrix4x4( 149 | [vector.X, 0, 0, 0, 150 | 0, vector.Y, 0, 0, 151 | 0, 0, vector.Z, 0, 152 | 0, 0, 0, 1] 153 | ) 154 | 155 | @property 156 | def M00(self): 157 | return self.M[0] 158 | 159 | @M00.setter 160 | def M00(self, value): 161 | self.M[0] = value 162 | 163 | @property 164 | def M10(self): 165 | return self.M[1] 166 | 167 | @M10.setter 168 | def M10(self, value): 169 | self.M[1] = value 170 | 171 | @property 172 | def M20(self): 173 | return self.M[2] 174 | 175 | @M20.setter 176 | def M20(self, value): 177 | self.M[2] = value 178 | 179 | @property 180 | def M30(self): 181 | return self.M[3] 182 | 183 | @M30.setter 184 | def M30(self, value): 185 | self.M[3] = value 186 | 187 | @property 188 | def M01(self): 189 | return self.M[4] 190 | 191 | @M01.setter 192 | def M01(self, value): 193 | self.M[4] = value 194 | 195 | @property 196 | def M11(self): 197 | return self.M[5] 198 | 199 | @M11.setter 200 | def M11(self, value): 201 | self.M[5] = value 202 | 203 | @property 204 | def M21(self): 205 | return self.M[6] 206 | 207 | @M21.setter 208 | def M21(self, value): 209 | self.M[6] = value 210 | 211 | @property 212 | def M31(self): 213 | return self.M[7] 214 | 215 | @M31.setter 216 | def M31(self, value): 217 | self.M[7] = value 218 | 219 | @property 220 | def M02(self): 221 | return self.M[8] 222 | 223 | @M02.setter 224 | def M02(self, value): 225 | self.M[8] = value 226 | 227 | @property 228 | def M12(self): 229 | return self.M[9] 230 | 231 | @M12.setter 232 | def M12(self, value): 233 | self.M[9] = value 234 | 235 | @property 236 | def M22(self): 237 | return self.M[10] 238 | 239 | @M22.setter 240 | def M22(self, value): 241 | self.M[10] = value 242 | 243 | @property 244 | def M32(self): 245 | return self.M[11] 246 | 247 | @M32.setter 248 | def M32(self, value): 249 | self.M[11] = value 250 | 251 | @property 252 | def M03(self): 253 | return self.M[12] 254 | 255 | @M03.setter 256 | def M03(self, value): 257 | self.M[12] = value 258 | 259 | @property 260 | def M13(self): 261 | return self.M[13] 262 | 263 | @M13.setter 264 | def M13(self, value): 265 | self.M[13] = value 266 | 267 | @property 268 | def M23(self): 269 | return self.M[14] 270 | 271 | @M23.setter 272 | def M23(self, value): 273 | self.M[14] = value 274 | 275 | @property 276 | def M33(self): 277 | return self.M[15] 278 | 279 | @M33.setter 280 | def M33(self, value): 281 | self.M[15] = value 282 | -------------------------------------------------------------------------------- /UnityPy/export/AudioClipConverter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ctypes 4 | import os 5 | import platform 6 | from threading import Lock 7 | from typing import TYPE_CHECKING, Dict 8 | 9 | from UnityPy.streams import EndianBinaryWriter 10 | 11 | from ..helpers.ResourceReader import get_resource_data 12 | 13 | try: 14 | import numpy as np 15 | except ImportError: 16 | np = None 17 | import struct 18 | 19 | if TYPE_CHECKING: 20 | from ..classes import AudioClip 21 | 22 | # pyfmodex loads the dll/so/dylib on import 23 | # so we have to adjust the environment vars 24 | # before importing it 25 | # This is done in import_pyfmodex() 26 | # which will replace the global pyfmodex var 27 | pyfmodex = None 28 | 29 | 30 | def get_fmod_path( 31 | system: str, # "Windows", "Linux", "Darwin" 32 | arch: str, # "x64", "x86", "arm", "arm64" 33 | ) -> str: 34 | if system == "Darwin": 35 | # universal dylib 36 | return "lib/FMOD/Darwin/libfmod.dylib" 37 | if system == "Windows": 38 | return f"lib/FMOD/Windows/{arch}/fmod.dll" 39 | if system == "Linux": 40 | if arch == "x64": 41 | arch = "x86_64" 42 | return f"lib/FMOD/Linux/{arch}/libfmod.so" 43 | 44 | raise NotImplementedError(f"Unsupported system: {system}") 45 | 46 | 47 | def import_pyfmodex(): 48 | global pyfmodex 49 | if pyfmodex is not None: 50 | return 51 | 52 | # determine system - Windows, Darwin, Linux, Android 53 | system = platform.system() 54 | arch = platform.architecture()[0] 55 | machine = platform.machine() 56 | 57 | if "arm" in machine: 58 | arch = "arm" 59 | elif "aarch64" in machine: 60 | if system == "Linux": 61 | arch = "arm64" 62 | else: 63 | arch = "arm" 64 | elif arch == "32bit": 65 | arch = "x86" 66 | elif arch == "64bit": 67 | arch = "x64" 68 | 69 | fmod_rel_path = get_fmod_path(system, arch) 70 | fmod_path = os.path.join( 71 | os.path.dirname(os.path.dirname(os.path.realpath(__file__))), fmod_rel_path 72 | ) 73 | os.environ["PYFMODEX_DLL_PATH"] = fmod_path 74 | 75 | # build path and load library 76 | # prepare the environment for pyfmodex 77 | if system != "Windows": 78 | # hotfix ctypes for pyfmodex for non windows systems 79 | ctypes.windll = None 80 | 81 | import pyfmodex 82 | 83 | 84 | def extract_audioclip_samples( 85 | audio: AudioClip, convert_pcm_float: bool = True 86 | ) -> Dict[str, bytes]: 87 | """extracts all the samples from an AudioClip 88 | :param audio: AudioClip 89 | :type audio: AudioClip 90 | :return: {filename : sample(bytes)} 91 | :rtype: dict 92 | """ 93 | if audio.m_AudioData: 94 | audio_data = audio.m_AudioData 95 | else: 96 | resource = audio.m_Resource 97 | audio_data = get_resource_data( 98 | resource.m_Source, 99 | audio.object_reader.assets_file, 100 | resource.m_Offset, 101 | resource.m_Size, 102 | ) 103 | 104 | magic = memoryview(audio_data)[:8] 105 | if magic[:4] == b"OggS": 106 | return {f"{audio.m_Name}.ogg": audio_data} 107 | elif magic[:4] == b"RIFF": 108 | return {f"{audio.m_Name}.wav": audio_data} 109 | elif magic[4:8] == b"ftyp": 110 | return {f"{audio.m_Name}.m4a": audio_data} 111 | return dump_samples(audio, audio_data, convert_pcm_float) 112 | 113 | 114 | SYSTEM_INSTANCES = {} # (channels, flags) -> (pyfmodex_system_instance, lock) 115 | SYSTEM_GLOBAL_LOCK = Lock() 116 | 117 | def get_pyfmodex_system_instance(channels: int, flags: int): 118 | global pyfmodex, SYSTEM_INSTANCES, SYSTEM_GLOBAL_LOCK 119 | with SYSTEM_GLOBAL_LOCK: 120 | instance_key = (channels, flags) 121 | if instance_key in SYSTEM_INSTANCES: 122 | return SYSTEM_INSTANCES[instance_key] 123 | 124 | system = pyfmodex.System() 125 | system.init(channels, flags, None) 126 | lock = Lock() 127 | SYSTEM_INSTANCES[instance_key] = (system, lock) 128 | return system, lock 129 | 130 | 131 | def dump_samples( 132 | clip: AudioClip, audio_data: bytes, convert_pcm_float: bool = True 133 | ) -> Dict[str, bytes]: 134 | global pyfmodex 135 | if pyfmodex is None: 136 | import_pyfmodex() 137 | if not pyfmodex: 138 | return {} 139 | 140 | system, lock = get_pyfmodex_system_instance(clip.m_Channels, pyfmodex.flags.INIT_FLAGS.NORMAL) 141 | with lock: 142 | sound = system.create_sound( 143 | bytes(audio_data), 144 | pyfmodex.flags.MODE.OPENMEMORY, 145 | exinfo=pyfmodex.structure_declarations.CREATESOUNDEXINFO( 146 | length=len(audio_data), 147 | numchannels=clip.m_Channels, 148 | defaultfrequency=clip.m_Frequency, 149 | ), 150 | ) 151 | 152 | # iterate over subsounds 153 | samples = {} 154 | for i in range(sound.num_subsounds): 155 | if i > 0: 156 | filename = "%s-%i.wav" % (clip.m_Name, i) 157 | else: 158 | filename = "%s.wav" % clip.m_Name 159 | subsound = sound.get_subsound(i) 160 | samples[filename] = subsound_to_wav(subsound, convert_pcm_float) 161 | subsound.release() 162 | 163 | sound.release() 164 | return samples 165 | 166 | 167 | def subsound_to_wav(subsound, convert_pcm_float: bool = True) -> bytes: 168 | # get sound settings 169 | sound_format = subsound.format.format 170 | sound_data_length = subsound.get_length(pyfmodex.enums.TIMEUNIT.PCMBYTES) 171 | channels = subsound.format.channels 172 | bits = subsound.format.bits 173 | sample_rate = int(subsound.default_frequency) 174 | 175 | if sound_format in [ 176 | pyfmodex.enums.SOUND_FORMAT.PCM8, 177 | pyfmodex.enums.SOUND_FORMAT.PCM16, 178 | pyfmodex.enums.SOUND_FORMAT.PCM24, 179 | pyfmodex.enums.SOUND_FORMAT.PCM32, 180 | ]: 181 | # format is PCM integer 182 | audio_format = 1 183 | wav_data_length = sound_data_length 184 | convert_pcm_float = False 185 | elif sound_format == pyfmodex.enums.SOUND_FORMAT.PCMFLOAT: 186 | # format is IEEE 754 float 187 | if convert_pcm_float: 188 | audio_format = 1 189 | bits = 16 190 | wav_data_length = sound_data_length // 2 191 | else: 192 | audio_format = 3 193 | wav_data_length = sound_data_length 194 | else: 195 | raise NotImplementedError("Sound format " + sound_format + " is not supported.") 196 | 197 | w = EndianBinaryWriter(endian="<") 198 | 199 | # RIFF header 200 | w.write(b"RIFF") # chunk id 201 | w.write_int( 202 | wav_data_length + 36 203 | ) # chunk size - 4 + (8 + 16 (sub chunk 1 size)) + (8 + length (sub chunk 2 size)) 204 | w.write(b"WAVE") # format 205 | 206 | # fmt chunk - sub chunk 1 207 | w.write(b"fmt ") # sub chunk 1 id 208 | w.write_int(16) # sub chunk 1 size, 16 for PCM 209 | w.write_short(audio_format) # audio format, 1: PCM integer, 3: IEEE 754 float 210 | w.write_short(channels) # number of channels 211 | w.write_int(sample_rate) # sample rate 212 | w.write_int(sample_rate * channels * bits // 8) # byte rate 213 | w.write_short(channels * bits // 8) # block align 214 | w.write_short(bits) # bits per sample 215 | 216 | # data chunk - sub chunk 2 217 | w.write(b"data") # sub chunk 2 id 218 | w.write_int(wav_data_length) # sub chunk 2 size 219 | # sub chunk 2 data 220 | lock = subsound.lock(0, sound_data_length) 221 | for ptr, sound_data_length in lock: 222 | ptr_data = ctypes.string_at(ptr, sound_data_length.value) 223 | if convert_pcm_float: 224 | if np is not None: 225 | ptr_data = np.frombuffer(ptr_data, dtype=np.float32) 226 | ptr_data = (ptr_data * (1 << 15)).astype(np.int16).tobytes() 227 | else: 228 | ptr_data = struct.unpack("<%df" % (len(ptr_data) // 4), ptr_data) 229 | ptr_data = struct.pack( 230 | "<%dh" % len(ptr_data), *[int(f * (1 << 15)) for f in ptr_data] 231 | ) 232 | 233 | w.write(ptr_data) 234 | subsound.unlock(*lock) 235 | 236 | return w.bytes 237 | -------------------------------------------------------------------------------- /UnityPy/tools/libil2cpp_helper/il2cpp_class.py: -------------------------------------------------------------------------------- 1 | from .helper import * 2 | 3 | 4 | class Il2CppCodeRegistration(MetaDataClass): 5 | methodPointersCount: Union[long, Version(Max=24.1)] 6 | methodPointers: Union[ulong, Version(Max=24.1)] 7 | delegateWrappersFromNativeToManagedCount: Union[ulong, Version(Max=21)] 8 | delegateWrappersFromNativeToManaged: Union[ 9 | ulong, Version(Max=21) 10 | ] # note the double indirection to handle different calling conventions 11 | reversePInvokeWrapperCount: Union[long, Version(Min=22)] 12 | reversePInvokeWrappers: Union[ulong, Version(Min=22)] 13 | delegateWrappersFromManagedToNativeCount: Union[ulong, Version(Max=22)] 14 | delegateWrappersFromManagedToNative: Union[ulong, Version(Max=22)] 15 | marshalingFunctionsCount: Union[ulong, Version(Max=22)] 16 | marshalingFunctions: Union[ulong, Version(Max=22)] 17 | ccwMarshalingFunctionsCount: Union[ulong, Version(Min=21, Max=22)] 18 | ccwMarshalingFunctions: Union[ulong, Version(Min=21, Max=22)] 19 | genericMethodPointersCount: long 20 | genericMethodPointers: ulong 21 | genericAdjustorThunks: Union[ulong, Version(Min=24.4, Max=24.4), Version(Min=27.1)] 22 | invokerPointersCount: long 23 | invokerPointers: ulong 24 | customAttributeCount: Union[long, Version(Max=24.4)] 25 | customAttributeGenerators: Union[ulong, Version(Max=24.4)] 26 | guidCount: Union[long, Version(Min=21, Max=22)] 27 | guids: Union[ulong, Version(Min=21, Max=22)] # Il2CppGuid 28 | unresolvedVirtualCallCount: Union[long, Version(Min=22)] 29 | unresolvedVirtualCallPointers: Union[ulong, Version(Min=22)] 30 | interopDataCount: Union[ulong, Version(Min=23)] 31 | interopData: Union[ulong, Version(Min=23)] 32 | windowsRuntimeFactoryCount: Union[ulong, Version(Min=24.3)] 33 | windowsRuntimeFactoryTable: Union[ulong, Version(Min=24.3)] 34 | codeGenModulesCount: Union[long, Version(Min=24.2)] 35 | codeGenModules: Union[ulong, Version(Min=24.2)] 36 | 37 | 38 | class Il2CppMetadataRegistration(MetaDataClass): 39 | genericClassesCount: long 40 | genericClasses: ulong 41 | genericInstsCount: long 42 | genericInsts: ulong 43 | genericMethodTableCount: long 44 | genericMethodTable: ulong 45 | typesCount: long 46 | types: ulong 47 | methodSpecsCount: long 48 | methodSpecs: ulong 49 | methodReferencesCount: Union[long, Version(Max=16)] 50 | methodReferences: Union[ulong, Version(Max=16)] 51 | 52 | fieldOffsetsCount: long 53 | fieldOffsets: ulong 54 | 55 | typeDefinitionsSizesCount: long 56 | typeDefinitionsSizes: ulong 57 | metadataUsagesCount: Union[ulong, Version(Min=19)] 58 | metadataUsages: Union[ulong, Version(Min=19)] 59 | 60 | 61 | class Il2CppTypeEnum(uint, Enum): 62 | IL2CPP_TYPE_END = 0x00 # End of List 63 | IL2CPP_TYPE_VOID = 0x01 64 | IL2CPP_TYPE_BOOLEAN = 0x02 65 | IL2CPP_TYPE_CHAR = 0x03 66 | IL2CPP_TYPE_I1 = 0x04 67 | IL2CPP_TYPE_U1 = 0x05 68 | IL2CPP_TYPE_I2 = 0x06 69 | IL2CPP_TYPE_U2 = 0x07 70 | IL2CPP_TYPE_I4 = 0x08 71 | IL2CPP_TYPE_U4 = 0x09 72 | IL2CPP_TYPE_I8 = 0x0A 73 | IL2CPP_TYPE_U8 = 0x0B 74 | IL2CPP_TYPE_R4 = 0x0C 75 | IL2CPP_TYPE_R8 = 0x0D 76 | IL2CPP_TYPE_STRING = 0x0E 77 | IL2CPP_TYPE_PTR = 0x0F # arg: token 78 | IL2CPP_TYPE_BYREF = 0x10 # arg: token 79 | IL2CPP_TYPE_VALUETYPE = 0x11 # arg: token 80 | IL2CPP_TYPE_CLASS = 0x12 # arg: token 81 | IL2CPP_TYPE_VAR = 0x13 # Generic parameter in a generic type definition, represented as number (compressed unsigned integer) number 82 | IL2CPP_TYPE_ARRAY = 0x14 # type, rank, boundsCount, bound1, loCount, lo1 83 | IL2CPP_TYPE_GENERICINST = 0x15 # \x{2026} 84 | IL2CPP_TYPE_TYPEDBYREF = 0x16 85 | IL2CPP_TYPE_I = 0x18 86 | IL2CPP_TYPE_U = 0x19 87 | IL2CPP_TYPE_FNPTR = 0x1B # arg: full method signature 88 | IL2CPP_TYPE_OBJECT = 0x1C 89 | IL2CPP_TYPE_SZARRAY = 0x1D # 0-based one-dim-array 90 | IL2CPP_TYPE_MVAR = 0x1E # Generic parameter in a generic method definition, represented as number (compressed unsigned integer) 91 | IL2CPP_TYPE_CMOD_REQD = 0x1F # arg: typedef or typeref token 92 | IL2CPP_TYPE_CMOD_OPT = 0x20 # optional arg: typedef or typref token 93 | IL2CPP_TYPE_INTERNAL = 0x21 # CLR internal type 94 | 95 | IL2CPP_TYPE_MODIFIER = 0x40 # Or with the following types 96 | IL2CPP_TYPE_SENTINEL = 0x41 # Sentinel for varargs method signature 97 | IL2CPP_TYPE_PINNED = 0x45 # Local var that points to pinned object 98 | 99 | IL2CPP_TYPE_ENUM = 0x55 # an enumeration 100 | 101 | 102 | # class Il2CppType(MetaDataClass): 103 | # datapoint: ulong 104 | # bits: uint 105 | # data { get: Union set; } 106 | # attrs { get: uint set; } 107 | # type { get: Il2CppTypeEnum set; } 108 | # num_mods { get: uint set; } 109 | # byref { get: uint set; } 110 | # pinned { get: uint set; } 111 | 112 | # public void Init() 113 | # { 114 | # attrs = bits & 0xffff; 115 | # type = (Il2CppTypeEnum)((bits >> 16) & 0xff); 116 | # num_mods = (bits >> 24) & 0x3f; 117 | # byref = (bits >> 30) & 1; 118 | # pinned = bits >> 31; 119 | # data = new Union { dummy = datapoint }; 120 | # } 121 | 122 | # public class Union 123 | # { 124 | # dummy: ulong 125 | # #/ 126 | # #/ for VALUETYPE and CLASS 127 | # #/ 128 | # klassIndex => (long)dummy: long 129 | # #/ 130 | # #/ for VALUETYPE and CLASS at runtime 131 | # #/ 132 | # typeHandle => dummy: ulong 133 | # #/ 134 | # #/ for PTR and SZARRAY 135 | # #/ 136 | # type => dummy: ulong 137 | # #/ 138 | # #/ for ARRAY 139 | # #/ 140 | # array => dummy: ulong 141 | # #/ 142 | # #/ for VAR and MVAR 143 | # #/ 144 | # genericParameterIndex => (long)dummy: long 145 | # #/ 146 | # #/ for VAR and MVAR at runtime 147 | # #/ 148 | # genericParameterHandle => dummy: ulong 149 | # #/ 150 | # #/ for GENERICINST 151 | # #/ 152 | # generic_class => dummy: ulong 153 | # } 154 | # } 155 | 156 | 157 | class Il2CppGenericContext(MetaDataClass): 158 | # The instantiation corresponding to the class generic parameters 159 | class_inst: ulong 160 | # The instantiation corresponding to the method generic parameters 161 | method_inst: ulong 162 | 163 | 164 | class Il2CppGenericClass(MetaDataClass): 165 | typeDefinitionIndex: Union[long, Version(Max=24.4)] # the generic type definition 166 | type: Union[ulong, Version(Min=27)] # the generic type definition 167 | context: Il2CppGenericContext # a context that contains the type instantiation doesn't contain any method instantiation 168 | cached_class: ulong # if present, the Il2CppClass corresponding to the instantiation. 169 | 170 | 171 | class Il2CppGenericInst(MetaDataClass): 172 | type_argc: long 173 | type_argv: ulong 174 | 175 | 176 | class Il2CppArrayType(MetaDataClass): 177 | etype: ulong 178 | rank: byte 179 | numsizes: byte 180 | numlobounds: byte 181 | sizes: ulong 182 | lobounds: ulong 183 | 184 | 185 | class Il2CppGenericMethodIndices(MetaDataClass): 186 | methodIndex: int 187 | invokerIndex: int 188 | adjustorThunk: Union[int, Version(Min=24.4, Max=24.4), Version(Min=27.1)] 189 | 190 | 191 | class Il2CppGenericMethodFunctionsDefinitions(MetaDataClass): 192 | genericMethodIndex: int 193 | indices: Il2CppGenericMethodIndices 194 | 195 | 196 | class Il2CppMethodSpec(MetaDataClass): 197 | methodDefinitionIndex: int 198 | classIndexIndex: int 199 | methodIndexIndex: int 200 | 201 | 202 | class Il2CppCodeGenModule(MetaDataClass): 203 | moduleName: ulong 204 | methodPointerCount: long 205 | methodPointers: ulong 206 | adjustorThunkCount: Union[long, Version(Min=24.4, Max=24.4), Version(Min=27.1)] 207 | adjustorThunks: Union[ulong, Version(Min=24.4, Max=24.4), Version(Min=27.1)] 208 | invokerIndices: ulong 209 | reversePInvokeWrapperCount: ulong 210 | reversePInvokeWrapperIndices: ulong 211 | rgctxRangesCount: long 212 | rgctxRanges: ulong 213 | rgctxsCount: long 214 | rgctxs: ulong 215 | debuggerMetadata: ulong 216 | customAttributeCacheGenerator: Union[ulong, Version(Min=27)] 217 | moduleInitializer: Union[ulong, Version(Min=27)] 218 | staticConstructorTypeIndices: Union[ulong, Version(Min=27)] 219 | metadataRegistration: Union[ulong, Version(Min=27)] # Per-assembly mode only 220 | codeRegistaration: Union[ulong, Version(Min=27)] # Per-assembly mode only 221 | 222 | 223 | class Il2CppRange(MetaDataClass): 224 | start: int 225 | length: int 226 | 227 | 228 | class Il2CppTokenRangePair(MetaDataClass): 229 | token: uint 230 | range: Il2CppRange 231 | -------------------------------------------------------------------------------- /UnityPy/enums/GraphicsFormat.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from .TextureFormat import TextureFormat 4 | 5 | 6 | class GraphicsFormat(IntEnum): 7 | NONE = 0 8 | R8_SRGB = 1 9 | R8G8_SRGB = 2 10 | R8G8B8_SRGB = 3 11 | R8G8B8A8_SRGB = 4 12 | 13 | R8_UNorm = 5 14 | R8G8_UNorm = 6 15 | R8G8B8_UNorm = 7 16 | R8G8B8A8_UNorm = 8 17 | 18 | R8_SNorm = 9 19 | R8G8_SNorm = 10 20 | R8G8B8_SNorm = 11 21 | R8G8B8A8_SNorm = 12 22 | 23 | R8_UInt = 13 24 | R8G8_UInt = 14 25 | R8G8B8_UInt = 15 26 | R8G8B8A8_UInt = 16 27 | 28 | R8_SInt = 17 29 | R8G8_SInt = 18 30 | R8G8B8_SInt = 19 31 | R8G8B8A8_SInt = 20 32 | 33 | R16_UNorm = 21 34 | R16G16_UNorm = 22 35 | R16G16B16_UNorm = 23 36 | R16G16B16A16_UNorm = 24 37 | 38 | R16_SNorm = 25 39 | R16G16_SNorm = 26 40 | R16G16B16_SNorm = 27 41 | R16G16B16A16_SNorm = 28 42 | 43 | R16_UInt = 29 44 | R16G16_UInt = 30 45 | R16G16B16_UInt = 31 46 | R16G16B16A16_UInt = 32 47 | 48 | R16_SInt = 33 49 | R16G16_SInt = 34 50 | R16G16B16_SInt = 35 51 | R16G16B16A16_SInt = 36 52 | 53 | R32_UInt = 37 54 | R32G32_UInt = 38 55 | R32G32B32_UInt = 39 56 | R32G32B32A32_UInt = 40 57 | 58 | R32_SInt = 41 59 | R32G32_SInt = 42 60 | R32G32B32_SInt = 43 61 | R32G32B32A32_SInt = 44 62 | 63 | R16_SFloat = 45 64 | R16G16_SFloat = 46 65 | R16G16B16_SFloat = 47 66 | R16G16B16A16_SFloat = 48 67 | R32_SFloat = 49 68 | R32G32_SFloat = 50 69 | R32G32B32_SFloat = 51 70 | R32G32B32A32_SFloat = 52 71 | 72 | B8G8R8_SRGB = 56 73 | B8G8R8A8_SRGB = 57 74 | B8G8R8_UNorm = 58 75 | B8G8R8A8_UNorm = 59 76 | B8G8R8_SNorm = 60 77 | B8G8R8A8_SNorm = 61 78 | B8G8R8_UInt = 62 79 | B8G8R8A8_UInt = 63 80 | B8G8R8_SInt = 64 81 | B8G8R8A8_SInt = 65 82 | 83 | R4G4B4A4_UNormPack16 = 66 84 | B4G4R4A4_UNormPack16 = 67 85 | R5G6B5_UNormPack16 = 68 86 | B5G6R5_UNormPack16 = 69 87 | R5G5B5A1_UNormPack16 = 70 88 | B5G5R5A1_UNormPack16 = 71 89 | A1R5G5B5_UNormPack16 = 72 90 | 91 | E5B9G9R9_UFloatPack32 = 73 92 | B10G11R11_UFloatPack32 = 74 93 | 94 | A2B10G10R10_UNormPack32 = 75 95 | A2B10G10R10_UIntPack32 = 76 96 | A2B10G10R10_SIntPack32 = 77 97 | A2R10G10B10_UNormPack32 = 78 98 | A2R10G10B10_UIntPack32 = 79 99 | A2R10G10B10_SIntPack32 = 80 100 | A2R10G10B10_XRSRGBPack32 = 81 101 | A2R10G10B10_XRUNormPack32 = 82 102 | R10G10B10_XRSRGBPack32 = 83 103 | R10G10B10_XRUNormPack32 = 84 104 | A10R10G10B10_XRSRGBPack32 = 85 105 | A10R10G10B10_XRUNormPack32 = 86 106 | 107 | D16_UNorm = 90 108 | D24_UNorm = 91 109 | D24_UNorm_S8_UInt = 92 110 | D32_SFloat = 93 111 | D32_SFloat_S8_UInt = 94 112 | S8_UInt = 95 113 | 114 | RGB_DXT1_SRGB = 96 115 | RGBA_DXT1_SRGB = 96 116 | RGB_DXT1_UNorm = 97 117 | RGBA_DXT1_UNorm = 97 118 | RGBA_DXT3_SRGB = 98 119 | RGBA_DXT3_UNorm = 99 120 | RGBA_DXT5_SRGB = 100 121 | RGBA_DXT5_UNorm = 101 122 | R_BC4_UNorm = 102 123 | R_BC4_SNorm = 103 124 | RG_BC5_UNorm = 104 125 | RG_BC5_SNorm = 105 126 | RGB_BC6H_UFloat = 106 127 | RGB_BC6H_SFloat = 107 128 | RGBA_BC7_SRGB = 108 129 | RGBA_BC7_UNorm = 109 130 | 131 | RGB_PVRTC_2Bpp_SRGB = 110 132 | RGB_PVRTC_2Bpp_UNorm = 111 133 | RGB_PVRTC_4Bpp_SRGB = 112 134 | RGB_PVRTC_4Bpp_UNorm = 113 135 | RGBA_PVRTC_2Bpp_SRGB = 114 136 | RGBA_PVRTC_2Bpp_UNorm = 115 137 | RGBA_PVRTC_4Bpp_SRGB = 116 138 | RGBA_PVRTC_4Bpp_UNorm = 117 139 | 140 | RGB_ETC_UNorm = 118 141 | RGB_ETC2_SRGB = 119 142 | RGB_ETC2_UNorm = 120 143 | RGB_A1_ETC2_SRGB = 121 144 | RGB_A1_ETC2_UNorm = 122 145 | RGBA_ETC2_SRGB = 123 146 | RGBA_ETC2_UNorm = 124 147 | 148 | R_EAC_UNorm = 125 149 | R_EAC_SNorm = 126 150 | RG_EAC_UNorm = 127 151 | RG_EAC_SNorm = 128 152 | 153 | RGBA_ASTC4X4_SRGB = 129 154 | RGBA_ASTC4X4_UNorm = 130 155 | RGBA_ASTC5X5_SRGB = 131 156 | RGBA_ASTC5X5_UNorm = 132 157 | RGBA_ASTC6X6_SRGB = 133 158 | RGBA_ASTC6X6_UNorm = 134 159 | RGBA_ASTC8X8_SRGB = 135 160 | RGBA_ASTC8X8_UNorm = 136 161 | RGBA_ASTC10X10_SRGB = 137 162 | RGBA_ASTC10X10_UNorm = 138 163 | RGBA_ASTC12X12_SRGB = 139 164 | RGBA_ASTC12X12_UNorm = 140 165 | 166 | YUV2 = 141 167 | 168 | RGBA_ASTC4X4_UFloat = 145 169 | RGBA_ASTC5X5_UFloat = 146 170 | RGBA_ASTC6X6_UFloat = 147 171 | RGBA_ASTC8X8_UFloat = 148 172 | RGBA_ASTC10X10_UFloat = 149 173 | RGBA_ASTC12X12_UFloat = 150 174 | 175 | D16_UNorm_S8_UInt = 151 176 | 177 | 178 | # very experimental & untested 179 | GRAPHICS_TO_TEXTURE_MAP = { 180 | GraphicsFormat.R8_SRGB: TextureFormat.R8, 181 | GraphicsFormat.R8G8_SRGB: TextureFormat.RG16, 182 | GraphicsFormat.R8G8B8_SRGB: TextureFormat.RGB24, 183 | GraphicsFormat.R8G8B8A8_SRGB: TextureFormat.RGBA32, 184 | GraphicsFormat.R8_UNorm: TextureFormat.R8, 185 | GraphicsFormat.R8G8_UNorm: TextureFormat.RG16, 186 | GraphicsFormat.R8G8B8_UNorm: TextureFormat.RGB24, 187 | GraphicsFormat.R8G8B8A8_UNorm: TextureFormat.RGBA32, 188 | GraphicsFormat.R8_SNorm: TextureFormat.R8_SIGNED, 189 | GraphicsFormat.R8G8_SNorm: TextureFormat.RG16_SIGNED, 190 | GraphicsFormat.R8G8B8_SNorm: TextureFormat.RGB24_SIGNED, 191 | GraphicsFormat.R8G8B8A8_SNorm: TextureFormat.RGBA32_SIGNED, 192 | GraphicsFormat.R8_UInt: TextureFormat.R16, 193 | GraphicsFormat.R8G8_UInt: TextureFormat.RG32, 194 | GraphicsFormat.R8G8B8_UInt: TextureFormat.RGB48, 195 | GraphicsFormat.R8G8B8A8_UInt: TextureFormat.RGBA64, 196 | GraphicsFormat.R8_SInt: TextureFormat.R16_SIGNED, 197 | GraphicsFormat.R8G8_SInt: TextureFormat.RG32_SIGNED, 198 | GraphicsFormat.R8G8B8_SInt: TextureFormat.RGB48_SIGNED, 199 | GraphicsFormat.R8G8B8A8_SInt: TextureFormat.RGBA64_SIGNED, 200 | GraphicsFormat.R16_UNorm: TextureFormat.R16, 201 | GraphicsFormat.R16G16_UNorm: TextureFormat.RG32, 202 | GraphicsFormat.R16G16B16_UNorm: TextureFormat.RGB48, 203 | GraphicsFormat.R16G16B16A16_UNorm: TextureFormat.RGBA64, 204 | GraphicsFormat.R16_SNorm: TextureFormat.R16_SIGNED, 205 | GraphicsFormat.R16G16_SNorm: TextureFormat.RG32_SIGNED, 206 | GraphicsFormat.R16G16B16_SNorm: TextureFormat.RGB48_SIGNED, 207 | GraphicsFormat.R16G16B16A16_SNorm: TextureFormat.RGBA64_SIGNED, 208 | GraphicsFormat.R16_UInt: TextureFormat.R16, 209 | GraphicsFormat.R16G16_UInt: TextureFormat.RG32, 210 | GraphicsFormat.R16G16B16_UInt: TextureFormat.RGB48, 211 | GraphicsFormat.R16G16B16A16_UInt: TextureFormat.RGBA64, 212 | GraphicsFormat.R16_SInt: TextureFormat.R16_SIGNED, 213 | GraphicsFormat.R16G16_SInt: TextureFormat.RG32_SIGNED, 214 | GraphicsFormat.R16G16B16_SInt: TextureFormat.RGB48_SIGNED, 215 | GraphicsFormat.R16G16B16A16_SInt: TextureFormat.RGBA64_SIGNED, 216 | GraphicsFormat.B8G8R8_SRGB: TextureFormat.BGR24, 217 | GraphicsFormat.B8G8R8A8_SRGB: TextureFormat.BGRA32, 218 | GraphicsFormat.B8G8R8_UNorm: TextureFormat.BGR24, 219 | GraphicsFormat.B8G8R8A8_UNorm: TextureFormat.BGRA32, 220 | # GraphicsFormat.B8G8R8_SNorm: TextureFormat.BGR24_SIGNED, 221 | # GraphicsFormat.B8G8R8A8_SNorm: TextureFormat.BGRA32_SIGNED, 222 | # GraphicsFormat.B8G8R8_UInt: TextureFormat.BGR48, 223 | GraphicsFormat.RGB_DXT1_SRGB: TextureFormat.DXT1, 224 | GraphicsFormat.RGBA_DXT1_SRGB: TextureFormat.DXT1, 225 | GraphicsFormat.RGB_DXT1_UNorm: TextureFormat.DXT1, 226 | GraphicsFormat.RGBA_DXT1_UNorm: TextureFormat.DXT1, 227 | GraphicsFormat.RGBA_DXT3_SRGB: TextureFormat.DXT3, 228 | GraphicsFormat.RGBA_DXT3_UNorm: TextureFormat.DXT3, 229 | GraphicsFormat.RGBA_DXT5_SRGB: TextureFormat.DXT5, 230 | GraphicsFormat.RGBA_DXT5_UNorm: TextureFormat.DXT5, 231 | GraphicsFormat.R_BC4_UNorm: TextureFormat.BC4, 232 | # GraphicsFormat.R_BC4_SNorm: TextureFormat.BC4_SIGNED, 233 | GraphicsFormat.RG_BC5_UNorm: TextureFormat.BC5, 234 | # GraphicsFormat.RG_BC5_SNorm: TextureFormat.BC5_SIGNED, 235 | GraphicsFormat.RGB_BC6H_UFloat: TextureFormat.BC6H, 236 | # GraphicsFormat.RGB_BC6H_SFloat: TextureFormat.BC6H_SIGNED, 237 | GraphicsFormat.RGBA_BC7_SRGB: TextureFormat.BC7, 238 | GraphicsFormat.RGBA_BC7_UNorm: TextureFormat.BC7, 239 | GraphicsFormat.RGB_PVRTC_2Bpp_SRGB: TextureFormat.PVRTC_RGB2, 240 | GraphicsFormat.RGB_PVRTC_2Bpp_UNorm: TextureFormat.PVRTC_RGB2, 241 | GraphicsFormat.RGB_PVRTC_4Bpp_SRGB: TextureFormat.PVRTC_RGB4, 242 | GraphicsFormat.RGB_PVRTC_4Bpp_UNorm: TextureFormat.PVRTC_RGB4, 243 | GraphicsFormat.RGBA_PVRTC_2Bpp_SRGB: TextureFormat.PVRTC_RGBA2, 244 | GraphicsFormat.RGBA_PVRTC_2Bpp_UNorm: TextureFormat.PVRTC_RGBA2, 245 | GraphicsFormat.RGBA_PVRTC_4Bpp_SRGB: TextureFormat.PVRTC_RGBA4, 246 | GraphicsFormat.RGBA_PVRTC_4Bpp_UNorm: TextureFormat.PVRTC_RGBA4, 247 | GraphicsFormat.RGB_ETC_UNorm: TextureFormat.ETC_RGB4, 248 | GraphicsFormat.RGB_ETC2_SRGB: TextureFormat.ETC2_RGB, 249 | GraphicsFormat.RGB_ETC2_UNorm: TextureFormat.ETC2_RGB, 250 | GraphicsFormat.RGB_A1_ETC2_SRGB: TextureFormat.ETC2_RGBA1, 251 | GraphicsFormat.RGB_A1_ETC2_UNorm: TextureFormat.ETC2_RGBA1, 252 | GraphicsFormat.RGBA_ETC2_SRGB: TextureFormat.ETC2_RGBA8, 253 | GraphicsFormat.RGBA_ETC2_UNorm: TextureFormat.ETC2_RGBA8, 254 | GraphicsFormat.R_EAC_UNorm: TextureFormat.EAC_R, 255 | GraphicsFormat.R_EAC_SNorm: TextureFormat.EAC_R_SIGNED, 256 | GraphicsFormat.RG_EAC_UNorm: TextureFormat.EAC_RG, 257 | GraphicsFormat.RG_EAC_SNorm: TextureFormat.EAC_RG_SIGNED, 258 | GraphicsFormat.RGBA_ASTC4X4_SRGB: TextureFormat.ASTC_RGBA_4x4, 259 | GraphicsFormat.RGBA_ASTC4X4_UNorm: TextureFormat.ASTC_RGBA_4x4, 260 | GraphicsFormat.RGBA_ASTC5X5_SRGB: TextureFormat.ASTC_RGBA_5x5, 261 | GraphicsFormat.RGBA_ASTC5X5_UNorm: TextureFormat.ASTC_RGBA_5x5, 262 | GraphicsFormat.RGBA_ASTC6X6_SRGB: TextureFormat.ASTC_RGBA_6x6, 263 | GraphicsFormat.RGBA_ASTC6X6_UNorm: TextureFormat.ASTC_RGBA_6x6, 264 | GraphicsFormat.RGBA_ASTC8X8_SRGB: TextureFormat.ASTC_RGBA_8x8, 265 | GraphicsFormat.RGBA_ASTC8X8_UNorm: TextureFormat.ASTC_RGBA_8x8, 266 | GraphicsFormat.RGBA_ASTC10X10_SRGB: TextureFormat.ASTC_RGBA_10x10, 267 | GraphicsFormat.RGBA_ASTC10X10_UNorm: TextureFormat.ASTC_RGBA_10x10, 268 | GraphicsFormat.RGBA_ASTC12X12_SRGB: TextureFormat.ASTC_RGBA_12x12, 269 | GraphicsFormat.RGBA_ASTC12X12_UNorm: TextureFormat.ASTC_RGBA_12x12, 270 | GraphicsFormat.YUV2: TextureFormat.YUY2, 271 | # GraphicsFormat.RGBA_ASTC4X4_UFloat: TextureFormat.ASTC_RGBA_4x4, 272 | # GraphicsFormat.RGBA_ASTC5X5_UFloat: TextureFormat.ASTC_RGBA_5x5, 273 | # GraphicsFormat.RGBA_ASTC6X6_UFloat: TextureFormat.ASTC_RGBA_6x6, 274 | # GraphicsFormat.RGBA_ASTC8X8_UFloat: TextureFormat.ASTC_RGBA_8x8, 275 | # GraphicsFormat.RGBA_ASTC10X10_UFloat: TextureFormat.ASTC_RGBA_10x10, 276 | # GraphicsFormat.RGBA_ASTC12X12_UFloat: TextureFormat.ASTC_RGBA_12x12, 277 | } 278 | --------------------------------------------------------------------------------