Skip to content

Commit 2da57a3

Browse files
committed
TypeTreeGenerator impl
1 parent 48360d0 commit 2da57a3

File tree

5 files changed

+191
-26
lines changed

5 files changed

+191
-26
lines changed

README.md

+47-10
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88

99
A Unity asset extractor for Python based on [AssetStudio](https://github.com/Perfare/AssetStudio).
1010

11-
Next to extraction, it also supports editing Unity assets.
12-
So far following obj types can be edited:
13-
14-
- Texture2D
15-
- Sprite(indirectly via linked Texture2D)
16-
- TextAsset
17-
- MonoBehaviour (and all other types that you have the typetree of)
11+
Next to extraction, UnityPy also supports editing Unity assets.
12+
Via the typetree structure all objects types can be edited.
13+
```py
14+
# modification via dict:
15+
raw_dict = obj.read_typetree()
16+
# modify raw dict
17+
obj.save_typetree(raw_dict)
18+
# modification via parsed class
19+
instance = obj.read()
20+
# modify instance
21+
obj.save(instance)
22+
```
1823

1924
If you need advice or if you want to talk about (game) data-mining,
2025
feel free to join the [UnityPy Discord](https://discord.gg/C6txv7M).
@@ -270,9 +275,8 @@ for obj in env.objects:
270275
### [MonoBehaviour](UnityPy/classes/MonoBehaviour.py)
271276

272277
MonoBehaviour assets are usually used to save the class instances with their values.
273-
If a type tree exists, it can be used to read the whole data,
274-
but if it doesn't exist, then it is usually necessary to investigate the class that loads the specific MonoBehaviour to extract the data.
275-
([example](examples/CustomMonoBehaviour/get_scriptable_texture.py))
278+
The structure/typetree for these classes might not be contained in the asset files.
279+
In such cases see the 2nd example (TypeTreeGenerator) below.
276280

277281
- `.m_Name`
278282
- `.m_Script`
@@ -303,6 +307,39 @@ for obj in env.objects:
303307
data.save(raw_data = f.read())
304308
```
305309

310+
**TypeTreeGenerator**
311+
312+
UnityPy can generate the typetrees of MonoBehaviours from the game assemblies using an optional package, ``TypeTreeGeneratorAPI``, which has to be installed via pip.
313+
UnityPy will automatically try to generate the typetree of MonoBehaviours if the typetree is missing in the assets and ``env.typetree_generator`` is set.
314+
315+
```py
316+
import UnityPy
317+
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
318+
319+
# create generator
320+
GAME_ROOT_DIR: str
321+
# e.g. r"D:\Program Files (x86)\Steam\steamapps\common\Aethermancer Demo"
322+
GAME_UNITY_VERSION: str
323+
# you can get the version via an object
324+
# e.g. objects[0].assets_file.unity_version
325+
326+
generator = TypeTreeGenerator(GAME_UNITY_VERSION)
327+
generator.load_local_game(GAME_ROOT_DIR)
328+
# generator.load_local_game(root_dir: str) - for a Windows game
329+
# generator.load_dll_folder(dll_dir: str) - for mono / non-il2cpp or generated dummies
330+
# generator.load_dll(dll: bytes)
331+
# generator.load_il2cpp(il2cpp: bytes, metadata: bytes)
332+
333+
env = UnityPy.load(fp)
334+
# assign generator to env
335+
env.typetree_generator = generator
336+
for obj in objects:
337+
if obj.type.name == "MonoBehaviour":
338+
# automatically tries to use the generator in the background if necessary
339+
x = obj.read()
340+
```
341+
342+
306343
### [AudioClip](UnityPy/classes/AudioClip.py)
307344

308345
- `.samples` - `{sample-name : sample-data}`

UnityPy/environment.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import ntpath
33
import os
44
import re
5-
from typing import Callable, Dict, List, Optional, Union
5+
from typing import Callable, Dict, List, Optional, Union, TYPE_CHECKING
66
from zipfile import ZipFile
77

88
from fsspec import AbstractFileSystem
@@ -18,6 +18,9 @@
1818
)
1919
from .streams import EndianBinaryReader
2020

21+
if TYPE_CHECKING:
22+
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
23+
2124
reSplit = re.compile(r"(.*?([^\/\\]+?))\.split\d+")
2225

2326

@@ -27,6 +30,7 @@ class Environment:
2730
path: str
2831
local_files: List[str]
2932
local_files_simple: List[str]
33+
typetree_generator: Optional["TypeTreeGenerator"] = None
3034

3135
def __init__(self, *args: FileSourceType, fs: Optional[AbstractFileSystem] = None):
3236
self.files = {}

UnityPy/files/ObjectReader.py

+42-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Generic, List, Optional, Tuple, Type, TypeVar, Union
3+
from typing import (
4+
TYPE_CHECKING,
5+
Generic,
6+
List,
7+
Optional,
8+
Tuple,
9+
Type,
10+
TypeVar,
11+
Union,
12+
Any,
13+
)
414

5-
from ..classes import NamedObject
615
from ..classes.ClassIDTypeToClassMap import ClassIDTypeToClassMap
716
from ..enums import ClassIDType, BuildTarget
817
from ..exceptions import TypeTreeError
@@ -215,20 +224,20 @@ def __repr__(self):
215224

216225
def dump_typetree_structure(
217226
self,
218-
nodes: Optional[Union[TypeTreeNode, List[dict]]] = None,
227+
nodes: Optional[Union[TypeTreeNode, List[dict[str, Union[str, int]]]]] = None,
219228
indent: str = " ",
220229
) -> str:
221230
node = self._get_typetree_node(nodes)
222231
return node.dump_structure(indent=indent)
223232

224233
def read_typetree(
225234
self,
226-
nodes: Optional[Union[TypeTreeNode, List[dict]]] = None,
235+
nodes: Optional[Union[TypeTreeNode, List[dict[str, Union[str, int]]]]] = None,
227236
wrap: bool = False,
228237
check_read: bool = True,
229238
) -> Union[dict, T]:
230-
self.reset()
231239
node = self._get_typetree_node(nodes)
240+
self.reset()
232241
ret = TypeTreeHelper.read_typetree(
233242
node,
234243
self.reader,
@@ -244,7 +253,7 @@ def read_typetree(
244253
def save_typetree(
245254
self,
246255
tree: dict,
247-
nodes: Optional[Union[TypeTreeNode, List[dict]]] = None,
256+
nodes: Optional[Union[TypeTreeNode, List[dict[str, Union[str, int]]]]] = None,
248257
writer: EndianBinaryWriter = None,
249258
):
250259
node = self._get_typetree_node(nodes)
@@ -263,7 +272,8 @@ def get_raw_data(self) -> bytes:
263272
return ret
264273

265274
def _get_typetree_node(
266-
self, node: Optional[Union[TypeTreeNode, List[dict]]] = None
275+
self,
276+
node: Optional[Union[TypeTreeNode, List[dict[str, Union[str, int]]]]] = None,
267277
) -> TypeTreeNode:
268278
if isinstance(node, TypeTreeNode):
269279
return node
@@ -276,13 +286,33 @@ def _get_typetree_node(
276286
node = self.serialized_type.node
277287
if not node:
278288
node = get_typetree_node(self.class_id, self.version)
289+
if node.m_Type == "MonoBehaviour":
290+
try:
291+
node = self._try_monobehaviour_node(node)
292+
except Exception:
293+
pass
279294
if not node:
280295
raise TypeTreeError("There are no TypeTree nodes for this object.")
281296
return node
282297

283298
# UnityPy 2 syntax early implementation
284-
def parse_as_object(self) -> T:
285-
return self.read()
286-
287-
def parse_as_dict(self) -> dict:
288-
return self.read_typetree()
299+
def parse_as_object(self, check_read: bool = True) -> T:
300+
return self.read(check_read)
301+
302+
def parse_as_dict(self, check_read: bool = True) -> Union[dict[str, Any], T]:
303+
return self.read_typetree(check_read=check_read)
304+
305+
def _try_monobehaviour_node(self, base_node: TypeTreeNode) -> TypeTreeNode:
306+
env = self.assets_file.environment
307+
generator = env.typetree_generator
308+
if generator is None:
309+
raise ValueError("No typetree generator set!")
310+
monobehaviour = self.read_typetree(base_node, check_read=False, wrap=True)
311+
script = monobehaviour.m_Script.deref_parse_as_object()
312+
node = generator.get_nodes_up(
313+
script.m_AssemblyName, f"{script.m_Namespace}.{script.m_ClassName}"
314+
)
315+
if node:
316+
return node
317+
else:
318+
raise ValueError("Failed to get custom MonoBehaviour node!")

UnityPy/helpers/TypeTreeGenerator.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import os
2+
from typing import Dict, List, Tuple
3+
from .TypeTreeNode import TypeTreeNode
4+
5+
try:
6+
from TypeTreeGeneratorAPI import TypeTreeGenerator as TypeTreeGeneratorBase
7+
except ImportError:
8+
9+
class TypeTreeGeneratorBase:
10+
def __init__(self, unity_version: str):
11+
raise ImportError("TypeTreeGeneratorAPI isn't installed!")
12+
13+
def load_dll(self, dll: bytes): ...
14+
def load_il2cpp(self, il2cpp: bytes, metadata: bytes): ...
15+
def get_nodes_as_json(self, assembly: str, fullname: str) -> str: ...
16+
def get_nodes(self, assembly: str, fullname: str) -> List[TypeTreeNode]: ...
17+
18+
19+
class TypeTreeGenerator(TypeTreeGeneratorBase):
20+
cache: Dict[Tuple[str, str], TypeTreeNode]
21+
22+
def __init__(self, unity_version: str):
23+
super().__init__(unity_version)
24+
self.cache = {}
25+
26+
def load_local_game(self, root_dir: str):
27+
root_files = os.listdir(root_dir)
28+
data_dir = os.path.join(
29+
root_dir, next(f for f in root_files if f.endswith("_Data"))
30+
)
31+
if "GameAssembly.dll" in root_files:
32+
ga_fp = os.path.join(root_dir, "GameAssembly.dll")
33+
gm_fp = os.path.join(
34+
data_dir, "il2cpp_data", "Metadata", "global-metadata.dat"
35+
)
36+
ga_raw = open(ga_fp, "rb").read()
37+
gm_raw = open(gm_fp, "rb").read()
38+
self.load_il2cpp(ga_raw, gm_raw)
39+
else:
40+
self.load_local_dll_folder(os.path.join(data_dir, "Managed"))
41+
42+
def load_local_dll_folder(self, dll_dir: str):
43+
for f in os.listdir(dll_dir):
44+
fp = os.path.join(dll_dir, f)
45+
with open(fp, "rb") as f:
46+
data = f.read()
47+
self.load_dll(data)
48+
49+
def get_nodes_up(self, assembly: str, fullname: str) -> TypeTreeNode:
50+
root = self.cache.get((assembly, fullname))
51+
if root is not None:
52+
return root
53+
54+
base_nodes = self.get_nodes(f"{assembly}.dll", fullname)
55+
56+
base_root = base_nodes[0]
57+
root = TypeTreeNode(
58+
base_root.m_Level,
59+
base_root.m_Type,
60+
base_root.m_Name,
61+
0,
62+
0,
63+
m_MetaFlag=base_root.m_MetaFlag,
64+
)
65+
stack: List[TypeTreeNode] = []
66+
parent = root
67+
prev = root
68+
69+
for base_node in base_nodes[1:]:
70+
node = TypeTreeNode(
71+
base_node.m_Level,
72+
base_node.m_Type,
73+
base_node.m_Name,
74+
0,
75+
0,
76+
m_MetaFlag=base_node.m_MetaFlag,
77+
)
78+
if node.m_Level > prev.m_Level:
79+
stack.append(parent)
80+
parent = prev
81+
elif node.m_Level < prev.m_Level:
82+
while node.m_Level <= parent.m_Level:
83+
parent = stack.pop()
84+
85+
parent.m_Children.append(node)
86+
prev = node
87+
88+
self.cache[(assembly, fullname)] = root
89+
return root
90+
91+
92+
__all__ = ("TypeTreeGenerator",)

pyproject.toml

+5-3
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,18 @@ dependencies = [
5454
]
5555
dynamic = ["version"]
5656

57+
[project.optional-dependencies]
58+
typetree_api = ["TypeTreeAPI"]
59+
full = ["UnityPy[typetree_api]"]
60+
tests = ["pytest", "pillow", "psutil", "UnityPy[full]"]
61+
5762
[project.urls]
5863
"Homepage" = "https://github.com/K0lb3/UnityPy"
5964
"Bug Tracker" = "https://github.com/K0lb3/UnityPy/issues"
6065

6166
[tool.setuptools.dynamic]
6267
version = { attr = "UnityPy.__version__" }
6368

64-
[project.optional-dependencies]
65-
tests = ["pytest", "pillow", "psutil"]
66-
6769
[tool.pytest.ini_options]
6870
testpaths = ["tests"]
6971

0 commit comments

Comments
 (0)