Skip to content

Commit c200c87

Browse files
committed
feat(UnityVersion)!: use a reimplemented UnityVersion class for Unity files and internal version handling, deprecated file.build_type
1 parent 9697d48 commit c200c87

File tree

8 files changed

+256
-115
lines changed

8 files changed

+256
-115
lines changed

UnityPy/files/BundleFile.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# TODO: implement encryption for saving files
22
import re
33
from collections import namedtuple
4-
from typing import Optional, Tuple, Union, cast
4+
from typing import Optional, Union, cast
55

66
from .. import config
77
from ..enums import ArchiveFlags, ArchiveFlagsOld, CompressionFlags
8-
from ..exceptions import UnityVersionFallbackError
98
from ..helpers import ArchiveStorageManager, CompressionHelper
9+
from ..helpers.UnityVersion import UnityVersion
1010
from ..streams import EndianBinaryReader, EndianBinaryWriter
1111
from . import File
1212

@@ -98,7 +98,7 @@ def read_fs(self, reader: EndianBinaryReader):
9898
uncompressedSize = reader.read_u_int()
9999
dataflagsValue = reader.read_u_int()
100100

101-
version = self.get_version_tuple()
101+
version = self.parse_version()
102102
# https://issuetracker.unity3d.com/issues/files-within-assetbundles-do-not-start-on-aligned-boundaries-breaking-patching-on-nintendo-switch
103103
# Unity CN introduced encryption before the alignment fix was introduced.
104104
# Unity CN used the same flag for the encryption as later on the alignment fix,
@@ -513,17 +513,17 @@ def decompress_data(
513513
else:
514514
raise ValueError(f"Unknown compression! flag: {flags}, compression flag: {comp_flag.value}")
515515

516-
def get_version_tuple(self) -> Tuple[int, int, int]:
516+
def parse_version(self) -> UnityVersion:
517517
"""Returns the version as a tuple."""
518-
version = self.version_engine
519-
match = None
520-
if version and version != "0.0.0":
521-
match = reVersion.match(version)
522-
if not match or len(match.groups()) < 3:
523-
match = None
524-
if not match:
525-
match = reVersion.match(config.get_fallback_version())
526-
if not match or len(match.groups()) < 3:
527-
raise UnityVersionFallbackError("Illegal fallback version format")
528-
map_ = map(int, match.groups())
529-
return (next(map_), next(map_), next(map_))
518+
version = None
519+
version_str = self.version_engine
520+
try:
521+
version = UnityVersion.from_str(version_str)
522+
except ValueError:
523+
pass
524+
525+
if version is None or version.major == 0:
526+
version_str = config.get_fallback_version()
527+
version = UnityVersion.from_str(version_str)
528+
529+
return version

UnityPy/files/ObjectReader.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
from ..helpers import TypeTreeHelper
2222
from ..helpers.Tpk import get_typetree_node
2323
from ..helpers.TypeTreeNode import TypeTreeNode
24+
from ..helpers.UnityVersion import UnityVersion
2425
from ..streams import EndianBinaryReader, EndianBinaryWriter
2526

2627
if TYPE_CHECKING:
27-
from ..files.SerializedFile import BuildType, SerializedFile, SerializedType
28+
from ..files.SerializedFile import SerializedFile, SerializedType
2829

2930
T = TypeVar("T")
3031
NodeInput = Union[TypeTreeNode, List[Dict[str, Union[str, int]]]]
@@ -34,10 +35,9 @@ class ObjectReader(Generic[T]):
3435
assets_file: SerializedFile
3536
reader: EndianBinaryReader
3637
data: bytes
37-
version: Tuple[int, int, int, int]
38+
version: UnityVersion
3839
version2: int
3940
platform: BuildTarget
40-
build_type: BuildType
4141
path_id: int
4242
byte_start_offset: Tuple[int, int]
4343
byte_start: int
@@ -61,7 +61,6 @@ def __init__(self, assets_file: SerializedFile, reader: EndianBinaryReader):
6161
self.version = assets_file.version
6262
self.version2 = assets_file.header.version
6363
self.platform = assets_file.target_platform
64-
self.build_type = assets_file.build_type
6564

6665
header = assets_file.header
6766
types = assets_file.types

UnityPy/files/SerializedFile.py

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import re
43
from ntpath import basename
54
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
65

@@ -10,6 +9,7 @@
109
from ..enums import BuildTarget, ClassIDType, CommonString
1110
from ..helpers.ContainerHelper import ContainerHelper
1211
from ..helpers.TypeTreeHelper import TypeTreeNode
12+
from ..helpers.UnityVersion import UnityVersion
1313
from ..streams import EndianBinaryWriter
1414
from . import BundleFile, File, ObjectReader
1515

@@ -93,22 +93,6 @@ def write(self, header: SerializedFileHeader, writer: EndianBinaryWriter):
9393
writer.write_string_to_null(self.path)
9494

9595

96-
@define(slots=True)
97-
class BuildType:
98-
build_type: str
99-
100-
def __init__(self, build_type):
101-
self.build_type = build_type
102-
103-
@property
104-
def IsAlpha(self):
105-
return self.build_type == "a"
106-
107-
@property
108-
def IsPatch(self):
109-
return self.build_type == "p"
110-
111-
11296
@define(slots=True, init=False)
11397
class SerializedType:
11498
class_id: int
@@ -220,9 +204,8 @@ def nodes(self) -> Optional[TypeTreeNode]:
220204

221205
class SerializedFile(File.File):
222206
reader: EndianBinaryReader
223-
version: Tuple[int, int, int, int]
207+
version: UnityVersion
224208
unity_version: str
225-
build_type: BuildType
226209
target_platform: BuildTarget
227210
_enable_type_tree: bool
228211
types: List[SerializedType]
@@ -253,8 +236,6 @@ def __init__(self, reader: EndianBinaryReader, parent=None, name=None, **kwargs)
253236
self.reader = reader
254237

255238
self.unity_version = "2.5.0f5"
256-
self.version = (0, 0, 0, 0)
257-
self.build_type = BuildType("")
258239
self.target_platform = BuildTarget.UnknownPlatform
259240
self._enable_type_tree = True
260241
self.types = []
@@ -375,10 +356,7 @@ def set_version(self, string_version: str):
375356
string_version = self.parent.version_engine
376357
if not string_version or string_version == "0.0.0":
377358
string_version = config.get_fallback_version()
378-
build_type = re.findall(r"([^\d.])", string_version)
379-
self.build_type = BuildType(build_type[0] if build_type else "")
380-
version_split = re.split(r"\D", string_version)
381-
self.version = tuple(int(x) for x in version_split[:4])
359+
self.version = UnityVersion.from_str(string_version)
382360

383361
def get_writeable_cab(self, name: str = "CAB-UnityPy_Mod.resS"):
384362
"""

UnityPy/helpers/Tpk.py

Lines changed: 20 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
from importlib.resources import open_binary
55
from io import BytesIO
66
from struct import Struct
7-
from typing import Any, Dict, List, Optional, Tuple
7+
from typing import Any, Dict, List, Optional, Tuple, TypeVar
88

99
from .TypeTreeHelper import TypeTreeNode
10+
from .UnityVersion import UnityVersion
1011

11-
TPKTYPETREE: TpkTypeTreeBlob = None
12-
CLASSES_CACHE: Dict[Tuple[int, tuple], TypeTreeNode] = {}
12+
T = TypeVar("T")
13+
14+
TPKTYPETREE: TpkTypeTreeBlob = None # pyright: ignore[reportAssignmentType]
15+
CLASSES_CACHE: Dict[Tuple[int, UnityVersion], TypeTreeNode] = {}
1316
NODES_CACHE: Dict[TpkUnityClass, TypeTreeNode] = {}
1417

1518

@@ -24,14 +27,14 @@ def init():
2427
TPKTYPETREE = blob
2528

2629

27-
def get_typetree_node(class_id: int, version: tuple):
30+
def get_typetree_node(class_id: int, version: UnityVersion):
2831
global CLASSES_CACHE
2932
key = (class_id, version)
3033
cached = CLASSES_CACHE.get(key)
3134
if cached:
3235
return cached
3336

34-
class_info = TPKTYPETREE.ClassInformation[class_id].getVersionedClass(UnityVersion.fromList(*version))
37+
class_info = TPKTYPETREE.ClassInformation[class_id].getVersionedClass(version)
3538
if class_info is None:
3639
raise ValueError("Could not find class info for class id {}".format(class_id))
3740

@@ -87,15 +90,6 @@ class TpkCompressionType(IntEnum):
8790
Brotli = 3
8891

8992

90-
class UnityVersionType(IntEnum):
91-
Alpha = 0
92-
Beta = 1
93-
China = 2
94-
Final = 3
95-
Patch = 4
96-
Experimental = 5
97-
98-
9993
class TpkDataType(IntEnum):
10094
TypeTreeInformation = 0
10195
Collection = 1
@@ -168,7 +162,7 @@ def __init__(self, stream: BytesIO):
168162
raise Exception("Invalid compressed size")
169163

170164
def GetDataBlob(self) -> TpkDataBlob:
171-
decompressed = None
165+
decompressed: bytes
172166
if self.CompressionType == TpkCompressionType.NONE:
173167
decompressed = self.CompressedBytes
174168

@@ -185,7 +179,7 @@ def GetDataBlob(self) -> TpkDataBlob:
185179
elif self.CompressionType == TpkCompressionType.Brotli:
186180
import brotli
187181

188-
decompressed: bytes = brotli.decompress(self.CompressedBytes)
182+
decompressed = brotli.decompress(self.CompressedBytes)
189183

190184
else:
191185
raise Exception("Invalid compression type")
@@ -228,7 +222,7 @@ class TpkTypeTreeBlob(TpkDataBlob):
228222
def __init__(self, stream: BytesIO) -> None:
229223
(self.CreationTime,) = INT64.unpack(stream.read(INT64.size))
230224
(versionCount,) = INT32.unpack(stream.read(INT32.size))
231-
self.Versions = [UnityVersion.fromStream(stream) for _ in range(versionCount)]
225+
self.Versions = [read_version(stream) for _ in range(versionCount)]
232226
(classCount,) = INT32.unpack(stream.read(INT32.size))
233227
self.ClassInformation = {x.ID: x for x in (TpkClassInformation(stream) for _ in range(classCount))}
234228
self.CommonString = TpkCommonString(stream)
@@ -282,52 +276,6 @@ def __init__(self, stream: BytesIO) -> None:
282276
######################################################################################
283277

284278

285-
class UnityVersion(int):
286-
# https://github.com/AssetRipper/VersionUtilities/blob/master/VersionUtilities/UnityVersion.cs
287-
"""
288-
use following static methods instead of the constructor(__init__):
289-
UnityVersion.fromStream(stream: BytesIO)
290-
UnityVersion.fromString(version: str)
291-
UnityVersion.fromList(major: int, minor: int, patch: int, build: int)
292-
"""
293-
294-
@staticmethod
295-
def fromStream(stream: BytesIO) -> UnityVersion:
296-
(m_data,) = UINT64.unpack(stream.read(UINT64.size))
297-
return UnityVersion(m_data)
298-
299-
@staticmethod
300-
def fromString(version: str) -> UnityVersion:
301-
return UnityVersion.fromList(*map(int, version.split(".")))
302-
303-
@staticmethod
304-
def fromList(major: int = 0, minor: int = 0, patch: int = 0, build: int = 0) -> UnityVersion:
305-
return UnityVersion(major << 48 | minor << 32 | patch << 16 | build)
306-
307-
@property
308-
def major(self) -> int:
309-
return (self >> 48) & 0xFFFF
310-
311-
@property
312-
def minor(self) -> int:
313-
return (self >> 32) & 0xFFFF
314-
315-
@property
316-
def build(self) -> int:
317-
return (self >> 16) & 0xFFFF
318-
319-
@property
320-
def type(self) -> int:
321-
return UnityVersionType(self >> 8) & 0xFF
322-
323-
@property
324-
def type_number(self) -> int:
325-
return self & 0xFF
326-
327-
def __repr__(self) -> str:
328-
return f"UnityVersion {self.major}.{self.minor}.{self.build}.{self.type_number}"
329-
330-
331279
class TpkUnityClass:
332280
__slots__ = ("Name", "Base", "Flags", "EditorRootNode", "ReleaseRootNode")
333281
Struct = Struct("<HHb")
@@ -374,20 +322,20 @@ class TpkClassInformation:
374322
__slots__ = ("ID", "Classes")
375323
ID: int
376324
# TODO - might want to use dict
377-
Classes: List[Tuple[UnityVersion, TpkUnityClass]]
325+
Classes: List[Tuple[UnityVersion, Optional[TpkUnityClass]]]
378326

379327
def __init__(self, stream: BytesIO) -> None:
380328
(self.ID,) = INT32.unpack(stream.read(INT32.size))
381329
(count,) = INT32.unpack(stream.read(INT32.size))
382330
self.Classes = [
383331
(
384-
UnityVersion.fromStream(stream),
332+
read_version(stream),
385333
TpkUnityClass(stream) if stream.read(1)[0] else None,
386334
)
387335
for _ in range(count)
388336
]
389337

390-
def getVersionedClass(self, version: UnityVersion) -> TpkUnityClass:
338+
def getVersionedClass(self, version: UnityVersion) -> Optional[TpkUnityClass]:
391339
return get_item_for_version(version, self.Classes)
392340

393341

@@ -469,7 +417,7 @@ class TpkCommonString:
469417

470418
def __init__(self, stream: BytesIO) -> None:
471419
(versionCount,) = INT32.unpack(stream.read(INT32.size))
472-
self.VersionInformation = [(UnityVersion.fromStream(stream), stream.read(1)[0]) for _ in range(versionCount)]
420+
self.VersionInformation = [(read_version(stream), stream.read(1)[0]) for _ in range(versionCount)]
473421
(indicesCount,) = INT32.unpack(stream.read(INT32.size))
474422
indicesStruct = Struct(f"<{indicesCount}H")
475423
self.StringBufferIndices = indicesStruct.unpack(stream.read(indicesStruct.size))
@@ -512,7 +460,11 @@ def read_data(stream: BytesIO) -> bytes:
512460
return stream.read(INT32.unpack(stream.read(INT32.size))[0])
513461

514462

515-
def get_item_for_version(exactVersion: UnityVersion, items: List[Tuple[UnityVersion, Any]]) -> Any:
463+
def read_version(stream: BytesIO) -> UnityVersion:
464+
return UnityVersion(UINT64.unpack(stream.read(UINT64.size))[0])
465+
466+
467+
def get_item_for_version(exactVersion: UnityVersion, items: List[Tuple[UnityVersion, T]]) -> T:
516468
ret = None
517469
for version, item in items:
518470
if exactVersion >= version:

0 commit comments

Comments
 (0)