From cc20a65d1742296631a0a35deefab290f10a3993 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Tue, 7 Apr 2026 11:11:06 +0200 Subject: [PATCH 01/19] fix(UnityVersion): handle optional postfixes, fixes #369 --- UnityPy/helpers/UnityVersion.py | 23 +++++++++++++++++++++-- tests/test_UnityVersion.py | 12 +++--------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/UnityPy/helpers/UnityVersion.py b/UnityPy/helpers/UnityVersion.py index 480fc20e..3f001f6f 100644 --- a/UnityPy/helpers/UnityVersion.py +++ b/UnityPy/helpers/UnityVersion.py @@ -4,7 +4,10 @@ from enum import IntEnum from typing import Optional, Tuple, Union -VersionPattern = re.compile(r"^(?P\d+)\.(?P\d+)\.(?P\d+)(?P.+?)?(?P\d+)?$") +VersionPattern = re.compile( + r"^(?P\d+)\.(?P\d+)\.(?P\d+)(?P.+?)?(?P\d+)?(?P.*)$", + flags=re.DOTALL, +) class UnityVersionType(IntEnum): @@ -20,6 +23,7 @@ class UnityVersionType(IntEnum): class UnityVersion(int): # https://github.com/AssetRipper/VersionUtilities/blob/master/VersionUtilities/UnityVersion.cs _type_str: Optional[str] + _postfix: Optional[str] @property def major(self): @@ -41,6 +45,10 @@ def type(self): def type_str(self): return getattr(self, "_type_str", self.type.name) + @property + def postfix(self): + return getattr(self, "_postfix", "") + @property def type_number(self): return self & 0xFF @@ -56,6 +64,7 @@ def from_str(cls, version: str): # formats: # old: 5.0.0, .. # new: 2018.1.1f2 .. + # the new format string can be followed by a custom postfix match = VersionPattern.match(version) if not match: raise ValueError(f"Invalid version string: {version}") @@ -64,6 +73,7 @@ def from_str(cls, version: str): build = int(match.group("build")) type_str = match.group("type_str") type_number = int(match.group("type_number") or 0) + postfix = match.group("postfix") if type_str is None: return cls.from_list(major, minor, build) @@ -72,10 +82,19 @@ def from_str(cls, version: str): obj = cls.from_list(major, minor, build, type, type_number) if type is UnityVersionType.u: obj._type_str = type_str + if postfix: + obj._postfix = postfix + return obj + def __str__(self) -> str: + if self.major <= 5: + return f"{self.major}.{self.minor}.{self.build}" + else: + return f"{self.major}.{self.minor}{self.type_str}{self.type_number}{self.postfix}" + def __repr__(self) -> str: - return f"UnityVersion {self.major}.{self.minor}{self.type_str}{self.type_number}" + return f"UnityVersion {self.__str__()}" def __getitem__(self, idx: Union[int, slice]) -> Union[int, Tuple[int, ...]]: values = ( diff --git a/tests/test_UnityVersion.py b/tests/test_UnityVersion.py index 67581071..a0125563 100644 --- a/tests/test_UnityVersion.py +++ b/tests/test_UnityVersion.py @@ -14,6 +14,7 @@ ("2021.1.0c1", (2021, 1, 0, UnityVersionType.c.value, 1)), ("2022.2.0x1", (2022, 2, 0, UnityVersionType.x.value, 1)), ("2018.1.1z2", (2018, 1, 1, UnityVersionType.u.value, 2)), # unknown type + ("2022.3.62f2\n2", (2022, 3, 62, UnityVersionType.f.value, 2)), ], ) def test_parse_unity_version(version_str, expected_tuple): @@ -24,6 +25,7 @@ def test_parse_unity_version(version_str, expected_tuple): assert v.build == expected_tuple[2] assert v.type.value == expected_tuple[3] assert v.type_number == expected_tuple[4] + assert UnityVersion.from_list(*expected_tuple) == v @pytest.mark.parametrize( @@ -58,6 +60,7 @@ def test_comparison_with_tuple(version_str, compare_tuple): ("2018.1.1f2", "2018.1.1f1"), ("2018.1.1f2", "2018.1.2f2"), ("2018.1.1f2", "2018.2.1f2"), + ("2022.3.62f2\n2", "2022.3.62f2"), ], ) def test_comparison_with_unityversion(version_str, other_str): @@ -69,12 +72,3 @@ def test_comparison_with_unityversion(version_str, other_str): assert (v1 <= v2) == (v1.as_tuple() <= v2.as_tuple()) assert (v1 > v2) == (v1.as_tuple() > v2.as_tuple()) assert (v1 >= v2) == (v1.as_tuple() >= v2.as_tuple()) - - -def test_repr_and_str(): - v = UnityVersion.from_str("2018.1.1f2") - assert "UnityVersion" in repr(v) - assert str(v.major) in repr(v) - assert str(v.minor) in repr(v) - assert v.type_str in repr(v) - assert str(v.type_number) in repr(v) From 34726461bbdd170f400584a71c02dd96f3a63a21 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Tue, 7 Apr 2026 11:11:44 +0200 Subject: [PATCH 02/19] release: 1.25.1 --- UnityPy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityPy/__init__.py b/UnityPy/__init__.py index ed10d0af..96f2b0ea 100644 --- a/UnityPy/__init__.py +++ b/UnityPy/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.25.0" +__version__ = "1.25.1" from .environment import Environment as Environment from .helpers.ArchiveStorageManager import ( From 129b18709b79b8a078a2a426ad9b76e1cecd4c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ZX=E5=A4=8F=E5=A4=9C=E4=B9=8B=E9=A3=8E?= Date: Sat, 30 May 2026 13:13:40 +0800 Subject: [PATCH 03/19] Fix segfault in TypeTreeHelper boost when reading past buffer end Add an 'exhausted' flag to ReaderT that is set when any bounds check fails. Check this flag at the entry of read_typetree_value and read_typetree_value_array to immediately return NULL (with a Python ValueError set) instead of continuing to operate on invalid state. Also add NULL checks after PyList_New calls that could fail when allocation is requested for a large count derived from misaligned data. --- UnityPyBoost/TypeTreeHelper.cpp | 42 +++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/UnityPyBoost/TypeTreeHelper.cpp b/UnityPyBoost/TypeTreeHelper.cpp index 65e4e304..12926ccf 100644 --- a/UnityPyBoost/TypeTreeHelper.cpp +++ b/UnityPyBoost/TypeTreeHelper.cpp @@ -15,6 +15,7 @@ typedef struct Reader uint8_t *ptr; uint8_t *end; uint8_t *start; + bool exhausted; } ReaderT; typedef struct TypeTreeReaderConfig @@ -75,6 +76,7 @@ inline PyObject *read_bool(ReaderT *reader) { if (reader->ptr + 1 > reader->end) { + reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_bool out of bounds"); return NULL; } @@ -87,7 +89,8 @@ inline PyObject *read_bool_array(ReaderT *reader, int32_t count) { if (reader->ptr + count > reader->end) { - PyErr_SetString(PyExc_ValueError, "read_bool out of bounds"); + reader->exhausted = true; + PyErr_SetString(PyExc_ValueError, "read_bool_array out of bounds"); return NULL; } PyObject *list = PyList_New(count); @@ -104,6 +107,7 @@ inline PyObject *read_u8(ReaderT *reader) { if (reader->ptr + 1 > reader->end) { + reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_u8 out of bounds"); return NULL; } @@ -114,7 +118,8 @@ inline PyObject *read_u8_array(ReaderT *reader, int32_t count) { if (reader->ptr + count > reader->end) { - PyErr_SetString(PyExc_ValueError, "read_u8 out of bounds"); + reader->exhausted = true; + PyErr_SetString(PyExc_ValueError, "read_u8_array out of bounds"); return NULL; } PyObject *list = PyList_New(count); @@ -129,6 +134,7 @@ inline PyObject *read_s8(ReaderT *reader) { if (reader->ptr + 1 > reader->end) { + reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_s8 out of bounds"); return NULL; } @@ -139,7 +145,8 @@ inline PyObject *read_s8_array(ReaderT *reader, int32_t count) { if (reader->ptr + count > reader->end) { - PyErr_SetString(PyExc_ValueError, "read_s8 out of bounds"); + reader->exhausted = true; + PyErr_SetString(PyExc_ValueError, "read_s8_array out of bounds"); return NULL; } PyObject *list = PyList_New(count); @@ -159,6 +166,7 @@ inline PyObject *read_num(ReaderT *reader) if (reader->ptr + sizeof(T) > reader->end) { + reader->exhausted = true; return PyErr_Format(PyExc_ValueError, "read_%s out of bounds", typeid(T).name()); } T value = *(T *)reader->ptr; @@ -206,6 +214,7 @@ inline PyObject *read_num_array(ReaderT *reader, int32_t count) if (reader->ptr + sizeof(T) * count > reader->end) { + reader->exhausted = true; return PyErr_Format(PyExc_ValueError, "read_%s_array out of bounds", typeid(T).name()); } PyObject *list = PyList_New(count); @@ -260,6 +269,7 @@ inline bool _read_length(ReaderT *reader, int32_t *length) { if (reader->ptr + sizeof(int32_t) > reader->end) { + reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_length out of bounds"); return false; } @@ -287,6 +297,7 @@ inline PyObject *read_str(ReaderT *reader) } if (reader->ptr + length > reader->end) { + reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_str out of bounds"); return NULL; } @@ -306,6 +317,7 @@ inline PyObject *read_bytes(ReaderT *reader) } if (reader->ptr + length > reader->end) { + reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_bytes out of bounds"); return NULL; } @@ -355,6 +367,10 @@ inline PyObject *read_pair_array(ReaderT *reader, TypeTreeNodeObject *node, Type TypeTreeNodeObject *second_child = (TypeTreeNodeObject *)PyList_GET_ITEM(node->m_Children, 1); PyObject *list = PyList_New(count); + if (list == NULL) + { + return NULL; + } for (auto i = 0; i < count; i++) { PyObject *first = read_typetree_value(reader, first_child, config); @@ -689,6 +705,13 @@ const NodeDataType SUPPORTED_VALUE_ARRAY_READ_TYPES[] = { template PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTreeReaderConfigT *config) { + if (reader->exhausted) + { + if (!PyErr_Occurred()) + PyErr_SetString(PyExc_ValueError, "Read past end of typetree data"); + return NULL; + } + bool align = node->_align; PyObject *value = nullptr; @@ -827,6 +850,10 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre if (std::find(std::begin(SUPPORTED_VALUE_ARRAY_READ_TYPES), std::end(SUPPORTED_VALUE_ARRAY_READ_TYPES), child->_data_type) == std::end(SUPPORTED_VALUE_ARRAY_READ_TYPES)) { value = PyList_New(length); + if (value == NULL) + { + return NULL; + } for (int i = 0; i < length; i++) { PyObject *item = read_typetree_value(reader, child, config); @@ -869,6 +896,13 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre template PyObject *read_typetree_value_array(ReaderT *reader, TypeTreeNodeObject *node, TypeTreeReaderConfigT *config, int32_t count) { + if (reader->exhausted) + { + if (!PyErr_Occurred()) + PyErr_SetString(PyExc_ValueError, "Read past end of typetree data"); + return NULL; + } + bool align = node->_align; PyObject *value = nullptr; @@ -1012,7 +1046,7 @@ PyObject *read_typetree(PyObject *self, PyObject *args, PyObject *kwargs) } } - reader = {static_cast(view.buf), static_cast(view.buf) + view.len, static_cast(view.buf)}; + reader = {static_cast(view.buf), static_cast(view.buf) + view.len, static_cast(view.buf), false}; if (swap) { From 1e6d18a5c53b459a0096997ec9f58d16c0c90dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ZX=E5=A4=8F=E5=A4=9C=E4=B9=8B=E9=A3=8E?= Date: Tue, 16 Jun 2026 19:54:36 +0800 Subject: [PATCH 04/19] Remove exhausted flag, fix PyErr_Format return pattern --- UnityPyBoost/TypeTreeHelper.cpp | 37 +++++++-------------------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/UnityPyBoost/TypeTreeHelper.cpp b/UnityPyBoost/TypeTreeHelper.cpp index 12926ccf..00aaf7d8 100644 --- a/UnityPyBoost/TypeTreeHelper.cpp +++ b/UnityPyBoost/TypeTreeHelper.cpp @@ -15,7 +15,6 @@ typedef struct Reader uint8_t *ptr; uint8_t *end; uint8_t *start; - bool exhausted; } ReaderT; typedef struct TypeTreeReaderConfig @@ -76,7 +75,6 @@ inline PyObject *read_bool(ReaderT *reader) { if (reader->ptr + 1 > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_bool out of bounds"); return NULL; } @@ -89,7 +87,6 @@ inline PyObject *read_bool_array(ReaderT *reader, int32_t count) { if (reader->ptr + count > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_bool_array out of bounds"); return NULL; } @@ -107,7 +104,6 @@ inline PyObject *read_u8(ReaderT *reader) { if (reader->ptr + 1 > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_u8 out of bounds"); return NULL; } @@ -118,7 +114,6 @@ inline PyObject *read_u8_array(ReaderT *reader, int32_t count) { if (reader->ptr + count > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_u8_array out of bounds"); return NULL; } @@ -134,7 +129,6 @@ inline PyObject *read_s8(ReaderT *reader) { if (reader->ptr + 1 > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_s8 out of bounds"); return NULL; } @@ -145,7 +139,6 @@ inline PyObject *read_s8_array(ReaderT *reader, int32_t count) { if (reader->ptr + count > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_s8_array out of bounds"); return NULL; } @@ -166,8 +159,8 @@ inline PyObject *read_num(ReaderT *reader) if (reader->ptr + sizeof(T) > reader->end) { - reader->exhausted = true; - return PyErr_Format(PyExc_ValueError, "read_%s out of bounds", typeid(T).name()); + PyErr_Format(PyExc_ValueError, "read_%s out of bounds", typeid(T).name()); + return nullptr; } T value = *(T *)reader->ptr; if constexpr (swap) @@ -214,8 +207,8 @@ inline PyObject *read_num_array(ReaderT *reader, int32_t count) if (reader->ptr + sizeof(T) * count > reader->end) { - reader->exhausted = true; - return PyErr_Format(PyExc_ValueError, "read_%s_array out of bounds", typeid(T).name()); + PyErr_Format(PyExc_ValueError, "read_%s_array out of bounds", typeid(T).name()); + return nullptr; } PyObject *list = PyList_New(count); T *ptr = (T *)reader->ptr; @@ -269,7 +262,6 @@ inline bool _read_length(ReaderT *reader, int32_t *length) { if (reader->ptr + sizeof(int32_t) > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_length out of bounds"); return false; } @@ -297,7 +289,6 @@ inline PyObject *read_str(ReaderT *reader) } if (reader->ptr + length > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_str out of bounds"); return NULL; } @@ -317,7 +308,6 @@ inline PyObject *read_bytes(ReaderT *reader) } if (reader->ptr + length > reader->end) { - reader->exhausted = true; PyErr_SetString(PyExc_ValueError, "read_bytes out of bounds"); return NULL; } @@ -705,13 +695,6 @@ const NodeDataType SUPPORTED_VALUE_ARRAY_READ_TYPES[] = { template PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTreeReaderConfigT *config) { - if (reader->exhausted) - { - if (!PyErr_Occurred()) - PyErr_SetString(PyExc_ValueError, "Read past end of typetree data"); - return NULL; - } - bool align = node->_align; PyObject *value = nullptr; @@ -896,13 +879,6 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre template PyObject *read_typetree_value_array(ReaderT *reader, TypeTreeNodeObject *node, TypeTreeReaderConfigT *config, int32_t count) { - if (reader->exhausted) - { - if (!PyErr_Occurred()) - PyErr_SetString(PyExc_ValueError, "Read past end of typetree data"); - return NULL; - } - bool align = node->_align; PyObject *value = nullptr; @@ -945,7 +921,8 @@ PyObject *read_typetree_value_array(ReaderT *reader, TypeTreeNodeObject *node, T value = read_pair_array(reader, node, config, count); break; default: - value = PyErr_Format(PyExc_ValueError, "Unsupported type for read_typetree_value_array: %d", node->_data_type); + PyErr_Format(PyExc_ValueError, "Unsupported type for read_typetree_value_array: %d", node->_data_type); + value = nullptr; } if (align && value != NULL) { @@ -1046,7 +1023,7 @@ PyObject *read_typetree(PyObject *self, PyObject *args, PyObject *kwargs) } } - reader = {static_cast(view.buf), static_cast(view.buf) + view.len, static_cast(view.buf), false}; + reader = {static_cast(view.buf), static_cast(view.buf) + view.len, static_cast(view.buf)}; if (swap) { From 02cf4cf8f85c8b21b6e892c1f106d5c687f42872 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Thu, 9 Jul 2026 09:42:18 +0200 Subject: [PATCH 05/19] Fix(typetreehelper.cpp): replace remaining PyErr_Format(s) with PyErr_SetString and return null in their place --- UnityPyBoost/TypeTreeHelper.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/UnityPyBoost/TypeTreeHelper.cpp b/UnityPyBoost/TypeTreeHelper.cpp index 00aaf7d8..cbad558c 100644 --- a/UnityPyBoost/TypeTreeHelper.cpp +++ b/UnityPyBoost/TypeTreeHelper.cpp @@ -159,7 +159,8 @@ inline PyObject *read_num(ReaderT *reader) if (reader->ptr + sizeof(T) > reader->end) { - PyErr_Format(PyExc_ValueError, "read_%s out of bounds", typeid(T).name()); + std::string error_msg = "read_" + std::string(typeid(T).name()) + " out of bounds"; + PyErr_SetString(PyExc_EOFError, error_msg.c_str()); return nullptr; } T value = *(T *)reader->ptr; @@ -196,7 +197,9 @@ inline PyObject *read_num(ReaderT *reader) } else { - return PyErr_Format(PyExc_TypeError, "Unsupported type for read_num: %s", typeid(T).name()); + std::string error_msg = "Unsupported type for read_num: " + std::string(typeid(T).name()); + PyErr_SetString(PyExc_TypeError, error_msg.c_str()); + return nullptr; } } @@ -207,7 +210,8 @@ inline PyObject *read_num_array(ReaderT *reader, int32_t count) if (reader->ptr + sizeof(T) * count > reader->end) { - PyErr_Format(PyExc_ValueError, "read_%s_array out of bounds", typeid(T).name()); + std::string error_msg = "read_" + std::string(typeid(T).name()) + "_array out of bounds"; + PyErr_SetString(PyExc_EOFError, error_msg.c_str()); return nullptr; } PyObject *list = PyList_New(count); @@ -249,7 +253,9 @@ inline PyObject *read_num_array(ReaderT *reader, int32_t count) else { Py_DECREF(list); - return PyErr_Format(PyExc_TypeError, "Unsupported type for read_num_array: %s", typeid(T).name()); + std::string error_msg = "Unsupported type for read_num_array: " + std::string(typeid(T).name()); + PyErr_SetString(PyExc_TypeError, error_msg.c_str()); + return nullptr; } PyList_SET_ITEM(list, i, item); } @@ -262,7 +268,7 @@ inline bool _read_length(ReaderT *reader, int32_t *length) { if (reader->ptr + sizeof(int32_t) > reader->end) { - PyErr_SetString(PyExc_ValueError, "read_length out of bounds"); + PyErr_SetString(PyExc_EOFError, "read_length out of bounds"); return false; } *length = *(int32_t *)reader->ptr; @@ -289,7 +295,7 @@ inline PyObject *read_str(ReaderT *reader) } if (reader->ptr + length > reader->end) { - PyErr_SetString(PyExc_ValueError, "read_str out of bounds"); + PyErr_SetString(PyExc_EOFError, "read_str out of bounds"); return NULL; } PyObject *py_str = PyUnicode_DecodeUTF8((char *)reader->ptr, length, "surrogateescape"); @@ -921,7 +927,8 @@ PyObject *read_typetree_value_array(ReaderT *reader, TypeTreeNodeObject *node, T value = read_pair_array(reader, node, config, count); break; default: - PyErr_Format(PyExc_ValueError, "Unsupported type for read_typetree_value_array: %d", node->_data_type); + std::string error_msg = "Unsupported type for read_value_array: " + std::to_string(node->_data_type); + PyErr_SetString(PyExc_TypeError, error_msg.c_str()); value = nullptr; } if (align && value != NULL) @@ -947,7 +954,8 @@ static bool is_null_none_or_type(PyObject *obj, PyTypeObject *type, const char * { return true; } - PyErr_Format(PyExc_TypeError, "Expected %s or None for %s, got %R", type_name, field_name, obj); + std::string error_msg = "Expected " + std::string(type_name) + " or None for " + std::string(field_name); + PyErr_SetString(PyExc_TypeError, error_msg.c_str()); return false; } From 7c265e897d7f0309c604e8a73eb99fbc602f2139 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Thu, 9 Jul 2026 09:43:04 +0200 Subject: [PATCH 06/19] chore(typetreehelper.cpp): replace all NULL with nullptr --- UnityPyBoost/TypeTreeHelper.cpp | 114 ++++++++++++++++---------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/UnityPyBoost/TypeTreeHelper.cpp b/UnityPyBoost/TypeTreeHelper.cpp index cbad558c..16b6571e 100644 --- a/UnityPyBoost/TypeTreeHelper.cpp +++ b/UnityPyBoost/TypeTreeHelper.cpp @@ -76,7 +76,7 @@ inline PyObject *read_bool(ReaderT *reader) if (reader->ptr + 1 > reader->end) { PyErr_SetString(PyExc_ValueError, "read_bool out of bounds"); - return NULL; + return nullptr; } PyObject *value = *reader->ptr++ ? Py_True : Py_False; Py_INCREF(value); @@ -88,7 +88,7 @@ inline PyObject *read_bool_array(ReaderT *reader, int32_t count) if (reader->ptr + count > reader->end) { PyErr_SetString(PyExc_ValueError, "read_bool_array out of bounds"); - return NULL; + return nullptr; } PyObject *list = PyList_New(count); for (auto i = 0; i < count; i++) @@ -105,7 +105,7 @@ inline PyObject *read_u8(ReaderT *reader) if (reader->ptr + 1 > reader->end) { PyErr_SetString(PyExc_ValueError, "read_u8 out of bounds"); - return NULL; + return nullptr; } return PyLong_FromUnsignedLong(*reader->ptr++); } @@ -115,7 +115,7 @@ inline PyObject *read_u8_array(ReaderT *reader, int32_t count) if (reader->ptr + count > reader->end) { PyErr_SetString(PyExc_ValueError, "read_u8_array out of bounds"); - return NULL; + return nullptr; } PyObject *list = PyList_New(count); for (auto i = 0; i < count; i++) @@ -130,7 +130,7 @@ inline PyObject *read_s8(ReaderT *reader) if (reader->ptr + 1 > reader->end) { PyErr_SetString(PyExc_ValueError, "read_s8 out of bounds"); - return NULL; + return nullptr; } return PyLong_FromLong((int8_t)*reader->ptr++); } @@ -140,7 +140,7 @@ inline PyObject *read_s8_array(ReaderT *reader, int32_t count) if (reader->ptr + count > reader->end) { PyErr_SetString(PyExc_ValueError, "read_s8_array out of bounds"); - return NULL; + return nullptr; } PyObject *list = PyList_New(count); int8_t *ptr = (int8_t *)reader->ptr; @@ -291,12 +291,12 @@ inline PyObject *read_str(ReaderT *reader) int32_t length; if (!_read_length(reader, &length)) { - return NULL; + return nullptr; } if (reader->ptr + length > reader->end) { PyErr_SetString(PyExc_EOFError, "read_str out of bounds"); - return NULL; + return nullptr; } PyObject *py_str = PyUnicode_DecodeUTF8((char *)reader->ptr, length, "surrogateescape"); reader->ptr += length; @@ -310,12 +310,12 @@ inline PyObject *read_bytes(ReaderT *reader) int32_t length; if (!_read_length(reader, &length)) { - return NULL; + return nullptr; } if (reader->ptr + length > reader->end) { PyErr_SetString(PyExc_ValueError, "read_bytes out of bounds"); - return NULL; + return nullptr; } PyObject *bytes = PyBytes_FromStringAndSize((char *)reader->ptr, length); reader->ptr += length; @@ -328,19 +328,19 @@ inline PyObject *read_pair(ReaderT *reader, TypeTreeNodeObject *node, TypeTreeRe if (PyList_GET_SIZE(node->m_Children) != 2) { PyErr_SetString(PyExc_ValueError, "Pair node must have 2 children"); - return NULL; + return nullptr; } PyObject *first = read_typetree_value(reader, (TypeTreeNodeObject *)PyList_GET_ITEM(node->m_Children, 0), config); - if (first == NULL) + if (first == nullptr) { - return NULL; + return nullptr; } PyObject *second = read_typetree_value(reader, (TypeTreeNodeObject *)PyList_GET_ITEM(node->m_Children, 1), config); - if (second == NULL) + if (second == nullptr) { Py_DECREF(first); - return NULL; + return nullptr; } // PyTuple_Pack creates two strong references PyObject *pair = PyTuple_Pack(2, first, second); @@ -356,31 +356,31 @@ inline PyObject *read_pair_array(ReaderT *reader, TypeTreeNodeObject *node, Type if (PyList_GET_SIZE(node->m_Children) != 2) { PyErr_SetString(PyExc_ValueError, "Pair node must have 2 children"); - return NULL; + return nullptr; } TypeTreeNodeObject *first_child = (TypeTreeNodeObject *)PyList_GET_ITEM(node->m_Children, 0); TypeTreeNodeObject *second_child = (TypeTreeNodeObject *)PyList_GET_ITEM(node->m_Children, 1); PyObject *list = PyList_New(count); - if (list == NULL) + if (list == nullptr) { - return NULL; + return nullptr; } for (auto i = 0; i < count; i++) { PyObject *first = read_typetree_value(reader, first_child, config); - if (first == NULL) + if (first == nullptr) { Py_DECREF(list); - return NULL; + return nullptr; } PyObject *second = read_typetree_value(reader, second_child, config); - if (second == NULL) + if (second == nullptr) { Py_DECREF(first); Py_DECREF(list); - return NULL; + return nullptr; } PyList_SET_ITEM(list, i, PyTuple_Pack(2, first, second)); // pack creates two strong references // so we need to decref both values here to bring their ref count back to 1 @@ -412,10 +412,10 @@ inline PyObject *read_class(ReaderT *reader, TypeTreeNodeObject *node, TypeTreeR } } PyObject *child_value = read_typetree_value(reader, child, config); // child_value: 1 refcount - if (child_value == NULL) + if (child_value == nullptr) { Py_DECREF(value); // value: 0 refcount - return NULL; + return nullptr; } int set_item_result; if constexpr (as_dict == true) @@ -430,7 +430,7 @@ inline PyObject *read_class(ReaderT *reader, TypeTreeNodeObject *node, TypeTreeR { Py_DECREF(value); // value: 0 refcount Py_DECREF(child_value); // child_value: 0 refcount - return NULL; + return nullptr; } // PyDict_SetItem increases ref count, so we need to decref here Py_DECREF(child_value); // child_value: 1 refcount @@ -449,7 +449,7 @@ static PyObject *_get_annotations = nullptr; inline PyObject *get_annotations(PyObject *clz) { #if PY_VERSION_HEX >= 0x030e0000 - return PyObject_CallFunctionObjArgs(_get_annotations, clz, NULL); + return PyObject_CallFunctionObjArgs(_get_annotations, clz, nullptr); #else return PyObject_GetAttrString(clz, "__annotations__"); #endif @@ -468,9 +468,9 @@ inline PyObject *parse_class(PyObject *kwargs, TypeTreeNodeObject *node, TypeTre // slots check PyObject *slots = nullptr; - if (kwargs == NULL) + if (kwargs == nullptr) { - return NULL; + return nullptr; } if (node->_data_type == NodeDataType::PPtr) @@ -587,14 +587,14 @@ TypeTreeNodeObject *get_ref_type_node(PyObject *ref_object, PyObject *assetsfile if (assetsfile == Py_None) { PyErr_SetString(PyExc_ValueError, "Reference Type found but no SerializedFile passed as assetsfile to read_typetree!"); - return NULL; + return nullptr; } PyObject *ref_types = PyObject_GetAttrString(assetsfile, "ref_types"); if (!ref_types || !PyList_Check(ref_types)) { Py_XDECREF(ref_types); PyErr_SetString(PyExc_ValueError, "No SerializedFile.ref_types"); - return NULL; + return nullptr; } PyObject *type = PyDict_GetItemString(ref_object, "type"); @@ -602,12 +602,12 @@ TypeTreeNodeObject *get_ref_type_node(PyObject *ref_object, PyObject *assetsfile { Py_DECREF(ref_types); PyErr_SetString(PyExc_ValueError, "Failed to get 'type'"); - return NULL; + return nullptr; } - PyObject *cls = NULL; - PyObject *ns = NULL; - PyObject *asm_ = NULL; + PyObject *cls = nullptr; + PyObject *ns = nullptr; + PyObject *asm_ = nullptr; if (PyDict_Check(type)) { cls = PyDict_GetItemString(type, "class"); @@ -631,7 +631,7 @@ TypeTreeNodeObject *get_ref_type_node(PyObject *ref_object, PyObject *assetsfile Py_XDECREF(ns); Py_XDECREF(asm_); PyErr_SetString(PyExc_ValueError, "Failed to get 'class', 'ns' or 'asm'"); - return NULL; + return nullptr; } if (PyUnicode_GET_LENGTH(cls) == 0) @@ -644,7 +644,7 @@ TypeTreeNodeObject *get_ref_type_node(PyObject *ref_object, PyObject *assetsfile } Py_ssize_t ref_types_len = PyList_Size(ref_types); - TypeTreeNodeObject *ref_type_node = NULL; + TypeTreeNodeObject *ref_type_node = nullptr; for (Py_ssize_t i = 0; i < ref_types_len; i++) { PyObject *ref_type = PyList_GetItem(ref_types, i); @@ -762,7 +762,7 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre { PyErr_SetString(PyExc_ValueError, "Failed to get ref type node"); Py_DECREF(value); - return NULL; + return nullptr; } else if (ref_node == (TypeTreeNodeObject *)Py_None) { @@ -777,16 +777,16 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre child_value = read_typetree_value(reader, child, config); } - if (child_value == NULL) + if (child_value == nullptr) { Py_DECREF(value); - return NULL; + return nullptr; } if (PyDict_SetItem(value, child->m_Name, child_value)) { Py_DECREF(value); Py_DECREF(child_value); - return NULL; + return nullptr; } // dict increases ref count, so we need to decref here Py_DECREF(child_value); @@ -794,11 +794,11 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre if (!config->as_dict) { PyObject *clz = PyObject_GetAttrString(config->classes, "UnknownObject"); - if (clz == NULL) + if (clz == nullptr) { PyErr_SetString(PyExc_ValueError, "Failed to get class"); Py_DECREF(value); - return NULL; + return nullptr; } PyObject *args = PyTuple_Pack(1, (PyObject *)node); PyObject *instance = PyObject_Call(clz, args, value); @@ -822,7 +822,7 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre if (PyList_GET_SIZE(child->m_Children) != 2) { PyErr_SetString(PyExc_ValueError, "Array node must have 2 children"); - return NULL; + return nullptr; } if (child->_align) @@ -832,24 +832,24 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre int32_t length; if (!_read_length(reader, &length)) { - return NULL; + return nullptr; } child = (TypeTreeNodeObject *)PyList_GET_ITEM(child->m_Children, 1); if (std::find(std::begin(SUPPORTED_VALUE_ARRAY_READ_TYPES), std::end(SUPPORTED_VALUE_ARRAY_READ_TYPES), child->_data_type) == std::end(SUPPORTED_VALUE_ARRAY_READ_TYPES)) { value = PyList_New(length); - if (value == NULL) + if (value == nullptr) { - return NULL; + return nullptr; } for (int i = 0; i < length; i++) { PyObject *item = read_typetree_value(reader, child, config); - if (item == NULL) + if (item == nullptr) { Py_DECREF(value); - return NULL; + return nullptr; } PyList_SET_ITEM(value, i, item); } @@ -874,7 +874,7 @@ PyObject *read_typetree_value(ReaderT *reader, TypeTreeNodeObject *node, TypeTre } } - if (align && value != NULL) + if (align && value != nullptr) { align4(reader); } @@ -931,7 +931,7 @@ PyObject *read_typetree_value_array(ReaderT *reader, TypeTreeNodeObject *node, T PyErr_SetString(PyExc_TypeError, error_msg.c_str()); value = nullptr; } - if (align && value != NULL) + if (align && value != nullptr) { align4(reader); } @@ -961,7 +961,7 @@ static bool is_null_none_or_type(PyObject *obj, PyTypeObject *type, const char * PyObject *read_typetree(PyObject *self, PyObject *args, PyObject *kwargs) { - const char *kwlist[] = {"data", "node", "endian", "as_dict", "assetsfile", "classes", NULL}; + const char *kwlist[] = {"data", "node", "endian", "as_dict", "assetsfile", "classes", nullptr}; Py_buffer view; PyObject *node = nullptr; int as_dict = 1; @@ -1027,7 +1027,7 @@ PyObject *read_typetree(PyObject *self, PyObject *args, PyObject *kwargs) Py_DECREF(config.assetfile); Py_DECREF(config.classes); PyErr_SetString(PyExc_ValueError, "Invalid endian"); - return NULL; + return nullptr; } } @@ -1052,7 +1052,7 @@ PyObject *read_typetree(PyObject *self, PyObject *args, PyObject *kwargs) Py_XDECREF(config.assetfile); Py_XDECREF(config.classes); - return (value != NULL) ? Py_BuildValue("(Nn)", value, bytes_read) : NULL; + return (value != nullptr) ? Py_BuildValue("(Nn)", value, bytes_read) : nullptr; } // TypeTreeNode impl @@ -1141,7 +1141,7 @@ static int TypeTreeNode_init(TypeTreeNodeObject *self, PyObject *args, PyObject "m_Index", "m_MetaFlag", "m_RefTypeHash", - NULL}; + nullptr}; // ensure all fields are set to 0 // in case init fails, so that dealloc doesn't segfault @@ -1244,16 +1244,16 @@ static PyMemberDef TypeTreeNode_members[] = { {"m_MetaFlag", T_OBJECT_EX, offsetof(TypeTreeNodeObject, m_MetaFlag), 0, ""}, {"m_RefTypeHash", T_OBJECT_EX, offsetof(TypeTreeNodeObject, m_RefTypeHash), 0, ""}, {"_clean_name", T_OBJECT_EX, offsetof(TypeTreeNodeObject, _clean_name), 0, ""}, - {NULL} /* Sentinel */ + {nullptr} /* Sentinel */ }; static PyTypeObject TypeTreeNodeType = []() -> PyTypeObject { PyTypeObject type = { #if PY_VERSION_HEX >= 0x03080000 - PyVarObject_HEAD_INIT(NULL, 0) + PyVarObject_HEAD_INIT(nullptr, 0) #else - PyObject_HEAD_INIT(NULL) 0 + PyObject_HEAD_INIT(nullptr) 0 #endif }; type.tp_name = "TypeTreeHelper.TypeTreeNode"; From 59af5e2bad5da6a5b5d290c3880ce061ed4c3de9 Mon Sep 17 00:00:00 2001 From: Tedy <53588129+TedyonGit@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:06:05 +0300 Subject: [PATCH 07/19] refractor(tpk.py): load tpk blob via importlib.resources (#373) * Checking for python version for the open_binary and files calls * chore(tpk.py): use if/else instead of try/except, unwrap function --------- Co-authored-by: Rudolf Kolbe --- UnityPy/helpers/Tpk.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/UnityPy/helpers/Tpk.py b/UnityPy/helpers/Tpk.py index 2f2f8965..5f9ae770 100644 --- a/UnityPy/helpers/Tpk.py +++ b/UnityPy/helpers/Tpk.py @@ -1,7 +1,7 @@ from __future__ import annotations +import sys from enum import IntEnum, IntFlag -from importlib.resources import open_binary from io import BytesIO from struct import Struct from typing import Any, Dict, List, Optional, Tuple, TypeVar @@ -18,11 +18,22 @@ def init(): - with open_binary("UnityPy.resources", "lzma.tpk") as f: - data = f.read() + package = "UnityPy.resources" + resource = "lzma.tpk" + + tpk_data: bytes + if sys.version_info >= (3, 9): + from importlib.resources import files + + tpk_data = files(package).joinpath(resource).read_bytes() + + else: + from importlib.resources import open_binary + + tpk_data = open_binary(package, resource).read() global TPKTYPETREE - with BytesIO(data) as stream: + with BytesIO(tpk_data) as stream: blob = TpkFile(stream).GetDataBlob() assert isinstance(blob, TpkTypeTreeBlob) TPKTYPETREE = blob From 3dda24b7dc5873964a947c8202b912feaf494d2e Mon Sep 17 00:00:00 2001 From: StarHeart Date: Thu, 9 Jul 2026 16:18:18 +0800 Subject: [PATCH 08/19] feat: read preload table to get path id (#372) * feat: read preload table to get path id * refactor: only build container index once * fix: encapsulate lock * fix: condition --- UnityPy/environment.py | 13 +++++++++++++ UnityPy/files/ObjectReader.py | 3 +++ UnityPy/helpers/ContainerHelper.py | 26 +++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/UnityPy/environment.py b/UnityPy/environment.py index a44d228f..c4a60aea 100644 --- a/UnityPy/environment.py +++ b/UnityPy/environment.py @@ -32,6 +32,7 @@ class Environment: local_files: List[str] local_files_simple: List[str] typetree_generator: Optional["TypeTreeGenerator"] = None + _container_index_built: bool = False def __init__(self, *args: FileSourceType, fs: Optional[AbstractFileSystem] = None, path: Optional[str] = None): self.files = {} @@ -39,6 +40,7 @@ def __init__(self, *args: FileSourceType, fs: Optional[AbstractFileSystem] = Non self.fs = fs or LocalFileSystem() self.local_files = [] self.local_files_simple = [] + self._container_index_built = False if path is None: # if no path is given, use the current working directory @@ -203,9 +205,19 @@ def search(item): return search(self) + def _build_container_index(self) -> None: + if self._container_index_built: + return + + self._container_index_built = True + for f in self.cabs.values(): + if isinstance(f, SerializedFile): + f.container.parse_preload_table() + @property def container(self) -> ContainerHelper: """Returns a dictionary of all objects in the Environment.""" + self._build_container_index() container = [] for f in self.cabs.values(): if isinstance(f, SerializedFile) and not f.is_dependency: @@ -249,6 +261,7 @@ def register_cab(self, name: str, item: Union[SerializedFile, EndianBinaryReader The file to register. """ self.cabs[simplify_name(name)] = item + self._container_index_built = False def get_cab(self, name: str) -> Union[SerializedFile, EndianBinaryReader, None]: """ diff --git a/UnityPy/files/ObjectReader.py b/UnityPy/files/ObjectReader.py index fc03b874..b5fe1dbd 100644 --- a/UnityPy/files/ObjectReader.py +++ b/UnityPy/files/ObjectReader.py @@ -198,6 +198,9 @@ def peek_name(self) -> Union[str, None]: @property def container(self): + env = self.assets_file.environment + if env is not None: + env._build_container_index() return self.assets_file._container.path_dict.get(self.path_id) @property diff --git a/UnityPy/helpers/ContainerHelper.py b/UnityPy/helpers/ContainerHelper.py index 95c3d30e..f2ad977c 100644 --- a/UnityPy/helpers/ContainerHelper.py +++ b/UnityPy/helpers/ContainerHelper.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Generator, Iterator, List, Tuple, Union +from typing import TYPE_CHECKING, Dict, Generator, Iterator, List, Optional, Tuple, Union from attrs import define @@ -16,13 +16,37 @@ class ContainerHelper: container: List[Tuple[str, AssetInfo]] container_dict: Dict[str, PPtr[Object]] path_dict: Dict[int, str] + _preload_table: Optional[List[PPtr[Object]]] = None def __init__(self, container: Union[List[Tuple[str, AssetInfo]], AssetBundle]) -> None: + preload_table: Optional[List[PPtr[Object]]] = None if not isinstance(container, (list)): + preload_table = container.m_PreloadTable container = container.m_Container self.container = container self.container_dict = {key: value.asset for key, value in container} self.path_dict = {value.asset.path_id: key for key, value in container} + self._preload_table = preload_table + + def parse_preload_table(self) -> None: + if self._preload_table is None: + return + + for path, info in self.container: + start = info.preloadIndex + size = info.preloadSize + if start < 0 or size <= 0 or start + size > len(self._preload_table): + continue + for pptr in self._preload_table[start : start + size]: + if not pptr: + continue + try: + target = pptr.deref() + except (FileNotFoundError, KeyError): + continue + target.assets_file._container.path_dict.setdefault(pptr.path_id, path) + + self._preload_table = None def items(self) -> Generator[Tuple[str, PPtr[Object]], None, None]: return ((key, value.asset) for key, value in self.container) From d06d6d768ac6b9ac92f55dd1c5ce0393ba4a35a1 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Thu, 9 Jul 2026 11:01:13 +0200 Subject: [PATCH 09/19] release: 1.25.2 --- UnityPy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityPy/__init__.py b/UnityPy/__init__.py index 96f2b0ea..a2dcf6c6 100644 --- a/UnityPy/__init__.py +++ b/UnityPy/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.25.1" +__version__ = "1.25.2" from .environment import Environment as Environment from .helpers.ArchiveStorageManager import ( From 9a7c0ab9655bacd716e2af36fb950a92ec5109a5 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Thu, 9 Jul 2026 11:03:03 +0200 Subject: [PATCH 10/19] fix(Texture2DConverter): use RGB for BC6 fixes: #371 --- UnityPy/export/Texture2DConverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityPy/export/Texture2DConverter.py b/UnityPy/export/Texture2DConverter.py index f3462a41..cdb743ee 100644 --- a/UnityPy/export/Texture2DConverter.py +++ b/UnityPy/export/Texture2DConverter.py @@ -512,7 +512,7 @@ def rgb9e5float(image_data: bytes, width: int, height: int) -> Image.Image: TF.RGB9e5Float: (rgb9e5float, ()), TF.BC4: (pillow, ("L", "bcn", 4)), TF.BC5: (pillow, ("RGB", "bcn", 5)), - TF.BC6H: (pillow, ("RGBA", "bcn", 6)), + TF.BC6H: (pillow, ("RGB", "bcn", 6)), TF.BC7: (pillow, ("RGBA", "bcn", 7)), TF.DXT1Crunched: (pillow, ("RGBA", "bcn", 1)), TF.DXT5Crunched: (pillow, ("RGBA", "bcn", 3)), From e5c30dbbcf35f87bed1ca866dd17f6353f256d14 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Thu, 9 Jul 2026 11:05:14 +0200 Subject: [PATCH 11/19] chore(UnityPyBoost): replace all NULL with nullptr --- UnityPyBoost/ArchiveStorageDecryptor.cpp | 8 ++++---- UnityPyBoost/Mesh.cpp | 10 +++++----- UnityPyBoost/UnityPyBoost.cpp | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/UnityPyBoost/ArchiveStorageDecryptor.cpp b/UnityPyBoost/ArchiveStorageDecryptor.cpp index 65713267..b26534ff 100644 --- a/UnityPyBoost/ArchiveStorageDecryptor.cpp +++ b/UnityPyBoost/ArchiveStorageDecryptor.cpp @@ -62,15 +62,15 @@ PyObject *decrypt_block(PyObject *self, PyObject *args) { if (index_data.buf) PyBuffer_Release(&index_data); if (substitute_data.buf) PyBuffer_Release(&substitute_data); if (data.buf) PyBuffer_Release(&data); - return NULL; + return nullptr; } - PyObject *result = PyBytes_FromStringAndSize(NULL, data.len); - if (result == NULL) { + PyObject *result = PyBytes_FromStringAndSize(nullptr, data.len); + if (result == nullptr) { PyBuffer_Release(&index_data); PyBuffer_Release(&substitute_data); PyBuffer_Release(&data); - return NULL; + return nullptr; } unsigned char *result_raw = (unsigned char *)PyBytes_AS_STRING(result); diff --git a/UnityPyBoost/Mesh.cpp b/UnityPyBoost/Mesh.cpp index beb66629..30a9a061 100644 --- a/UnityPyBoost/Mesh.cpp +++ b/UnityPyBoost/Mesh.cpp @@ -39,7 +39,7 @@ PyObject *unpack_vertexdata(PyObject *self, PyObject *args) { PyBuffer_Release(&vertexDataView); } - return NULL; + return nullptr; } uint8_t *vertexData = (uint8_t *)vertexDataView.buf; @@ -52,14 +52,14 @@ PyObject *unpack_vertexdata(PyObject *self, PyObject *args) { PyBuffer_Release(&vertexDataView); PyErr_SetString(PyExc_ValueError, "Vertex data access out of bounds"); - return NULL; + return nullptr; } PyObject *res = PyBytes_FromStringAndSize(nullptr, componentBytesLength); if (!res) { PyBuffer_Release(&vertexDataView); - return NULL; + return nullptr; } uint8_t *componentBytes = (uint8_t *)PyBytes_AS_STRING(res); @@ -102,7 +102,7 @@ PyObject *unpack_vertexdata(PyObject *self, PyObject *args) // uint32_t itemCount = componentBytesLength / componentByteSize; // PyObject *lst = PyList_New(itemCount); // if (!lst) - // return NULL; + // return nullptr; // switch (format) // { @@ -124,7 +124,7 @@ PyObject *unpack_vertexdata(PyObject *self, PyObject *args) // double x = _PyFloat_Unpack2(items++, 0); // if (x == -1.0 && PyErr_Occurred()) // { - // return NULL; + // return nullptr; // } // PyList_SetItem(lst, i, PyFloat_FromDouble(x)); // } diff --git a/UnityPyBoost/UnityPyBoost.cpp b/UnityPyBoost/UnityPyBoost.cpp index 92908bda..4b3ed8fa 100644 --- a/UnityPyBoost/UnityPyBoost.cpp +++ b/UnityPyBoost/UnityPyBoost.cpp @@ -19,10 +19,10 @@ static struct PyMethodDef method_table[] = { (PyCFunction)decrypt_block, METH_VARARGS, "replacement for ArchiveStorageDecryptor.decrypt_block"}, - {NULL, - NULL, + {nullptr, + nullptr, 0, - NULL} // Sentinel value ending the table + nullptr} // Sentinel value ending the table }; // A struct contains the definition of a module @@ -32,10 +32,10 @@ static PyModuleDef UnityPyBoost_module = { "TODO", -1, // Optional size of the module state memory method_table, - NULL, // Optional slot definitions - NULL, // Optional traversal function - NULL, // Optional clear function - NULL // Optional module deallocation function + nullptr, // Optional slot definitions + nullptr, // Optional traversal function + nullptr, // Optional clear function + nullptr // Optional module deallocation function }; // The module init function From 5567c5eddc9dbeaef27b5113f5927226bee4f8ca Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Thu, 9 Jul 2026 11:48:26 +0200 Subject: [PATCH 12/19] feat(mesh): speedup vertex data unpacking --- UnityPy/helpers/MeshHelper.py | 31 +++-- UnityPyBoost/Mesh.cpp | 215 +++++++++++++--------------------- 2 files changed, 101 insertions(+), 145 deletions(-) diff --git a/UnityPy/helpers/MeshHelper.py b/UnityPy/helpers/MeshHelper.py index bbcde5dd..371e8baf 100644 --- a/UnityPy/helpers/MeshHelper.py +++ b/UnityPy/helpers/MeshHelper.py @@ -375,19 +375,26 @@ def read_vertex_data(self, m_Channels: list[ChannelInfo], m_Streams: list[Stream swap, ) else: - componentBytes = bytearray(m_VertexCount * channel_dimension * component_byte_size) + channelSize = channel_dimension * component_byte_size - vertexBaseOffset = m_Stream.offset + m_Channel.offset - for v in range(m_VertexCount): - vertexOffset = vertexBaseOffset + m_Stream.stride * v - for d in range(channel_dimension): - componentOffset = vertexOffset + component_byte_size * d - vertexDataSrc = componentOffset - componentDataSrc = component_byte_size * (v * channel_dimension + d) - buff = m_VertexData.m_DataSize[vertexDataSrc : vertexDataSrc + component_byte_size] - if swap: # swap bytes - buff = buff[::-1] - componentBytes[componentDataSrc : componentDataSrc + component_byte_size] = buff + componentBytes = bytearray(m_VertexCount * channel_dimension * component_byte_size) + vertexData = m_VertexData.m_DataSize + + componentOffset = 0 + vertexOffset = m_Stream.offset + m_Channel.offset + + for _ in range(m_VertexCount): + componentBytes[componentOffset : componentOffset + channelSize] = vertexData[ + vertexOffset : vertexOffset + channelSize + ] + componentOffset += channelSize + vertexOffset += m_Stream.stride + + if swap: + for offset in range(0, len(componentBytes), component_byte_size): + item = componentBytes[offset : offset + component_byte_size] + item.reverse() + componentBytes[offset : offset + component_byte_size] = item component_data = list(struct.iter_unpack(f">{channel_dimension}{component_dtype}", componentBytes)) self.assign_channel_vertex_data(chn, component_data) diff --git a/UnityPyBoost/Mesh.cpp b/UnityPyBoost/Mesh.cpp index 30a9a061..e1baf036 100644 --- a/UnityPyBoost/Mesh.cpp +++ b/UnityPyBoost/Mesh.cpp @@ -1,5 +1,6 @@ #include "Mesh.hpp" #include +#include #include #define MAX(x, y) (((x) > (y)) ? (x) : (y)) @@ -20,6 +21,64 @@ enum VertexFormat kVertexFormatSInt32 }; +template +void unpack_vertexdata_template(uint8_t *componentBytes, uint8_t *vertexData, uint32_t m_VertexCount, uint32_t m_StreamOffset, uint32_t m_StreamStride, uint32_t m_ChannelOffset, uint32_t m_ChannelDimension) +{ + const auto channelSize = componentByteSize * m_ChannelDimension; + + uint8_t *componentCur = componentBytes; + uint8_t *vertexCur = vertexData; + + // move vertexCur to the first vertex + vertexCur += m_StreamOffset + m_ChannelOffset; + + for (uint32_t v = 0; v < m_VertexCount; v++) + { + memcpy(componentCur, vertexCur, channelSize); + componentCur += channelSize; + vertexCur += m_StreamStride; + } +} + +template +void swap_vertexdata(uint8_t *componentBytes, uint32_t m_VertexCount, uint32_t m_ChannelDimension) +{ + if constexpr (componentByteSize == 1) + { + // do nothing + } + else if constexpr (componentByteSize == 2) + { + uint16_t *componentUints = (uint16_t *)componentBytes; + for (uint32_t i = 0; i < m_VertexCount * m_ChannelDimension; i++) + { + swap_any_inplace(componentUints++); + } + } + else if constexpr (componentByteSize == 4) + { + uint32_t *componentUints = (uint32_t *)componentBytes; + for (uint32_t i = 0; i < m_VertexCount * m_ChannelDimension; i++) + { + swap_any_inplace(componentUints++); + } + } + else if constexpr (componentByteSize == 8) + { + uint64_t *componentUints = (uint64_t *)componentBytes; + for (uint32_t i = 0; i < m_VertexCount * m_ChannelDimension; i++) + { + swap_any_inplace(componentUints++); + } + } + else + { + const auto compoentByteSizeStr = std::to_string(componentByteSize); + const auto error_message = "Swap not implemented for this size: " + compoentByteSizeStr; + PyErr_SetString(PyExc_ValueError, error_message.c_str()); + } +} + PyObject *unpack_vertexdata(PyObject *self, PyObject *args) { // define vars @@ -63,148 +122,38 @@ PyObject *unpack_vertexdata(PyObject *self, PyObject *args) } uint8_t *componentBytes = (uint8_t *)PyBytes_AS_STRING(res); - for (uint32_t v = 0; v < m_VertexCount; v++) + switch (componentByteSize) { - uint32_t vertexOffset = m_StreamOffset + m_ChannelOffset + m_StreamStride * v; - for (uint32_t d = 0; d < m_ChannelDimension; d++) + case 1: + unpack_vertexdata_template<1>(componentBytes, vertexData, m_VertexCount, m_StreamOffset, m_StreamStride, m_ChannelOffset, m_ChannelDimension); + break; + case 2: + unpack_vertexdata_template<2>(componentBytes, vertexData, m_VertexCount, m_StreamOffset, m_StreamStride, m_ChannelOffset, m_ChannelDimension); + if (swap) { - uint32_t vertexDataOffset = vertexOffset + componentByteSize * d; - uint32_t componentOffset = componentByteSize * (v * m_ChannelDimension + d); - memcpy(componentBytes + componentOffset, vertexData + vertexDataOffset, componentByteSize); + swap_vertexdata<2>(componentBytes, m_VertexCount, m_ChannelDimension); } - } - - if (swap) // swap bytes - { - if (componentByteSize == 2) + break; + case 4: + unpack_vertexdata_template<4>(componentBytes, vertexData, m_VertexCount, m_StreamOffset, m_StreamStride, m_ChannelOffset, m_ChannelDimension); + if (swap) { - uint16_t *componentUints = (uint16_t *)componentBytes; - for (uint32_t i = 0; i < componentBytesLength; i += 2) - { - swap_any_inplace(componentUints++); - } + swap_vertexdata<4>(componentBytes, m_VertexCount, m_ChannelDimension); } - else if (componentByteSize == 4) + break; + case 8: + unpack_vertexdata_template<8>(componentBytes, vertexData, m_VertexCount, m_StreamOffset, m_StreamStride, m_ChannelOffset, m_ChannelDimension); + if (swap) { - - uint32_t *componentUints = (uint32_t *)componentBytes; - for (uint32_t i = 0; i < componentBytesLength; i += 4) - { - swap_any_inplace(componentUints++); - } + swap_vertexdata<8>(componentBytes, m_VertexCount, m_ChannelDimension); } + break; + default: + PyBuffer_Release(&vertexDataView); + PyErr_SetString(PyExc_ValueError, "Unsupported component byte size"); + return nullptr; } PyBuffer_Release(&vertexDataView); return res; - - // fast enough in Python - // uint32_t itemCount = componentBytesLength / componentByteSize; - // PyObject *lst = PyList_New(itemCount); - // if (!lst) - // return nullptr; - - // switch (format) - // { - // case kVertexFormatFloat: - // { - // float *items = (float *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)*items++)); - // } - // // result[i] = BitConverter.ToSingle(inputBytes, i * 4); - // break; - // } - // case kVertexFormatFloat16: - // { - // uint16_t *items = (uint16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // double x = _PyFloat_Unpack2(items++, 0); - // if (x == -1.0 && PyErr_Occurred()) - // { - // return nullptr; - // } - // PyList_SetItem(lst, i, PyFloat_FromDouble(x)); - // } - // // result[i] = Half.ToHalf(inputBytes, i * 2); - // break; - // } - // case kVertexFormatUNorm8: - // { - // uint8_t *items = componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)(*items++ / 255.0f))); - // } - // // result[i] = inputBytes[i] / 255f; - // break; - // } - // case kVertexFormatSNorm8: - // { - // int8_t *items = (int8_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)MAX((*items++ / 127.0f), -1.0f))); - // } - // // result[i] = Math.Max((sbyte)inputBytes[i] / 127f, -1f); - // break; - // } - // case kVertexFormatUNorm16: - // { - // uint16_t *items = (uint16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)(*items++ / 65535.0f))); - // } - // // result[i] = BitConverter.ToUInt16(inputBytes, i * 2) / 65535f; - // break; - // } - // case kVertexFormatSNorm16: - // { - // int16_t *items = (int16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyFloat_FromDouble((double)MAX((*items++ / 32767.0f), -1.0f))); - // } - // // result[i] = Math.Max(BitConverter.ToInt16(inputBytes, i * 2) / 32767f, -1f); - // break; - // } - // case kVertexFormatUInt8: - // case kVertexFormatSInt8: - // { - // uint8_t *items = componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyLong_FromUnsignedLong((uint32_t)*items++)); - // } - // // result[i] = inputBytes[i]; - // break; - // } - // case kVertexFormatUInt16: - // case kVertexFormatSInt16: - // { - // uint16_t *items = (uint16_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyLong_FromUnsignedLong((uint32_t)*items++)); - // } - // // result[i] = BitConverter.ToInt16(inputBytes, i * 2); - // break; - // } - // case kVertexFormatUInt32: - // case kVertexFormatSInt32: - // { - // uint32_t *items = (uint32_t *)componentBytes; - // for (uint32_t i = 0; i < itemCount; i++) - // { - // PyList_SetItem(lst, i, PyLong_FromUnsignedLong(*items++)); - // } - // // result[i] = BitConverter.ToInt32(inputBytes, i * 4); - // break; - // } - // } - // free(componentBytes); - // return lst; } From 68c7469cd111c417e7ef750170d51c8be727b1a5 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Sat, 1 Aug 2026 20:33:12 +0200 Subject: [PATCH 13/19] feat(tpk): replace internal tpk parser with moved out project, use @cache instead of dict caches --- UnityPy/cli/update_tpk.py | 26 +- UnityPy/enums/CommonString.py | 110 ------- UnityPy/enums/__init__.py | 2 - UnityPy/files/SerializedFile.py | 5 +- UnityPy/helpers/Tpk.py | 458 ++--------------------------- UnityPy/helpers/TypeTreeNode.py | 32 +- UnityPy/tools/TpkClassGenerator.py | 8 +- pyproject.toml | 2 + 8 files changed, 54 insertions(+), 589 deletions(-) delete mode 100644 UnityPy/enums/CommonString.py diff --git a/UnityPy/cli/update_tpk.py b/UnityPy/cli/update_tpk.py index 6cb776c9..f2a51273 100644 --- a/UnityPy/cli/update_tpk.py +++ b/UnityPy/cli/update_tpk.py @@ -1,27 +1,29 @@ import os -from io import BytesIO -from urllib.request import urlopen -from zipfile import ZipFile -from UnityPy.tools.TpkClassGenerator import generate_classes +from tpk_ar.utils import download_tpk -URL = "https://nightly.link/AssetRipper/Tpk/workflows/type_tree_tpk/master/lzma_file.zip" RESOURCE_PATH = os.path.join(os.path.dirname(__file__), "..", "resources") def update_tpk(): print("Updating TPK file...") print("\tDownloading...") - with urlopen(URL) as response: - if response.status != 200: - raise Exception(f"Failed to download TPK file: {response.status} {response.reason}") - zip_data = response.read() - print("\tExtracting...") - with ZipFile(BytesIO(zip_data)) as zip_file: - zip_file.extract("lzma.tpk", path=RESOURCE_PATH) + tpk_data = download_tpk() + + print("\tSaving...") + with open(os.path.join(RESOURCE_PATH, "lzma.tpk"), "wb") as f: + f.write(tpk_data) + print("\tGenerating classes...") + # import here to avoid loading a potentially broken or missing tpk file + + from UnityPy.tools.TpkClassGenerator import generate_classes + generate_classes() print("\tDone.") __all__ = ["update_tpk"] + +if __name__ == "__main__": + update_tpk() diff --git a/UnityPy/enums/CommonString.py b/UnityPy/enums/CommonString.py deleted file mode 100644 index 0491b660..00000000 --- a/UnityPy/enums/CommonString.py +++ /dev/null @@ -1,110 +0,0 @@ -CommonString = { - 0: "AABB", - 5: "AnimationClip", - 19: "AnimationCurve", - 34: "AnimationState", - 49: "Array", - 55: "Base", - 60: "BitField", - 69: "bitset", - 76: "bool", - 81: "char", - 86: "ColorRGBA", - 96: "Component", - 106: "data", - 111: "deque", - 117: "double", - 124: "dynamic_array", - 138: "FastPropertyName", - 155: "first", - 161: "float", - 167: "Font", - 172: "GameObject", - 183: "Generic Mono", - 196: "GradientNEW", - 208: "GUID", - 213: "GUIStyle", - 222: "int", - 226: "list", - 231: "long long", - 241: "map", - 245: "Matrix4x4f", - 256: "MdFour", - 263: "MonoBehaviour", - 277: "MonoScript", - 288: "m_ByteSize", - 299: "m_Curve", - 307: "m_EditorClassIdentifier", - 331: "m_EditorHideFlags", - 349: "m_Enabled", - 359: "m_ExtensionPtr", - 374: "m_GameObject", - 387: "m_Index", - 395: "m_IsArray", - 405: "m_IsStatic", - 416: "m_MetaFlag", - 427: "m_Name", - 434: "m_ObjectHideFlags", - 452: "m_PrefabInternal", - 469: "m_PrefabParentObject", - 490: "m_Script", - 499: "m_StaticEditorFlags", - 519: "m_Type", - 526: "m_Version", - 536: "Object", - 543: "pair", - 548: "PPtr", - 564: "PPtr", - 581: "PPtr", - 596: "PPtr", - 616: "PPtr", - 633: "PPtr", - 646: "PPtr", - 659: "PPtr", - 672: "PPtr", - 688: "PPtr", - 702: "PPtr", - 718: "PPtr", - 734: "Prefab", - 741: "Quaternionf", - 753: "Rectf", - 759: "RectInt", - 767: "RectOffset", - 778: "second", - 785: "set", - 789: "short", - 795: "size", - 800: "SInt16", - 807: "SInt32", - 814: "SInt64", - 821: "SInt8", - 827: "staticvector", - 840: "string", - 847: "TextAsset", - 857: "TextMesh", - 866: "Texture", - 874: "Texture2D", - 884: "Transform", - 894: "TypelessData", - 907: "UInt16", - 914: "UInt32", - 921: "UInt64", - 928: "UInt8", - 934: "unsigned int", - 947: "unsigned long long", - 966: "unsigned short", - 981: "vector", - 988: "Vector2f", - 997: "Vector3f", - 1006: "Vector4f", - 1015: "m_ScriptingClassIdentifier", - 1042: "Gradient", - 1051: "Type*", - 1057: "int2_storage", - 1070: "int3_storage", - 1083: "BoundsInt", - 1093: "m_CorrespondingSourceObject", - 1121: "m_PrefabInstance", - 1138: "m_PrefabAsset", - 1152: "FileSize", -} diff --git a/UnityPy/enums/__init__.py b/UnityPy/enums/__init__.py index fe4db176..c9a99d83 100644 --- a/UnityPy/enums/__init__.py +++ b/UnityPy/enums/__init__.py @@ -2,7 +2,6 @@ from .BuildTarget import BuildTarget from .BundleFile import ArchiveFlags, ArchiveFlagsOld, CompressionFlags from .ClassIDType import ClassIDType -from .CommonString import CommonString from .FileType import FileType from .GfxPrimitiveType import GfxPrimitiveType from .GraphicsFormat import GraphicsFormat @@ -25,7 +24,6 @@ "ArchiveFlagsOld", "CompressionFlags", "ClassIDType", - "CommonString", "FileType", "GfxPrimitiveType", "GraphicsFormat", diff --git a/UnityPy/files/SerializedFile.py b/UnityPy/files/SerializedFile.py index 1ca608a6..d8853378 100644 --- a/UnityPy/files/SerializedFile.py +++ b/UnityPy/files/SerializedFile.py @@ -6,8 +6,9 @@ from attrs import define from .. import config -from ..enums import BuildTarget, ClassIDType, CommonString +from ..enums import BuildTarget, ClassIDType from ..helpers.ContainerHelper import ContainerHelper +from ..helpers.Tpk import get_common_strings from ..helpers.TypeTreeHelper import TypeTreeNode from ..helpers.UnityVersion import UnityVersion from ..streams import EndianBinaryWriter @@ -500,4 +501,4 @@ def read_string(string_buffer_reader: EndianBinaryReader, value: int) -> str: return string_buffer_reader.read_string_to_null() offset = value & 0x7FFFFFFF - return CommonString.get(offset, str(offset)) + return get_common_strings().get(offset, str(offset)) diff --git a/UnityPy/helpers/Tpk.py b/UnityPy/helpers/Tpk.py index 5f9ae770..3ccdfbef 100644 --- a/UnityPy/helpers/Tpk.py +++ b/UnityPy/helpers/Tpk.py @@ -1,23 +1,22 @@ from __future__ import annotations import sys -from enum import IntEnum, IntFlag +from functools import cache from io import BytesIO -from struct import Struct -from typing import Any, Dict, List, Optional, Tuple, TypeVar +from typing import TYPE_CHECKING, Dict, Optional, cast -from .CompressionHelper import decompress_lzma -from .TypeTreeHelper import TypeTreeNode -from .UnityVersion import UnityVersion +from tpk_ar import TpkFile, TpkTypeTreeBlob, TpkUnityClass, TpkUnityNode +from tpk_ar import UnityVersion as TpkUnityVersion -T = TypeVar("T") +from . import TypeTreeHelper +from .UnityVersion import UnityVersion -TPKTYPETREE: TpkTypeTreeBlob = None # pyright: ignore[reportAssignmentType] -CLASSES_CACHE: Dict[Tuple[int, UnityVersion], TypeTreeNode] = {} -NODES_CACHE: Dict[TpkUnityClass, TypeTreeNode] = {} +if TYPE_CHECKING: + from .TypeTreeHelper import TypeTreeNode -def init(): +@cache +def get_typetree() -> TpkTypeTreeBlob: package = "UnityPy.resources" resource = "lzma.tpk" @@ -32,39 +31,32 @@ def init(): tpk_data = open_binary(package, resource).read() - global TPKTYPETREE with BytesIO(tpk_data) as stream: - blob = TpkFile(stream).GetDataBlob() - assert isinstance(blob, TpkTypeTreeBlob) - TPKTYPETREE = blob + tree = TpkFile.parse(stream).GetDataBlob() + assert isinstance(tree, TpkTypeTreeBlob) + return tree +@cache def get_typetree_node(class_id: int, version: UnityVersion): - global CLASSES_CACHE - key = (class_id, version) - cached = CLASSES_CACHE.get(key) - if cached: - return cached - - class_info = TPKTYPETREE.ClassInformation[class_id].getVersionedClass(version) + tpk_version = cast(TpkUnityVersion, version) + class_info = get_typetree().ClassInformation[class_id].getVersionedClass(tpk_version) if class_info is None: raise ValueError("Could not find class info for class id {}".format(class_id)) node = generate_node(class_info) - CLASSES_CACHE[key] = node return node -def generate_node(class_info: TpkUnityClass) -> TypeTreeNode: - global NODES_CACHE - cached = NODES_CACHE.get(class_info) - if cached: - return cached - +@cache +def generate_node(class_info: TpkUnityClass) -> "TypeTreeNode": assert class_info.ReleaseRootNode is not None, "Class {} has no ReleaseRootNode".format(class_info) + TypeTreeNode = TypeTreeHelper.TypeTreeNode + nodes = [] - NODES = TPKTYPETREE.NodeBuffer + NODES = get_typetree().NodeBuffer + STRINGBUFFER = get_typetree().StringBuffer stack = [(class_info.ReleaseRootNode, 0)] index = 0 while stack: @@ -77,408 +69,16 @@ def generate_node(class_info: TpkUnityClass) -> TypeTreeNode: m_Version=node.Version, m_MetaFlag=node.MetaFlag, m_Level=level, - m_Type=TPKTYPETREE.StringBuffer[node.TypeName], - m_Name=TPKTYPETREE.StringBuffer[node.Name], + m_Type=STRINGBUFFER[node.TypeName], + m_Name=STRINGBUFFER[node.Name], ) ) stack = [(node_id, level + 1) for node_id in node.SubNodes] + stack index += 1 - result = TypeTreeNode.from_list(nodes) - NODES_CACHE[class_info] = result - return result - - -###################################################################################### -# -# Enums -# -###################################################################################### - - -class TpkCompressionType(IntEnum): - NONE = 0 - Lz4 = 1 - Lzma = 2 - Brotli = 3 - - -class TpkDataType(IntEnum): - TypeTreeInformation = 0 - Collection = 1 - FileSystem = 2 - Json = 3 - ReferenceAssemblies = 4 - EngineAssets = 5 - - def ToBlob(self, stream): - if self.value == TpkDataType.TypeTreeInformation: - return TpkTypeTreeBlob(stream) - elif self.value == TpkDataType.Collection: - return TpkCollectionBlob(stream) - elif self.value == TpkDataType.FileSystem: - return TpkFileSystemBlob(stream) - elif self.value == TpkDataType.Json: - return TpkJsonBlob(stream) - else: - raise Exception("Unimplemented TpkDataType -> Blob conversion") - - -class TpkUnityClassFlags(IntFlag): - NONE = 0 - IsAbstract = 1 - IsSealed = 2 - IsEditorOnly = 4 - IsReleaseOnly = 8 - IsStripped = 16 - Reserved = 32 - HasEditorRootNode = 64 - HasReleaseRootNode = 128 - - -###################################################################################### -# -# Main Class -# -###################################################################################### - - -class TpkFile: - Struct = Struct(" TpkDataBlob: - decompressed: bytes - if self.CompressionType == TpkCompressionType.NONE: - decompressed = self.CompressedBytes - - elif self.CompressionType == TpkCompressionType.Lz4: - import lz4.block - - decompressed = lz4.block.decompress(self.CompressedBytes, self.UncompressedSize) - - elif self.CompressionType == TpkCompressionType.Lzma: - decompressed = decompress_lzma(self.CompressedBytes) - - elif self.CompressionType == TpkCompressionType.Brotli: - import brotli - - decompressed = brotli.decompress(self.CompressedBytes) - - else: - raise Exception("Invalid compression type") - - return self.DataType.ToBlob(BytesIO(decompressed)) - - -###################################################################################### -# -# Blobs -# -###################################################################################### - - -class TpkDataBlob: - __slots__ = ("DataType",) - DataType: TpkDataType - - def __init__(self, stream: BytesIO) -> None: - raise NotImplementedError("TpkDataBlob is an abstract class") - - -class TpkTypeTreeBlob(TpkDataBlob): - __slots__ = ( - "CreationTime", - "Versions", - "ClassInformation", - "CommonString", - "NodeBuffer", - "StringBuffer", - ) - CreationTime: int - Versions: List[UnityVersion] - ClassInformation: Dict[int, TpkClassInformation] # List[TpkClassInformation] - CommonString: TpkCommonString - NodeBuffer: TpkUnityNodeBuffer - StringBuffer: TpkStringBuffer - DataType: TpkDataType = TpkDataType.TypeTreeInformation - - def __init__(self, stream: BytesIO) -> None: - (self.CreationTime,) = INT64.unpack(stream.read(INT64.size)) - (versionCount,) = INT32.unpack(stream.read(INT32.size)) - self.Versions = read_versions(stream, versionCount) - (classCount,) = INT32.unpack(stream.read(INT32.size)) - self.ClassInformation = {x.ID: x for x in (TpkClassInformation(stream) for _ in range(classCount))} - self.CommonString = TpkCommonString(stream) - self.NodeBuffer = TpkUnityNodeBuffer(stream) - self.StringBuffer = TpkStringBuffer(stream) - - -class TpkCollectionBlob(TpkDataBlob): - __slots__ = "Blobs" - Blobs: List[Tuple[str, TpkDataBlob]] - - def __init__(self, stream: BytesIO) -> None: - (count,) = INT32.unpack(stream.read(INT32.size)) - self.Blobs = [ - # relativePath, data - ( - read_string(stream), - TpkDataType(BYTE.unpack(stream.read(1))[0]).ToBlob(stream), - ) - for _ in range(count) - ] - - -class TpkFileSystemBlob(TpkDataBlob): - __slots__ = ("Files",) - # TODO: check if dict might be better - Files: List[Tuple[str, bytes]] - - def __init__(self, stream: BytesIO) -> None: - (count,) = INT32.unpack(stream.read(INT32.size)) - self.Files = [ - # relativePath, data - (read_string(stream), read_data(stream)) - for _ in range(count) - ] - - -class TpkJsonBlob(TpkDataBlob): - __slots__ = "Text" - Text: str - DataType = TpkDataType.Json - - def __init__(self, stream: BytesIO) -> None: - self.Text = read_string(stream) - - -###################################################################################### -# -# Unity -# -###################################################################################### - - -class TpkUnityClass: - __slots__ = ("Name", "Base", "Flags", "EditorRootNode", "ReleaseRootNode") - Struct = Struct(" None: - self.Name, self.Base, Flags = TpkUnityClass.Struct.unpack(stream.read(TpkUnityClass.Struct.size)) - self.Flags = TpkUnityClassFlags(Flags) - self.EditorRootNode = self.ReleaseRootNode = None - if self.Flags & TpkUnityClassFlags.HasEditorRootNode: - (self.EditorRootNode,) = UINT16.unpack(stream.read(UINT16.size)) - if self.Flags & TpkUnityClassFlags.HasReleaseRootNode: - (self.ReleaseRootNode,) = UINT16.unpack(stream.read(UINT16.size)) - - def to_dict(self) -> Dict[str, Any]: - return { - "Name": self.Name, - "Base": self.Base, - "Flags": self.Flags, - "EditorRootNode": self.EditorRootNode, - "ReleaseRootNode": self.ReleaseRootNode, - } - - def __eq__(self, other: object) -> bool: - if not isinstance(other, TpkUnityClass): - return False - return self.to_dict() == other.to_dict() - - def __hash__(self) -> int: - return hash( - ( - self.Name, - self.Base, - self.Flags, - self.EditorRootNode, - self.ReleaseRootNode, - ) - ) - - -class TpkClassInformation(List[Tuple[UnityVersion, Optional[TpkUnityClass]]]): - ID: int - - def __init__(self, stream: BytesIO) -> None: - (self.ID,) = INT32.unpack(stream.read(INT32.size)) - (count,) = INT32.unpack(stream.read(INT32.size)) - self.extend( - ( - read_version(stream), - TpkUnityClass(stream) if stream.read(1)[0] else None, - ) - for _ in range(count) - ) - - def getVersionedClass(self, version: UnityVersion) -> Optional[TpkUnityClass]: - return get_item_for_version(version, self) - - -class TpkUnityNode: - __slots__ = ( - "TypeName", - "Name", - "ByteSize", - "Version", - "TypeFlags", - "MetaFlag", - "SubNodes", - ) - Struct = Struct(" None: - ( - self.TypeName, - self.Name, - self.ByteSize, - self.Version, - self.TypeFlags, - self.MetaFlag, - count, - ) = TpkUnityNode.Struct.unpack(stream.read(TpkUnityNode.Struct.size)) - - SubNodeStruct = Struct(f"<{count}H") - self.SubNodes = list(SubNodeStruct.unpack(stream.read(SubNodeStruct.size))) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, TpkUnityNode): - return False - return self.__dict__ == other.__dict__ - - def __hash__(self) -> int: - # TODO - return hash(self.__dict__) - - -class TpkUnityNodeBuffer(List[TpkUnityNode]): - def __init__(self, stream: BytesIO) -> None: - (count,) = INT32.unpack(stream.read(INT32.size)) - self.extend(TpkUnityNode(stream) for _ in range(count)) - - -###################################################################################### -# -# Strings -# -###################################################################################### - - -class TpkStringBuffer(List[str]): - def __init__(self, stream: BytesIO) -> None: - count = INT32.unpack(stream.read(INT32.size))[0] - self.extend(read_string(stream) for _ in range(count)) - - -class TpkCommonString: - __slots__ = ("VersionInformation", "StringBufferIndices") - VersionInformation: List[Tuple[UnityVersion, int]] - StringBufferIndices: Tuple[int, ...] - - def __init__(self, stream: BytesIO) -> None: - (versionCount,) = INT32.unpack(stream.read(INT32.size)) - self.VersionInformation = [(read_version(stream), stream.read(1)[0]) for _ in range(versionCount)] - (indicesCount,) = INT32.unpack(stream.read(INT32.size)) - indicesStruct = Struct(f"<{indicesCount}H") - self.StringBufferIndices = indicesStruct.unpack(stream.read(indicesStruct.size)) - - def GetStrings(self, buffer: TpkStringBuffer) -> List[str]: - return [buffer[i] for i in self.StringBufferIndices] - - def GetCount(self, exactVersion: UnityVersion) -> int: - return get_item_for_version(exactVersion, self.VersionInformation) - - -###################################################################################### -# -# helper functions -# -###################################################################################### - -BYTE = Struct("b") -UINT16 = Struct(" str: - # varint - shift = 0 - length = 0 - while True: - (i,) = stream.read(1) - length |= (i & 0x7F) << shift - shift += 7 - if not (i & 0x80): - break - # string - return stream.read(length).decode("utf-8") - - -def read_data(stream: BytesIO) -> bytes: - return stream.read(INT32.unpack(stream.read(INT32.size))[0]) - - -def read_version(stream: BytesIO) -> UnityVersion: - return UnityVersion(UINT64.unpack(stream.read(UINT64.size))[0]) - - -def read_versions(stream: BytesIO, count: int) -> List[UnityVersion]: - struct = Struct(f"<{count}Q") - return [UnityVersion(x) for x in struct.unpack(stream.read(struct.size))] - - -def get_item_for_version(exactVersion: UnityVersion, items: List[Tuple[UnityVersion, T]]) -> T: - ret = None - for version, item in items: - if exactVersion >= version: - ret = item - else: - break - if ret: - return ret - raise ValueError("Could not find exact version") + return TypeTreeNode.from_list(nodes) -init() +@cache +def get_common_strings(version: Optional[UnityVersion] = None) -> Dict[int, str]: + tpk_version: TpkUnityVersion | None = cast(TpkUnityVersion, version) if version is not None else None + return get_typetree().CommonString.BuildMap(get_typetree().StringBuffer, tpk_version) diff --git a/UnityPy/helpers/TypeTreeNode.py b/UnityPy/helpers/TypeTreeNode.py index 31aacc91..d373341f 100644 --- a/UnityPy/helpers/TypeTreeNode.py +++ b/UnityPy/helpers/TypeTreeNode.py @@ -16,12 +16,10 @@ from attrs import define, field +from ..helpers.Tpk import get_common_strings from ..streams.EndianBinaryReader import EndianBinaryReader from ..streams.EndianBinaryWriter import EndianBinaryWriter -if TYPE_CHECKING: - from .Tpk import UnityVersion - try: from ..UnityPyBoost import TypeTreeNode as TypeTreeNodeC # type: ignore except ImportError: @@ -302,32 +300,6 @@ def __eq__(self, other: TypeTreeNode) -> bool: # type: ignore return self.to_dict() == other.to_dict() and self.m_Children == other.m_Children -COMMONSTRING_CACHE: Dict[Optional[UnityVersion], Dict[int, str]] = {} - - -def get_common_strings(version: Optional[UnityVersion] = None) -> Dict[int, str]: - if version in COMMONSTRING_CACHE: - return COMMONSTRING_CACHE[version] - - from .Tpk import TPKTYPETREE - - tree = TPKTYPETREE - common_string = tree.CommonString - strings = common_string.GetStrings(tree.StringBuffer) - if version: - count = common_string.GetCount(version) - strings = strings[:count] - - ret: Dict[int, str] = {} - offset = 0 - for string in strings: - ret[offset] = string - offset += len(string) + 1 - - COMMONSTRING_CACHE[version] = ret - return ret - - def _get_blob_node_struct(endian: str, version: int) -> tuple[Struct, list[str]]: struct_type = f"{endian}hBBIIiii" keys = [ @@ -355,7 +327,7 @@ def clean_name(name: str) -> str: name = name[6:] if name.endswith("?"): name = name[:-1] - name = re.sub(r"[ \.:\-\[\]]", "_", name) + name = re.sub(r"[ \.:\-\[\]\*]", "_", name) if name in ["pass", "from"]: name += "_" if name[0].isdigit(): diff --git a/UnityPy/tools/TpkClassGenerator.py b/UnityPy/tools/TpkClassGenerator.py index ebef4272..2536644e 100644 --- a/UnityPy/tools/TpkClassGenerator.py +++ b/UnityPy/tools/TpkClassGenerator.py @@ -10,11 +10,11 @@ # import UnityPy from the parent directory instead of the installed package ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(ROOT) -from UnityPy.helpers.Tpk import TPKTYPETREE, TpkUnityNode # noqa: E402 +from UnityPy.helpers.Tpk import TpkUnityNode, get_typetree # noqa: E402 from UnityPy.helpers.TypeTreeNode import clean_name # noqa: E402 -NODES = TPKTYPETREE.NodeBuffer -STRINGS = TPKTYPETREE.StringBuffer +NODES = get_typetree().NodeBuffer +STRINGS = get_typetree().StringBuffer BASE_TYPE_MAP = { "char": "int", # used for byte data @@ -310,7 +310,7 @@ def generate_classes(): main_classes: Set[str] = set() deps: Dict[str, List[str]] = {} - for _class_id, class_info in TPKTYPETREE.ClassInformation.items(): + for _class_id, class_info in get_typetree().ClassInformation.items(): abstract = True base = None cls_name: Optional[str] = None diff --git a/pyproject.toml b/pyproject.toml index 3ab9e435..cad2658f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,8 @@ dependencies = [ "fsspec", # better classes "attrs", + # tpk handling + "tpk_ar" ] dynamic = ["version"] From 8196d88f963d5f67749bf5061b8a71ff1ff76236 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Sat, 1 Aug 2026 20:33:29 +0200 Subject: [PATCH 14/19] chore: update tpk file --- UnityPy/classes/generated.py | 458 +++++++++++++++++++++++++++++++++-- UnityPy/resources/lzma.tpk | 4 +- 2 files changed, 440 insertions(+), 22 deletions(-) diff --git a/UnityPy/classes/generated.py b/UnityPy/classes/generated.py index 70a336bb..5804e4f2 100644 --- a/UnityPy/classes/generated.py +++ b/UnityPy/classes/generated.py @@ -117,16 +117,18 @@ class BuiltAssetBundleInfoSet(Object): @unitypy_define class ContentSummary(Object): m_assetStatsList: List[AssetStats] - m_generatedFileCount: int - m_generatedFileSize: int m_headerSize: int m_objectCount: int m_resourceDataSize: int m_resourceFileCount: int m_serializedFileCount: int m_serializedFileSize: int - m_sizeReusedContentInOutputDirectory: int m_typeStatsList: List[TypeStats] + m_generatedFileCount: Optional[int] = None + m_generatedFileSize: Optional[int] = None + m_reusedSerializedFileCount: Optional[int] = None + m_reusedSerializedFileSize: Optional[int] = None + m_sizeReusedContentInOutputDirectory: Optional[int] = None @unitypy_define @@ -1521,6 +1523,7 @@ class Terrain(Behaviour): m_CastShadows: Optional[bool] = None m_DefaultSmoothness: Optional[float] = None m_DrawInstanced: Optional[bool] = None + m_EnableHeightmapLODFrustumCulling: Optional[bool] = None m_EnableHeightmapRayTracing: Optional[bool] = None m_EnableTreesAndDetailsRayTracing: Optional[bool] = None m_ExplicitProbeSetHash: Optional[Hash128] = None @@ -2535,6 +2538,7 @@ class SpriteRenderer(Renderer): m_Sprite: PPtr[Sprite] m_StaticBatchRoot: PPtr[Transform] m_AdaptiveModeThreshold: Optional[float] = None + m_BlendShapeWeights: Optional[List[float]] = None m_DrawMode: Optional[int] = None m_DynamicOccludee: Optional[int] = None m_FlipX: Optional[bool] = None @@ -3240,6 +3244,8 @@ class FBXImporter(ModelImporter): calculateBlendshapeNormalsDeltaFromImportedNormals: Optional[bool] = None generateMeshLods: Optional[bool] = None generateSecondaryUV: Optional[bool] = None + importUVs: Optional[int] = None + importVertexColors: Optional[bool] = None indexFormat: Optional[int] = None keepQuads: Optional[bool] = None legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: Optional[bool] = None @@ -3306,6 +3312,8 @@ class FBXImporter(ModelImporter): m_NodeNameCollisionStrategy: Optional[int] = None m_OldHashIdentity: Optional[MdFour] = None m_OptimizeGameObjects: Optional[bool] = None + m_PreBakeConvexCollisionMesh: Optional[bool] = None + m_PreBakeTriangleCollisionMesh: Optional[bool] = None m_PreserveHierarchy: Optional[bool] = None m_PreviousCalculatedGlobalScale: Optional[float] = None m_ReferencedClips: Optional[List[GUID]] = None @@ -3315,6 +3323,7 @@ class FBXImporter(ModelImporter): m_ResampleRotations: Optional[bool] = None m_RigImportErrors: Optional[str] = None m_RigImportWarnings: Optional[str] = None + m_SearchTexturesGlobally: Optional[bool] = None m_SortHierarchyByName: Optional[bool] = None m_SplitAnimations: Optional[bool] = None m_StrictVertexDataChecks: Optional[bool] = None @@ -3370,6 +3379,8 @@ class Mesh3DSImporter(ModelImporter): calculateBlendshapeNormalsDeltaFromImportedNormals: Optional[bool] = None generateMeshLods: Optional[bool] = None generateSecondaryUV: Optional[bool] = None + importUVs: Optional[int] = None + importVertexColors: Optional[bool] = None indexFormat: Optional[int] = None keepQuads: Optional[bool] = None legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: Optional[bool] = None @@ -3436,6 +3447,8 @@ class Mesh3DSImporter(ModelImporter): m_NodeNameCollisionStrategy: Optional[int] = None m_OldHashIdentity: Optional[MdFour] = None m_OptimizeGameObjects: Optional[bool] = None + m_PreBakeConvexCollisionMesh: Optional[bool] = None + m_PreBakeTriangleCollisionMesh: Optional[bool] = None m_PreserveHierarchy: Optional[bool] = None m_PreviousCalculatedGlobalScale: Optional[float] = None m_ReferencedClips: Optional[List[GUID]] = None @@ -3445,6 +3458,7 @@ class Mesh3DSImporter(ModelImporter): m_ResampleRotations: Optional[bool] = None m_RigImportErrors: Optional[str] = None m_RigImportWarnings: Optional[str] = None + m_SearchTexturesGlobally: Optional[bool] = None m_SortHierarchyByName: Optional[bool] = None m_SplitAnimations: Optional[bool] = None m_StrictVertexDataChecks: Optional[bool] = None @@ -3543,6 +3557,8 @@ class SketchUpImporter(ModelImporter): blendShapeNormalImportMode: Optional[int] = None calculateBlendshapeNormalsDeltaFromImportedNormals: Optional[bool] = None generateMeshLods: Optional[bool] = None + importUVs: Optional[int] = None + importVertexColors: Optional[bool] = None indexFormat: Optional[int] = None legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: Optional[bool] = None m_AddHumanoidExtraRootOnlyWhenUsingAvatar: Optional[bool] = None @@ -3573,6 +3589,8 @@ class SketchUpImporter(ModelImporter): m_MaterialLocation: Optional[int] = None m_Materials: Optional[List[SourceAssetIdentifier]] = None m_NodeNameCollisionStrategy: Optional[int] = None + m_PreBakeConvexCollisionMesh: Optional[bool] = None + m_PreBakeTriangleCollisionMesh: Optional[bool] = None m_PreserveHierarchy: Optional[bool] = None m_PreviousCalculatedGlobalScale: Optional[float] = None m_RemapMaterialsIfMaterialImportModeIsNone: Optional[bool] = None @@ -3581,6 +3599,7 @@ class SketchUpImporter(ModelImporter): m_ResampleRotations: Optional[bool] = None m_RigImportErrors: Optional[str] = None m_RigImportWarnings: Optional[str] = None + m_SearchTexturesGlobally: Optional[bool] = None m_SortHierarchyByName: Optional[bool] = None m_StrictVertexDataChecks: Optional[bool] = None m_SupportsEmbeddedMaterials: Optional[bool] = None @@ -3972,6 +3991,7 @@ class TextureImporter(AssetImporter): m_SpriteBorder: Optional[Vector4f] = None m_SpriteExtrude: Optional[int] = None m_SpriteGenerateFallbackPhysicsShape: Optional[int] = None + m_SpriteGeometrySubdivision: Optional[float] = None m_SpriteMeshType: Optional[int] = None m_SpriteMode: Optional[int] = None m_SpritePackingTag: Optional[str] = None @@ -3979,6 +3999,7 @@ class TextureImporter(AssetImporter): m_SpritePixelsToUnits: Optional[float] = None m_SpriteSheet: Optional[SpriteSheetMetaData] = None m_SpriteTessellationDetail: Optional[float] = None + m_SpriteTessellationMethod: Optional[int] = None m_StreamingMipmaps: Optional[int] = None m_StreamingMipmapsPriority: Optional[int] = None m_Swizzle: Optional[int] = None @@ -4281,6 +4302,193 @@ class BlockShaderSyntaxTree(NamedObject): source: str +@unitypy_define +class BuildProfilePlayerSettings(NamedObject): + AID: Hash128 + AndroidEnableSustainedPerformanceMode: bool + AndroidFilterTouchesWhenObscured: bool + AndroidProfiler: bool + Force_IOS_Speakers_When_Recording: bool + Prepare_IOS_For_Recording: bool + accelerometerFrequency: int + activeInputHandler: int + adjustIOSFPSUsingThermalState: bool + allowFullscreenSwitch: bool + allowHDRDisplaySupport: bool + allowedAutorotateToLandscapeLeft: bool + allowedAutorotateToLandscapeRight: bool + allowedAutorotateToPortrait: bool + allowedAutorotateToPortraitUpsideDown: bool + allowedHttpConnections: int + androidApplicationEntry: int + androidAutoRotationBehavior: int + androidBlitType: int + androidDefaultWindowHeight: int + androidDefaultWindowWidth: int + androidDisplayOptions: int + androidFullscreenMode: int + androidMaxAspectRatio: float + androidMinAspectRatio: float + androidMinimumWindowHeight: int + androidMinimumWindowWidth: int + androidPredictiveBackSupport: bool + androidRenderOutsideSafeArea: bool + androidRequestedVisibleInsets: int + androidResizeableActivity: bool + androidShowActivityIndicatorOnLoading: int + androidStartInFullscreen: bool + androidSupportedAspectRatio: int + androidSystemBarsBehavior: int + androidUseSwappy: bool + androidVulkanAllowFilterList: List[AndroidDeviceFilterData] + androidVulkanDenyFilterList: List[AndroidDeviceFilterData] + androidVulkanDeviceFilterListAsset: PPtr[VulkanDeviceFilterLists] + audioSpatialExperience: int + bakeCollisionMeshes: bool + bundleVersion: str + callOnDisableOnAssetBundleUnload: bool + cloudEnabled: bool + cloudProjectId: str + companyName: str + cursorHotspot: Vector2f + d3d12DeviceFilterListAsset: PPtr[D3D12DeviceFilterLists] + dedicatedServerOptimizations: bool + defaultCursor: PPtr[Texture2D] + defaultIsNativeResolution: bool + defaultScreenHeight: int + defaultScreenHeightWeb: int + defaultScreenOrientation: int + defaultScreenWidth: int + defaultScreenWidthWeb: int + deferSystemGesturesMode: int + disableDepthAndStencilBuffers: bool + enableDirectStorage: bool + enableFrameTimingStats: bool + enableOpenGLProfilerGPURecorders: bool + forceSingleInstance: bool + framebufferDepthMemorylessMode: int + fullscreenMode: int + gpuSkinning: bool + hdrBitDepth: int + hideHomeButton: bool + hmiLoadingImage: PPtr[Texture2D] + insecureHttpOption: int + invalidatedPatternTexture: PPtr[Texture2D] + iosShowActivityIndicatorOnLoading: int + iosUseCustomAppBackgroundBehavior: bool + legacyClampBlendShapeWeights: bool + loadStoreDebugModeEnabled: bool + m_ActiveColorSpace: int + m_ColorGamuts: List[int] + m_MTRendering: bool + m_Name: str + m_ShowUnitySplashLogo: bool + m_ShowUnitySplashScreen: bool + m_SplashScreenAnimation: int + m_SplashScreenBackgroundAnimationZoom: float + m_SplashScreenBackgroundColor: ColorRGBA + m_SplashScreenBackgroundLandscape: PPtr[Texture2D] + m_SplashScreenBackgroundLandscapeAspect: float + m_SplashScreenBackgroundLandscapeUvs: Rectf + m_SplashScreenBackgroundPortrait: PPtr[Texture2D] + m_SplashScreenBackgroundPortraitAspect: float + m_SplashScreenBackgroundPortraitUvs: Rectf + m_SplashScreenDrawMode: int + m_SplashScreenLogoAnimationZoom: float + m_SplashScreenLogoStyle: int + m_SplashScreenLogos: List[SplashScreenLogo] + m_SplashScreenOverlayOpacity: float + m_SpriteBatchMaxVertexCount: int + m_SpriteBatchVertexThreshold: int + m_StackTraceTypes: List[int] + m_StereoRenderingPath: int + m_UnitySplashLogo: PPtr[Sprite] + m_VirtualRealitySplashScreen: PPtr[Texture2D] + macAppStoreCategory: str + macRetinaSupport: bool + meshDeformation: int + metalFramebufferOnly: bool + metalUseMetalDisplayLink: bool + metroInputSource: int + mipStripping: bool + mobileMTRenderingBaked: bool + muteOtherAudioSources: bool + numberOfMipsStripped: int + numberOfMipsStrippedPerMipmapLimitGroup: List[Tuple[str, int]] + organizationId: str + platformRequiresReadableAssets: bool + playerMinOpenGLESVersion: int + preloadedAssets: List[PPtr[Object]] + preserveFramebufferAlpha: bool + productGUID: GUID + productName: str + projectName: str + qualitySettingsNames: List[str] + resetResolutionOnWindowResize: bool + resizableWindow: bool + resolutionScalingMode: int + runInBackground: bool + submitAnalytics: bool + switchAllowGpuScratchShrinking: bool + switchGpuScratchPoolGranularity: int + switchGraphicsJobsSyncAfterKick: bool + switchMaxWorkerMultiple: int + switchNVNDefaultPoolsGranularity: int + switchNVNGraphicsFirmwareMemory: int + switchNVNMaxPublicSamplerIDCount: int + switchNVNMaxPublicTextureIDCount: int + switchNVNOtherPoolsGranularity: int + switchNVNShaderPoolsGranularity: int + switchQueueCommandMemory: int + switchQueueComputeMemory: int + switchQueueControlMemory: int + targetDevice: int + targetPixelDensity: int + thermalStateCriticalIOSFPS: int + thermalStateSeriousIOSFPS: int + tvOSBundleVersion: str + unsupportedMSAAFallback: int + use32BitDisplayBuffer: bool + useFlipModelSwapchain: bool + useHDRDisplay: bool + useMacAppStoreValidation: bool + useOSAutorotation: bool + useOnDemandResources: bool + usePlayerLog: bool + virtualTexturingSupportEnabled: bool + visibleInBackground: bool + visionOSBundleVersion: str + vrSettings: VRSettings + vulkanEnableCommandBufferRecycling: bool + vulkanEnableLateAcquireNextImage: bool + vulkanEnablePreTransform: bool + vulkanEnableSetSRGBWrite: bool + vulkanNumSwapchainBuffers: int + webGPUDeviceFilterListAsset: PPtr[WebGPUDeviceFilterLists] + windowsGamepadBackendHint: int + wsaTransparentSwapchain: bool + xboxEnableAvatar: bool + xboxEnableFitness: bool + xboxEnableGuest: bool + xboxEnableHeadOrientation: bool + xboxEnableKinect: bool + xboxEnableKinectAutoTracking: bool + xboxEnablePIXSampling: bool + xboxOneDisableEsram: bool + xboxOneDisableKinectGpuReservation: bool + xboxOneEnable7thCore: bool + xboxOneEnableTypeOptimization: bool + xboxOneLoggingLevel: int + xboxOneMonoLoggingLevel: int + xboxOnePresentImmediateThreshold: int + xboxOneResolution: int + xboxOneSResolution: int + xboxOneXResolution: int + xboxPIXTextureCapture: bool + xboxSpeechDB: int + webProgressiveAssetLoading: Optional[bool] = None + + @unitypy_define class BuildReport(NamedObject): m_Appendices: List[PPtr[Object]] @@ -4608,7 +4816,6 @@ class Mesh(NamedObject): m_IndexBuffer: List[int] m_LocalAABB: AABB m_MeshCompression: int - m_MeshUsageFlags: int m_Name: str m_SubMeshes: List[SubMesh] m_BakedConvexCollisionMesh: Optional[List[int]] = None @@ -4626,7 +4833,10 @@ class Mesh(NamedObject): m_MeshLodInfo: Optional[MeshLodInfo] = None m_MeshMetrics_0_: Optional[float] = None m_MeshMetrics_1_: Optional[float] = None + m_MeshUsageFlags: Optional[int] = None m_Normals: Optional[List[Vector3f]] = None + m_PreBakeConvexCollisionMesh: Optional[bool] = None + m_PreBakeTriangleCollisionMesh: Optional[bool] = None m_RootBoneNameHash: Optional[int] = None m_ShapeVertices: Optional[List[MeshBlendShapeVertex]] = None m_Shapes: Optional[Union[BlendShapeData, List[MeshBlendShape]]] = None @@ -4886,11 +5096,11 @@ class Sprite(NamedObject): class SpriteAtlas(NamedObject): m_IsVariant: bool m_Name: str - m_PackedSpriteNamesToIndex: List[str] - m_PackedSprites: List[PPtr[Sprite]] m_RenderDataMap: List[Tuple[Tuple[GUID, int], SpriteAtlasData]] m_Tag: str m_Guid: Optional[GUID] = None + m_PackedSpriteNamesToIndex: Optional[List[str]] = None + m_PackedSprites: Optional[List[PPtr[Sprite]]] = None @unitypy_define @@ -5262,6 +5472,12 @@ class Texture3D(Texture): m_UsageMode: Optional[int] = None +@unitypy_define +class UIAnimationClip(NamedObject): + m_AnimationClip: PPtr[AnimationClip] + m_Name: str + + @unitypy_define class VideoClip(NamedObject): Height: int @@ -5328,6 +5544,13 @@ class VulkanDeviceFilterLists(NamedObject): m_VulkanDenyFilterList: List[AndroidDeviceFilterData] +@unitypy_define +class WebGPUDeviceFilterLists(NamedObject): + m_AllowFilterList: List[WebGPUDeviceFilterData] + m_DenyFilterList: List[WebGPUDeviceFilterData] + m_Name: str + + @unitypy_define class EditorExtensionImpl(Object): gFlattenedTypeTree: Optional[List[int]] = None @@ -5350,6 +5573,7 @@ class EditorSettings(Object): m_CacheServerEnableTls: Optional[bool] = None m_CacheServerEnableUpload: Optional[bool] = None m_CacheServerEndpoint: Optional[str] = None + m_CacheServerImportResultCachingEnabled: Optional[bool] = None m_CacheServerMode: Optional[int] = None m_CacheServerNamespacePrefix: Optional[str] = None m_CacheServerValidationMode: Optional[int] = None @@ -5359,6 +5583,7 @@ class EditorSettings(Object): m_DisableCookiesInLightmapper: Optional[bool] = None m_EnableEditorAsyncCPUTextureLoading: Optional[bool] = None m_EnableEnlightenBakedGI: Optional[bool] = None + m_EnableMSBuildCompilationPipeline: Optional[bool] = None m_EnableRoslynAnalyzers: Optional[bool] = None m_EnableTextureStreamingInEditMode: Optional[bool] = None m_EnableTextureStreamingInPlayMode: Optional[bool] = None @@ -5391,6 +5616,7 @@ class EditorSettings(Object): m_SpritePackerMode: Optional[int] = None m_SpritePackerPaddingPower: Optional[int] = None m_UnlockBlockShaders: Optional[bool] = None + m_UseLegacyHierarchy: Optional[bool] = None m_UseLegacyProbeSampleCount: Optional[bool] = None m_UserGeneratedProjectSuffix: Optional[str] = None m_WebSecurityEmulationEnabled: Optional[int] = None @@ -5569,6 +5795,7 @@ class EditorUserSettings(Object): m_SemanticMergeMode: Optional[int] = None m_StandbyImportWorkerCount: Optional[int] = None m_VCAllowAsyncUpdate: Optional[bool] = None + m_VCAutoRevertUnchangedFiles: Optional[bool] = None m_VCHierarchyOverlayIcons: Optional[bool] = None m_VCOtherOverlayIcons: Optional[bool] = None m_VCOverlayIcons: Optional[bool] = None @@ -5630,7 +5857,6 @@ class AudioManager(GlobalGameManager): @unitypy_define class BuildSettings(GlobalGameManager): - enableDynamicBatching: bool hasAdvancedVersion: bool hasPROVersion: bool hasPublishingRights: bool @@ -5639,6 +5865,7 @@ class BuildSettings(GlobalGameManager): m_Version: str buildGUID: Optional[Union[GUID, str]] = None buildTags: Optional[List[str]] = None + enableDynamicBatching: Optional[bool] = None enableMultipleDisplays: Optional[bool] = None enabledVRDevices: Optional[List[str]] = None hasClusterRendering: Optional[bool] = None @@ -5685,15 +5912,20 @@ class DelayedCallManager(GlobalGameManager): @unitypy_define class GraphicsSettings(GlobalGameManager): m_AlwaysIncludedShaders: List[PPtr[Shader]] + m_AdditionalWarmupCollections: Optional[List[PPtr[GraphicsStateCollection]]] = None m_AllowEnlightenSupportForUpgradedProject: Optional[bool] = None + m_CacheMissCollectionPath: Optional[str] = None m_CameraRelativeLightCulling: Optional[bool] = None m_CameraRelativeShadowCulling: Optional[bool] = None + m_CollectionStartupAction: Optional[int] = None m_CurrentRenderPipelineGlobalSettings: Optional[PPtr[Object]] = None m_CustomRenderPipeline: Optional[PPtr[MonoBehaviour]] = None m_DefaultRenderingLayerMask: Optional[int] = None m_Deferred: Optional[BuiltinShaderSettings] = None m_DeferredReflections: Optional[BuiltinShaderSettings] = None m_DepthNormals: Optional[BuiltinShaderSettings] = None + m_EnableCacheMissTracing: Optional[bool] = None + m_GraphicsStateCollection: Optional[PPtr[GraphicsStateCollection]] = None m_LegacyDeferred: Optional[BuiltinShaderSettings] = None m_LensFlare: Optional[BuiltinShaderSettings] = None m_LightHalo: Optional[BuiltinShaderSettings] = None @@ -5716,9 +5948,13 @@ class GraphicsSettings(GlobalGameManager): m_TierSettings_Tier1: Optional[TierGraphicsSettings] = None m_TierSettings_Tier2: Optional[TierGraphicsSettings] = None m_TierSettings_Tier3: Optional[TierGraphicsSettings] = None + m_TraceSavePath: Optional[str] = None + m_TraceSendToEditor: Optional[bool] = None m_TransparencySortAxis: Optional[Vector3f] = None m_TransparencySortMode: Optional[int] = None m_VideoShadersIncludeMode: Optional[int] = None + m_WarmupAsync: Optional[bool] = None + m_WarmupProgressivelyLimit: Optional[int] = None @unitypy_define @@ -5813,6 +6049,11 @@ class Physics2DSettings(GlobalGameManager): m_VelocityThreshold: Optional[float] = None +@unitypy_define +class PhysicsCoreProjectSettings2D(GlobalGameManager): + m_PhysicsCoreSettings: PPtr[Object] + + @unitypy_define class PhysicsManager(GlobalGameManager): m_BounceThreshold: float @@ -5862,6 +6103,7 @@ class PhysicsManager(GlobalGameManager): m_SolverIterationCount: Optional[int] = None m_SolverType: Optional[int] = None m_SolverVelocityIterations: Optional[int] = None + m_ThreadingMode: Optional[int] = None m_WorldBounds: Optional[AABB] = None m_WorldSubdivisions: Optional[int] = None @@ -5928,6 +6170,7 @@ class PlayerSettings(GlobalGameManager): bakeCollisionMeshes: Optional[bool] = None bundleIdentifier: Optional[str] = None bundleVersion: Optional[str] = None + callOnDisableOnAssetBundleUnload: Optional[bool] = None captureSingleScreen: Optional[bool] = None cloudEnabled: Optional[bool] = None cloudProjectId: Optional[str] = None @@ -5946,6 +6189,7 @@ class PlayerSettings(GlobalGameManager): disableDepthAndStencilBuffers: Optional[bool] = None disableOldInputManagerSupport: Optional[bool] = None displayResolutionDialog: Optional[int] = None + enableDirectStorage: Optional[bool] = None enableFrameTimingStats: Optional[bool] = None enableGamepadInput: Optional[bool] = None enableHWStatistics: Optional[bool] = None @@ -6012,6 +6256,7 @@ class PlayerSettings(GlobalGameManager): macRetinaSupport: Optional[bool] = None meshDeformation: Optional[int] = None metalFramebufferOnly: Optional[bool] = None + metalUseMetalDisplayLink: Optional[bool] = None metroEnableIndependentInputSource: Optional[bool] = None metroEnableLowLatencyPresentationAPI: Optional[bool] = None metroInputSource: Optional[int] = None @@ -6087,6 +6332,8 @@ class PlayerSettings(GlobalGameManager): vulkanEnableSetSRGBWrite: Optional[bool] = None vulkanNumSwapchainBuffers: Optional[int] = None vulkanUseSWCommandBuffers: Optional[bool] = None + webGPUDeviceFilterListAsset: Optional[PPtr[WebGPUDeviceFilterLists]] = None + webProgressiveAssetLoading: Optional[bool] = None wiiHio2Usage: Optional[int] = None wiiLoadingScreenBackground: Optional[ColorRGBA] = None wiiLoadingScreenFileName: Optional[str] = None @@ -6490,6 +6737,12 @@ class ShaderContainer(Object): pass +@unitypy_define +class ShaderIncludeReflection(Object): + m_Functions: List[ReflectedFunction] + m_ReflectionLog: ErrorLog + + @unitypy_define class SiblingDerived(Object): pass @@ -6547,6 +6800,11 @@ class TilemapEditorUserSettings(Object): m_LastUsedPalette: PPtr[GameObject] +@unitypy_define +class UIAnimationBinder(Object): + pass + + @unitypy_define class VersionControlSettings(Object): m_Mode: str @@ -6887,6 +7145,13 @@ class Axes: m_Type: int +@unitypy_define +class Binding: + m_Slot: int + m_EncodedData: Optional[int] = None + m_Set: Optional[int] = None + + @unitypy_define class BitField: m_Bits: int @@ -7088,6 +7353,15 @@ class BufferBinding: m_ArraySize: Optional[int] = None +@unitypy_define +class BufferBindingParameter: + m_ArraySize: int + m_NameIndex: int + m_Binding: Optional[Binding] = None + m_Index: Optional[int] = None + m_ResourceType: Optional[int] = None + + @unitypy_define class BuildReportFile: id: int @@ -7141,7 +7415,11 @@ class BuildSummary: buildContentOptions: Optional[int] = None buildGUID: Optional[GUID] = None buildManifestHash: Optional[Hash128] = None + buildName: Optional[str] = None + buildProfileGuid: Optional[GUID] = None + buildProfilePath: Optional[str] = None buildResult: Optional[int] = None + buildSessionGUID: Optional[GUID] = None buildStartTime: Optional[DateTime] = None buildType: Optional[int] = None dataPath: Optional[str] = None @@ -7522,7 +7800,7 @@ class ComputeShaderCB: @unitypy_define class ComputeShaderKernel: - builtinSamplers: List[ComputeShaderBuiltinSampler] + builtinSamplers: Union[List[ComputeShaderBuiltinSampler], List[SamplerParameter]] cbs: List[ComputeShaderResource] code: List[int] inBuffers: List[ComputeShaderResource] @@ -7567,10 +7845,12 @@ class ComputeShaderPlatformVariant: @unitypy_define class ComputeShaderResource: - bindPoint: int name: Union[FastPropertyName, str] + bindPoint: Optional[int] = None counter: Optional[ComputeBufferCounter] = None generatedName: Optional[Union[FastPropertyName, str]] = None + m_Binding: Optional[Binding] = None + m_SamplerBinding: Optional[Binding] = None resType: Optional[int] = None samplerBindPoint: Optional[int] = None secondaryBindPoint: Optional[int] = None @@ -7618,6 +7898,16 @@ class ConstantBuffer: m_StructParams: Optional[List[StructParameter]] = None +@unitypy_define +class ConstantBufferParameter: + m_IsPartialCB: bool + m_MatrixParams: List[MatrixParameter] + m_NameIndex: int + m_Size: int + m_StructParams: List[StructParameter] + m_VectorParams: List[VectorParameter] + + @unitypy_define class ConstantClip: data: List[float] @@ -7846,6 +8136,11 @@ class EmissionModule: time3: Optional[float] = None +@unitypy_define +class EnlightenCAHMap: + m_Map: List[Tuple[LookupKey, Hash128]] + + @unitypy_define class EnlightenRendererInformation: dynamicLightmapSTInSystem: Vector4f @@ -7860,6 +8155,7 @@ class EnlightenSceneMapping: m_SystemAtlases: List[EnlightenSystemAtlasInformation] m_Systems: List[EnlightenSystemInformation] m_TerrainChunks: List[EnlightenTerrainChunksInformation] + m_CAHMap: Optional[EnlightenCAHMap] = None m_Probesets: Optional[List[Hash128]] = None @@ -7902,6 +8198,12 @@ class Error: startLine: int +@unitypy_define +class ErrorLog: + m_HasErrors: bool + m_Messages: List[Message] + + @unitypy_define class ExpandedData: m_ClassID: int @@ -7923,6 +8225,10 @@ class Expression: data_3_: int op: int valueIndex: int + dataSize: Optional[int] = None + dataType: Optional[int] = None + jmpCode: Optional[int] = None + scalarSwitchCase: Optional[int] = None @unitypy_define @@ -8037,6 +8343,7 @@ class GenericBinding: classID: Optional[int] = None isIntCurve: Optional[int] = None isSerializeReferenceCurve: Optional[int] = None + metaData: Optional[int] = None typeID: Optional[int] = None @@ -8297,6 +8604,12 @@ class HierarchicalSceneData: m_SceneGUID: GUID +@unitypy_define +class Hint: + m_Key: str + m_Value: str + + @unitypy_define class HoloLens: depthFormat: int @@ -8791,6 +9104,12 @@ class LodSelectionCurve: m_LodSlope: float +@unitypy_define +class LookupKey: + extension: str + hash: Hash128 + + @unitypy_define class Lumin: depthFormat: int @@ -8826,10 +9145,11 @@ class MaterialInstanceSettings: @unitypy_define class MatrixParameter: m_ArraySize: int - m_Index: int m_NameIndex: int m_RowCount: int m_Type: int + m_Index: Optional[int] = None + m_OffsetInConstantBuffer: Optional[int] = None @unitypy_define @@ -8882,6 +9202,14 @@ class MeshLodSubMesh: m_Levels: List[MeshLodRange] +@unitypy_define +class Message: + m_Code: int + m_Location: SourceLocation + m_Severity: int + m_Text: Optional[str] = None + + @unitypy_define class MinMaxAABB: m_Max: Vector3f @@ -9392,6 +9720,19 @@ class ProceduralTextureAssignment: shaderProp: Union[FastPropertyName, str] +@unitypy_define +class ProgramParameters: + m_BufferParams: List[BufferBindingParameter] + m_ConstantBufferBindings: List[BufferBindingParameter] + m_ConstantBuffers: List[ConstantBufferParameter] + m_Samplers: List[SamplerParameter] + m_TextureParams: List[TextureParameter] + m_UAVParams: List[UAVParameter] + m_MatrixParams: Optional[List[MatrixParameter]] = None + m_SpecializationConstantParams: Optional[List[SpecializationConstantParameter]] = None + m_VectorParams: Optional[List[VectorParameter]] = None + + @unitypy_define class PropertyModification: objectReference: PPtr[Object] @@ -9547,6 +9888,7 @@ class RayTracingShaderResource: texDimension: int arraySize: Optional[int] = None multisampled: Optional[bool] = None + resType: Optional[int] = None @unitypy_define @@ -9574,6 +9916,24 @@ class Rectf: y: float +@unitypy_define +class ReflectedFunction: + m_Hints: List[Hint] + m_Name: str + m_Parameters: List[ReflectedParameter] + m_ReturnTypeName: str + m_Body: Optional[str] = None + m_Namespace: Optional[List[str]] = None + + +@unitypy_define +class ReflectedParameter: + m_Direction: int + m_Hints: List[Hint] + m_Name: str + m_TypeName: str + + @unitypy_define class RenderManager(GlobalGameManager): pass @@ -10009,8 +10369,9 @@ class SampleSettings: @unitypy_define class SamplerParameter: - bindPoint: int sampler: int + bindPoint: Optional[int] = None + m_Binding: Optional[Binding] = None @unitypy_define @@ -10112,6 +10473,7 @@ class SerializedPass: m_HasProceduralInstancingVariant: Optional[bool] = None m_LocalKeywordMask: Optional[List[int]] = None m_Platforms: Optional[List[int]] = None + m_SerializedDynamicBranchKeywordMask: Optional[List[int]] = None m_SerializedKeywordStateMask: Optional[List[int]] = None progRayTracing: Optional[SerializedProgram] = None @@ -10127,7 +10489,7 @@ class SerializedPlayerSubProgram: @unitypy_define class SerializedProgram: m_SubPrograms: List[SerializedSubProgram] - m_CommonParameters: Optional[SerializedProgramParameters] = None + m_CommonParameters: Optional[Union[ProgramParameters, SerializedProgramParameters]] = None m_ParameterBlobIndices: Optional[List[List[int]]] = None m_PlayerSubPrograms: Optional[List[List[SerializedPlayerSubProgram]]] = None m_SerializedKeywordStateMask: Optional[List[int]] = None @@ -10268,7 +10630,7 @@ class SerializedSubProgram: m_KeywordIndices: Optional[List[int]] = None m_LocalKeywordIndices: Optional[List[int]] = None m_MatrixParams: Optional[List[MatrixParameter]] = None - m_Parameters: Optional[SerializedProgramParameters] = None + m_Parameters: Optional[Union[ProgramParameters, SerializedProgramParameters]] = None m_Samplers: Optional[List[SamplerParameter]] = None m_ShaderRequirements: Optional[int] = None m_TextureParams: Optional[List[TextureParameter]] = None @@ -10489,6 +10851,12 @@ class SourceAssetIdentifier: type: str +@unitypy_define +class SourceLocation: + m_File: str + m_Position: int + + @unitypy_define class SourceTextureInformation: doesTextureContainAlpha: bool @@ -10498,6 +10866,12 @@ class SourceTextureInformation: sourceWasHDR: Optional[bool] = None +@unitypy_define +class SpecializationConstantParameter: + m_Binding: Binding + m_NameIndex: int + + @unitypy_define class SpeedTreeWind: BRANCH_DIRECTIONAL_1: bool @@ -10833,6 +11207,7 @@ class SpriteAtlasData: textureRect: Rectf textureRectOffset: Vector2f uvTransform: Vector4f + _spriteInstanceData: Optional[SpriteInstanceData] = None atlasRectOffset: Optional[Vector2f] = None secondaryTextures: Optional[List[SecondarySpriteTexture]] = None @@ -10879,6 +11254,23 @@ class SpriteData: sprite: PPtr[Object] +@unitypy_define +class SpriteInstanceData: + border: Vector4f + m_Bindpose: List[Matrix4x4f] + m_BlendShapes: BlendShapeData + m_IndexBuffer: List[int] + m_IndexFormat: int + m_SubMeshes: List[SubMesh] + m_VertexData: VertexData + physicsShape: List[List[Vector2f]] + pivot: Vector2f + pixelsToUnits: float + rect: Rectf + spriteBones: List[SpriteBone] + spriteName: str + + @unitypy_define class SpriteMetaData: m_Alignment: int @@ -10910,6 +11302,7 @@ class SpriteRenderData: downscaleMultiplier: Optional[float] = None indices: Optional[List[int]] = None m_Bindpose: Optional[List[Matrix4x4f]] = None + m_BlendShapes: Optional[BlendShapeData] = None m_IndexBuffer: Optional[List[int]] = None m_SourceSkin: Optional[List[BoneWeights4]] = None m_SubMeshes: Optional[List[SubMesh]] = None @@ -11073,11 +11466,12 @@ class StreamingInfo: @unitypy_define class StructParameter: m_ArraySize: int - m_Index: int m_MatrixMembers: List[MatrixParameter] m_NameIndex: int m_StructSize: int m_VectorMembers: List[VectorParameter] + m_Index: Optional[int] = None + m_OffsetInConstantBuffer: Optional[int] = None @unitypy_define @@ -11234,10 +11628,12 @@ class TextureImporterPlatformSettings: @unitypy_define class TextureParameter: m_Dim: int - m_Index: int m_NameIndex: int - m_SamplerIndex: int + m_Binding: Optional[Binding] = None + m_Index: Optional[int] = None m_MultiSampled: Optional[bool] = None + m_SamplerBinding: Optional[Binding] = None + m_SamplerIndex: Optional[int] = None @unitypy_define @@ -11415,9 +11811,11 @@ class TypeStats: @unitypy_define class UAVParameter: - m_Index: int m_NameIndex: int - m_OriginalIndex: int + m_Binding: Optional[Binding] = None + m_Index: Optional[int] = None + m_OriginalBinding: Optional[Binding] = None + m_OriginalIndex: Optional[int] = None @unitypy_define @@ -11516,6 +11914,7 @@ class VFXCPUBufferDesc: initialData: VFXCPUBufferData layout: List[VFXLayoutElementDesc] stride: int + debugName: Optional[str] = None @unitypy_define @@ -11593,6 +11992,7 @@ class VFXGPUBufferDesc: layout: List[VFXLayoutElementDesc] size: int stride: int + debugName: Optional[str] = None mode: Optional[int] = None target: Optional[int] = None type: Optional[int] = None @@ -11680,7 +12080,7 @@ class VFXSystemDesc: class VFXTaskDesc: buffers: List[VFXMapping] params: List[VFXMapping] - processor: PPtr[NamedObject] + processor: Union[PPtr[NamedObject], PPtr[Object]] type: int values: List[VFXMapping] instanceSplitIndex: Optional[int] = None @@ -11774,9 +12174,10 @@ class Vector3Curve: class VectorParameter: m_ArraySize: int m_Dim: int - m_Index: int m_NameIndex: int m_Type: int + m_Index: Optional[int] = None + m_OffsetInConstantBuffer: Optional[int] = None @unitypy_define @@ -11892,6 +12293,23 @@ class VulkanGraphicsJobsDeviceFilterData: preferredMode: int +@unitypy_define +class WebGPUDeviceFilterData: + browserName: str + browserVersion: str + browserVersionComparator: int + deviceType: int + features: List[int] + limits: List[WebGPUDeviceFilterLimit] + + +@unitypy_define +class WebGPUDeviceFilterLimit: + comparator: int + limit: int + value: int + + @unitypy_define class WheelFrictionCurve: asymptoteSlip: Optional[float] = None diff --git a/UnityPy/resources/lzma.tpk b/UnityPy/resources/lzma.tpk index c518569a..26e07449 100644 --- a/UnityPy/resources/lzma.tpk +++ b/UnityPy/resources/lzma.tpk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc67b99c06dd7575431c70207a2f78624501b17d5150092b511783bfe6db2127 -size 187318 +oid sha256:0b2277765f0f7a6253df04426abd83d2bf37f8d1ad30542cddd2d81ae484741b +size 207798 From a3fa5f6efb6702683a4aa499d82c3ee3c42ecad1 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Sat, 1 Aug 2026 20:34:45 +0200 Subject: [PATCH 15/19] chore(gitignore): update --- .gitignore | 141 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index b1405525..85f41872 100644 --- a/.gitignore +++ b/.gitignore @@ -2,18 +2,15 @@ test.py AssetStudio/ .vscode/ - +uv.lock # Byte-compiled / optimized / DLL files __pycache__/ -.dump -.idea/ -*.py[cod] +*.py[codz] *$py.class -# precompiled C extensions -UnityPy/*.so -UnityPy/*.pyd +# C extensions +*.so # Distribution / packaging .Python @@ -23,18 +20,21 @@ dist/ downloads/ eggs/ .eggs/ +lib/ +lib64/ parts/ sdist/ var/ wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -45,14 +45,18 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover +*.py.cover +*.lcov .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -62,6 +66,7 @@ coverage.xml *.log local_settings.py db.sqlite3 +db.sqlite3-journal # Flask stuff: instance/ @@ -74,22 +79,85 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints -# pyenv -.python-version +# IPython +profile_default/ +ipython_config.py -# celery beat schedule file -celerybeat-schedule +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule* +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -109,3 +177,50 @@ venv.bak/ # mypy .mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml From 2081f83beb93e09e3cdbcd4de5cf127f2aa3335e Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Sat, 1 Aug 2026 20:35:11 +0200 Subject: [PATCH 16/19] chore(LICENSE): update copyright years --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6bc7be1f..bf224f94 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2021 K0lb3 +Copyright (c) 2019-2026 K0lb3 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 06b10bce704b4271f6809e0ab0e596f20f465954 Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Sat, 1 Aug 2026 20:44:21 +0200 Subject: [PATCH 17/19] fix(TypeTree - clean_name): remove dead clean_name copy, change starting * handling --- UnityPy/classes/generated.py | 2 +- UnityPy/helpers/TypeTreeHelper.py | 14 -------------- UnityPy/helpers/TypeTreeNode.py | 9 ++++++--- UnityPyBoost/TypeTreeHelper.cpp | 7 ++----- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/UnityPy/classes/generated.py b/UnityPy/classes/generated.py index 5804e4f2..2e269735 100644 --- a/UnityPy/classes/generated.py +++ b/UnityPy/classes/generated.py @@ -11207,9 +11207,9 @@ class SpriteAtlasData: textureRect: Rectf textureRectOffset: Vector2f uvTransform: Vector4f - _spriteInstanceData: Optional[SpriteInstanceData] = None atlasRectOffset: Optional[Vector2f] = None secondaryTextures: Optional[List[SecondarySpriteTexture]] = None + spriteInstanceData: Optional[SpriteInstanceData] = None @unitypy_define diff --git a/UnityPy/helpers/TypeTreeHelper.py b/UnityPy/helpers/TypeTreeHelper.py index ee562b24..710bf95d 100644 --- a/UnityPy/helpers/TypeTreeHelper.py +++ b/UnityPy/helpers/TypeTreeHelper.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from sys import version_info as py_version_info from typing import TYPE_CHECKING, Any, Optional, Union @@ -365,19 +364,6 @@ def metaflag_is_aligned(meta_flag: int | None) -> bool: return ((meta_flag or 0) & kAlignBytes) != 0 -def clean_name(name: str) -> str: - if name.startswith("(int&)"): - name = name[6:] - if name.endswith("?"): - name = name[:-1] - name = re.sub(r"[ \.:\-\[\]]", "_", name) - if name in ["pass", "from"]: - name += "_" - if name[0].isdigit(): - name = f"x{name}" - return name - - FUNCTION_WRITE_MAP = { "SInt8": EndianBinaryWriter.write_byte, "UInt8": EndianBinaryWriter.write_u_byte, diff --git a/UnityPy/helpers/TypeTreeNode.py b/UnityPy/helpers/TypeTreeNode.py index d373341f..a0a09571 100644 --- a/UnityPy/helpers/TypeTreeNode.py +++ b/UnityPy/helpers/TypeTreeNode.py @@ -319,15 +319,18 @@ def _get_blob_node_struct(endian: str, version: int) -> tuple[Struct, list[str]] return Struct(struct_type), keys +CLEAN_NAME_REMOVE_RE = re.compile(r"[\?\*]") +CLEAN_NAME_REPLACE_RE = re.compile(r"[ \.:\-\[\]]") + + def clean_name(name: str) -> str: # keep in sync with TypeTreeHelper.cpp if len(name) == 0: return name if name.startswith("(int&)"): name = name[6:] - if name.endswith("?"): - name = name[:-1] - name = re.sub(r"[ \.:\-\[\]\*]", "_", name) + name = CLEAN_NAME_REMOVE_RE.sub("", name) + name = CLEAN_NAME_REPLACE_RE.sub("_", name) if name in ["pass", "from"]: name += "_" if name[0].isdigit(): diff --git a/UnityPyBoost/TypeTreeHelper.cpp b/UnityPyBoost/TypeTreeHelper.cpp index 16b6571e..ec9fa234 100644 --- a/UnityPyBoost/TypeTreeHelper.cpp +++ b/UnityPyBoost/TypeTreeHelper.cpp @@ -40,11 +40,8 @@ std::string clean_name(const std::string &name) cleaned_name = cleaned_name.substr(6); } - // Remove trailing "?" - if (!cleaned_name.empty() && cleaned_name.back() == '?') - { - cleaned_name.pop_back(); - } + // Remove certain characters + cleaned_name = std::regex_replace(cleaned_name, std::regex("[\\?\\*]"), ""); // Replace certain characters with "_" cleaned_name = std::regex_replace(cleaned_name, std::regex("[ \\.:\\-\\[\\]]"), "_"); From 38a1b909dfc1973215249ffa1efddd2cc5e0f41d Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Sat, 1 Aug 2026 20:49:38 +0200 Subject: [PATCH 18/19] release: 1.25.3 --- UnityPy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityPy/__init__.py b/UnityPy/__init__.py index a2dcf6c6..44828896 100644 --- a/UnityPy/__init__.py +++ b/UnityPy/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.25.2" +__version__ = "1.25.3" from .environment import Environment as Environment from .helpers.ArchiveStorageManager import ( From 79dfc74946c6a0e9c5bfc910385327043b6dfdbe Mon Sep 17 00:00:00 2001 From: Rudolf Kolbe Date: Sat, 1 Aug 2026 21:02:51 +0200 Subject: [PATCH 19/19] fix(TypeTreeHelper.py): array of TypelessData was using read_bytes instead of read_byte_array (non critical, as this path likely never gets hit) --- UnityPy/helpers/TypeTreeHelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityPy/helpers/TypeTreeHelper.py b/UnityPy/helpers/TypeTreeHelper.py index 710bf95d..7eb916eb 100644 --- a/UnityPy/helpers/TypeTreeHelper.py +++ b/UnityPy/helpers/TypeTreeHelper.py @@ -276,7 +276,7 @@ def read_value_array( elif node.m_Type == "string": value = [reader.read_aligned_string() for _ in range(size)] elif node.m_Type == "TypelessData": - value = [reader.read_bytes() for _ in range(size)] + value = [reader.read_byte_array() for _ in range(size)] elif node.m_Type == "pair": key_node = node.m_Children[0] value_node = node.m_Children[1]